Run details
- Model
- elorian/glimpse
- Route
- OpenRouter API, one user message
- Served by
- Together
- Reasoning
- provider default, 8,590 reasoning tokens
- Limits
- 60 min · 200,000 output tokens ($0.60 at $3/M)
- Date
- 22 Oct 2026
- Time
- 80 s
- Size
- 23 kB
- Tokens
- 282 in, 18,383 out
- List price
- $0.055 ($0.6 in / $3 out per M)
- Paid
- $0.055
Exact prompt
Make a Breakout game I can play right now with the keyboard or the mouse. Give it several levels, a score and some juice. It must be one complete, self-contained HTML file: inline all CSS and JavaScript, and make no network requests of any kind (no CDNs, web fonts or remote images). Reply with the file in a single ```html code block.
Source
23 kB 691 lines Raw
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>NEON BREAKOUT</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body {
height: 100%;
background: radial-gradient(ellipse at 50% 0%, #1a2040 0%, #0a0d1e 60%, #05060f 100%);
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
}
#wrap {
position: relative;
width: min(96vw, calc(96vh * 900 / 640));
aspect-ratio: 900 / 640;
filter: drop-shadow(0 0 40px rgba(80, 120, 255, 0.25));
}
canvas {
width: 100%;
height: 100%;
display: block;
border-radius: 12px;
background: #0a0e20;
cursor: none;
touch-action: none;
}
</style>
</head>
<body>
<div id="wrap"><canvas id="game" width="900" height="640"></canvas></div>
<script>
(function () {
"use strict";
const canvas = document.getElementById("game");
const ctx = canvas.getContext("2d");
const W = 900, H = 640;
const F = "system-ui, -apple-system, 'Segoe UI', sans-serif";
/* ------------------------------------------------------------------ audio */
let actx = null, muted = false;
function audio() {
if (!actx) { try { actx = new (window.AudioContext || window.webkitAudioContext)(); } catch (e) {} }
if (actx && actx.state === "suspended") actx.resume();
return actx;
}
function tone(freq, dur, type, vol, slide) {
const ac = audio();
if (!ac || muted) return;
const t = ac.currentTime;
const o = ac.createOscillator(), g = ac.createGain();
o.type = type || "square";
o.frequency.setValueAtTime(freq, t);
if (slide) o.frequency.exponentialRampToValueAtTime(Math.max(40, freq + slide), t + dur);
g.gain.setValueAtTime(vol || 0.15, t);
g.gain.exponentialRampToValueAtTime(0.0001, t + dur);
o.connect(g); g.connect(ac.destination);
o.start(t); o.stop(t + dur + 0.02);
}
function seq(notes) {
const ac = audio();
if (!ac || muted) return;
const t0 = ac.currentTime;
notes.forEach((n, i) => {
const t = t0 + i * 0.11;
const o = ac.createOscillator(), g = ac.createGain();
o.type = "triangle";
o.frequency.setValueAtTime(n, t);
g.gain.setValueAtTime(0.14, t);
g.gain.exponentialRampToValueAtTime(0.0001, t + 0.22);
o.connect(g); g.connect(ac.destination);
o.start(t); o.stop(t + 0.25);
});
}
const sfx = {
paddle: () => tone(300, 0.08, "square", 0.12, 140),
wall: () => tone(180, 0.05, "triangle", 0.08, -40),
brick: (c) => tone(480 + Math.min(c, 12) * 26, 0.09, "square", 0.13, 120),
tough: () => tone(140, 0.1, "sawtooth", 0.14, -60),
power: () => seq([520, 660, 830]),
life: () => tone(300, 0.5, "sawtooth", 0.16, -220),
clear: () => seq([523, 659, 784, 1047]),
over: () => seq([400, 320, 250, 180]),
win: () => seq([523, 659, 784, 1047, 784, 1047, 1319])
};
/* ------------------------------------------------------------------ levels */
const LEVELS = [
["11111111111","22222222222","33333333333","44444444444","55555555555"],
[".....1.....","....121....","...12321...","..1234321..",".123454321.","12345554321"],
["2.2.2.2.2.2","33333333333","2.2.2.2.2.2","44444444444","S.S.S.S.S.S"],
["55555555555","54444444445","54333333345","5433SSSS345","54333333345","54444444445","55555555555"],
(function(){ const r=[]; for (let y=0;y<7;y++){ let row="";
for (let x=0;x<11;x++){ const v=(x+y)%6;
row += v===0 ? "S" : String(((x+2*y)%5)+1); }
r.push(row); } return r; })(),
["55555555555","4SS44444SS4","444S555S444","3333S5S3333","22222S22222","222S2S2S222"]
];
const COLORS = { "1":"#ff5d8f","2":"#38e8ff","3":"#ffe14d","4":"#7dff6a","5":"#b98cff","S":"#9db8cc" };
const COLS = 11, BRICK_TOP = 92, BRICK_H = 26, GAP = 5;
const BRICK_W = (W - 160) / COLS;
/* ------------------------------------------------------------------ state */
let state = "menu"; // menu | playing | clear | over | win
let levelIdx = 0, score = 0, lives = 3, combo = 0, best = 0;
try { best = parseInt(localStorage.getItem("nb.best")) || 0; } catch (e) {}
const paddle = { x: W/2, y: H-52, w: 120, h: 14, target: W/2, squash: 0 };
let balls = [], bricks = [], particles = [], popups = [], powerups = [];
const effects = { expand: 0, slow: 0 };
let shakeT = 0, shakeMag = 0, flashT = 0, flashCol = "#fff", introT = 0, clearT = 0, tGlobal = 0;
let comboPulse = 0;
const keys = {};
let mouseInside = false;
function baseSpeed() { return Math.min(380 + levelIdx * 24, 520); }
/* ------------------------------------------------------------------ build */
function buildLevel() {
bricks = [];
const rows = LEVELS[levelIdx];
rows.forEach((row, r) => {
for (let c = 0; c < COLS; c++) {
const ch = row[c];
if (ch === "." || !ch) continue;
const tough = ch === "S";
bricks.push({
x: 80 + c * BRICK_W,
y: BRICK_TOP + r * (BRICK_H + GAP),
w: BRICK_W - GAP, h: BRICK_H,
color: COLORS[ch],
hp: tough ? 2 : 1, maxHp: tough ? 2 : 1,
shake: 0
});
}
});
powerups = []; particles = []; popups = [];
balls = [makeStuckBall()];
introT = 1.4;
}
function makeStuckBall() {
return { x: paddle.x, y: paddle.y - 20, r: 9, dx: 0, dy: -1, speed: baseSpeed(), stuck: true, trail: [] };
}
function launch(b) {
const a = (Math.random() - 0.5) * 0.6;
b.dx = Math.sin(a); b.dy = -Math.cos(a); b.stuck = false;
}
function startGame() {
score = 0; lives = 3; combo = 0; levelIdx = 0;
effects.expand = 0; effects.slow = 0;
paddle.w = 120;
buildLevel();
state = "playing";
}
/* ------------------------------------------------------------------ juice */
function shake(mag, dur) { shakeMag = Math.max(shakeMag, mag); shakeT = Math.max(shakeT, dur); }
function flash(col, dur) { flashCol = col; flashT = dur; }
function popup(x, y, text, col) { popups.push({ x, y, text, col, life: 0.9, max: 0.9 }); }
function burst(x, y, col, n) {
for (let i = 0; i < n; i++) {
const a = Math.random() * Math.PI * 2, sp = 60 + Math.random() * 260;
particles.push({
x, y, vx: Math.cos(a) * sp, vy: Math.sin(a) * sp - 40,
life: 0.5 + Math.random() * 0.5, max: 1,
size: 2 + Math.random() * 4, col, g: 420
});
}
}
function sparks(x, y, col, n) {
for (let i = 0; i < n; i++) {
const a = Math.random() * Math.PI * 2, sp = 40 + Math.random() * 120;
particles.push({
x, y, vx: Math.cos(a) * sp, vy: Math.sin(a) * sp,
life: 0.25 + Math.random() * 0.25, max: 0.5,
size: 1.5 + Math.random() * 2, col, g: 0
});
}
}
/* ------------------------------------------------------------------ power-ups */
const PU = [
{ id: "E", col: "#7dff6a", name: "EXPAND" },
{ id: "M", col: "#38e8ff", name: "MULTIBALL" },
{ id: "S", col: "#ffe14d", name: "SLOW-MO" }
];
function maybeDrop(x, y) {
if (Math.random() > 0.16) return;
const t = PU[Math.floor(Math.random() * PU.length)];
powerups.push({ x, y, w: 30, h: 18, vy: 150, type: t, t: 0 });
}
function applyPU(p) {
if (p.type.id === "E") effects.expand = 10;
if (p.type.id === "S") effects.slow = 8;
if (p.type.id === "M") {
const src = balls.filter(b => !b.stuck);
const srcs = src.length ? src : [balls[0]];
srcs.slice(0, 2).forEach(b => {
for (const da of [-0.5, 0.5]) {
const ca = Math.cos(da), sa = Math.sin(da);
const nx = b.dx * ca - b.dy * sa, ny = b.dx * sa + b.dy * ca;
balls.push({ x: b.x, y: b.y, r: b.r, dx: nx, dy: ny, speed: b.speed, stuck: false, trail: [] });
}
});
}
popup(p.x, p.y - 16, p.type.name, p.type.col);
burst(p.x, p.y, p.type.col, 16);
sfx.power();
}
/* ------------------------------------------------------------------ input */
function ptr(e) {
const r = canvas.getBoundingClientRect();
paddle.target = Math.max(paddle.w/2, Math.min(W - paddle.w/2, (e.clientX - r.left) * (W / r.width)));
mouseInside = true;
}
canvas.addEventListener("pointermove", ptr);
canvas.addEventListener("pointerdown", (e) => {
audio(); ptr(e);
if (state === "menu" || state === "over" || state === "win") { startGame(); return; }
if (state === "playing") {
const b = balls.find(b => b.stuck);
if (b) { launch(b); sfx.paddle(); }
}
});
canvas.addEventListener("pointerleave", () => mouseInside = false);
window.addEventListener("keydown", (e) => {
audio();
if (["Space","ArrowLeft","ArrowRight","ArrowUp","ArrowDown"].includes(e.code)) e.preventDefault();
keys[e.code] = true;
if (e.code === "Space") {
if (state === "menu" || state === "over" || state === "win") startGame();
else if (state === "playing") {
const b = balls.find(b => b.stuck);
if (b) { launch(b); sfx.paddle(); }
} else if (state === "paused") state = "playing";
}
if ((e.code === "KeyP" || e.code === "Escape") && (state === "playing" || state === "paused"))
state = state === "playing" ? "paused" : "playing";
if (e.code === "KeyM") muted = !muted;
if (e.code === "KeyR" && (state === "over" || state === "win")) startGame();
});
window.addEventListener("keyup", (e) => keys[e.code] = false);
window.addEventListener("blur", () => { if (state === "playing") state = "paused"; });
/* ------------------------------------------------------------------ update */
function update(dt) {
tGlobal += dt;
if (introT > 0) introT -= dt;
if (shakeT > 0) shakeT -= dt;
if (flashT > 0) flashT -= dt;
if (comboPulse > 0) comboPulse -= dt;
// particles & popups run in most states
if (state === "playing" || state === "clear" || state === "menu") {
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.vy += p.g * dt;
p.x += p.vx * dt; p.y += p.vy * dt;
}
for (let i = popups.length - 1; i >= 0; i--) {
const p = popups[i];
p.life -= dt; p.y -= 42 * dt;
if (p.life <= 0) popups.splice(i, 1);
}
}
if (state === "menu") return;
if (state === "clear") {
clearT += dt;
if (clearT > 1.7) {
levelIdx++;
if (levelIdx >= LEVELS.length) {
state = "win";
saveBest();
sfx.win();
} else { buildLevel(); state = "playing"; }
}
return;
}
if (state !== "playing") return;
/* timers */
for (const k in effects) if (effects[k] > 0) effects[k] = Math.max(0, effects[k] - dt);
/* paddle */
const kb = ((keys["ArrowRight"] || keys["KeyD"] ? 1 : 0) - (keys["ArrowLeft"] || keys["KeyA"] ? 1 : 0));
if (kb) paddle.target = Math.max(0, Math.min(W, paddle.target + kb * 560 * dt));
paddle.target = Math.max(paddle.w/2, Math.min(W - paddle.w/2, paddle.target));
paddle.x += (paddle.target - paddle.x) * Math.min(1, dt * 18);
paddle.w += ((effects.expand > 0 ? 180 : 120) - paddle.w) * Math.min(1, dt * 10);
if (paddle.squash > 0) paddle.squash = Math.max(0, paddle.squash - dt * 6);
const slowF = effects.slow > 0 ? 0.55 : 1;
/* balls */
for (let bi = balls.length - 1; bi >= 0; bi--) {
const b = balls[bi];
if (b.stuck) { b.x = paddle.x; b.y = paddle.y - 20 - b.r; b.trail.length = 0; continue; }
b.trail.push({ x: b.x, y: b.y });
if (b.trail.length > 12) b.trail.shift();
b.x += b.dx * b.speed * slowF * dt;
b.y += b.dy * b.speed * slowF * dt;
/* walls */
if (b.x - b.r < 0) { b.x = b.r; b.dx = Math.abs(b.dx); onWall(b); }
if (b.x + b.r > W) { b.x = W - b.r; b.dx = -Math.abs(b.dx); onWall(b); }
if (b.y - b.r < 0) { b.y = b.r; b.dy = Math.abs(b.dy); onWall(b); }
if (b.y - b.r > H + 20) { balls.splice(bi, 1); continue; }
/* paddle */
if (b.dy > 0 &&
b.x > paddle.x - paddle.w/2 - b.r && b.x < paddle.x + paddle.w/2 + b.r &&
b.y + b.r > paddle.y - paddle.h/2 && b.y - b.r < paddle.y + paddle.h/2) {
const hit = Math.max(-1, Math.min(1, (b.x - paddle.x) / (paddle.w / 2)));
const ang = hit * Math.PI / 3.1;
b.dx = Math.sin(ang); b.dy = -Math.cos(ang);
b.speed = Math.min(b.speed + 4, 620);
b.y = paddle.y - paddle.h/2 - b.r - 0.5;
combo = 0;
paddle.squash = 1;
sparks(b.x, paddle.y - 6, "#38e8ff", 5);
sfx.paddle();
}
/* bricks */
for (let i = bricks.length - 1; i >= 0; i--) {
const br = bricks[i];
const cx = Math.max(br.x, Math.min(b.x, br.x + br.w));
const cy = Math.max(br.y, Math.min(b.y, br.y + br.h));
const dx = b.x - cx, dy = b.y - cy;
if (dx*dx + dy*dy > b.r*b.r) continue;
const penX = (b.r + br.w/2) - Math.abs(b.x - (br.x + br.w/2));
const penY = (b.r + br.h/2) - Math.abs(b.y - (br.y + br.h/2));
if (penX < penY) { b.dx = -b.dx; b.x += (b.dx > 0 ? penX : -penX); }
else { b.dy = -b.dy; b.y += (b.dy > 0 ? penY : -penY); }
br.hp--;
if (br.hp > 0) {
br.shake = 0.18;
sparks(cx, cy, "#dfe9f5", 6);
sfx.tough();
} else {
bricks.splice(i, 1);
combo++;
comboPulse = 0.25;
const mult = Math.min(1 + combo, 10);
const pts = (br.maxHp > 1 ? 100 : 50) * mult;
score += pts;
const bx = br.x + br.w/2, by = br.y + br.h/2;
burst(bx, by, br.color, 16);
burst(bx, by, "#ffffff", 4);
popup(bx, by - 8, "+" + pts, br.color);
maybeDrop(bx, by);
shake(br.maxHp > 1 ? 6 : 3, 0.12);
sfx.brick(combo);
if (bricks.length === 0) {
const bonus = 250 * (levelIdx + 1);
score += bonus;
popup(W/2, H/2 - 40, "LEVEL CLEAR +" + bonus, "#38e8ff");
state = "clear"; clearT = 0;
flash("#38e8ff", 0.35);
sfx.clear();
saveBest();
}
}
break;
}
}
/* power-ups */
for (let i = powerups.length - 1; i >= 0; i--) {
const p = powerups[i];
p.t += dt;
p.y += p.vy * dt;
if (p.y > H + 20) { powerups.splice(i, 1); continue; }
if (p.y + p.h/2 > paddle.y - paddle.h/2 && p.y - p.h/2 < paddle.y + paddle.h/2 &&
p.x + p.w/2 > paddle.x - paddle.w/2 && p.x - p.w/2 < paddle.x + paddle.w/2) {
applyPU(p);
powerups.splice(i, 1);
}
}
/* life lost */
if (balls.length === 0) {
lives--; combo = 0;
flash("#ff4d6a", 0.4);
shake(10, 0.35);
sfx.life();
if (lives <= 0) { state = "over"; saveBest(); sfx.over(); }
else balls = [makeStuckBall()];
}
}
function onWall(b) { sparks(b.x, b.y, "#8ea6ff", 3); sfx.wall(); }
function saveBest() {
if (score > best) { best = score; try { localStorage.setItem("nb.best", best); } catch (e) {} }
}
/* ------------------------------------------------------------------ draw */
function rr(x, y, w, h, r) {
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 drawBackground() {
const g = ctx.createLinearGradient(0, 0, 0, H);
g.addColorStop(0, "#131a3a"); g.addColorStop(0.6, "#0a0e20"); g.addColorStop(1, "#070a18");
ctx.fillStyle = g; ctx.fillRect(0, 0, W, H);
ctx.strokeStyle = "rgba(120,160,255,0.05)"; ctx.lineWidth = 1;
for (let x = 0; x < W; x += 45) { ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, H); ctx.stroke(); }
for (let y = 0; y < H; y += 45) { ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(W, y); ctx.stroke(); }
}
function drawBrick(b) {
const sx = b.shake > 0 ? (Math.random() - 0.5) * 6 * b.shake / 0.18 : 0;
ctx.save();
ctx.shadowColor = b.color; ctx.shadowBlur = 14;
ctx.fillStyle = b.color;
rr(b.x + sx, b.y, b.w, b.h, 5); ctx.fill();
ctx.shadowBlur = 0;
ctx.fillStyle = "rgba(255,255,255,0.18)";
rr(b.x + sx + 2, b.y + 2, b.w - 4, b.h * 0.4, 3); ctx.fill();
if (b.hp < b.maxHp) {
ctx.strokeStyle = "rgba(10,15,30,0.7)"; ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(b.x + sx + b.w * 0.3, b.y + 3);
ctx.lineTo(b.x + sx + b.w * 0.5, b.y + b.h * 0.55);
ctx.lineTo(b.x + sx + b.w * 0.42, b.y + b.h - 3);
ctx.moveTo(b.x + sx + b.w * 0.5, b.y + b.h * 0.55);
ctx.lineTo(b.x + sx + b.w * 0.7, b.y + 5);
ctx.stroke();
}
ctx.restore();
if (b.shake > 0) b.shake -= 1/60;
}
function draw() {
ctx.clearRect(0, 0, W, H);
drawBackground();
ctx.save();
if (shakeT > 0) ctx.translate((Math.random() - 0.5) * shakeMag, (Math.random() - 0.5) * shakeMag);
/* bricks */
for (const b of bricks) drawBrick(b);
/* power-ups */
for (const p of powerups) {
ctx.save();
ctx.translate(p.x, p.y + Math.sin(p.t * 8) * 2);
ctx.shadowColor = p.type.col; ctx.shadowBlur = 16;
ctx.fillStyle = p.type.col;
rr(-p.w/2, -p.h/2, p.w, p.h, p.h/2); ctx.fill();
ctx.shadowBlur = 0;
ctx.fillStyle = "#0a0e20";
ctx.font = "bold 13px " + F;
ctx.textAlign = "center"; ctx.textBaseline = "middle";
ctx.fillText(p.type.id, 0, 1);
ctx.restore();
}
/* balls */
for (const b of balls) {
for (let i = 0; i < b.trail.length; i++) {
const t = b.trail[i], a = i / b.trail.length;
ctx.globalAlpha = a * 0.35;
ctx.fillStyle = "#9fd8ff";
ctx.beginPath(); ctx.arc(t.x, t.y, b.r * a * 0.9, 0, Math.PI * 2); ctx.fill();
}
ctx.globalAlpha = 1;
ctx.save();
ctx.shadowColor = "#6ecbff"; ctx.shadowBlur = 18;
ctx.fillStyle = "#ffffff";
ctx.beginPath(); ctx.arc(b.x, b.y, b.r, 0, Math.PI * 2); ctx.fill();
ctx.shadowBlur = 0;
ctx.fillStyle = "rgba(255,255,255,0.6)";
ctx.beginPath(); ctx.arc(b.x - 3, b.y - 3, b.r * 0.35, 0, Math.PI * 2); ctx.fill();
ctx.restore();
}
/* paddle */
ctx.save();
ctx.translate(paddle.x, paddle.y);
const sq = paddle.squash * 0.25;
ctx.scale(1 + sq, 1 - sq);
ctx.shadowColor = "#38e8ff"; ctx.shadowBlur = 16;
const pg = ctx.createLinearGradient(0, -paddle.h/2, 0, paddle.h/2);
pg.addColorStop(0, "#aef0ff"); pg.addColorStop(0.5, "#38e8ff"); pg.addColorStop(1, "#1e7fbf");
ctx.fillStyle = pg;
rr(-paddle.w/2, -paddle.h/2, paddle.w, paddle.h, paddle.h/2); ctx.fill();
ctx.restore();
/* particles */
for (const p of particles) {
ctx.globalAlpha = Math.max(0, p.life / p.max);
ctx.fillStyle = p.col;
ctx.fillRect(p.x - p.size/2, p.y - p.size/2, p.size, p.size);
}
ctx.globalAlpha = 1;
/* popups */
for (const p of popups) {
const a = Math.min(1, p.life / p.max);
ctx.globalAlpha = a;
ctx.font = "bold 16px " + F;
ctx.textAlign = "center";
ctx.fillStyle = p.col;
ctx.shadowColor = p.col; ctx.shadowBlur = 8;
ctx.fillText(p.text, p.x, p.y);
ctx.shadowBlur = 0;
}
ctx.globalAlpha = 1;
/* floor glow line */
const fg = ctx.createLinearGradient(0, H - 8, 0, H);
fg.addColorStop(0, "rgba(255,80,120,0)"); fg.addColorStop(1, "rgba(255,80,120,0.35)");
ctx.fillStyle = fg; ctx.fillRect(0, H - 8, W, 8);
ctx.restore(); /* shake */
/* flash */
if (flashT > 0) {
ctx.globalAlpha = Math.min(0.35, flashT);
ctx.fillStyle = flashCol; ctx.fillRect(0, 0, W, H);
ctx.globalAlpha = 1;
}
drawHUD();
drawOverlay();
}
function drawHUD() {
ctx.textBaseline = "alphabetic";
/* score */
ctx.textAlign = "left";
ctx.font = "bold 26px " + F;
ctx.fillStyle = "#ffffff";
ctx.fillText("SCORE " + score, 20, 38);
ctx.font = "12px " + F;
ctx.fillStyle = "rgba(160,180,220,0.8)";
ctx.fillText("BEST " + Math.max(best, score), 22, 56);
/* level */
ctx.textAlign = "center";
ctx.font = "bold 15px " + F;
ctx.fillStyle = "#8ea6ff";
ctx.fillText("LEVEL " + Math.min(levelIdx + 1, LEVELS.length) + " / " + LEVELS.length, W/2, 34);
/* combo */
if (combo > 1) {
const s = 1 + (comboPulse > 0 ? comboPulse * 1.6 : 0);
ctx.save();
ctx.translate(W/2, 62); ctx.scale(s, s);
ctx.font = "bold 17px " + F;
ctx.fillStyle = "#ffe14d";
ctx.shadowColor = "#ffe14d"; ctx.shadowBlur = 10;
ctx.fillText("COMBO ×" + Math.min(1 + combo, 10), 0, 0);
ctx.restore();
}
/* lives */
ctx.textAlign = "right";
ctx.font = "22px " + F;
ctx.fillStyle = "#ff5d8f";
ctx.shadowColor = "#ff5d8f"; ctx.shadowBlur = 8;
let hs = "";
for (let i = 0; i < lives; i++) hs += "\u2665 ";
ctx.fillText(hs, W - 20, 38);
ctx.shadowBlur = 0;
/* active effects */
ctx.textAlign = "right";
ctx.font = "11px " + F;
let ey = 58;
if (effects.expand > 0) { ctx.fillStyle = "#7dff6a"; ctx.fillText("EXPAND " + effects.expand.toFixed(1) + "s", W - 20, ey); ey += 15; }
if (effects.slow > 0) { ctx.fillStyle = "#ffe14d"; ctx.fillText("SLOW-MO " + effects.slow.toFixed(1) + "s", W - 20, ey); }
/* sound hint */
ctx.textAlign = "left";
ctx.font = "11px " + F;
ctx.fillStyle = "rgba(160,180,220,0.45)";
ctx.fillText(muted ? "SOUND: OFF (M)" : "SOUND: ON (M)", 20, H - 12);
/* level intro */
if (introT > 0 && state === "playing") {
const a = Math.min(1, introT / 0.5);
ctx.globalAlpha = a;
ctx.textAlign = "center";
ctx.font = "bold 54px " + F;
ctx.fillStyle = "#38e8ff";
ctx.shadowColor = "#38e8ff"; ctx.shadowBlur = 24;
ctx.fillText("LEVEL " + (levelIdx + 1), W/2, H/2 - 60);
ctx.shadowBlur = 0;
ctx.globalAlpha = 1;
}
}
function centerText(txt, y, size, col, glow) {
ctx.textAlign = "center";
ctx.font = "bold " + size + "px " + F;
if (glow) { ctx.shadowColor = glow; ctx.shadowBlur = 22; }
ctx.fillStyle = col;
ctx.fillText(txt, W/2, y);
ctx.shadowBlur = 0;
}
function drawOverlay() {
if (state === "clear") {
centerText("LEVEL " + (levelIdx + 1) + " CLEARED", H/2, 46, "#38e8ff", "#38e8ff");
return;
}
if (state === "paused") {
ctx.fillStyle = "rgba(5,8,20,0.7)"; ctx.fillRect(0, 0, W, H);
centerText("PAUSED", H/2 - 20, 52, "#ffffff", "#8ea6ff");
ctx.font = "16px " + F; ctx.fillStyle = "rgba(200,215,255,0.8)"; ctx.textAlign = "center";
ctx.fillText("press P / Esc or Space to resume", W/2, H/2 + 22);
return;
}
if (state !== "menu" && state !== "over" && state !== "win") return;
ctx.fillStyle = "rgba(5,8,20,0.78)"; ctx.fillRect(0, 0, W, H);
if (state === "menu") {
const hue = (tGlobal * 40) % 360;
centerText("NEON BREAKOUT", H/2 - 110, 64, "#ffffff", "hsl(" + hue + ",100%,70%)");
ctx.font = "16px " + F; ctx.textAlign = "center";
ctx.fillStyle = "rgba(200,215,255,0.85)";
ctx.fillText("Move with MOUSE or \u2190 \u2192 / A D keys", W/2, H/2 - 30);
ctx.fillText("Launch with CLICK or SPACE \u00b7 Pause with P \u00b7 Mute with M", W/2, H/2 - 2);
ctx.fillText("Break every brick \u00b7 Chain hits for combo multipliers \u00b7 Catch power-ups", W/2, H/2 + 26);
if (best > 0) {
ctx.fillStyle = "#ffe14d"; ctx.font = "bold 15px " + F;
ctx.fillText("BEST " + best, W/2, H/2 + 66);
}
}
if (state === "over") {
centerText("GAME OVER", H/2 - 80, 58, "#ff5d8f", "#ff5d8f");
centerText("SCORE " + score, H/2 - 20, 28, "#ffffff");
centerText("BEST " + Math.max(best, score), H/2 + 16, 16, "#ffe14d");
}
if (state === "win") {
centerText("YOU WIN!", H/2 - 80, 64, "#7dff6a", "#7dff6a");
centerText("ALL 6 LEVELS CLEARED", H/2 - 30, 20, "#ffffff");
centerText("SCORE " + score, H/2 + 8, 28, "#ffffff");
centerText("BEST " + Math.max(best, score), H/2 + 44, 16, "#ffe14d");
}
const blink = 0.5 + 0.5 * Math.sin(tGlobal * 5);
ctx.globalAlpha = blink;
ctx.font = "bold 20px " + F; ctx.textAlign = "center";
ctx.fillStyle = "#38e8ff";
ctx.fillText(state === "menu" ? "PRESS SPACE OR CLICK TO START" : "PRESS SPACE / R OR CLICK TO PLAY AGAIN", W/2, H/2 + 120);
ctx.globalAlpha = 1;
}
/* ------------------------------------------------------------------ loop */
let last = performance.now();
function frame(now) {
const dt = Math.min(0.033, (now - last) / 1000);
last = now;
update(dt);
draw();
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
})();
</script>
</body>
</html>

