| <!doctype html> | | <html lang="en"> | | <head> | | <meta charset="utf-8" /> | | <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover" /> | | <title>HOME FRONT — a frog's commute through the modern home</title> | | <link rel="preconnect" href="https://fonts.googleapis.com"> | | <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> | | <link href="https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:opsz,wght@12..96,600;12..96,700;12..96,800&family=Sora:wght@400;500;600;700&display=swap" rel="stylesheet"> | | <style> | | :root{ | | --ink:#20232b; | | --cream:#efe7d6; | | --cream-2:#e4d9c2; | | --paper:#f6f0e2; | | --stage:#16181d; | | --stage-2:#23262e; | | --bezel:#1b1d22; | | --coral:#ff5a3c; | | --coral-d:#d8442b; | | --gold:#f3b13d; | | --green:#7ed957; | | --green-d:#4ea632; | | --blue:#7fc6ff; | | --muted:#8a8576; | | --shadow:0 18px 40px -18px rgba(0,0,0,.65); | | } | | {box-sizing:border-box} | | html,body{height:100%} | | body{ | | margin:0; | | font-family:"Sora",system-ui,sans-serif; | | color:var(--cream); | | background: | | radial-gradient(120% 80% at 18% -10%, #2a2e38 0%, rgba(42,46,56,0) 55%), | | radial-gradient(120% 90% at 110% 110%, #2c1d18 0%, rgba(44,29,24,0) 50%), | | linear-gradient(160deg,#15171c 0%, #1d2027 60%, #14161b 100%); | | min-height:100dvh; | | display:flex; | | align-items:center; | | justify-content:center; | | padding:18px; | | overflow:hidden; | | } | | / grain / | | body::before{ | | content:"";position:fixed;inset:0;pointer-events:none;opacity:.05;mix-blend-mode:overlay; | | background-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='160' height='160'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/></filter><rect width='100%' height='100%' filter='url(%23n)' opacity='0.6'/></svg>"); | | } | | | | .cabinet{ | | position:relative; | | width:min(94vw, 560px); | | background:linear-gradient(180deg,var(--cream) 0%, var(--cream-2) 100%); | | border-radius:26px; | | padding:14px 14px 12px; | | box-shadow: | | 0 1px 0 rgba(255,255,255,.7) inset, | | 0 -6px 0 rgba(0,0,0,.06) inset, | | var(--shadow); | | border:1px solid rgba(0,0,0,.18); | | } | | / brass corner screws / | | .cabinet::before,.cabinet::after, | | .screw-bl,.screw-br{ | | content:"";position:absolute;width:11px;height:11px;border-radius:50%; | | background:radial-gradient(circle at 35% 30%, #f4d488, #b8862f 60%, #6e4d18); | | box-shadow:0 1px 1px rgba(0,0,0,.35); | | } | | .cabinet::before{top:9px;left:9px} | | .cabinet::after{top:9px;right:9px} | | .screw-bl{bottom:9px;left:9px} | | .screw-br{bottom:9px;right:9px} | | | | .topbar{ | | display:flex;align-items:center;justify-content:space-between;gap:10px; | | padding:4px 22px 10px; | | } | | .brand{display:flex;flex-direction:column;line-height:1} | | .brand h1{ | | font-family:"Bricolage Grotesque",sans-serif; | | font-weight:800;font-size:clamp(20px,4.6vw,26px); | | letter-spacing:.02em;margin:0;color:var(--ink); | | display:flex;align-items:center;gap:7px; | | } | | .brand h1 .dot{ | | width:11px;height:11px;border-radius:50%; | | background:var(--coral);box-shadow:0 0 0 3px rgba(255,90,60,.18); | | } | | .brand .sub{ | | font-size:10.5px;color:var(--muted);margin-top:5px;font-weight:500; | | letter-spacing:.06em;text-transform:uppercase; | | } | | .stats{display:flex;gap:7px;align-items:stretch} | | .chip{ | | background:#fff;border:1px solid rgba(0,0,0,.1);border-radius:11px; | | padding:5px 9px 6px;min-width:54px;text-align:center; | | box-shadow:0 1px 0 rgba(255,255,255,.8) inset, 0 2px 6px -3px rgba(0,0,0,.3); | | } | | .chip .k{display:block;font-size:8.5px;letter-spacing:.14em;color:var(--muted);font-weight:600;text-transform:uppercase} | | .chip .v{display:block;font-family:"Bricolage Grotesque",sans-serif;font-weight:800;font-size:17px;color:var(--ink);font-variant-numeric:tabular-nums;line-height:1.1} | | .chip.coral .v{color:var(--coral-d)} | | | | .lives{display:flex;gap:3px;align-items:center;justify-content:center;padding-top:2px} | | .lives .frog{width:16px;height:16px;display:block} | | | | .timerbar{ | | height:9px;border-radius:6px;margin:0 4px 8px; | | background:rgba(0,0,0,.16);overflow:hidden; | | box-shadow:0 1px 2px rgba(0,0,0,.2) inset; | | } | | .timerbar > i{ | | display:block;height:100%;width:100%; | | background:linear-gradient(90deg,var(--gold),var(--coral)); | | border-radius:6px;transition:width .12s linear, background .3s; | | } | | .timerbar.low > i{background:linear-gradient(90deg,var(--coral),#ff2d2d);animation:pulse .5s ease-in-out infinite} | | @keyframes pulse{50%{opacity:.55}} | | | | .screen-wrap{ | | position:relative; | | aspect-ratio:1/1; | | border-radius:16px; | | background:var(--bezel); | | padding:7px; | | box-shadow:0 0 0 1px rgba(0,0,0,.2), 0 10px 24px -14px rgba(0,0,0,.8); | | } | | canvas{ | | display:block;width:100%;height:100%; | | border-radius:11px; | | background:#0e0f12; | | image-rendering:auto; | | touch-action:none; | | } | | .vignette{ | | pointer-events:none;position:absolute;inset:7px;border-radius:11px; | | box-shadow:0 0 0 1px rgba(0,0,0,.25), 0 0 40px 6px rgba(0,0,0,.45) inset; | | } | | | | / overlays / | | .overlay{ | | position:absolute;inset:7px;border-radius:11px; | | display:flex;align-items:center;justify-content:center; | | background:rgba(14,15,18,.74);backdrop-filter:blur(3px); | | z-index:5;text-align:center;padding:22px; | | animation:fade .25s ease; | | } | | .overlay.hide{display:none} | | @keyframes fade{from{opacity:0}to{opacity:1}} | | .card{max-width:88%;color:var(--paper)} | | .card .eyebrow{font-size:10px;letter-spacing:.32em;text-transform:uppercase;color:var(--gold);font-weight:600} | | .card h2{ | | font-family:"Bricolage Grotesque",sans-serif;font-weight:800; | | font-size:clamp(28px,7vw,42px);margin:8px 0 4px;color:#fff;letter-spacing:.01em; | | } | | .card p{margin:8px auto;max-width:330px;font-size:13.5px;line-height:1.55;color:#d6d2c4;font-weight:400} | | .card .legend{ | | display:grid;grid-template-columns:1fr 1fr;gap:6px 14px;margin:14px auto;max-width:320px; | | font-size:11.5px;color:#c7c2b3;text-align:left; | | } | | .card .legend b{color:#fff;font-weight:600} | | .card .scoreline{font-family:"Bricolage Grotesque",sans-serif;font-size:20px;color:var(--green);font-weight:700;margin-top:6px} | | .card .scoreline small{color:#9b968a;font-weight:500;font-size:12px;font-family:"Sora"} | | .btn{ | | appearance:none;border:none;cursor:pointer; | | font-family:"Bricolage Grotesque",sans-serif;font-weight:800; | | font-size:16px;letter-spacing:.04em;text-transform:uppercase; | | color:#fff;background:linear-gradient(180deg,var(--coral),var(--coral-d)); | | padding:13px 26px;border-radius:13px;margin-top:8px; | | box-shadow:0 6px 0 #a8331f, 0 10px 18px -8px rgba(255,90,60,.7); | | transition:transform .06s, box-shadow .06s; | | } | | .btn:active{transform:translateY(4px);box-shadow:0 2px 0 #a8331f, 0 6px 12px -8px rgba(255,90,60,.7)} | | | | .footbar{ | | display:flex;align-items:center;justify-content:space-between;gap:10px; | | padding:10px 6px 2px;color:var(--muted);font-size:10.5px;font-weight:500; | | } | | .footbar .keys{display:flex;gap:6px;align-items:center;flex-wrap:wrap} | | .kbd{ | | display:inline-flex;align-items:center;justify-content:center; | | min-width:20px;height:20px;padding:0 5px;border-radius:5px; | | background:#fff;color:var(--ink);border:1px solid rgba(0,0,0,.16); | | font-family:"Bricolage Grotesque";font-weight:700;font-size:11px; | | box-shadow:0 1px 0 rgba(0,0,0,.12); | | } | | .iconbtn{ | | appearance:none;border:1px solid rgba(0,0,0,.16);background:#fff;color:var(--ink); | | border-radius:9px;padding:6px 9px;font-size:12px;font-weight:600;cursor:pointer; | | display:inline-flex;align-items:center;gap:5px; | | } | | .iconbtn:active{transform:translateY(1px)} | | | | / on-screen dpad / | | .dpad{ | | position:absolute;left:10px;bottom:10px;z-index:4; | | display:grid;grid-template-columns:repeat(3,40px);grid-template-rows:repeat(3,40px); | | gap:4px;opacity:.92; | | } | | .dpad button{ | | border:none;border-radius:10px;cursor:pointer; | | background:rgba(255,255,255,.12);color:#fff;backdrop-filter:blur(4px); | | border:1px solid rgba(255,255,255,.18); | | font-size:18px;display:flex;align-items:center;justify-content:center; | | -webkit-tap-highlight-color:transparent;user-select:none; | | } | | .dpad button:active{background:rgba(255,90,60,.55)} | | .dpad .up{grid-column:2;grid-row:1} | | .dpad .left{grid-column:1;grid-row:2} | | .dpad .right{grid-column:3;grid-row:2} | | .dpad .down{grid-column:2;grid-row:3} | | @media (hover:hover) and (pointer:fine){ | | .dpad{display:none} | | } | | @media (max-width:430px){ | | .stats{gap:5px}.chip{min-width:46px;padding:4px 7px} | | .chip .v{font-size:15px} | | .brand .sub{display:none} | | } | | </style> | | </head> | | <body> | | <main class="cabinet" role="application" aria-label="Home Front game"> | | <span class="screw-bl"></span><span class="screw-br"></span> | | | | <div class="topbar"> | | <div class="brand"> | | <h1><span class="dot"></span>HOME FRONT</h1> | | <span class="sub">a frog's commute through the modern home</span> | | </div> | | <div class="stats"> | | <div class="chip"><span class="k">Score</span><span class="v" id="score">0</span></div> | | <div class="chip coral"><span class="k">Hi</span><span class="v" id="hi">0</span></div> | | <div class="chip"><span class="k">Lvl</span><span class="v" id="level">1</span></div> | | </div> | | </div> | | | | <div class="timerbar" id="timerbar"><i id="timerfill"></i></div> | | | | <div class="screen-wrap"> | | <canvas id="game" width="572" height="572"></canvas> | | <div class="vignette"></div> | | | | <div class="dpad" id="dpad" aria-hidden="true"> | | <button class="up" data-dir="up" aria-label="up">▲</button> | | <button class="left" data-dir="left" aria-label="left">◀</button> | | <button class="right" data-dir="right" aria-label="right">▶</button> | | <button class="down" data-dir="down" aria-label="down">▼</button> | | </div> | | | | <!-- START --> | | <div class="overlay" id="startScreen"> | | <div class="card"> | | <div class="eyebrow">Arcade · Cozy Mode</div> | | <h2>HOME FRONT</h2> | | <p>You're a frog trying to get home across the living room, the driveway, and the office desk. Hop lane by lane — and don't let the everyday objects touch you.</p> | | <div class="legend"> | | <div><b>▲ ▼ ◀ ▶ / WASD</b><br>hop one tile</div> | | <div><b>Reach the nests</b><br>4 cozy alcoves up top</div> | | <div><b>Dodge</b><br>Tesla, Ferrari, Lambo…</div> | | <div><b>Avoid</b><br>phones, laptops, monitors</div> | | </div> | | <button class="btn" id="startBtn">Play</button> | | </div> | | </div> | | | | <!-- --> | | <div class="overlay hide" id="Screen"> | | <div class="card"> | | <div class="eyebrow">d</div> | | <h2>Take a Breath</h2> | | <p>Press <span class="kbd">P</span> or tap to resume.</p> | | <button class="btn" id="resumeBtn">Resume</button> | | </div> | | </div> | | | | <!-- GAME OVER --> | | <div class="overlay hide" id="overScreen"> | | <div class="card"> | | <div class="eyebrow" id="overEyebrow">Squashed</div> | | <h2 id="overTitle">Game Over</h2> | | <div class="scoreline" id="overScore">0 <small>pts</small></div> | | <p id="overMsg">The household claimed another hopper.</p> | | <button class="btn" id="againBtn">Play Again</button> | | </div> | | </div> | | </div> | | | | <div class="footbar"> | | <div class="keys"> | | <span class="kbd">←</span><span class="kbd">↑</span><span class="kbd">↓</span><span class="kbd">→</span> | | <span style="opacity:.7">move</span> | | <span style="margin-left:8px" class="kbd">P</span><span style="opacity:.7"></span> | | <span style="margin-left:8px" class="kbd">M</span><span style="opacity:.7">mute</span> | | </div> | | <button class="iconbtn" id="muteBtn" aria-label="toggle sound"> | | <span id="muteIcon">🔊</span><span>Sound</span> | | </button> | | </div> | | </main> | | | | <script> | | (function(){ | | "use strict"; | | / ============================================================ HOME FRONT | | A cozy, top-down Frogger-style game. Frog crosses a modern home: | | a desk (gadgets), a grass median, and a driveway (toy cars). | | Single file, vanilla canvas. No external assets. | | ========================================================================= / | | | | / ---------- Config ---------- / | | const COLS=13, ROWS=13, TILE=44; | | const W=COLSTILE, H=ROWSTILE; | | const HOP_DUR=0.13; | | const START_COL=6, START_ROW=ROWS-1; | | const GOAL_SLOTS=[[1,2],[4,5],[7,8],[10,11]]; // column ranges in goal row | | const BASE_TIME=40, MIN_TIME=22; | | const HI_KEY="homefront.hiscore.v1"; | | | | / ---------- DOM ---------- / | | const canvas=document.getElementById("game"); | | const ctx=canvas.getContext("2d"); | | const elScore=document.getElementById("score"); | | const elHi=document.getElementById("hi"); | | const elLevel=document.getElementById("level"); | | const elTimerBar=document.getElementById("timerbar"); | | const elTimerFill=document.getElementById("timerfill"); | | const startScreen=document.getElementById("startScreen"); | | const Screen=document.getElementById("Screen"); | | const overScreen=document.getElementById("overScreen"); | | | | / ---------- Canvas / DPR ---------- / | | let DPR=1, bgCanvas=document.createElement("canvas"), bgctx=bgCanvas.getContext("2d"); | | function resize(){ | | DPR=Math.min(window.devicePixelRatio||1,2); | | canvas.width=WDPR; canvas.height=HDPR; | | ctx.setTransform(DPR,0,0,DPR,0,0); | | ctx.imageSmoothingEnabled=true; | | buildBackground(); | | } | | window.addEventListener("resize",resize); | | | | / ---------- Helpers ---------- / | | const clamp=(v,a,b)=>v<a?a:v>b?b:v; | | const lerp=(a,b,t)=>a+(b-a)t; | | const rand=(a,b)=>a+Math.random()(b-a); | | const pick=arr=>arr[(Math.random()arr.length)|0]; | | function rr(x,y,w,h,r){ // rounded rect path | | r=Math.min(r,w/2,h/2); | | ctx.beginPath(); | | ctx.moveTo(x+r,y); | | ctx.arcTo(x+w,y,x+w,y+h,r); | | ctx.arcTo(x+w,y+h,x,y+h,r); | | ctx.arcTo(x,y+h,x,y,r); | | ctx.arcTo(x,y,x+w,y,r); | | ctx.closePath(); | | } | | function bg_rr(x,y,w,h,r){ | | r=Math.min(r,w/2,h/2); | | bgctx.beginPath(); | | bgctx.moveTo(x+r,y); | | bgctx.arcTo(x+w,y,x+w,y+h,r); | | bgctx.arcTo(x+w,y+h,x,y+h,r); | | bgctx.arcTo(x,y+h,x,y,r); | | bgctx.arcTo(x,y,x+w,y,r); | | bgctx.closePath(); | | } | | const colToX=c=>cTILE+TILE/2; | | const rowToY=r=>rTILE+TILE/2; | | | | /* ---------- Audio (Web Audio synth) ---------- / | | let actx=null, master=null, muted=false; | | function ensureAudio(){ | | if(actx) return; | | try{ | | actx=new (window.AudioContext||window.webkitAudioContext)(); | | master=actx.createGain(); master.gain.value=0.5; master.connect(actx.destination); | | }catch(e){actx=null;} | | } | | function blip(freq,dur,type,vol,slideTo){ | | if(!actx||muted) return; | | const t=actx.currentTime; | | const o=actx.createOscillator(), g=actx.createGain(); | | o.type=type||"square"; o.frequency.setValueAtTime(freq,t); | | if(slideTo) o.frequency.exponentialRampToValueAtTime(slideTo,t+dur); | | g.gain.setValueAtTime(0.0001,t); | | g.gain.exponentialRampToValueAtTime(vol||0.25,t+0.012); | | g.gain.exponentialRampToValueAtTime(0.0001,t+dur); | | o.connect(g); g.connect(master); o.start(t); o.stop(t+dur+0.02); | | } | | function noise(dur,vol){ | | if(!actx||muted) return; | | const t=actx.currentTime; | | const n=Math.floor(actx.sampleRatedur); | | const buf=actx.createBuffer(1,n,actx.sampleRate); | | const d=buf.getChannelData(0); | | for(let i=0;i<n;i++) d[i]=(Math.random()2-1)(1-i/n); | | const s=actx.createBufferSource(); s.buffer=buf; | | const g=actx.createGain(); g.gain.value=vol||0.3; | | const f=actx.createBiquadFilter(); f.type="lowpass"; f.frequency.value=900; | | s.connect(f); f.connect(g); g.connect(master); s.start(t); | | } | | const sfx={ | | hop(){blip(520,0.07,"triangle",0.18,640);}, | | goal(){blip(523,0.09,"square",0.2);setTimeout(()=>blip(659,0.09,"square",0.2),70);setTimeout(()=>blip(784,0.14,"square",0.22),140);}, | | die(){noise(0.35,0.35);blip(300,0.4,"sawtooth",0.18,70);}, | | wall(){blip(140,0.18,"square",0.2,90);noise(0.12,0.18);}, | | level(){[523,659,784,1046].forEach((f,i)=>setTimeout(()=>blip(f,0.16,"triangle",0.22),i90));}, | | over(){[392,330,262].forEach((f,i)=>setTimeout(()=>blip(f,0.3,"sawtooth",0.2),i150));} | | }; | | | | /* ---------- Variant tables ---------- / | | const CARS=[ | | {name:"tesla", w:1.70, body:"#e9edf2", roof:"#cfd6dd", glass:"#a9dcff", stripe:"#dfe4ea", dark:"#9aa0a8"}, | | {name:"ferrari", w:1.60, body:"#e8392b", roof:"#b22619", glass:"#bfe9ff", stripe:"#ffe9c2", dark:"#7a1a12"}, | | {name:"lambo", w:1.66, body:"#f4c542", roof:"#d6a72f", glass:"#cfeaff", stripe:"#1b1b1b", dark:"#9c7a1f"}, | | {name:"cybertruck",w:1.95, body:"#b7bcc4", roof:"#9aa0a8", glass:"#8fc0db", stripe:"#8a8f97", dark:"#6c7178"}, | | {name:"porsche", w:1.60, body:"#27344f", roof:"#19212f", glass:"#bfe9ff", stripe:"#3c4d6b", dark:"#10151f"} | | ]; | | const GADGETS=[ | | {name:"iphone", w:0.82, body:"#15171c", screen:"#1d2740", tile:"#3a4a6b"}, | | {name:"tablet", w:1.04, body:"#1b1e25", screen:"#243049", tile:"#3c4d72"}, | | {name:"laptop", w:1.36, body:"#cbd0d8", base:"#aab0ba", screen:"#27314a"}, | | {name:"monitor",w:1.26, body:"#14161b", screen:"#243552", stand:"#3b4049"} | | ]; | | | | / ---------- Lane plan ---------- / | | const LANE_PLAN=[ | | // row, dir, speed, count, possible variants, kind | | {row:1, dir:-1, speed:52, count:3, kinds:["iphone","tablet"], kind:"gadget"}, | | {row:2, dir: 1, speed:80, count:2, kinds:["laptop"], kind:"gadget"}, | | {row:3, dir:-1, speed:44, count:3, kinds:["monitor","tablet"], kind:"gadget"}, | | {row:4, dir: 1, speed:94, count:3, kinds:["iphone"], kind:"gadget"}, | | {row:5, dir:-1, speed:62, count:2, kinds:["tablet","laptop"], kind:"gadget"}, | | {row:7, dir: 1, speed:70, count:3, kinds:["tesla","porsche"], kind:"car"}, | | {row:8, dir:-1, speed:98, count:2, kinds:["ferrari"], kind:"car"}, | | {row:9, dir: 1, speed:58, count:2, kinds:["cybertruck"], kind:"car"}, | | {row:10,dir:-1, speed:114, count:3, kinds:["lambo","ferrari"], kind:"car"}, | | {row:11,dir: 1, speed:50, count:3, kinds:["tesla","porsche"], kind:"car"} | | ]; | | | | function buildLanes(){ | | return LANE_PLAN.map(p=>{ | | const tbl = p.kind==="car"?CARS:GADGETS; | | const items=[]; | | let maxW=0; | | for(let i=0;i<p.count;i++){ | | const v=tbl.find(x=>x.name===pick(p.kinds))||tbl[0]; | | const wpx=v.wTILE; | | items.push({variant:v, w:wpx}); | | if(wpx>maxW)maxW=wpx; | | } | | const pad=rand(30,140); | | const cycleLen=W+maxW+pad; | | const spacing=cycleLen/p.count; | | items.forEach((it,i)=>{ it.pos0 = ispacing - it.w/2; }); | | return {row:p.row, dir:p.dir, speed:p.speed, kind:p.kind, items, cycleLen, phase:rand(0,cycleLen)}; | | }); | | } | | | | / ---------- Game state ---------- / | | const game={ | | state:"boot", // boot | play | dead | levelclear | gameover | | d:false, | | score:0, hi:0, level:1, lives:3, | | timer:BASE_TIME, maxTime:BASE_TIME, speedMul:1, | | goals:[false,false,false,false], | | deadTimer:0, lcTimer:0, | | toast:null, toastLife:0, | | flash:0 | | }; | | const frog={ | | col:START_COL, row:START_ROW, fx:colToX(START_COL), fy:rowToY(START_ROW), | | fromX:0, fromY:0, toX:0, toY:0, hopT:0, hopping:false, facing:0, alive:true, | | minRow:START_ROW, hopPulse:0 | | }; | | let lanes=[]; let particles=[]; | | | | / ---------- Flow ---------- / | | function startGame(){ | | ensureAudio(); | | game.score=0; game.level=1; game.lives=3; game.speedMul=1; | | game.maxTime=BASE_TIME; game.goals=[false,false,false,false]; | | game.d=false; | | lanes=buildLanes(); | | respawn(); | | hideOverlay(startScreen); hideOverlay(Screen); hideOverlay(overScreen); | | game.state="play"; | | updateHUD(); | | } | | function respawn(){ | | frog.col=START_COL; frog.row=START_ROW; | | frog.fx=colToX(START_COL); frog.fy=rowToY(START_ROW); | | frog.hopping=false; frog.alive=true; frog.facing=0; frog.minRow=START_ROW; | | game.timer=game.maxTime; | | } | | function loseLife(){ | | game.lives--; | | updateHUD(); | | if(game.lives<=0){ gameOver(); } | | else { respawn(); game.state="play"; } | | } | | function reachGoal(slot){ | | game.goals[slot]=true; | | const tb=Math.floor(game.timer)10; | | addScore(200+tb); | | spawnParticles(colToX((GOAL_SLOTS[slot][0]+GOAL_SLOTS[slot][1])/2), rowToY(0)+TILE0.4, 22, "goal"); | | sfx.goal(); | | if(game.goals.every(Boolean)){ | | addScore(1000); | | game.state="levelclear"; game.lcTimer=1.9; | | showToast("LEVEL "+game.level+" CLEAR","+1000 bonus"); | | sfx.level(); | | }else{ | | respawn(); | | } | | updateHUD(); | | } | | function nextLevel(){ | | game.level++; | | game.speedMul=1+(game.level-1)0.15; | | game.maxTime=Math.max(MIN_TIME,BASE_TIME-(game.level-1)2); | | game.goals=[false,false,false,false]; | | lanes=buildLanes(); | | respawn(); | | game.state="play"; | | showToast("LEVEL "+game.level,"faster traffic"); | | updateHUD(); | | } | | function killFrog(reason){ | | if(!frog.alive) return; | | frog.alive=false; | | game.state="dead"; game.deadTimer=0.95; | | game.flash=0.6; | | spawnParticles(frog.fx,frog.fy,18,reason==="time"?"drown":"splat"); | | if(reason==="wall") sfx.wall(); else sfx.die(); | | } | | function gameOver(){ | | game.state="gameover"; | | if(game.score>game.hi){ game.hi=game.score; try{localStorage.setItem(HI_KEY,String(game.hi));}catch(e){} } | | document.getElementById("overScore").innerHTML = game.score+" <small>pts</small>"; | | document.getElementById("overTitle").textContent = game.score===0?"Try Again":"Game Over"; | | document.getElementById("overMsg").textContent = | | game.score>=1500?"A legend of the living room.":game.score>=600?"Respectable hopping.":"The household claimed another hopper."; | | document.getElementById("overEyebrow").textContent = game.lives<=0?"Out of lives":"Time's up"; | | showOverlay(overScreen); | | updateHUD(); | | sfx.over(); | | } | | function addScore(n){ game.score+=n; updateHUD(); } | | | | / ---------- HUD ---------- / | | function updateHUD(){ | | elScore.textContent=game.score; | | elHi.textContent=Math.max(game.hi,game.score); | | elLevel.textContent=game.level; | | elTimerFill.style.width=clamp(game.timer/game.maxTime,0,1)100+"%"; | | elTimerBar.classList.toggle("low", game.timer/game.maxTime<0.25); | | } | | function showToast(text,sub){ game.toast={text,sub}; game.toastLife=2.0; } | | function showOverlay(el){ el.classList.remove("hide"); } | | function hideOverlay(el){ el.classList.add("hide"); } | | | | / ---------- Input ---------- / | | const DIR={ up:{dc:0,dr:-1,a:0}, down:{dc:0,dr:1,a:Math.PI}, left:{dc:-1,dr:0,a:-Math.PI/2}, right:{dc:1,dr:0,a:Math.PI/2} }; | | function tryMove(name){ | | if(game.state==="boot"){ startGame(); return; } | | if(game.state==="gameover"){ return; } | | if(game.d){ toggle(); return; } | | if(game.state!=="play"||frog.hopping||!frog.alive) return; | | const d=DIR[name]; if(!d) return; | | const nc=clamp(frog.col+d.dc,0,COLS-1); | | const nr=clamp(frog.row+d.dr,0,ROWS-1); | | if(nc===frog.col&&nr===frog.row) return; // edge, no-op | | frog.facing=d.a; | | frog.fromX=frog.fx; frog.fromY=frog.fy; | | frog.col=nc; frog.row=nr; | | frog.toX=colToX(nc); frog.toY=rowToY(nr); | | frog.hopping=true; frog.hopT=0; | | if(nr<frog.minRow){ game.score+=(frog.minRow-nr)10; frog.minRow=nr; updateHUD(); } | | sfx.hop(); | | } | | function toggle(){ | | if(game.state!=="play") return; | | game.d=!game.d; | | if(game.d) showOverlay(Screen); else hideOverlay(Screen); | | } | | window.addEventListener("keydown",e=>{ | | const k=e.key; | | if(["ArrowUp","ArrowDown","ArrowLeft","ArrowRight"," "].includes(k)) e.preventDefault(); | | if(k==="ArrowUp"||k==="w"||k==="W") tryMove("up"); | | else if(k==="ArrowDown"||k==="s"||k==="S") tryMove("down"); | | else if(k==="ArrowLeft"||k==="a"||k==="A") tryMove("left"); | | else if(k==="ArrowRight"||k==="d"||k==="D") tryMove("right"); | | else if(k==="p"||k==="P"||k==="Escape"){ toggle(); } | | else if(k==="m"||k==="M"){ toggleMute(); } | | else if(k==="Enter"){ if(game.state==="boot") startGame(); else if(game.state==="gameover") startGame(); } | | },{passive:false}); | | | | / on-screen dpad + swipe / | | document.querySelectorAll("#dpad button").forEach(b=>{ | | const fire=ev=>{ev.preventDefault(); ensureAudio(); tryMove(b.dataset.dir);}; | | b.addEventListener("touchstart",fire,{passive:false}); | | b.addEventListener("mousedown",fire); | | }); | | let tsx=0,tsy=0,tracking=false; | | canvas.addEventListener("touchstart",e=>{ ensureAudio(); if(e.touches.length){tsx=e.touches[0].clientX;tsy=e.touches[0].clientY;tracking=true;} },{passive:true}); | | canvas.addEventListener("touchend",e=>{ | | if(!tracking) return; tracking=false; | | const t=e.changedTouches[0]; const dx=t.clientX-tsx, dy=t.clientY-tsy; | | if(Math.abs(dx)<22&&Math.abs(dy)<22){ // tap | | if(game.state==="boot") startGame(); | | return; | | } | | if(Math.abs(dx)>Math.abs(dy)) tryMove(dx>0?"right":"left"); | | else tryMove(dy>0?"down":"up"); | | },{passive:true}); | | | | / buttons / | | document.getElementById("startBtn").addEventListener("click",()=>{ensureAudio();startGame();}); | | document.getElementById("againBtn").addEventListener("click",()=>{ensureAudio();startGame();}); | | document.getElementById("resumeBtn").addEventListener("click",toggle); | | const muteBtn=document.getElementById("muteBtn"), muteIcon=document.getElementById("muteIcon"); | | function toggleMute(){ | | muted=!muted; | | muteIcon.textContent = muted?"🔇":"🔊"; | | muteBtn.style.opacity = muted?".6":"1"; | | } | | muteBtn.addEventListener("click",toggleMute); | | | | / when tab hidden / | | document.addEventListener("visibilitychange",()=>{ | | if(document.hidden && game.state==="play" && !game.d){ game.d=true; showOverlay(Screen); } | | }); | | | | / ---------- Particles ---------- / | | function spawnParticles(x,y,n,type){ | | const palettes={ | | splat:["#7ed957","#4ea632","#a7e76a","#e8ffce"], | | drown:["#7fc6ff","#3f7fd6","#bfe6ff","#e8f5ff"], | | goal:["#ff5a3c","#f3b13d","#7ed957","#ffffff","#ff8a5c"] | | }; | | const pal=palettes[type]||palettes.splat; | | for(let i=0;i<n;i++){ | | const a=Math.random()Math.PI2, sp=rand(40,180); | | particles.push({ | | x,y, vx:Math.cos(a)sp, vy:Math.sin(a)sp - (type==="goal"?60:0), | | life:rand(0.5,1.0), max:1.0, size:rand(2.5,5.5), | | color:pick(pal), spin:rand(-6,6), rot:rand(0,6.28), | | shape: type==="goal"? (Math.random()<0.5?"star":"rect") : "leaf" | | }); | | } | | } | | | | / ---------- Update ---------- / | | function update(dt){ | | // toast & flash & particles always tick (even overlays) for liveliness | | if(game.toastLife>0) game.toastLife-=dt; | | if(game.flash>0) game.flash=Math.max(0,game.flash-dt1.6); | | updateParticles(dt); | | | | if(game.d) return; | | | | if(game.state==="play"){ | | updateFrog(dt); | | updateLanes(dt); | | checkCollisions(); | | game.timer-=dt; | | if(game.timer<=0){ game.timer=0; killFrog("time"); } | | updateHUD(); | | } else if(game.state==="dead"){ | | updateLanes(dt); | | game.deadTimer-=dt; | | if(game.deadTimer<=0) loseLife(); | | } else if(game.state==="levelclear"){ | | updateLanes(dt); | | game.lcTimer-=dt; | | if(game.lcTimer<=0) nextLevel(); | | } | | } | | function updateFrog(dt){ | | frog.hopPulse+=dt; | | if(!frog.hopping) return; | | frog.hopT+=dt/HOP_DUR; | | if(frog.hopT>=1){ | | frog.hopT=1; frog.hopping=false; | | frog.fx=frog.toX; frog.fy=frog.toY; | | if(frog.row===0) handleGoalRow(); | | return; | | } | | const e=1-Math.pow(1-frog.hopT,2.2); // ease-out | | frog.fx=lerp(frog.fromX,frog.toX,e); | | frog.fy=lerp(frog.fromY,frog.toY,e); | | } | | function handleGoalRow(){ | | const col=frog.col; | | const slot=GOAL_SLOTS.findIndex(r=>col>=r[0]&&col<=r[1]); | | if(slot===-1){ killFrog("wall"); return; } | | if(game.goals[slot]){ killFrog("wall"); return; } | | reachGoal(slot); | | } | | function updateLanes(dt){ | | for(const lane of lanes){ | | lane.phase=(lane.phase + lane.dirlane.speedgame.speedMuldt)%lane.cycleLen; | | for(const it of lane.items){ | | let x=(it.pos0+lane.phase)%lane.cycleLen; | | if(x<0) x+=lane.cycleLen; | | it.curX=x; | | } | | } | | } | | function checkCollisions(){ | | if(!frog.alive) return; | | const hw=TILE0.28, hh=TILE0.30; | | for(const lane of lanes){ | | const top=lane.rowTILE, bot=top+TILE; | | if(bot<=frog.fy-hh||top>=frog.fy+hh) continue; | | for(const it of lane.items){ | | for(const off of [0,-lane.cycleLen,lane.cycleLen]){ | | const sx=it.curX+off; | | if(sx>frog.fx+hw||sx+it.w<frog.fx-hw) continue; | | killFrog("hit"); return; | | } | | } | | } | | } | | function updateParticles(dt){ | | for(let i=particles.length-1;i>=0;i--){ | | const p=particles[i]; | | p.life-=dt; | | if(p.life<=0){ particles.splice(i,1); continue; } | | p.x+=p.vxdt; p.y+=p.vydt; | | p.vx*=0.92; p.vy=p.vy0.92+ (p.shape==="leaf"?120:40)dt; | | p.rot+=p.spindt; | | } | | } | | | | / ============================================================ RENDER | | ========================================================================= / | | function render(){ | | ctx.clearRect(0,0,W,H); | | ctx.drawImage(bgCanvas,0,0,W,H); | | drawGoalRow(); | | drawLanes(); | | if(frog.alive && (game.state==="play"||game.state==="boot")) drawFrog(ctx,frog.fx,frog.fy,1,frog.facing,frog.hopping?frog.hopT:0,frog.hopPulse); | | drawParticles(); | | if(game.flash>0){ ctx.fillStyle="rgba(255,90,60,"+game.flash0.5+")"; ctx.fillRect(0,0,W,H); } | | if(game.toastLife>0&&game.toast) drawToast(); | | } | | | | /* ---------- Background prerender ---------- / | | function buildBackground(){ | | bgCanvas.width=WDPR; bgCanvas.height=HDPR; | | bgctx.setTransform(DPR,0,0,DPR,0,0); | | const g=bgctx; | | g.clearRect(0,0,W,H); | | for(let r=0;r<ROWS;r++){ | | const y=rTILE; | | if(r===0) paintGoalWall(g,y); | | else if(r>=1&&r<=5) paintDesk(g,y,r); | | else if(r===6) paintGrass(g,y); | | else if(r>=7&&r<=11) paintAsphalt(g,y,r); | | else if(r===12) paintStartFloor(g,y); | | // separators | | g.strokeStyle="rgba(0,0,0,0.18)"; g.lineWidth=1; | | g.beginPath(); g.moveTo(0,y+TILE-0.5); g.lineTo(W,y+TILE-0.5); g.stroke(); | | } | | // outer frame | | g.strokeStyle="rgba(0,0,0,0.35)"; g.lineWidth=2; g.strokeRect(1,1,W-2,H-2); | | } | | function paintGoalWall(g,y){ | | g.fillStyle="#3a2f26"; g.fillRect(0,y,W,TILE); | | // wood grain | | g.strokeStyle="rgba(0,0,0,0.18)"; g.lineWidth=1; | | for(let i=0;i<6;i++){ g.beginPath(); g.moveTo(0,y+6+i7); g.bezierCurveTo(W0.3,y+8+i7,W0.6,y+4+i7,W,y+7+i7); g.stroke(); } | | // walls between slots (cols 0,3,6,9,12) | | g.fillStyle="#2a211b"; | | for(const c of [0,3,6,9,12]){ | | g.fillRect(cTILE,y,TILE,TILE); | | g.fillStyle="rgba(255,255,255,0.04)"; g.fillRect(cTILE,y,TILE,3); | | g.fillStyle="#2a211b"; | | } | | } | | function paintDesk(g,y,r){ | | // birch desk surface | | g.fillStyle="#e7d3a8"; g.fillRect(0,y,W,TILE); | | g.fillStyle="rgba(255,255,255,0.18)"; g.fillRect(0,y,W,TILE0.5); | | g.strokeStyle="rgba(150,120,70,0.22)"; g.lineWidth=1; | | for(let i=0;i<3;i++){ const yy=y+8+i12; g.beginPath(); g.moveTo(0,yy); g.bezierCurveTo(W0.3,yy+2,W0.6,yy-2,W,yy+1); g.stroke(); } | | // a coffee stain on one lane for character | | if(r===3){ | | g.save(); g.globalAlpha=0.10; g.fillStyle="#5a3a1a"; | | g.beginPath(); g.arc(W0.78,y+TILE0.5,TILE0.42,0,6.29); g.fill(); | | g.beginPath(); g.arc(W0.78,y+TILE0.5,TILE0.30,0,6.29); g.fillStyle="#e7d3a8"; g.fill(); | | g.restore(); | | } | | // direction chevrons | | const dir=LANE_PLAN.find(p=>p.row===r)?.dir||1; | | drawChevrons(g,y,dir,"rgba(120,90,40,0.25)"); | | } | | function paintGrass(g,y){ | | g.fillStyle="#7fb15a"; g.fillRect(0,y,W,TILE); | | g.fillStyle="#6ba247"; | | for(let i=0;i<26;i++){ | | const x=(i/26)W + ((i53)%18); | | g.fillRect(x,y+ ( (i7)%(TILE-8) )+4, 2, 6+ (i%3)2); | | } | | // little flowers | | const fc=["#f3b13d","#ff5a3c","#ffffff"]; | | for(let i=0;i<8;i++){ | | const x=rand(8,W-8), yy=y+rand(8,TILE-8); | | g.fillStyle=fc[i%3]; | | for(let a=0;a<5;a++){ const ang=a/56.28; g.beginPath(); g.arc(x+Math.cos(ang)2.4,yy+Math.sin(ang)2.4,1.7,0,6.29); g.fill(); } | | g.fillStyle="#fff3c2"; g.beginPath(); g.arc(x,yy,1.3,0,6.29); g.fill(); | | } | | } | | function paintAsphalt(g,y,r){ | | g.fillStyle="#26282d"; g.fillRect(0,y,W,TILE); | | // speckle | | g.fillStyle="rgba(255,255,255,0.03)"; | | for(let i=0;i<40;i++){ g.fillRect(rand(0,W),y+rand(0,TILE),1,1); } | | g.fillStyle="rgba(0,0,0,0.25)"; | | for(let i=0;i<14;i++){ g.fillRect(rand(0,W),y+rand(0,TILE),rand(2,5),2); } | | // dashed lane line at top edge | | if(r>7){ | | g.strokeStyle="rgba(244,237,220,0.55)"; g.lineWidth=2; g.setLineDash([12,12]); | | g.beginPath(); g.moveTo(0,y); g.lineTo(W,y); g.stroke(); g.setLineDash([]); | | } | | const dir=LANE_PLAN.find(p=>p.row===r)?.dir||1; | | drawChevrons(g,y,dir,"rgba(244,237,220,0.10)"); | | } | | function paintStartFloor(g,y){ | | // warm wood planks | | g.fillStyle="#c98a5a"; g.fillRect(0,y,W,TILE); | | g.fillStyle="#b87642"; | | for(let c=0;c<COLS;c++){ | | if(c%2===0){ g.fillRect(cTILE,y,1,TILE); } | | } | | g.strokeStyle="rgba(0,0,0,0.18)"; g.lineWidth=1; | | for(let c=0;c<=COLS;c++){ g.beginPath(); g.moveTo(cTILE,y); g.lineTo(cTILE,y+TILE); g.stroke(); } | | // a little rug under start | | g.save(); | | g.translate(W/2,y+TILE0.5); | | g.fillStyle="#b3422b"; bg_rr(-TILE1.6,-TILE0.34,TILE3.2,TILE0.68,10); g.fill(); | | g.strokeStyle="rgba(255,255,255,0.18)"; g.lineWidth=2; g.setLineDash([5,5]); | | bg_rr(-TILE1.45,-TILE0.28,TILE2.9,TILE0.56,8); g.stroke(); g.setLineDash([]); | | g.restore(); | | } | | function drawChevrons(g,y,dir,color){ | | g.save(); g.globalAlpha=1; g.fillStyle=color; | | const n=4; | | for(let i=0;i<n;i++){ | | const x=(i+0.5)(W/n); | | const cx=x, cy=y+TILE/2; | | g.beginPath(); | | if(dir>0){ g.moveTo(cx-6,cy-5); g.lineTo(cx+2,cy); g.lineTo(cx-6,cy+5); } | | else { g.moveTo(cx+6,cy-5); g.lineTo(cx-2,cy); g.lineTo(cx+6,cy+5); } | | g.closePath(); g.fill(); | | } | | g.restore(); | | } | | | | /* ---------- Goal row (dynamic) ---------- / | | function drawGoalRow(){ | | const y=0; | | for(let s=0;s<GOAL_SLOTS.length;s++){ | | const [c0,c1]=GOAL_SLOTS[s]; | | const x=c0TILE, w=(c1-c0+1)TILE; | | const cx=x+w/2, cy=y+TILE/2; | | // alcove | | ctx.save(); | | ctx.fillStyle="#efe2c4"; rr(x+3,y+3,w-6,TILE-6,8); ctx.fill(); | | // nest cushion | | ctx.fillStyle = game.goals[s] ? "#9be36b" : "#d9c79a"; | | rr(x+8,y+8,w-16,TILE-16,12); ctx.fill(); | | if(game.goals[s]){ | | // sleeping frog | | drawFrog(ctx,cx,cy+2,0.78,0,0,performance.now()/600,true); | | }else{ | | // pulsing invite glow | | const p=0.5+0.5Math.sin(performance.now()/420 + s); | | ctx.globalAlpha=0.25+0.25p; | | ctx.fillStyle="#fff7a8"; rr(x+12,y+12,w-24,TILE-24,10); ctx.fill(); | | ctx.globalAlpha=1; | | } | | ctx.restore(); | | } | | } | | | | / ---------- Lanes ---------- / | | function drawLanes(){ | | for(const lane of lanes){ | | const cy=rowToY(lane.row); | | for(const it of lane.items){ | | for(const off of [0,-lane.cycleLen,lane.cycleLen]){ | | const sx=it.curX+off; | | if(sx>W+4||sx+it.w<-4) continue; | | if(lane.kind==="car") drawCar(ctx,sx,cy-laneH(it)/2,it.w,laneH(it),it.variant,lane.dir); | | else drawGadget(ctx,sx,cy-laneH(it)/2,it.w,laneH(it),it.variant,lane.dir); | | } | | } | | } | | } | | function laneH(it){ return TILE0.80; } | | | | /* ---------- Sprite: FROG ---------- / | | function drawFrog(c,cx,cy,scale,angle,hopT,pulse,asleep){ | | const lift = hopT>0 ? Math.sin(hopTMath.PI) : 0; // 0..1 airborne amount | | const stretchY = 1 + 0.22lift, stretchX = 1 - 0.10lift; | | const pop = 1 + 0.10lift; | | const bob = asleep ? Math.sin(pulse)1.2 : 0; | | c.save(); | | c.translate(cx,cy+bob); | | // shadow | | c.save(); | | c.scale(1,0.5); | | c.fillStyle="rgba(0,0,0,"+(0.32(1-0.45lift))+")"; | | c.beginPath(); c.arc(0, TILE0.5, TILE0.34*(1-0.18lift)scale, 0, 6.29); c.fill(); | | c.restore(); | | | | c.rotate(angle); | | c.scale(stretchXpopscale, stretchYpopscale); | | const u=TILE; // unit | | // hind legs | | c.fillStyle="#5fb33a"; | | ellipse(c,-0.30u,0.22u,0.16u,0.12u,0.4); | | ellipse(c, 0.30u,0.22u,0.16u,0.12u,-0.4); | | // body | | c.fillStyle="#7ed957"; | | bodyPath(c,0,0,0.34u,0.30u); c.fill(); | | // belly highlight | | c.fillStyle="rgba(255,255,255,0.18)"; | | ellipse(c,0,0.04u,0.20u,0.14u,0); | | // back spots | | c.fillStyle="#57b635"; | | ellipse(c,-0.12u,-0.02u,0.05u,0.04u,0); | | ellipse(c, 0.12u,-0.04u,0.05u,0.04u,0); | | ellipse(c,0,0.10u,0.04u,0.03u,0); | | // front feet | | c.fillStyle="#5fb33a"; | | ellipse(c,-0.20u,-0.20u,0.09u,0.06u,0.5); | | ellipse(c, 0.20u,-0.20u,0.09u,0.06u,-0.5); | | // eyes (head is toward -y => up) | | const ey=-0.18u, ex=0.16u; | | // eye humps | | c.fillStyle="#7ed957"; | | ellipse(c,-ex,ey,0.11u,0.10u,0); | | ellipse(c, ex,ey,0.11u,0.10u,0); | | // whites | | c.fillStyle="#ffffff"; | | ellipse(c,-ex,ey-0.01u,0.075u,0.075u,0); | | ellipse(c, ex,ey-0.01u,0.075u,0.075u,0); | | // pupils | | c.fillStyle="#15240c"; | | const py = asleep ? ey : ey-0.015u; | | ellipse(c,-ex,py,0.035u,asleep?0.012u:0.04u,0); | | ellipse(c, ex,py,0.035u,asleep?0.012u:0.04u,0); | | // eye shine | | c.fillStyle="rgba(255,255,255,0.9)"; | | ellipse(c,-ex+0.02u,py-0.02u,0.014u,0.014u,0); | | ellipse(c, ex+0.02u,py-0.02u,0.014u,0.014u,0); | | c.restore(); | | } | | function ellipse(c,x,y,rx,ry,rot){ c.save(); c.translate(x,y); c.rotate(rot||0); c.beginPath(); c.ellipse(0,0,rx,ry,0,0,6.2832); c.fill(); c.restore(); } | | function bodyPath(c,x,y,rx,ry){ | | // rounded blob, slightly pointed at top (head) | | c.beginPath(); | | c.moveTo(x, y-ry); | | c.bezierCurveTo(x+rx,y-ry, x+rx,y+ry0.9, x+rx0.5,y+ry); | | c.bezierCurveTo(x+rx0.2,y+ry1.05, x-rx0.2,y+ry1.05, x-rx0.5,y+ry); | | c.bezierCurveTo(x-rx,y+ry0.9, x-rx,y-ry, x,y-ry); | | c.closePath(); | | } | | | | / ---------- Sprite: CAR ---------- / | | function drawCar(c,x,y,w,h,v,dir){ | | c.save(); | | // shadow | | c.fillStyle="rgba(0,0,0,0.28)"; | | rr(x+2,y+h0.16,w-2,h0.86,h0.32); c.fill(); | | // body | | const flip = dir<0; | | c.save(); | | if(flip){ c.translate(x+w/2,0); c.scale(-1,1); c.translate(-(x+w/2),0); } | | // chassis | | c.fillStyle=v.body; rr(x,y,w,h,h0.34); c.fill(); | | // lower skirt | | c.fillStyle=v.dark; rr(x+w0.04,y+h0.62,w0.92,h0.3,h0.2); c.fill(); | | // cabin/roof | | c.fillStyle=v.roof; rr(x+w0.20,y+h0.16,w0.5,h0.5,h0.22); c.fill(); | | // windshield (front, toward +x = right since not flipped here in local) | | c.fillStyle=v.glass; | | c.beginPath(); | | c.moveTo(x+w0.66,y+h0.20); c.lineTo(x+w0.78,y+h0.20); c.lineTo(x+w0.74,y+h0.80); c.lineTo(x+w0.64,y+h0.80); c.closePath(); c.fill(); | | // rear window | | c.beginPath(); | | c.moveTo(x+w0.22,y+h0.20); c.lineTo(x+w0.34,y+h0.20); c.lineTo(x+w0.34,y+h0.80); c.lineTo(x+w0.24,y+h0.80); c.closePath(); c.fill(); | | // stripe | | c.fillStyle=v.stripe; c.fillRect(x+w0.06,y+h0.47,w0.88,h0.06); | | // headlights (front = right side locally) | | c.fillStyle="#fff6c8"; | | c.fillRect(x+w0.93,y+h0.18,w0.05,h0.12); | | c.fillRect(x+w0.93,y+h0.70,w0.05,h0.12); | | // taillights (left) | | c.fillStyle="#ff4d4d"; | | c.fillRect(x+w0.02,y+h0.18,w0.04,h0.12); | | c.fillRect(x+w0.02,y+h0.70,w0.04,h0.12); | | // wheels | | c.fillStyle="#15171c"; | | c.fillRect(x+w0.10,y+h0.04,w0.18,h0.10); | | c.fillRect(x+w0.10,y+h0.86,w0.18,h0.10); | | c.fillRect(x+w0.72,y+h0.04,w0.18,h0.10); | | c.fillRect(x+w0.72,y+h0.86,w0.18,h0.10); | | // cybertruck angularity | | if(v.name==="cybertruck"){ | | c.strokeStyle="rgba(0,0,0,0.25)"; c.lineWidth=1.5; | | c.beginPath(); c.moveTo(x+w0.5,y+h0.05); c.lineTo(x+w0.96,y+h0.5); c.lineTo(x+w0.5,y+h0.95); c.lineTo(x+w0.04,y+h0.5); c.closePath(); c.stroke(); | | } | | // tesla screen hint | | if(v.name==="tesla"){ c.fillStyle="#0c1218"; rr(x+w0.42,y+h0.30,w0.10,h0.20,2); c.fill(); } | | c.restore(); | | c.restore(); | | } | | | | / ---------- Sprite: GADGET ---------- / | | function drawGadget(c,x,y,w,h,v,dir){ | | c.save(); | | // shadow | | c.fillStyle="rgba(0,0,0,0.22)"; | | rr(x+2,y+h0.1,w,h0.9,h0.18); c.fill(); | | if(v.name==="laptop"){ | | // base | | c.fillStyle=v.base; rr(x,y+h0.42,w,h0.58,h0.14); c.fill(); | | // trackpad | | c.fillStyle="rgba(0,0,0,0.10)"; rr(x+w0.30,y+h0.66,w0.40,h0.26,h0.06); c.fill(); | | // keyboard dots | | c.fillStyle="rgba(0,0,0,0.22)"; | | const cols=8,rowsK=3,kw=w/cols0.6; | | for(let r=0;r<rowsK;r++) for(let cc=0;cc<cols;cc++){ | | c.fillRect(x+w0.10+cc*(w0.8/cols), y+h0.46+rh0.06, kw, h0.035); | | } | | // screen (lid) — hinged at top edge | | c.fillStyle=v.body; rr(x+w0.06,y,w0.88,h0.44,h0.1); c.fill(); | | c.fillStyle=v.screen; rr(x+w0.10,y+h0.05,w0.80,h0.34,h0.06); c.fill(); | | // desktop bar | | c.fillStyle="#5a7bbf"; c.fillRect(x+w0.10,y+h0.05,w0.80,h0.06); | | c.fillStyle="rgba(255,255,255,0.25)"; | | for(let i=0;i<4;i++) c.fillRect(x+w0.14+iw0.18,y+h0.16,w0.10,h0.10); | | } else if(v.name==="monitor"){ | | // screen | | c.fillStyle=v.body; rr(x+w0.04,y,w0.92,h0.74,h0.1); c.fill(); | | c.fillStyle=v.screen; rr(x+w0.10,y+h0.07,w0.80,h0.6,h0.04); c.fill(); | | // content | | c.fillStyle="#5a7bbf"; c.fillRect(x+w0.10,y+h0.07,w0.80,h0.07); | | c.fillStyle="rgba(255,255,255,0.22)"; | | c.fillRect(x+w0.14,y+h0.20,w0.30,h0.18); | | c.fillRect(x+w0.48,y+h0.20,w0.38,h0.08); | | c.fillRect(x+w0.48,y+h0.32,w0.38,h0.06); | | // stand neck + foot | | c.fillStyle=v.stand; | | c.fillRect(x+w0.46,y+h0.70,w0.08,h0.14); | | rr(x+w0.32,y+h0.82,w0.36,h0.12,h0.05); c.fill(); | | } else { | | // phone / tablet | | const rad = v.name==="tablet" ? h0.1 : h0.18; | | c.fillStyle=v.body; rr(x,y,w,h,rad); c.fill(); | | c.fillStyle=v.screen; rr(x+w0.12,y+h0.07,w0.76,h0.86,rad0.6); c.fill(); | | // notch (phone) | | if(v.name==="iphone"){ | | c.fillStyle=v.body; rr(x+w0.38,y+h0.03,w0.24,h0.06,h0.03); c.fill(); | | } | | // app grid | | c.fillStyle=v.tile; | | const cols = v.name==="tablet"?5:4; | | const rowsG=6, pad=w0.04, aw=(w0.76-pad*(cols+1))/cols, ah=(h0.86-pad(rowsG+1))/rowsG; | | for(let r=0;r<rowsG;r++) for(let cc=0;cc<cols;cc++){ | | const ax=x+w0.12+pad+cc(aw+pad), ay=y+h0.07+pad+r(ah+pad); | | c.fillStyle = ["#ff5a3c","#f3b13d","#7ed957","#7fc6ff","#c98a5a"][(rcols+cc)%5]; | | rr(ax,ay,aw,ah,Math.min(aw,ah)0.25); c.fill(); | | } | | // reflection | | c.fillStyle="rgba(255,255,255,0.12)"; | | c.beginPath(); c.moveTo(x+w0.12,y+h0.07); c.lineTo(x+w0.12+w0.3,y+h0.07); c.lineTo(x+w0.12,y+h0.07+h0.4); c.closePath(); c.fill(); | | } | | c.restore(); | | } | | | | /* ---------- Particles ---------- / | | function drawParticles(){ | | for(const p of particles){ | | const a=clamp(p.life/p.max,0,1); | | ctx.globalAlpha=a; | | ctx.fillStyle=p.color; | | if(p.shape==="star"){ | | ctx.save(); ctx.translate(p.x,p.y); ctx.rotate(p.rot); | | ctx.beginPath(); | | for(let i=0;i<5;i++){ | | const ang=i/56.28 - 1.57; | | ctx.lineTo(Math.cos(ang)p.size, Math.sin(ang)p.size); | | const ang2=ang+0.63; | | ctx.lineTo(Math.cos(ang2)p.size0.45, Math.sin(ang2)p.size0.45); | | } | | ctx.closePath(); ctx.fill(); ctx.restore(); | | } else { | | ctx.save(); ctx.translate(p.x,p.y); ctx.rotate(p.rot); | | ctx.fillStyle=p.color; ellipse(ctx,0,0,p.size0.6,p.size1.4,0); | | ctx.restore(); | | } | | } | | ctx.globalAlpha=1; | | } | | | | /* ---------- Toast ---------- / | | function drawToast(){ | | const t=game.toast; if(!t) return; | | const a = clamp(game.toastLife,0,1) * (game.toastLife>1.7?(2-game.toastLife)/0.3:1); | | ctx.save(); | | ctx.globalAlpha=clamp(a,0,1); | | ctx.translate(W/2,H0.42); | | const w=TILE7.2, h=TILE2.0; | | ctx.fillStyle="rgba(14,15,18,0.9)"; | | rr(-w/2,-h/2,w,h,16); ctx.fill(); | | ctx.strokeStyle="#f3b13d"; ctx.lineWidth=2; rr(-w/2,-h/2,w,h,16); ctx.stroke(); | | ctx.fillStyle="#fff"; ctx.textAlign="center"; ctx.textBaseline="middle"; | | ctx.font="800 "+Math.floor(TILE0.5)+"px 'Bricolage Grotesque',sans-serif"; | | ctx.fillText(t.text,0,-TILE0.18); | | ctx.font="500 "+Math.floor(TILE0.3)+"px 'Sora',sans-serif"; | | ctx.fillStyle="#f3b13d"; ctx.fillText(t.sub,0,TILE0.32); | | ctx.restore(); | | } | | | | /* ---------- Loop ---------- / | | let last=0; | | function frame(ts){ | | if(!last) last=ts; | | let dt=(ts-last)/1000; last=ts; | | if(dt>0.05) dt=0.05; | | update(dt); | | render(); | | requestAnimationFrame(frame); | | } | | | | / ---------- Init ---------- */ | | function init(){ | | try{ game.hi=parseInt(localStorage.getItem(HI_KEY)||"0",10)||0; }catch(e){ game.hi=0; } | | resize(); | | updateHUD(); | | // idle ambient: show a frog on the start rug behind the overlay | | requestAnimationFrame(frame); | | } | | init(); | | })(); | | </script> | | </body> | | </html> |
The Leading Deepfake Expert No Longer Trusts His Own Eyes