<!
DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Canvas Racing Game</title>
<style>
:root { --road:#1c1f26; --line:#e8e8e8; --accent:#19f; --danger:#ff5a5f; --
good:#35c759; }
* { box-sizing: border-box; }
html, body { height: 100%; margin: 0; font-family: system-ui, -apple-system,
Segoe UI, Roboto, sans-serif; background: radial-gradient(1000px 600px at 50% -
200px, #10131a 0%, #0a0c10 60%); color: #e8e8e8; }
.wrap { display:flex; align-items:center; justify-content:center; height:100%;
padding:16px; }
.frame { position:relative; width:min(92vw, 520px); aspect-ratio: 9/16; border-
radius: 24px; box-shadow: 0 20px 60px rgba(0,0,0,.6), inset 0 0 0 1px
rgba(255,255,255,.06); overflow:hidden; background:#0b0f14; }
canvas { width:100%; height:100%; display:block; background: linear-
gradient(#0c1016, #0a0d12); }
.hud { position:absolute; inset:12px; display:flex; justify-content:space-
between; align-items:flex-start; pointer-events:none; }
.badge { background:rgba(0,0,0,.35); padding:6px 10px; border-radius:12px;
font-weight:600; letter-spacing:.2px; box-shadow: 0 0 0 1px rgba(255,255,255,.06)
inset; }
.center-top { position:absolute; top:12px; left:50%; transform:translateX(-
50%); pointer-events:none; font-weight:700; opacity:.8; }
.controls { position:absolute; bottom:12px; left:50%; transform:translateX(-
50%); display:flex; gap:10px; }
.btn { -webkit-tap-highlight-color:transparent; border:0;
background:rgba(255,255,255,.08); color:#e8e8e8; width:64px; height:64px; border-
radius:16px; font-size:22px; font-weight:800; box-shadow: 0 0 0 1px
rgba(255,255,255,.08) inset, 0 8px 20px rgba(0,0,0,.45); cursor:pointer; }
.btn:active { transform: translateY(1px) scale(.98); }
.btn--wide { width:140px; }
.overlay { position:absolute; inset:0; display:grid; place-items:center;
background:linear-gradient(180deg, rgba(0,0,0,.55), rgba(0,0,0,.45)); backdrop-
filter: blur(2px); }
.card { text-align:center; padding:22px 20px; background:rgba(15,18,24,.86);
border-radius:16px; box-shadow: 0 20px 40px rgba(0,0,0,.5), inset 0 0 0 1px
rgba(255,255,255,.06); max-width: 90%; }
.title { font-size:22px; margin:0 0 6px; }
.muted { opacity:.85; font-size:14px; line-height:1.4; }
.chip { display:inline-block; margin-top:10px; padding:6px 10px; border-
radius:999px; background:rgba(255,255,255,.08); font-size:12px; letter-
spacing:.3px; }
.row { display:flex; gap:10px; justify-content:center; margin-top:14px; }
.tag { position:absolute; right:12px; bottom:92px; font-size:12px;
opacity:.7; }
</style>
</head>
<body>
<div class="wrap">
<div class="frame" id="frame">
<canvas id="game" width="540" height="960"></canvas>
<!-- HUD -->
<div class="hud">
<div class="badge" id="score">Score: 0</div>
<div class="badge" id="hi">Best: 0</div>
</div>
<div class="center-top badge">Canvas Racing</div>
<!-- Touch Controls -->
<div class="controls" aria-label="Touch controls">
<button class="btn" id="left" title="Left">⟵</button>
<button class="btn btn--wide" id="start">Start</button>
<button class="btn" id="right" title="Right">⟶</button>
</div>
<div class="tag">Keys: ← → to steer · P to pause · R to restart</div>
<!-- Overlays -->
<div class="overlay" id="intro">
<div class="card">
<h2 class="title">Mini Racing</h2>
<p class="muted">Dodge traffic and drive as far as you can. The car snaps
to 3 lanes. Speed rises over time.</p>
<p class="chip">Controls: ← → / Tap arrows</p>
<div class="row">
<button class="btn btn--wide" id="playBtn">Play</button>
</div>
</div>
</div>
<div class="overlay" id="gameOver" style="display:none">
<div class="card">
<h2 class="title">Crash!</h2>
<p class="muted" id="final">Your score: 0</p>
<div class="row">
<button class="btn btn--wide" id="retry">Retry</button>
</div>
</div>
</div>
<div class="overlay" id="paused" style="display:none">
<div class="card">
<h2 class="title">Paused</h2>
<div class="row">
<button class="btn btn--wide" id="resume">Resume</button>
</div>
</div>
</div>
</div>
</div>
<script>
// ====== Utility helpers ======
const rnd = (min, max) => Math.random() * (max - min) + min;
// ====== Game constants ======
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
const LANES = 3;
const ROAD_W = canvas.width * 0.76; // road width
const LANE_W = ROAD_W / LANES;
const ROAD_X = (canvas.width - ROAD_W) / 2;
// Car model
const CAR_W = LANE_W * 0.6;
const CAR_H = CAR_W * 1.6;
const CAR_Y = canvas.height - CAR_H - 40;
// Gameplay
const SPAWN_MIN = 500; // ms
const SPAWN_MAX = 1100; // ms
// State
let running = false, paused = false, over = false;
let score = 0, hi = Number(localStorage.getItem('racing_hi')||0);
let speed = 6; // pixels per frame base
let lastSpawn = 0, spawnRate = SPAWN_MAX;
let last = 0; // last raf time
// Objects
const car = { lane: 1, x: 0, y: CAR_Y, w: CAR_W, h: CAR_H };
const traffic = []; // array of o {lane, y, speed}
const particles = []; // for lane dash motion
// HUD
const scoreEl = document.getElementById('score');
const hiEl = document.getElementById('hi');
hiEl.textContent = 'Best: ' + hi;
// ====== Setup functions ======
function laneX(lane) { return ROAD_X + lane * LANE_W + (LANE_W - CAR_W)/2; }
function reset() {
score = 0; speed = 6; spawnRate = SPAWN_MAX; lastSpawn = 0; last = 0;
car.lane = 1; car.x = laneX(car.lane);
traffic.length = 0; particles.length = 0; over = false; paused = false;
spawnDashes();
}
function spawnDashes(){
// create dashed lane markers as particles
particles.length = 0;
for (let lane = 1; lane < LANES; lane++) {
for (let y = -200; y < canvas.height + 200; y += 80) {
particles.push({ x: ROAD_X + lane*LANE_W, y, w: 6, h: 32 });
}
}
}
function spawnTraffic() {
const lane = Math.floor(rnd(0, LANES));
const y = -CAR_H - 20;
const s = speed * rnd(0.9, 1.25);
// avoid spawning directly over another in same lane too close
const tooClose = traffic.some(t => t.lane === lane && Math.abs(t.y - y) <
CAR_H * 2.2);
if (!tooClose) traffic.push({ lane, x: laneX(lane), y, w: CAR_W, h: CAR_H,
s });
}
// ====== Drawing ======
function drawRoad(){
// road background
ctx.fillStyle =
getComputedStyle(document.documentElement).getPropertyValue('--road');
ctx.fillRect(ROAD_X, 0, ROAD_W, canvas.height);
// side gradients (shoulders)
const grdL = ctx.createLinearGradient(ROAD_X-20,0,ROAD_X,0);
grdL.addColorStop(0,'rgba(0,0,0,.0)'); grdL.addColorStop(1,'#0d1016');
ctx.fillStyle = grdL; ctx.fillRect(ROAD_X-20,0,20,canvas.height);
const grdR = ctx.createLinearGradient(ROAD_X+ROAD_W,0,ROAD_X+ROAD_W+20,0);
grdR.addColorStop(0,'#0d1016'); grdR.addColorStop(1,'rgba(0,0,0,.0)');
ctx.fillStyle = grdR; ctx.fillRect(ROAD_X+ROAD_W,0,20,canvas.height);
// moving dashes
ctx.fillStyle =
getComputedStyle(document.documentElement).getPropertyValue('--line');
particles.forEach(p => { ctx.fillRect(p.x-3, p.y, p.w, p.h); });
}
function drawCar(x, y, baseColor){
const r = 10;
ctx.fillStyle = baseColor;
roundRect(ctx, x, y, CAR_W, CAR_H, r, true);
// windshield
ctx.fillStyle = 'rgba(255,255,255,.2)';
roundRect(ctx, x+CAR_W*0.14, y+CAR_H*0.14, CAR_W*0.72, CAR_H*0.26, 8, true);
// cockpit
ctx.fillStyle = 'rgba(0,0,0,.25)';
roundRect(ctx, x+CAR_W*0.2, y+CAR_H*0.44, CAR_W*0.6, CAR_H*0.18, 8, true);
// lights
ctx.fillStyle = '#ffd15c';
ctx.fillRect(x+CAR_W*0.1, y+CAR_H*0.9, 10, 8);
ctx.fillRect(x+CAR_W*0.8-10, y+CAR_H*0.9, 10, 8);
}
function roundRect(ctx, x, y, w, h, r, fill){
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();
if (fill) ctx.fill();
}
// ====== Loop ======
function step(t){
if (!running) return;
const dt = t - last; last = t;
if (paused || over) { requestAnimationFrame(step); return; }
// clear
ctx.clearRect(0,0,canvas.width, canvas.height);
// update background dashes
particles.forEach(p => { p.y += speed * 1.4; if (p.y > canvas.height + 40)
p.y = -80; });
drawRoad();
// move traffic
for (let i=traffic.length-1; i>=0; i--){
const o = traffic[i];
o.y += o.s;
if (o.y > canvas.height + 60) traffic.splice(i,1);
}
// draw traffic cars
traffic.forEach((o, idx) => drawCar(o.x, o.y, idx%2? '#19f' : '#35c759'));
// player car
car.x = laneX(car.lane);
drawCar(car.x, car.y, '#ff5a5f');
// collisions
const hit = traffic.some(o =>
Math.abs(o.lane - car.lane) < 0.1 &&
o.y + o.h*0.8 > car.y &&
o.y < car.y + car.h*0.8
);
if (hit) return gameOver();
// scoring & difficulty
score += (speed * dt) / 100; // distance-based
speed += 0.0009 * dt; // slowly accelerate
spawnRate = Math.max(SPAWN_MIN, SPAWN_MAX - score * 0.6);
// spawn timing
lastSpawn += dt;
if (lastSpawn > spawnRate){
spawnTraffic();
lastSpawn = 0;
}
// hud
scoreEl.textContent = 'Score: ' + Math.floor(score);
requestAnimationFrame(step);
}
function gameOver(){
over = true; running = false;
const s = Math.floor(score);
if (s > hi){ hi = s; localStorage.setItem('racing_hi', hi); }
hiEl.textContent = 'Best: ' + hi;
document.getElementById('final').textContent = `Your score: ${s} · Best: $
{hi}`;
show('gameOver');
}
// ====== Controls ======
function steer(dir){
if (over || paused) return;
car.lane = Math.min(LANES-1, Math.max(0, car.lane + dir));
}
window.addEventListener('keydown', e => {
if (e.key === 'ArrowLeft') steer(-1);
else if (e.key === 'ArrowRight') steer(1);
else if (e.key.toLowerCase() === 'p') togglePause();
else if (e.key.toLowerCase() === 'r') restart();
});
document.getElementById('left').addEventListener('pointerdown', () => steer(-
1));
document.getElementById('right').addEventListener('pointerdown', () =>
steer(1));
// Buttons
function hide(id){ document.getElementById(id).style.display='none'; }
function show(id){ document.getElementById(id).style.display='grid'; }
document.getElementById('playBtn').onclick = start;
document.getElementById('start').onclick = start;
document.getElementById('retry').onclick = restart;
document.getElementById('resume').onclick = () => { paused=false;
hide('paused'); running=true; requestAnimationFrame(step); };
function start(){ hide('intro'); hide('gameOver'); reset(); running = true;
requestAnimationFrame(step); }
function restart(){ hide('gameOver'); reset(); running = true;
requestAnimationFrame(step); }
function togglePause(){
if (over || !running) return; paused = !paused;
if (paused) { show('paused'); running = false; } else { hide('paused');
running = true; requestAnimationFrame(step); }
}
// Fit canvas to container size while preserving internal resolution
const frame = document.getElementById('frame');
const resize = () => {
// Canvas displayed size handled by CSS; we keep internal resolution fixed
for physics
spawnDashes();
};
window.addEventListener('resize', resize);
resize();
// Initialize car placement
car.x = laneX(car.lane);
</script>
</body>
</html>