// loadingdecoding pod stream
15.1 KB · main
grav-dodge/index.html
1<!doctype html>2<html lang="en">3<head>4<meta charset="utf-8" />5<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover" />6<title>GRAVITY DODGE</title>7<style>8 :root{9 --bg:#07070d;10 --fg:#e8f1ff;11 --dim:#6a7a99;12 --accent:#7df9ff;13 --warn:#ff4d6d;14 --gold:#ffd166;15 }16 *{box-sizing:border-box;margin:0;padding:0}17 html,body{height:100%;width:100%;overflow:hidden;background:var(--bg);color:var(--fg);18 font:14px/1.4 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;19 -webkit-font-smoothing:antialiased;user-select:none;-webkit-user-select:none}20 #wrap{position:fixed;inset:0;display:grid;place-items:center}21 #game{display:block;background:radial-gradient(ellipse at 50% 30%,#101428 0%,#05060c 70%,#02020a 100%);22 image-rendering:pixelated;touch-action:none;cursor:crosshair;max-width:100vw;max-height:100vh}23 /* HUD */24 .hud{position:fixed;pointer-events:none;font-weight:600;letter-spacing:.08em;text-transform:uppercase;25 text-shadow:0 0 8px rgba(125,249,255,.5),0 0 2px rgba(0,0,0,.8)}26 #score{top:18px;left:22px;font-size:18px}27 #best{top:18px;right:22px;font-size:14px;color:var(--dim)}28 #speed{top:42px;left:22px;font-size:11px;color:var(--dim)}29 #title{position:fixed;inset:0;display:grid;place-items:center;background:rgba(2,3,8,.72);30 backdrop-filter:blur(6px);-webkit-backdrop-filter:blur(6px);text-align:center;padding:24px}31 #title h1{font-size:clamp(40px,8vw,84px);letter-spacing:.18em;margin-bottom:14px;32 background:linear-gradient(180deg,#fff 0%,var(--accent) 60%,#4ad8ff 100%);33 -webkit-background-clip:text;background-clip:text;color:transparent;34 text-shadow:0 0 30px rgba(125,249,255,.25)}35 #title p{color:var(--dim);max-width:520px;margin:0 auto 22px}36 #title .keys{display:inline-block;padding:6px 10px;border:1px solid #2a3550;border-radius:6px;color:var(--fg);37 background:#0c1224;margin:0 3px;font-size:12px}38 #title .start{display:inline-block;margin-top:8px;padding:14px 26px;border:1px solid var(--accent);39 color:var(--accent);background:rgba(125,249,255,.08);border-radius:8px;40 letter-spacing:.2em;font-weight:700;animation:pulse 1.6s ease-in-out infinite}41 #title .credits{margin-top:26px;font-size:11px;color:#44506a;letter-spacing:.16em}42 @keyframes pulse{0%,100%{box-shadow:0 0 0 0 rgba(125,249,255,.5)}50%{box-shadow:0 0 0 12px rgba(125,249,255,0)}}43 .hidden{display:none !important}44 #flash{position:fixed;inset:0;background:#ff4d6d;mix-blend-mode:screen;opacity:0;pointer-events:none;45 transition:opacity .25s ease-out}46</style>47</head>48<body>49<div id="wrap">50 <canvas id="game" width="540" height="780"></canvas>51</div>5253<div id="score" class="hud">Score <span id="s">0</span></div>54<div id="best" class="hud">Best <span id="b">0</span></div>55<div id="speed" class="hud">Speed <span id="sp">1.0</span>×</div>5657<div id="title">58 <div>59 <h1>GRAVITY DODGE</h1>60 <p>Pilot a fragile probe through a rain of debris. Move with mouse, touch, or arrow keys. Touch nothing. Survive as long as you can. Each second the universe gets faster.</p>61 <div>62 <span class="keys">←/→</span> or <span class="keys">A/D</span> or <span class="keys">MOUSE</span> to steer63 </div>64 <div style="margin-top:22px"><span class="start" id="go">PRESS ANY KEY / TAP TO START</span></div>65 <div class="credits">A 5KB single-file game · No assets · No dependencies</div>66 </div>67</div>6869<div id="flash"></div>7071<script>72(() => {73 'use strict';74 // ---- canvas + DPR setup ---------------------------------------------------75 const cvs = document.getElementById('game');76 const ctx = cvs.getContext('2d');77 const W = 540, H = 780;78 cvs.width = W; cvs.height = H;7980 // Fit canvas to viewport while preserving aspect ratio81 function fit(){82 const vw = innerWidth, vh = innerHeight;83 const scale = Math.min(vw / W, vh / H);84 cvs.style.width = Math.floor(W * scale) + 'px';85 cvs.style.height = Math.floor(H * scale) + 'px';86 }87 addEventListener('resize', fit); fit();8889 // ---- helpers --------------------------------------------------------------90 const rand = (a,b)=>a + Math.random()*(b-a);91 const irand = (a,b)=> Math.floor(rand(a,b+1));92 const clamp = (v,a,b)=> v<a?a : v>b?b : v;93 const lerp = (a,b,t)=> a + (b-a)*t;94 const TAU = Math.PI*2;9596 // ---- input ----------------------------------------------------------------97 const keys = new Set();98 const mouse = { x: W/2, active:false };99 let steer = 0; // -1..1100 const title = document.getElementById('title');101 const goBtn = document.getElementById('go');102 const flash = document.getElementById('flash');103104 function startGame(){105 if (!state.running){106 state.running = true;107 state.t = 0;108 state.dead = false;109 state.flash = 0;110 title.classList.add('hidden');111 }112 }113 function endGame(){114 state.dead = true;115 state.running = false;116 title.classList.remove('hidden');117 goBtn.textContent = 'GAME OVER · TAP TO RETRY';118 flash.style.opacity = '0.6';119 setTimeout(()=> flash.style.opacity = '0', 220);120 if (state.score > state.best){121 state.best = state.score;122 try { localStorage.setItem('gravdodge_best', String(state.best|0)); } catch(_){}123 }124 ui();125 }126127 addEventListener('keydown', e => {128 if (['ArrowLeft','ArrowRight','ArrowUp','ArrowDown',' ','a','A','d','D','w','W','s','S'].includes(e.key)) e.preventDefault();129 if (state.running){130 keys.add(e.key.toLowerCase());131 } else {132 startGame();133 }134 }, {passive:false});135 addEventListener('keyup', e => keys.delete(e.key.toLowerCase()));136 cvs.addEventListener('mousemove', e => {137 const r = cvs.getBoundingClientRect();138 mouse.x = (e.clientX - r.left) * (W / r.width);139 mouse.active = true;140 });141 cvs.addEventListener('mouseleave', ()=> mouse.active = false);142 cvs.addEventListener('touchstart', e => {143 e.preventDefault();144 if (!state.running) startGame();145 const t = e.touches[0];146 const r = cvs.getBoundingClientRect();147 mouse.x = (t.clientX - r.left) * (W / r.width);148 mouse.active = true;149 }, {passive:false});150 cvs.addEventListener('touchmove', e => {151 e.preventDefault();152 const t = e.touches[0];153 const r = cvs.getBoundingClientRect();154 mouse.x = (t.clientX - r.left) * (W / r.width);155 }, {passive:false});156 // Tap/click on title or canvas to start157 title.addEventListener('click', startGame);158 cvs.addEventListener('click', ()=> { if(!state.running) startGame(); });159160 // ---- state ----------------------------------------------------------------161 const state = {162 running:false,163 t:0, // seconds since start164 score:0,165 best: (()=>{ try { return +(localStorage.getItem('gravdodge_best')||0); } catch(_){ return 0; } })(),166 dead:false,167 flash:0,168 };169170 // ship171 const ship = { x: W/2, y: H - 90, r: 11, vx: 0, targetX: W/2, trail: [] };172173 // obstacles & particles174 const obstacles = [];175 const particles = [];176 const stars = Array.from({length:90}, () => ({177 x: Math.random()*W, y: Math.random()*H, s: Math.random()*1.4 + 0.3, v: 8 + Math.random()*22178 }));179180 // ---- UI -------------------------------------------------------------------181 const elS = document.getElementById('s');182 const elB = document.getElementById('b');183 const elSP = document.getElementById('sp');184 function ui(){185 elS.textContent = (state.score|0).toString();186 elB.textContent = state.best.toString();187 elSP.textContent = (1 + state.t * 0.06).toFixed(1);188 }189 ui();190191 // ---- spawn / update -------------------------------------------------------192 function spawnObstacle(){193 const r = irand(14, 32);194 const x = rand(r, W - r);195 const kind = Math.random();196 let color = '#7df9ff';197 let vy = rand(140, 220) * (1 + state.t*0.018);198 let vx = 0, spin = 0;199 if (kind < 0.18){200 color = '#ffd166';201 vy *= 1.15;202 } else if (kind < 0.34){203 // homing204 color = '#ff8a5b';205 vx = (ship.x - x) * 0.35;206 } else if (kind < 0.50){207 // spinning mine208 color = '#c084fc';209 spin = rand(-2,2);210 } else if (kind < 0.62){211 // big slow boulder212 color = '#9aa6c2';213 } else {214 color = '#7df9ff';215 }216 // shape: pentagon if mine, square if gold, hex otherwise217 const sides = (color === '#c084fc') ? 6218 : (color === '#ffd166') ? 4219 : (color === '#ff8a5b') ? 5220 : 3;221 obstacles.push({ x, y:-r, vx, vy, r, rot:0, spin, color, sides, alive:true });222 }223224 function spawnExplosion(x, y, color, n=22){225 for (let i=0; i<n; i++){226 const a = Math.random()*TAU;227 const sp = rand(60, 320);228 particles.push({229 x, y, vx: Math.cos(a)*sp, vy: Math.sin(a)*sp,230 life: rand(0.4, 0.9), age: 0, color, size: rand(1.5, 3.2)231 });232 }233 }234235 function spawnTrail(x, y){236 particles.push({237 x: x + rand(-2,2), y: y + rand(-1,1),238 vx: rand(-20,20), vy: rand(-30, -5),239 life: rand(0.25, 0.55), age: 0,240 color: Math.random()<0.3 ? '#7df9ff' : '#9ad8ff',241 size: rand(1.2, 2.4), trail: true242 });243 }244245 // ---- draw helpers ---------------------------------------------------------246 function drawStarfield(dt){247 ctx.save();248 for (const s of stars){249 s.y += s.v * dt * (1 + state.t*0.01);250 if (s.y > H) { s.y = -2; s.x = Math.random()*W; }251 ctx.globalAlpha = 0.5 + 0.5*Math.sin(state.t*2 + s.x);252 ctx.fillStyle = '#cdd9ff';253 ctx.fillRect(s.x|0, s.y|0, s.s, s.s);254 }255 ctx.globalAlpha = 1;256 ctx.restore();257 }258259 function drawShip(){260 // thruster trail261 ship.trail.push({x: ship.x, y: ship.y + ship.r*0.6, life: 0.35, age:0});262 if (ship.trail.length > 24) ship.trail.shift();263 for (const t of ship.trail){264 const a = 1 - t.age/t.life;265 ctx.fillStyle = `rgba(125,249,255,${a*0.7})`;266 ctx.beginPath();267 ctx.arc(t.x, t.y, 2.5*a+0.3, 0, TAU);268 ctx.fill();269 t.age += 1/60;270 }271 while (ship.trail.length && ship.trail[0].age > ship.trail[0].life) ship.trail.shift();272273 // ship body — little triangle probe274 ctx.save();275 ctx.translate(ship.x, ship.y);276 // tilt from velocity277 const tilt = clamp(ship.vx / 240, -0.5, 0.5);278 ctx.rotate(tilt);279 // glow280 const g = ctx.createRadialGradient(0,0,1, 0,0,22);281 g.addColorStop(0, 'rgba(125,249,255,0.55)');282 g.addColorStop(1, 'rgba(125,249,255,0)');283 ctx.fillStyle = g;284 ctx.beginPath(); ctx.arc(0,0,22,0,TAU); ctx.fill();285286 // hull287 ctx.fillStyle = '#e8f1ff';288 ctx.strokeStyle = '#7df9ff';289 ctx.lineWidth = 1.6;290 ctx.beginPath();291 ctx.moveTo(0, -ship.r);292 ctx.lineTo( ship.r*0.9, ship.r*0.9);293 ctx.lineTo( 0, ship.r*0.4);294 ctx.lineTo(-ship.r*0.9, ship.r*0.9);295 ctx.closePath();296 ctx.fill(); ctx.stroke();297 // cockpit298 ctx.fillStyle = '#7df9ff';299 ctx.beginPath(); ctx.arc(0, 0, 2.2, 0, TAU); ctx.fill();300 ctx.restore();301 }302303 function drawObstacle(o){304 ctx.save();305 ctx.translate(o.x, o.y);306 ctx.rotate(o.rot);307 // glow308 const g = ctx.createRadialGradient(0,0,1, 0,0,o.r*1.8);309 g.addColorStop(0, o.color + '55');310 g.addColorStop(1, o.color + '00');311 ctx.fillStyle = g;312 ctx.beginPath(); ctx.arc(0,0,o.r*1.8,0,TAU); ctx.fill();313314 ctx.fillStyle = o.color;315 ctx.strokeStyle = 'rgba(255,255,255,0.9)';316 ctx.lineWidth = 1.4;317 ctx.beginPath();318 for (let i=0; i<o.sides; i++){319 const a = (i/o.sides)*TAU;320 const px = Math.cos(a)*o.r;321 const py = Math.sin(a)*o.r;322 if (i===0) ctx.moveTo(px,py); else ctx.lineTo(px,py);323 }324 ctx.closePath();325 ctx.fill(); ctx.stroke();326 // core dot327 ctx.fillStyle = 'rgba(255,255,255,0.85)';328 ctx.beginPath(); ctx.arc(0,0,o.r*0.25,0,TAU); ctx.fill();329 ctx.restore();330 }331332 // ---- collision ------------------------------------------------------------333 function hit(a, b){334 const dx = a.x - b.x, dy = a.y - b.y;335 const r = a.r + b.r * 0.78; // forgiving336 return dx*dx + dy*dy < r*r;337 }338339 // ---- main loop ------------------------------------------------------------340 let last = performance.now();341 let spawnTimer = 0;342343 function frame(now){344 const dt = Math.min(0.04, (now - last) / 1000);345 last = now;346347 // always update background348 ctx.clearRect(0,0,W,H);349 drawStarfield(dt);350351 if (state.running){352 state.t += dt;353 state.score += dt * 10;354 ui();355356 // spawn rate scales with time357 const baseInterval = lerp(0.85, 0.22, clamp(state.t/60, 0, 1));358 spawnTimer += dt;359 while (spawnTimer > baseInterval){360 spawnTimer -= baseInterval;361 spawnObstacle();362 // chance to double-spawn as time goes on363 if (state.t > 25 && Math.random() < clamp((state.t-25)/90, 0, 0.5)){364 spawnObstacle();365 }366 }367368 // input -> target steering369 let want = 0;370 if (keys.has('arrowleft') || keys.has('a')) want -= 1;371 if (keys.has('arrowright')|| keys.has('d')) want += 1;372 if (mouse.active){373 // mouse: pull ship toward cursor X374 const diff = mouse.x - ship.x;375 want = clamp(diff / 120, -1, 1);376 }377 steer = lerp(steer, want, 0.18);378379 // ship physics380 const accel = 900;381 ship.vx += steer * accel * dt;382 ship.vx *= 0.92;383 ship.x += ship.vx * dt;384 // walls385 if (ship.x < ship.r){ ship.x = ship.r; ship.vx = 0; }386 if (ship.x > W - ship.r){ ship.x = W - ship.r; ship.vx = 0; }387388 // ship trail particles389 if (Math.random() < 0.8) spawnTrail(ship.x, ship.y + ship.r*0.4);390391 // update obstacles392 for (const o of obstacles){393 o.x += o.vx * dt;394 o.y += o.vy * dt;395 o.rot += o.spin * dt;396397 // collision398 if (o.alive && hit(o, ship)){399 o.alive = false;400 spawnExplosion(o.x, o.y, o.color, 26);401 spawnExplosion(ship.x, ship.y, '#ffffff', 18);402 endGame();403 }404 }405 // remove off-screen406 for (let i = obstacles.length-1; i>=0; i--){407 if (obstacles[i].y - obstacles[i].r > H + 20) obstacles.splice(i,1);408 }409410 // particles411 for (const p of particles){412 p.x += p.vx * dt;413 p.y += p.vy * dt;414 p.vx *= 0.96;415 p.vy *= 0.96;416 p.age += dt;417 }418 for (let i = particles.length-1; i>=0; i--){419 if (particles[i].age >= particles[i].life) particles.splice(i,1);420 }421 } else {422 // idle: gentle drift background only, but render ship + obstacles423 }424425 // draw obstacles426 for (const o of obstacles) if (o.alive) drawObstacle(o);427 // draw particles428 for (const p of particles){429 const a = 1 - p.age/p.life;430 ctx.fillStyle = p.color;431 ctx.globalAlpha = a;432 ctx.beginPath(); ctx.arc(p.x, p.y, p.size * a, 0, TAU); ctx.fill();433 }434 ctx.globalAlpha = 1;435 // draw ship436 drawShip();437438 // vignette439 const vg = ctx.createRadialGradient(W/2, H/2, H*0.35, W/2, H/2, H*0.7);440 vg.addColorStop(0, 'rgba(0,0,0,0)');441 vg.addColorStop(1, 'rgba(0,0,0,0.55)');442 ctx.fillStyle = vg;443 ctx.fillRect(0,0,W,H);444445 requestAnimationFrame(frame);446 }447 requestAnimationFrame(frame);448449 // Expose for debugging (harmless)450 window.__gd = { state, ship, obstacles };451})();452</script>453</body>454</html>