Skip to content

Instantly share code, notes, and snippets.

@jkot
Created August 7, 2025 14:55
Show Gist options
  • Save jkot/90f2ade508b2e90ac7bd3703fada5835 to your computer and use it in GitHub Desktop.
Save jkot/90f2ade508b2e90ac7bd3703fada5835 to your computer and use it in GitHub Desktop.
<!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" />
<title>Flappy Getaway — One-File Game</title>
<style>
:root {
color-scheme: dark;
}
html, body {
margin: 0;
height: 100%;
background: #0b0f14 radial-gradient(1200px 600px at 80% -20%, #1a2230 0%, transparent 60%) no-repeat;
font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, "Helvetica Neue", Arial, "Noto Sans", "Liberation Sans", sans-serif;
overflow: hidden;
user-select: none;
-webkit-touch-callout: none;
-webkit-tap-highlight-color: transparent;
}
#game {
display: block;
width: 100vw;
height: 100vh;
}
.ui {
position: fixed;
inset: 0;
pointer-events: none;
display: grid;
place-items: start stretch;
}
.topbar {
display: flex;
justify-content: space-between;
align-items: center;
padding: clamp(8px, 2.2vmin, 18px);
color: #dbe6ff;
font-weight: 700;
text-shadow: 0 1px 0 #000, 0 0 12px rgba(123,167,255,0.4);
}
.chip {
background: rgba(10,16,28,0.6);
border: 1px solid rgba(114,169,255,0.25);
padding: .35em .7em;
border-radius: 9px;
backdrop-filter: blur(4px);
box-shadow: 0 2px 14px rgba(0,0,0,.35) inset, 0 0 20px rgba(102,153,255,.12);
}
.center {
pointer-events: none;
position: fixed;
inset: 0;
display: grid;
place-items: center;
text-align: center;
color: #e9f1ff;
padding: 24px;
}
.card {
pointer-events: auto;
max-width: min(720px, 92vw);
background: linear-gradient(180deg, rgba(10,16,28,0.85), rgba(8,12,20,0.8));
border: 1px solid rgba(114,169,255,0.25);
border-radius: 14px;
padding: clamp(16px, 3vmin, 28px);
box-shadow: 0 14px 50px rgba(0,0,0,.5), 0 0 80px rgba(114,169,255,.15);
}
.title {
font-size: clamp(28px, 6.3vmin, 52px);
margin: 0 0 .25em;
letter-spacing: .5px;
}
.subtitle {
margin: 0 0 1em;
opacity: .9;
}
.row {
display: flex;
gap: 12px;
justify-content: center;
flex-wrap: wrap;
}
.btn {
cursor: pointer;
background: linear-gradient(180deg, #2b6cf5, #1946c9);
color: #fff;
border: 0;
padding: .75em 1.1em;
border-radius: 12px;
font-weight: 800;
letter-spacing: .4px;
box-shadow: 0 10px 30px rgba(43,108,245,.35);
transition: transform .06s ease;
}
.btn:active { transform: translateY(1px) scale(.99); }
.kbd {
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
background: rgba(255,255,255,.08);
border: 1px solid rgba(255,255,255,.15);
border-bottom-width: 2px;
padding: .15em .45em;
border-radius: 6px;
}
.footer {
position: fixed;
bottom: 6px; left: 6px; right: 6px;
text-align: center;
color: #9fb4df;
font-size: 12px;
opacity: .7;
pointer-events: none;
}
</style>
</head>
<body>
<canvas id="game"></canvas>
<div class="ui">
<div class="topbar">
<div class="chip" id="scoreChip">Score: 0</div>
<div class="chip" id="wantedChip">Wanted: 0%</div>
<div class="chip" id="bestChip">Best: 0</div>
</div>
</div>
<div class="center" id="menu">
<div class="card">
<h1 class="title">Flappy Getaway</h1>
<p class="subtitle">Dodge the skyline, grab cash, and keep your Wanted level down.</p>
<div style="margin: .6em 0 1.1em; color:#bcd2ff;">
Controls:
<span class="kbd">Space</span> or <span class="kbd">Click / Tap</span> to flap,
<span class="kbd">P</span> to pause.
</div>
<div class="row" style="margin-top: .4em;">
<button class="btn" id="startBtn">Start</button>
</div>
</div>
</div>
<div class="center" id="gameover" style="display:none;">
<div class="card">
<h2 class="title" style="font-size: clamp(24px,5.2vmin,42px);">Busted!</h2>
<p class="subtitle" id="finalStats"></p>
<div class="row" style="margin-top:.4em;">
<button class="btn" id="retryBtn">Retry</button>
<button class="btn" id="menuBtn" style="background:linear-gradient(180deg,#596a85,#3c485e); box-shadow:0 10px 30px rgba(60,72,94,.3);">Menu</button>
</div>
</div>
</div>
<div class="footer">This is an original mini-game inspired by chase vibes and flappy mechanics — no assets from any existing franchise.</div>
<script>
/*
Flappy Getaway — single-file canvas game
- Dodge building gaps (Flappy-style).
- Collect cash to score + reduce Wanted.
- Wanted rises over time and ramps difficulty (speed + spawn rate + siren).
- Desktop + Mobile. Minimal sounds via WebAudio after first interaction.
*/
// Canvas setup
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d', { alpha: false });
// UI elements
const scoreChip = document.getElementById('scoreChip');
const wantedChip = document.getElementById('wantedChip');
const bestChip = document.getElementById('bestChip');
const menuOverlay = document.getElementById('menu');
const gameoverOverlay = document.getElementById('gameover');
const startBtn = document.getElementById('startBtn');
const retryBtn = document.getElementById('retryBtn');
const menuBtn = document.getElementById('menuBtn');
const finalStats = document.getElementById('finalStats');
// Persistent best
const LS_KEY = 'flappyGetawayBest';
let bestScore = +localStorage.getItem(LS_KEY) || 0;
bestChip.textContent = 'Best: ' + bestScore;
// World / game state
let W = 0, H = 0, DPR = Math.max(1, Math.min(2, window.devicePixelRatio || 1));
let lastT = 0;
let running = true;
let state = 'menu'; // 'menu' | 'play' | 'pause' | 'dead'
let sirenPhase = 0;
// Player
const player = {
x: 0, y: 0, w: 52, h: 28, vy: 0, rot: 0,
tint: '#9fd3ff'
};
// Physics, difficulty
let gravity = 1800; // px/s^2
let jumpVel = -560; // px/s
let scrollSpeed = 220; // px/s base, scales with wanted
let spawnInterval = 1.25; // seconds, scales with wanted
let timeSinceSpawn = 0;
let wanted = 0; // 0..100
let score = 0;
let passedCount = 0;
// World objects
const pipes = []; // {x, w, gapY, gapH, passed, coin:{y, taken}}
const particles = []; // lightweight visual bits
const skyline1 = []; // parallax
const skyline2 = [];
// Audio
let AudioCtx = window.AudioContext || window.webkitAudioContext;
let actx = null;
// Input
let flapQueued = false;
// Resize and init
function resize() {
DPR = Math.max(1, Math.min(2, window.devicePixelRatio || 1));
canvas.width = Math.floor(window.innerWidth * DPR);
canvas.height = Math.floor(window.innerHeight * DPR);
canvas.style.width = window.innerWidth + 'px';
canvas.style.height = window.innerHeight + 'px';
W = canvas.width;
H = canvas.height;
// Rebuild skylines for parallax
buildSkyline(skyline1, 10, H*0.35, H*0.55, 0.55, '#0e1624');
buildSkyline(skyline2, 12, H*0.25, H*0.45, 0.8, '#121c2f');
}
window.addEventListener('resize', resize);
// Skyline helpers
function buildSkyline(arr, count, minH, maxH, coverage, color) {
arr.length = 0;
let x = 0;
const maxW = W * coverage;
const baseY = H * 0.72;
while (x < W + 200) {
const bw = rand(60, 160) * (coverage);
const bh = rand(minH, maxH);
arr.push({ x, y: baseY - bh, w: bw, h: bh, color });
x += bw + rand(18, 46);
}
}
function drawSkyline(arr, speedFactor) {
ctx.save();
for (const b of arr) {
b.x -= (scrollSpeed * speedFactor) * dt;
if (b.x + b.w < -50) {
// recycle
const last = arr.reduce((m, cur) => cur.x > m.x ? cur : m, arr[0]);
b.x = last.x + last.w + rand(18, 46);
b.w = rand(60, 160) * speedFactor * 1.2;
b.h = clamp(b.h + rand(-40, 40), H*0.2, H*0.6);
b.y = (H * 0.72) - b.h;
}
ctx.fillStyle = b.color;
roundRect(b.x, b.y, b.w, b.h, 6);
ctx.fill();
// windows speckles
ctx.globalAlpha = 0.05;
ctx.fillStyle = '#9fc3ff';
for (let i = 0; i < 6; i++) {
const ww = 6, wh = 12;
const wx = b.x + rand(8, b.w-14);
const wy = b.y + rand(8, b.h-20);
ctx.fillRect(wx, wy, ww, wh);
}
ctx.globalAlpha = 1;
}
ctx.restore();
}
// Utility
function rand(min, max) { return Math.random() * (max - min) + min; }
function clamp(v, a, b) { return Math.max(a, Math.min(b, v)); }
// Pipe management
function spawnPipe() {
const pipeW = Math.max(60, Math.min(120, W*0.08));
const minGap = clamp(H * 0.18, 120, 240);
const maxGap = clamp(H * 0.28, 160, 340);
const baseGap = clamp(maxGap - score*2 - wanted*0.8, minGap, maxGap);
const gapH = baseGap;
const margin = H * 0.14;
const gapY = rand(margin + gapH/2, H - (H*0.18) - gapH/2); // avoid floor
const coinChance = 0.82;
const pipe = {
x: W + pipeW,
w: pipeW,
gapY,
gapH,
passed: false,
coin: Math.random() < coinChance ? { y: gapY, taken: false, sway: rand(0, 6.28) } : null,
};
pipes.push(pipe);
}
function resetGame() {
score = 0;
passedCount = 0;
wanted = 0;
pipes.length = 0;
particles.length = 0;
timeSinceSpawn = 0;
scrollSpeed = Math.max(W, 800) * 0.22; // scale with width
spawnInterval = 1.25;
// player placement
player.w = Math.max(46, Math.min(64, W * 0.06));
player.h = player.w * 0.54;
player.x = W * 0.28;
player.y = H * 0.46;
player.vy = 0;
player.rot = 0;
}
function startGame() {
hide(menuOverlay);
hide(gameoverOverlay);
state = 'play';
resetGame();
ping('start');
}
function toMenu() {
state = 'menu';
show(menuOverlay);
hide(gameoverOverlay);
}
function gameOver() {
state = 'dead';
shakeTimer = 0.45;
ping('crash');
bestScore = Math.max(bestScore, score);
localStorage.setItem(LS_KEY, bestScore);
bestChip.textContent = 'Best: ' + bestScore;
finalStats.innerHTML = `Score: <b>${score}</b> &bullet; Best: <b>${bestScore}</b> &bullet; Wanted: <b>${Math.round(wanted)}%</b>`;
setTimeout(()=>show(gameoverOverlay), 200);
}
// Drawing helpers
function roundRect(x, y, w, h, r) {
const rr = Math.min(r, w/2, h/2);
ctx.beginPath();
ctx.moveTo(x+rr, y);
ctx.arcTo(x+w, y, x+w, y+h, rr);
ctx.arcTo(x+w, y+h, x, y+h, rr);
ctx.arcTo(x, y+h, x, y, rr);
ctx.arcTo(x, y, x+w, y, rr);
ctx.closePath();
}
function drawCar(p) {
ctx.save();
ctx.translate(p.x, p.y);
ctx.rotate(p.rot);
const w = p.w, h = p.h;
// Body
ctx.fillStyle = '#1e2b48';
roundRect(-w/2, -h/2, w, h, 6);
ctx.fill();
// stripes
ctx.fillStyle = '#3ea7ff';
ctx.fillRect(-w*0.35, -h*0.5, w*0.7, h*0.18);
ctx.fillStyle = '#6bd6ff';
ctx.fillRect(-w*0.25, -h*0.32, w*0.5, h*0.12);
// windows
ctx.fillStyle = '#a6d8ff';
roundRect(-w*0.18, -h*0.22, w*0.36, h*0.22, 4); ctx.fill();
// Wheels
ctx.fillStyle = '#0a0f16';
roundRect(-w*0.43, h*0.2, w*0.22, h*0.24, 5); ctx.fill();
roundRect(w*0.21, h*0.2, w*0.22, h*0.24, 5); ctx.fill();
// Headlight glow
const g = ctx.createLinearGradient(0, 0, w*0.9, 0);
g.addColorStop(0, 'rgba(255,255,200,0.7)');
g.addColorStop(1, 'rgba(255,255,200,0)');
ctx.fillStyle = g;
ctx.beginPath();
ctx.moveTo(w*0.5, -h*0.18);
ctx.lineTo(w*0.5, h*0.18);
ctx.lineTo(w*1.2, h*0.08);
ctx.lineTo(w*1.2, -h*0.08);
ctx.closePath();
ctx.fill();
// Tail spark
ctx.globalAlpha = 0.7;
ctx.fillStyle = '#ff814a';
ctx.beginPath();
ctx.ellipse(-w*0.55, 0, w*0.12 + Math.abs(player.vy)*0.02, h*0.12, 0, 0, Math.PI*2);
ctx.fill();
ctx.globalAlpha = 1;
ctx.restore();
}
function drawPipe(pipe) {
const x = pipe.x, w = pipe.w;
const topH = pipe.gapY - pipe.gapH/2;
const botY = pipe.gapY + pipe.gapH/2;
const botH = Math.max(0, (H - H*0.18) - botY);
ctx.fillStyle = '#0f1827';
// Top
roundRect(x, 0, w, topH, 8); ctx.fill();
// Bottom
roundRect(x, botY, w, botH, 8); ctx.fill();
// Neon edges
ctx.globalAlpha = 0.22 + wanted*0.005;
ctx.fillStyle = '#3c8bff';
ctx.fillRect(x-2, 0, 2, topH);
ctx.fillRect(x+w, 0, 2, topH);
ctx.fillRect(x-2, botY, 2, botH);
ctx.fillRect(x+w, botY, 2, botH);
ctx.globalAlpha = 1;
// Coin
if (pipe.coin && !pipe.coin.taken) {
const cy = pipe.coin.y + Math.sin(pipe.coin.sway)*10;
pipe.coin.sway += 2.2 * dt;
const r = Math.max(10, Math.min(18, H*0.02));
const cx = x + w/2;
const grd = ctx.createRadialGradient(cx, cy, r*0.2, cx, cy, r);
grd.addColorStop(0, '#fff6b0');
grd.addColorStop(1, '#f3c021');
ctx.fillStyle = grd;
ctx.beginPath(); ctx.arc(cx, cy, r, 0, Math.PI*2); ctx.fill();
ctx.lineWidth = 2*DPR; ctx.strokeStyle = 'rgba(0,0,0,0.35)'; ctx.stroke();
ctx.fillStyle = '#1a1a1a';
ctx.font = `${Math.round(r*1.2)}px ui-sans-serif`;
ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
ctx.fillText('$', cx, cy+1*DPR);
}
}
function drawRoad() {
const roadH = H * 0.18;
// road base
const g = ctx.createLinearGradient(0, H-roadH, 0, H);
g.addColorStop(0, '#0b111b');
g.addColorStop(1, '#05080e');
ctx.fillStyle = g;
ctx.fillRect(0, H-roadH, W, roadH);
// lane dashes
const dashW = 60 * DPR, dashH = 6 * DPR;
const laneY = H - roadH*0.55;
ctx.fillStyle = 'rgba(255,255,255,0.22)';
let offset = (sirenPhase * 140) % (dashW * 3);
for (let x = -dashW*2 + offset; x < W + dashW; x += dashW*3) {
ctx.fillRect(x, laneY, dashW, dashH);
}
// curb glow
ctx.fillStyle = 'rgba(60,140,255,0.08)';
ctx.fillRect(0, H-roadH, W, 2*DPR);
}
// Particles
function spawnBurst(x, y, color, n = 12) {
for (let i=0;i<n;i++) {
particles.push({
x, y,
vx: rand(-180, 180), vy: rand(-260, 40),
life: rand(.35, .7),
t: 0,
color,
size: rand(2,5)*DPR
});
}
}
function updateParticles(dt) {
for (let i=particles.length-1;i>=0;i--) {
const p = particles[i];
p.t += dt;
if (p.t >= p.life) { particles.splice(i,1); continue; }
p.vx *= 0.98;
p.vy += 1200*dt;
p.x += p.vx*dt;
p.y += p.vy*dt;
}
}
function drawParticles() {
for (const p of particles) {
const a = 1 - (p.t / p.life);
ctx.globalAlpha = a;
ctx.fillStyle = p.color;
ctx.beginPath();
ctx.arc(p.x, p.y, p.size, 0, Math.PI*2);
ctx.fill();
ctx.globalAlpha = 1;
}
}
// Collision helpers
function rectsIntersect(ax, ay, aw, ah, bx, by, bw, bh) {
return ax < bx + bw && ax + aw > bx && ay < by + bh && ay + ah > by;
}
function circleRectCollision(cx, cy, r, rx, ry, rw, rh) {
// clamped point
const x = clamp(cx, rx, rx + rw);
const y = clamp(cy, ry, ry + rh);
const dx = cx - x;
const dy = cy - y;
return (dx*dx + dy*dy) <= r*r;
}
// Audio helpers
function ensureAudio() {
if (!actx) {
actx = new AudioCtx();
}
}
function tone(freq = 600, dur = 0.08, type='sine', gain=0.05) {
if (!actx) return;
const o = actx.createOscillator();
const g = actx.createGain();
o.type = type;
o.frequency.value = freq;
g.gain.value = gain;
o.connect(g).connect(actx.destination);
o.start();
o.stop(actx.currentTime + dur);
}
function ping(kind) {
if (!actx) return;
if (kind === 'cash') {
tone(740, .06, 'triangle', 0.08);
tone(1180, .06, 'triangle', 0.05);
} else if (kind === 'pass') {
tone(540, .05, 'sine', 0.05);
} else if (kind === 'crash') {
tone(100, .12, 'square', 0.12);
setTimeout(()=>tone(80, .18, 'sawtooth', 0.08), 60);
} else if (kind === 'start') {
tone(420, .06, 'triangle', 0.05);
setTimeout(()=>tone(520, .06, 'triangle', 0.05), 60);
}
}
// Input handling
function flap() {
if (state !== 'play') return;
ensureAudio();
player.vy = jumpVel;
spawnBurst(player.x - player.w*0.55, player.y, 'rgba(255,145,90,0.7)', 4);
}
window.addEventListener('pointerdown', (e) => {
if (state === 'menu') { ensureAudio(); startGame(); return; }
if (state === 'dead') return;
flapQueued = true;
});
window.addEventListener('keydown', (e) => {
if (e.code === 'Space') {
e.preventDefault();
if (state === 'menu') { ensureAudio(); startGame(); return; }
if (state === 'dead') return;
flapQueued = true;
} else if (e.key.toLowerCase() === 'p') {
if (state === 'play') { state = 'pause'; }
else if (state === 'pause') { state = 'play'; }
} else if (e.key.toLowerCase() === 'r') {
if (state === 'dead') { startGame(); }
}
});
// Buttons
startBtn.addEventListener('click', () => { ensureAudio(); startGame(); });
retryBtn.addEventListener('click', () => { ensureAudio(); startGame(); });
menuBtn.addEventListener('click', () => toMenu());
// Overlay helpers
function show(el) { el.style.display = ''; }
function hide(el) { el.style.display = 'none'; }
// Camera shake
let shakeTimer = 0;
function applyShake() {
if (shakeTimer > 0) {
const s = shakeTimer;
const dx = (Math.random()*2-1)*8*DPR*s;
const dy = (Math.random()*2-1)*8*DPR*s;
ctx.setTransform(1,0,0,1, dx, dy);
shakeTimer = Math.max(0, shakeTimer - dt*2.2);
} else {
ctx.setTransform(1,0,0,1, 0, 0);
}
}
// Main loop
let dt = 0;
function loop(t) {
if (!lastT) lastT = t;
dt = Math.min(0.032, (t - lastT) / 1000); // cap dt
lastT = t;
if (running) update(dt);
draw();
requestAnimationFrame(loop);
}
requestAnimationFrame(loop);
// Update
function update(dt) {
sirenPhase += dt;
if (state === 'play') {
if (flapQueued) { flap(); flapQueued = false; }
// Difficulty ramp
wanted += dt * 5; // rises over time
wanted = clamp(wanted, 0, 100);
// base speed + ramp with wanted
const speedBoost = (wanted > 50) ? (wanted-50) * 3.5 : 0;
scrollSpeed = Math.max(W*0.18, W*0.22 + speedBoost);
// spawn rate tightens with wanted
const targetSpawn = clamp(1.25 - wanted*0.004, 0.7, 1.25);
spawnInterval += (targetSpawn - spawnInterval) * 0.03;
timeSinceSpawn += dt;
if (timeSinceSpawn >= spawnInterval) {
timeSinceSpawn = 0;
spawnPipe();
}
// Update pipes
for (let i=pipes.length-1;i>=0;i--) {
const p = pipes[i];
p.x -= scrollSpeed * dt;
// Passing score
if (!p.passed && p.x + p.w < player.x - player.w/2) {
p.passed = true;
score++;
ping('pass');
}
// Coin collection
if (p.coin && !p.coin.taken) {
const cx = p.x + p.w/2;
const cy = p.coin.y + Math.sin(p.coin.sway)*10;
const r = Math.max(10, Math.min(18, H*0.02));
if (circleRectCollision(cx, cy, r, player.x - player.w/2, player.y - player.h/2, player.w, player.h)) {
p.coin.taken = true;
score += 4;
wanted = Math.max(0, wanted - 14);
spawnBurst(cx, cy, 'rgba(255,230,120,0.9)', 14);
ping('cash');
}
}
// Cleanup
if (p.x + p.w < -80) pipes.splice(i,1);
}
// Player physics
player.vy += gravity * dt;
player.y += player.vy * dt;
player.rot = clamp((player.vy / 1000), -0.45, 1.0);
// Collisions with pipes
const px = player.x - player.w/2;
const py = player.y - player.h/2;
for (const p of pipes) {
const topH = p.gapY - p.gapH/2;
const botY = p.gapY + p.gapH/2;
const botH = Math.max(0, (H - H*0.18) - botY);
if (rectsIntersect(px, py, player.w, player.h, p.x, 0, p.w, topH) ||
rectsIntersect(px, py, player.w, player.h, p.x, botY, p.w, botH)) {
gameOver();
break;
}
}
// Floor/Ceiling
const floorY = H - H*0.18;
if (player.y + player.h/2 >= floorY) {
player.y = floorY - player.h/2;
gameOver();
} else if (player.y - player.h/2 <= 0) {
player.y = player.h/2;
player.vy = 0;
gameOver();
}
// Particles
updateParticles(dt);
} else {
// Not playing: gentle idle WANTED decay
wanted = Math.max(0, wanted - dt*10);
}
// Update UI
scoreChip.textContent = 'Score: ' + score;
wantedChip.textContent = 'Wanted: ' + Math.round(wanted) + '%';
}
// Draw
function draw() {
// Clear
ctx.setTransform(1,0,0,1, 0, 0);
ctx.fillStyle = '#0b0f14';
ctx.fillRect(0,0,W,H);
// Sky gradient
const sky = ctx.createLinearGradient(0, 0, 0, H);
sky.addColorStop(0, '#0d1421');
sky.addColorStop(1, '#0b0f14');
ctx.fillStyle = sky;
ctx.fillRect(0,0,W,H);
// Stars
ctx.globalAlpha = 0.25;
ctx.fillStyle = '#b7d0ff';
const starCount = Math.floor((W*H) / (22000));
for (let i=0;i<starCount;i++) {
const x = (i*1315423911 % W);
const y = (i*2654435761 % Math.floor(H*0.6));
ctx.fillRect((x + Math.floor(sirenPhase*50*i)) % W, y, 1*DPR, 1*DPR);
}
ctx.globalAlpha = 1;
// Parallax skyline layers
drawSkyline(skyline1, 0.08);
drawSkyline(skyline2, 0.16);
// Pipes and coins
for (const p of pipes) drawPipe(p);
// Particles behind player
drawParticles();
// Player
applyShake();
drawCar(player);
ctx.setTransform(1,0,0,1, 0, 0);
// Road
drawRoad();
// Siren overlay at high wanted
if (wanted > 60) {
const a = clamp((wanted-60)/40, 0, 1) * 0.35;
const blink = (Math.sin(sirenPhase*6) + 1) / 2;
ctx.globalAlpha = a * (0.9*blink + 0.1);
// left red
ctx.fillStyle = '#ff2c45';
ctx.fillRect(0, 0, W*0.06, H);
// right blue
ctx.fillStyle = '#3a7bff';
ctx.fillRect(W - W*0.06, 0, W*0.06, H);
ctx.globalAlpha = 1;
}
// State overlays text hints (subtle)
if (state === 'pause') {
ctx.fillStyle = 'rgba(10,16,28,.6)';
ctx.fillRect(0,0,W,H);
ctx.fillStyle = '#dbe6ff';
ctx.font = `${Math.round(32*DPR)}px ui-sans-serif`;
ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
ctx.fillText('Paused — press P or tap to resume', W/2, H/2);
}
}
// Kick things off
resize();
toMenu();
// ————————————————————————————————
// Optional: start on first user gesture for iOS audio
// ————————————————————————————————
// Prevent context menu on long-press
window.addEventListener('contextmenu', (e)=>e.preventDefault(), {passive:false});
// Note: Using global dt variable inside skyline draw
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment