← back to Games Agentabrams
games/racer/NeonDrifter.aa
883 lines
<!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>NEON DRIFTER</title>
<style>
:root {
--bg: #0a0e14;
--neon: #ff2fd0;
--cyan: #22e7ff;
--amber: #ffd23f;
--edge: #7a3cff;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
html, body {
width: 100%; height: 100%;
background: var(--bg);
overflow: hidden;
touch-action: none;
-webkit-user-select: none;
user-select: none;
-webkit-tap-highlight-color: transparent;
font-family: "Segoe UI", Helvetica, Arial, sans-serif;
}
#wrap {
position: fixed;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
background: radial-gradient(ellipse at 50% 120%, #16203a 0%, #0a0e14 60%, #05070c 100%);
}
canvas { display: block; touch-action: none; }
/* Touch controls */
.touch {
position: fixed;
bottom: 0;
width: 40%;
height: 45%;
max-height: 320px;
z-index: 20;
display: none;
align-items: flex-end;
justify-content: center;
padding-bottom: 22px;
-webkit-user-select: none;
user-select: none;
}
.touch.left { left: 0; }
.touch.right { right: 0; }
.touch .btn {
width: 92px; height: 92px;
border-radius: 50%;
border: 2px solid rgba(34,231,255,0.55);
background: rgba(34,231,255,0.08);
color: #cffcff;
font-size: 40px;
line-height: 88px;
text-align: center;
box-shadow: 0 0 18px rgba(34,231,255,0.35), inset 0 0 14px rgba(34,231,255,0.15);
backdrop-filter: blur(2px);
}
.touch.active .btn {
background: rgba(255,47,208,0.22);
border-color: rgba(255,47,208,0.8);
box-shadow: 0 0 24px rgba(255,47,208,0.6), inset 0 0 16px rgba(255,47,208,0.25);
}
@media (pointer: coarse) {
.touch { display: flex; }
}
</style>
</head>
<body>
<div id="wrap">
<canvas id="game"></canvas>
</div>
<div class="touch left" id="touchLeft"><div class="btn">←</div></div>
<div class="touch right" id="touchRight"><div class="btn">→</div></div>
<script>
(function () {
"use strict";
function boot() {
var canvas = document.getElementById("game");
if (!canvas) return;
var ctx = canvas.getContext("2d");
if (!ctx) return;
var BEST_KEY = "abrams-racer-best";
// ---- logical dimensions (portrait road) ----
var W = 480, H = 800; // logical
var DPR = 1;
function fitCanvas() {
var vw = Math.max(1, window.innerWidth || document.documentElement.clientWidth || 480);
var vh = Math.max(1, window.innerHeight || document.documentElement.clientHeight || 800);
var targetAR = W / H;
var winAR = vw / vh;
var cssW, cssH;
if (winAR > targetAR) { cssH = vh; cssW = vh * targetAR; }
else { cssW = vw; cssH = vw / targetAR; }
DPR = Math.min(window.devicePixelRatio || 1, 2);
canvas.style.width = cssW + "px";
canvas.style.height = cssH + "px";
canvas.width = Math.round(W * DPR);
canvas.height = Math.round(H * DPR);
ctx.setTransform(DPR, 0, 0, DPR, 0, 0);
}
window.addEventListener("resize", fitCanvas, { passive: true });
window.addEventListener("orientationchange", fitCanvas, { passive: true });
fitCanvas();
// ---- road geometry ----
var LANES = 4;
var ROAD_MARGIN = 56; // side margin to road edge
var roadLeft = ROAD_MARGIN;
var roadRight = W - ROAD_MARGIN;
var roadW = roadRight - roadLeft;
var laneW = roadW / LANES;
function laneCenter(i) { return roadLeft + laneW * (i + 0.5); }
// ---- game state ----
var STATE_MENU = 0, STATE_PLAY = 1, STATE_OVER = 2;
var state = STATE_MENU;
var best = 0;
try {
var stored = localStorage.getItem(BEST_KEY);
if (stored != null) { var b = parseFloat(stored); if (isFinite(b)) best = b; }
} catch (e) {}
var player, traffic, pickups, particles;
var dashOffset, scroll, distance, score, speed, baseSpeed, elapsed, spawnTimer, pickTimer;
var boost, shakeMag, newBest;
function resetGame() {
player = {
x: laneCenter(1),
y: H - 150,
w: 46, h: 84,
vx: 0,
color: "#22e7ff"
};
traffic = [];
pickups = [];
particles = [];
dashOffset = 0;
scroll = 0;
distance = 0;
score = 0;
baseSpeed = 260; // px/sec world scroll
speed = baseSpeed;
elapsed = 0;
spawnTimer = 0.6;
pickTimer = 3.0;
boost = 0;
shakeMag = 0;
newBest = false;
}
resetGame();
// ---- input ----
var keys = { left: false, right: false, up: false, down: false };
var touchL = false, touchR = false;
function onKeyDown(e) {
var k = e.key;
var handled = true;
if (k === "ArrowLeft" || k === "a" || k === "A") keys.left = true;
else if (k === "ArrowRight" || k === "d" || k === "D") keys.right = true;
else if (k === "ArrowUp" || k === "w" || k === "W") keys.up = true;
else if (k === "ArrowDown" || k === "s" || k === "S") keys.down = true;
else if (k === " " || k === "Enter") {
if (state === STATE_MENU) startPlay();
else if (state === STATE_OVER) { resetGame(); startPlay(); }
} else handled = false;
if (handled) {
// resume/init audio on first gesture
ensureAudio();
if (state === STATE_MENU && (k === "ArrowLeft" || k === "ArrowRight" || k === "a" || k === "A" || k === "d" || k === "D")) {
startPlay();
}
e.preventDefault();
}
}
function onKeyUp(e) {
var k = e.key;
if (k === "ArrowLeft" || k === "a" || k === "A") keys.left = false;
else if (k === "ArrowRight" || k === "d" || k === "D") keys.right = false;
else if (k === "ArrowUp" || k === "w" || k === "W") keys.up = false;
else if (k === "ArrowDown" || k === "s" || k === "S") keys.down = false;
}
window.addEventListener("keydown", onKeyDown, { passive: false });
window.addEventListener("keyup", onKeyUp, { passive: false });
// touch buttons
function bindTouch(el, setter) {
if (!el) return;
function down(e) {
e.preventDefault();
ensureAudio();
el.classList.add("active");
setter(true);
if (state === STATE_MENU) startPlay();
else if (state === STATE_OVER) { resetGame(); startPlay(); }
}
function up(e) {
e.preventDefault();
el.classList.remove("active");
setter(false);
}
el.addEventListener("touchstart", down, { passive: false });
el.addEventListener("touchend", up, { passive: false });
el.addEventListener("touchcancel", up, { passive: false });
el.addEventListener("mousedown", down);
window.addEventListener("mouseup", up);
}
bindTouch(document.getElementById("touchLeft"), function (v) { touchL = v; });
bindTouch(document.getElementById("touchRight"), function (v) { touchR = v; });
// tap canvas to start/restart
canvas.addEventListener("pointerdown", function (e) {
ensureAudio();
if (state === STATE_MENU) { e.preventDefault(); startPlay(); }
else if (state === STATE_OVER) { e.preventDefault(); resetGame(); startPlay(); }
});
// prevent scroll gestures
document.addEventListener("touchmove", function (e) { e.preventDefault(); }, { passive: false });
// ---- audio (lazy) ----
var actx = null, engineOsc = null, engineGain = null, engineFilter = null;
function ensureAudio() {
try {
if (!actx) {
var AC = window.AudioContext || window.webkitAudioContext;
if (!AC) return;
actx = new AC();
engineGain = actx.createGain();
engineGain.gain.value = 0.0;
engineFilter = actx.createBiquadFilter();
engineFilter.type = "lowpass";
engineFilter.frequency.value = 700;
engineOsc = actx.createOscillator();
engineOsc.type = "sawtooth";
engineOsc.frequency.value = 70;
engineOsc.connect(engineFilter);
engineFilter.connect(engineGain);
engineGain.connect(actx.destination);
engineOsc.start();
}
if (actx.state === "suspended") actx.resume();
} catch (e) { actx = null; }
}
function blip(freq, dur, type, vol) {
if (!actx) return;
try {
var o = actx.createOscillator();
var g = actx.createGain();
o.type = type || "square";
o.frequency.value = freq;
g.gain.value = 0.0001;
o.connect(g); g.connect(actx.destination);
var t = actx.currentTime;
g.gain.setValueAtTime(0.0001, t);
g.gain.exponentialRampToValueAtTime(vol || 0.15, t + 0.01);
g.gain.exponentialRampToValueAtTime(0.0001, t + dur);
o.start(t);
o.stop(t + dur + 0.02);
} catch (e) {}
}
function crashSound() {
if (!actx) return;
try {
var o = actx.createOscillator();
var g = actx.createGain();
o.type = "sawtooth";
o.frequency.setValueAtTime(220, actx.currentTime);
o.frequency.exponentialRampToValueAtTime(40, actx.currentTime + 0.45);
g.gain.setValueAtTime(0.3, actx.currentTime);
g.gain.exponentialRampToValueAtTime(0.0001, actx.currentTime + 0.5);
o.connect(g); g.connect(actx.destination);
o.start();
o.stop(actx.currentTime + 0.55);
} catch (e) {}
}
function updateEngine() {
if (!actx || !engineGain || !engineOsc) return;
try {
var target = state === STATE_PLAY ? 0.05 : 0.0;
engineGain.gain.setTargetAtTime(target, actx.currentTime, 0.1);
var f = 55 + (speed - baseSpeed) * 0.06 + (boost > 0 ? 40 : 0);
engineOsc.frequency.setTargetAtTime(f, actx.currentTime, 0.08);
engineFilter.frequency.setTargetAtTime(600 + (speed - 200) * 1.2, actx.currentTime, 0.1);
} catch (e) {}
}
function startPlay() {
if (state === STATE_PLAY) return;
state = STATE_PLAY;
ensureAudio();
blip(660, 0.08, "square", 0.12);
}
// ---- spawning ----
var TRAFFIC_COLORS = ["#ff2fd0", "#ffd23f", "#9d5bff", "#ff5a3c", "#3cff8a"];
function spawnTraffic() {
var lane = Math.floor(Math.random() * LANES);
var w = 46, h = 84;
// occasionally a wider slow truck
if (Math.random() < 0.18) { w = 54; h = 110; }
var carSpeed = 60 + Math.random() * 120; // slower than world; downward relative
traffic.push({
lane: lane,
x: laneCenter(lane),
y: -h - 20,
w: w, h: h,
rel: carSpeed,
color: TRAFFIC_COLORS[(Math.random() * TRAFFIC_COLORS.length) | 0]
});
}
function spawnPickup() {
var lane = Math.floor(Math.random() * LANES);
var type = Math.random() < 0.4 ? "boost" : "coin";
pickups.push({
x: laneCenter(lane),
y: -30,
r: 14,
type: type,
t: 0
});
}
function laneOccupiedNear(lane, y, minGap) {
for (var i = 0; i < traffic.length; i++) {
var t = traffic[i];
if (t.lane === lane && Math.abs(t.y - y) < minGap) return true;
}
return false;
}
// ---- particles ----
function addParticles(x, y, color, n, spread) {
for (var i = 0; i < n; i++) {
particles.push({
x: x, y: y,
vx: (Math.random() - 0.5) * spread,
vy: (Math.random() - 0.5) * spread - 30,
life: 0.4 + Math.random() * 0.5,
age: 0,
color: color,
size: 2 + Math.random() * 3
});
}
}
// ---- collision ----
function rectsOverlap(ax, ay, aw, ah, bx, by, bw, bh) {
return Math.abs(ax - bx) * 2 < (aw + bw) && Math.abs(ay - by) * 2 < (ah + bh);
}
// ---- update ----
function update(dt) {
if (state === STATE_PLAY) {
elapsed += dt;
// speed ramps over time
baseSpeed = 260 + elapsed * 9;
if (baseSpeed > 720) baseSpeed = 720;
var boostFactor = boost > 0 ? 1.55 : 1;
// throttle (up/down) small modifier
var throttle = 1;
if (keys.up) throttle = 1.18;
else if (keys.down) throttle = 0.72;
speed = baseSpeed * boostFactor * throttle;
if (boost > 0) boost -= dt;
// steering
var steerLeft = keys.left || touchL;
var steerRight = keys.right || touchR;
var accel = 900;
if (steerLeft && !steerRight) player.vx -= accel * dt;
else if (steerRight && !steerLeft) player.vx += accel * dt;
else player.vx *= Math.pow(0.001, dt); // strong damping when no input
// clamp lateral speed
var maxVx = 420;
if (player.vx > maxVx) player.vx = maxVx;
if (player.vx < -maxVx) player.vx = -maxVx;
player.x += player.vx * dt;
// road clamp
var half = player.w / 2 + 4;
if (player.x < roadLeft + half) { player.x = roadLeft + half; player.vx = 0; }
if (player.x > roadRight - half) { player.x = roadRight - half; player.vx = 0; }
// scroll + distance
scroll += speed * dt;
dashOffset = (dashOffset + speed * dt) % 80;
distance += speed * dt * 0.02; // meters-ish
score += speed * dt * 0.05;
// spawn traffic
spawnTimer -= dt;
var spawnEvery = Math.max(0.35, 1.1 - elapsed * 0.01);
if (spawnTimer <= 0) {
spawnTimer = spawnEvery * (0.6 + Math.random() * 0.8);
// try a lane that isn't crowded near top
var tries = 3;
while (tries-- > 0) {
var lane = Math.floor(Math.random() * LANES);
if (!laneOccupiedNear(lane, -60, 160)) {
var w = 46, h = 84;
if (Math.random() < 0.18) { w = 54; h = 110; }
traffic.push({
lane: lane, x: laneCenter(lane), y: -h - 20,
w: w, h: h, rel: 60 + Math.random() * 120,
color: TRAFFIC_COLORS[(Math.random() * TRAFFIC_COLORS.length) | 0]
});
break;
}
}
}
// spawn pickups
pickTimer -= dt;
if (pickTimer <= 0) {
pickTimer = 3.5 + Math.random() * 3.5;
var plane = Math.floor(Math.random() * LANES);
if (!laneOccupiedNear(plane, -40, 120)) {
pickups.push({
x: laneCenter(plane), y: -30, r: 14,
type: Math.random() < 0.4 ? "boost" : "coin", t: 0
});
}
}
// move traffic (world scrolls down; traffic moves down toward player)
for (var i = traffic.length - 1; i >= 0; i--) {
var t = traffic[i];
t.y += (speed - t.rel) * dt; // relative to player
// engine exhaust trail
if (Math.random() < 0.3) addParticles(t.x, t.y - t.h / 2, "rgba(120,140,180,0.4)", 1, 20);
if (t.y - t.h > H + 40) { traffic.splice(i, 1); continue; }
// collision
if (rectsOverlap(player.x, player.y, player.w - 8, player.h - 10, t.x, t.y, t.w - 8, t.h - 10)) {
gameOver();
return;
}
}
// move pickups
for (var j = pickups.length - 1; j >= 0; j--) {
var p = pickups[j];
p.y += speed * dt;
p.t += dt;
if (p.y - p.r > H + 40) { pickups.splice(j, 1); continue; }
var dx = p.x - player.x, dy = p.y - player.y;
if (Math.abs(dx) < (player.w / 2 + p.r) && Math.abs(dy) < (player.h / 2 + p.r)) {
if (p.type === "boost") {
boost = 2.2;
score += 50;
addParticles(p.x, p.y, "#22e7ff", 18, 220);
blip(880, 0.14, "sawtooth", 0.16);
blip(1320, 0.14, "sawtooth", 0.1);
} else {
score += 100;
addParticles(p.x, p.y, "#ffd23f", 14, 180);
blip(1046, 0.09, "square", 0.14);
}
pickups.splice(j, 1);
}
}
// player exhaust / speed particles
if (Math.random() < (boost > 0 ? 0.9 : 0.4)) {
addParticles(player.x + (Math.random() - 0.5) * 18, player.y + player.h / 2,
boost > 0 ? "#22e7ff" : "rgba(255,120,220,0.6)", 1, 40);
}
}
// particles always update
for (var k = particles.length - 1; k >= 0; k--) {
var pt = particles[k];
pt.age += dt;
if (pt.age >= pt.life) { particles.splice(k, 1); continue; }
pt.x += pt.vx * dt;
pt.y += pt.vy * dt;
pt.vy += 60 * dt;
}
// shake decay
if (shakeMag > 0) { shakeMag -= dt * 40; if (shakeMag < 0) shakeMag = 0; }
updateEngine();
}
function gameOver() {
state = STATE_OVER;
shakeMag = 18;
addParticles(player.x, player.y, "#ff5a3c", 40, 400);
addParticles(player.x, player.y, "#ffd23f", 30, 320);
crashSound();
if (distance > best) {
best = distance;
newBest = true;
try { localStorage.setItem(BEST_KEY, String(Math.floor(best))); } catch (e) {}
}
}
// ---- rendering ----
function drawCar(x, y, w, h, color, isPlayer) {
ctx.save();
ctx.translate(x, y);
// glow
ctx.shadowColor = color;
ctx.shadowBlur = isPlayer ? 22 : 14;
// body
ctx.fillStyle = color;
roundRect(-w / 2, -h / 2, w, h, 10);
ctx.fill();
ctx.shadowBlur = 0;
// cockpit / windows
ctx.fillStyle = "rgba(6,10,20,0.85)";
roundRect(-w / 2 + 7, -h / 2 + 12, w - 14, h * 0.28, 6);
ctx.fill();
roundRect(-w / 2 + 7, h / 2 - h * 0.32, w - 14, h * 0.20, 6);
ctx.fill();
// center stripe
ctx.fillStyle = "rgba(255,255,255,0.18)";
ctx.fillRect(-2, -h / 2 + 6, 4, h - 12);
// headlights/taillights
if (isPlayer) {
ctx.fillStyle = "#fff6c8";
ctx.fillRect(-w / 2 + 5, -h / 2 + 2, 8, 5);
ctx.fillRect(w / 2 - 13, -h / 2 + 2, 8, 5);
} else {
ctx.fillStyle = "#ff3b3b";
ctx.shadowColor = "#ff3b3b"; ctx.shadowBlur = 8;
ctx.fillRect(-w / 2 + 5, h / 2 - 7, 8, 5);
ctx.fillRect(w / 2 - 13, h / 2 - 7, 8, 5);
ctx.shadowBlur = 0;
}
ctx.restore();
}
function roundRect(x, y, w, h, r) {
if (r > w / 2) r = w / 2;
if (r > h / 2) r = 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 draw() {
ctx.save();
// screen shake
if (shakeMag > 0) {
ctx.translate((Math.random() - 0.5) * shakeMag, (Math.random() - 0.5) * shakeMag);
}
// background
ctx.fillStyle = "#05070c";
ctx.fillRect(-40, -40, W + 80, H + 80);
// side "grass"/gradient synth glow bands
var g = ctx.createLinearGradient(0, 0, 0, H);
g.addColorStop(0, "#0c1226");
g.addColorStop(1, "#141a33");
ctx.fillStyle = g;
ctx.fillRect(-40, -40, W + 80, H + 80);
// road surface
var rg = ctx.createLinearGradient(0, 0, 0, H);
rg.addColorStop(0, "#12141f");
rg.addColorStop(1, "#1a1d2e");
ctx.fillStyle = rg;
ctx.fillRect(roadLeft, -40, roadW, H + 80);
// glowing road edges
drawGlowLine(roadLeft, 0, roadLeft, H, "#ff2fd0", 4);
drawGlowLine(roadRight, 0, roadRight, H, "#22e7ff", 4);
// moving dashed lane markers
ctx.save();
ctx.lineWidth = 5;
ctx.setLineDash([32, 48]);
ctx.lineDashOffset = -dashOffset;
ctx.strokeStyle = "rgba(180,200,255,0.55)";
ctx.shadowColor = "rgba(120,150,255,0.5)";
ctx.shadowBlur = 8;
for (var li = 1; li < LANES; li++) {
var lx = roadLeft + laneW * li;
ctx.beginPath();
ctx.moveTo(lx, -40);
ctx.lineTo(lx, H + 40);
ctx.stroke();
}
ctx.restore();
// horizon glow lines (perspective illusion) faint moving cross-lines
ctx.save();
ctx.strokeStyle = "rgba(122,60,255,0.10)";
ctx.lineWidth = 2;
for (var hy = 0; hy < H + 80; hy += 80) {
var yy = (hy - (dashOffset % 80));
ctx.beginPath();
ctx.moveTo(roadLeft, yy);
ctx.lineTo(roadRight, yy);
ctx.stroke();
}
ctx.restore();
// pickups
for (var i = 0; i < pickups.length; i++) {
var p = pickups[i];
var pulse = 1 + Math.sin(p.t * 8) * 0.12;
ctx.save();
ctx.translate(p.x, p.y);
ctx.scale(pulse, pulse);
if (p.type === "boost") {
ctx.shadowColor = "#22e7ff"; ctx.shadowBlur = 18;
ctx.fillStyle = "#22e7ff";
// lightning bolt
ctx.beginPath();
ctx.moveTo(-4, -14); ctx.lineTo(6, -2); ctx.lineTo(0, -2);
ctx.lineTo(4, 14); ctx.lineTo(-6, 0); ctx.lineTo(0, 0);
ctx.closePath();
ctx.fill();
} else {
ctx.shadowColor = "#ffd23f"; ctx.shadowBlur = 16;
ctx.fillStyle = "#ffd23f";
ctx.beginPath();
ctx.arc(0, 0, 12, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = "rgba(120,80,0,0.55)";
ctx.font = "bold 15px sans-serif";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText("$", 0, 1);
}
ctx.restore();
}
// traffic
for (var t = 0; t < traffic.length; t++) {
var c = traffic[t];
drawCar(c.x, c.y, c.w, c.h, c.color, false);
}
// particles
for (var pp = 0; pp < particles.length; pp++) {
var pt = particles[pp];
var a = 1 - pt.age / pt.life;
ctx.globalAlpha = a < 0 ? 0 : a;
ctx.fillStyle = pt.color;
ctx.fillRect(pt.x - pt.size / 2, pt.y - pt.size / 2, pt.size, pt.size);
}
ctx.globalAlpha = 1;
// player
if (state !== STATE_OVER || (Math.floor(elapsed * 10) % 2 === 0)) {
// boost aura
if (boost > 0) {
ctx.save();
ctx.globalAlpha = 0.5 + Math.sin(elapsed * 30) * 0.2;
ctx.shadowColor = "#22e7ff"; ctx.shadowBlur = 30;
ctx.fillStyle = "rgba(34,231,255,0.15)";
roundRect(player.x - player.w / 2 - 6, player.y - player.h / 2 - 6, player.w + 12, player.h + 12, 12);
ctx.fill();
ctx.restore();
}
drawCar(player.x, player.y, player.w, player.h, player.color, true);
}
ctx.restore(); // shake
// HUD
drawHUD();
// overlays
if (state === STATE_MENU) drawMenu();
else if (state === STATE_OVER) drawGameOver();
}
function drawGlowLine(x1, y1, x2, y2, color, width) {
ctx.save();
ctx.strokeStyle = color;
ctx.lineWidth = width;
ctx.shadowColor = color;
ctx.shadowBlur = 16;
ctx.beginPath();
ctx.moveTo(x1, y1);
ctx.lineTo(x2, y2);
ctx.stroke();
ctx.restore();
}
function drawHUD() {
ctx.save();
ctx.textBaseline = "top";
// distance (big)
ctx.textAlign = "left";
ctx.fillStyle = "rgba(180,220,255,0.55)";
ctx.font = "600 13px 'Segoe UI', sans-serif";
ctx.fillText("DISTANCE", 18, 18);
ctx.fillStyle = "#e8f6ff";
ctx.shadowColor = "#22e7ff"; ctx.shadowBlur = 12;
ctx.font = "800 30px 'Segoe UI', sans-serif";
ctx.fillText(Math.floor(distance) + " m", 18, 32);
ctx.shadowBlur = 0;
// score (right)
ctx.textAlign = "right";
ctx.fillStyle = "rgba(255,180,240,0.55)";
ctx.font = "600 13px 'Segoe UI', sans-serif";
ctx.fillText("SCORE", W - 18, 18);
ctx.fillStyle = "#ffe3f8";
ctx.shadowColor = "#ff2fd0"; ctx.shadowBlur = 12;
ctx.font = "800 30px 'Segoe UI', sans-serif";
ctx.fillText(String(Math.floor(score)), W - 18, 32);
ctx.shadowBlur = 0;
// best (top center)
ctx.textAlign = "center";
ctx.fillStyle = "rgba(255,210,63,0.75)";
ctx.font = "700 13px 'Segoe UI', sans-serif";
ctx.fillText("BEST " + Math.floor(best) + " m", W / 2, 22);
// speedometer bar
var spNorm = Math.min(1, (speed - 200) / 560);
var barW = 160, barH = 8, barX = W / 2 - barW / 2, barY = 44;
ctx.fillStyle = "rgba(255,255,255,0.08)";
roundRect(barX, barY, barW, barH, 4); ctx.fill();
var sg = ctx.createLinearGradient(barX, 0, barX + barW, 0);
sg.addColorStop(0, "#22e7ff");
sg.addColorStop(0.6, "#ff2fd0");
sg.addColorStop(1, "#ff5a3c");
ctx.fillStyle = sg;
roundRect(barX, barY, Math.max(6, barW * spNorm), barH, 4); ctx.fill();
if (boost > 0) {
ctx.textAlign = "center";
ctx.fillStyle = "#22e7ff";
ctx.shadowColor = "#22e7ff"; ctx.shadowBlur = 14;
ctx.font = "800 16px 'Segoe UI', sans-serif";
ctx.fillText("⚡ BOOST", W / 2, 58);
ctx.shadowBlur = 0;
}
ctx.restore();
}
function drawTitle(cx, cy) {
ctx.save();
ctx.textAlign = "center";
ctx.textBaseline = "middle";
// "NEON DRIFTER" uppercase, letter-spaced
var title = "N E O N D R I F T E R";
ctx.font = "800 34px 'Segoe UI', sans-serif";
ctx.fillStyle = "#22e7ff";
ctx.shadowColor = "#ff2fd0"; ctx.shadowBlur = 24;
ctx.fillText(title, cx, cy);
ctx.shadowColor = "#22e7ff"; ctx.shadowBlur = 18;
ctx.fillStyle = "#ff2fd0";
ctx.fillText(title, cx + 1.5, cy + 1.5);
ctx.restore();
}
function overlayPanel() {
ctx.save();
ctx.fillStyle = "rgba(5,7,12,0.72)";
ctx.fillRect(0, 0, W, H);
ctx.restore();
}
function drawMenu() {
overlayPanel();
drawTitle(W / 2, H * 0.32);
ctx.save();
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillStyle = "rgba(200,225,255,0.85)";
ctx.font = "600 17px 'Segoe UI', sans-serif";
ctx.fillText("Weave through neon traffic. Don't crash.", W / 2, H * 0.44);
var pulse = 0.6 + Math.sin(elapsed * 4) * 0.4;
ctx.globalAlpha = pulse;
ctx.fillStyle = "#ffd23f";
ctx.shadowColor = "#ffd23f"; ctx.shadowBlur = 14;
ctx.font = "800 22px 'Segoe UI', sans-serif";
ctx.fillText("TAP / PRESS ← → TO START", W / 2, H * 0.55);
ctx.globalAlpha = 1;
ctx.shadowBlur = 0;
ctx.fillStyle = "rgba(160,190,230,0.6)";
ctx.font = "500 14px 'Segoe UI', sans-serif";
ctx.fillText("Arrows / A-D steer • Up/Down throttle", W / 2, H * 0.63);
ctx.fillText("⚡ = boost $ = coin", W / 2, H * 0.67);
ctx.fillText("BEST " + Math.floor(best) + " m", W / 2, H * 0.73);
ctx.restore();
}
function drawGameOver() {
overlayPanel();
ctx.save();
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillStyle = "#ff5a3c";
ctx.shadowColor = "#ff2fd0"; ctx.shadowBlur = 22;
ctx.font = "800 44px 'Segoe UI', sans-serif";
ctx.fillText("CRASHED", W / 2, H * 0.30);
ctx.shadowBlur = 0;
if (newBest) {
ctx.fillStyle = "#ffd23f";
ctx.shadowColor = "#ffd23f"; ctx.shadowBlur = 16;
ctx.font = "800 22px 'Segoe UI', sans-serif";
ctx.fillText("★ NEW BEST ★", W / 2, H * 0.40);
ctx.shadowBlur = 0;
}
ctx.fillStyle = "#e8f6ff";
ctx.font = "700 26px 'Segoe UI', sans-serif";
ctx.fillText(Math.floor(distance) + " m", W / 2, H * 0.47);
ctx.fillStyle = "rgba(200,225,255,0.7)";
ctx.font = "600 16px 'Segoe UI', sans-serif";
ctx.fillText("SCORE " + Math.floor(score), W / 2, H * 0.52);
ctx.fillText("BEST " + Math.floor(best) + " m", W / 2, H * 0.56);
var pulse = 0.6 + Math.sin(elapsed * 5) * 0.4;
ctx.globalAlpha = pulse;
ctx.fillStyle = "#22e7ff";
ctx.shadowColor = "#22e7ff"; ctx.shadowBlur = 14;
ctx.font = "800 20px 'Segoe UI', sans-serif";
ctx.fillText("TAP / PRESS SPACE TO RACE AGAIN", W / 2, H * 0.66);
ctx.globalAlpha = 1;
ctx.restore();
}
// ---- main loop ----
var lastT = 0;
function frame(ts) {
if (!lastT) lastT = ts;
var dt = (ts - lastT) / 1000;
lastT = ts;
if (dt > 0.05) dt = 0.05; // clamp big gaps
if (dt < 0) dt = 0;
// on game over let elapsed keep advancing for pulsing text
if (state === STATE_OVER || state === STATE_MENU) elapsed += dt;
try {
update(dt);
draw();
} catch (e) {
// never throw out of RAF
}
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", boot);
} else {
boot();
}
})();
</script>
</body>
</html>