Run details
- Model
- xai/grok-4.7
- Route
- Command Code CLI
- Reasoning
- provider default
- Limits
- 60 min
- Date
- 2 Oct 2026
- Time
- 76 s
- Size
- 22 kB
- Tokens
- 63,564 in, 8,815 out
- List price
- $0.18 ($2 in / $6 out per M)
- Paid
- Not billed per run (CLI plan)
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). Save it as index.html in the current directory, then stop. Do not ask questions.
Source
22 kB 799 lines Raw
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Breakout</title>
<style>
:root {
--bg: #07060d;
--ink: #f4efe6;
--muted: #9a9186;
--accent: #ff5a3c;
--gold: #ffd166;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
html, body {
height: 100%;
background: var(--bg);
color: var(--ink);
font-family: "Avenir Next", "Segoe UI", system-ui, sans-serif;
overflow: hidden;
}
body {
display: flex;
align-items: center;
justify-content: center;
background:
radial-gradient(900px 500px at 50% -10%, #2a1848 0%, transparent 60%),
radial-gradient(700px 400px at 100% 100%, #1a2a3a 0%, transparent 55%),
var(--bg);
}
#wrap {
position: relative;
width: min(960px, 100vw);
height: min(720px, 100vh);
}
canvas {
display: block;
width: 100%;
height: 100%;
border-radius: 12px;
cursor: none;
box-shadow: 0 20px 80px rgba(0,0,0,.55), inset 0 0 0 1px rgba(255,255,255,.06);
}
#hud {
position: absolute;
left: 18px;
right: 18px;
top: 12px;
display: flex;
justify-content: space-between;
pointer-events: none;
font-variant-numeric: tabular-nums;
letter-spacing: .04em;
text-transform: uppercase;
font-size: 13px;
color: var(--muted);
}
#hud b { color: var(--ink); font-weight: 700; }
#overlay {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
pointer-events: none;
padding: 24px;
}
#overlay h1 {
font-size: clamp(42px, 7vw, 78px);
letter-spacing: -.04em;
line-height: .9;
font-weight: 800;
}
#overlay p {
margin-top: 14px;
max-width: 420px;
color: var(--muted);
font-size: 15px;
line-height: 1.45;
}
#overlay .hint {
margin-top: 22px;
color: var(--gold);
font-size: 13px;
letter-spacing: .12em;
text-transform: uppercase;
}
#flash {
position: absolute;
inset: 0;
pointer-events: none;
opacity: 0;
background: white;
mix-blend-mode: screen;
}
</style>
</head>
<body>
<div id="wrap">
<canvas id="c" width="960" height="720"></canvas>
<div id="hud">
<span>Score <b id="score">0</b></span>
<span>Level <b id="level">1</b></span>
<span>Lives <b id="lives">3</b></span>
</div>
<div id="overlay">
<h1 id="title">BREAKOUT</h1>
<p id="blurb">Clear the bricks. Don't drop the ball. Mouse or arrows to move, click or space to launch.</p>
<div class="hint" id="hint">Click or press space</div>
</div>
<div id="flash"></div>
</div>
<script>
(() => {
const canvas = document.getElementById("c");
const ctx = canvas.getContext("2d");
const W = canvas.width;
const H = canvas.height;
const scoreEl = document.getElementById("score");
const levelEl = document.getElementById("level");
const livesEl = document.getElementById("lives");
const overlay = document.getElementById("overlay");
const titleEl = document.getElementById("title");
const blurbEl = document.getElementById("blurb");
const hintEl = document.getElementById("hint");
const flashEl = document.getElementById("flash");
const LEVELS = [
{ name: "Warmup", rows: [
"........",
".RRRRRR.",
".OOOOOO.",
".YYYYYY.",
".GGGGGG.",
]},
{ name: "Columns", rows: [
"R.O.Y.G.",
"R.O.Y.G.",
"R.O.Y.G.",
"R.O.Y.G.",
"R.O.Y.G.",
"........",
".CCCCCC.",
]},
{ name: "Diamond", rows: [
"...RR...",
"..ROOR..",
".ROYYOR.",
"ROYGGYOR",
".ROYYOR.",
"..ROOR..",
"...RR...",
]},
{ name: "Fortress", rows: [
"SSSSSSSS",
"R......R",
"R.OOOO.R",
"R.O..O.R",
"R.OOOO.R",
"R......R",
"YYYYYYYY",
]},
{ name: "Checker", rows: [
"R.R.R.R.",
".R.R.R.R",
"O.O.O.O.",
".O.O.O.O",
"Y.Y.Y.Y.",
".Y.Y.Y.Y",
"G.G.G.G.",
]},
{ name: "Finale", rows: [
"SS.SS.SS",
"RORORORO",
"YGYGYGYG",
".CCCCCC.",
"YGYGYGYG",
"RORORORO",
"SS.SS.SS",
]},
];
const COLORS = {
R: ["#ff5a3c", "#ff8a6a"],
O: ["#ff8a2b", "#ffb36a"],
Y: ["#ffd166", "#ffe7a8"],
G: ["#3ddc97", "#8ff0c4"],
C: ["#4cc9f0", "#9ae6ff"],
S: ["#c9c4bc", "#f4efe6"],
};
const PADDLE_Y = H - 48;
const BALL_R = 7;
let audioCtx = null;
function ac() {
if (!audioCtx) audioCtx = new (window.AudioContext || window.webkitAudioContext)();
if (audioCtx.state === "suspended") audioCtx.resume();
return audioCtx;
}
function tone(freq, dur, type, gain, slide) {
try {
const a = ac();
const t = a.currentTime;
const o = a.createOscillator();
const g = a.createGain();
o.type = type || "square";
o.frequency.setValueAtTime(freq, t);
if (slide) o.frequency.exponentialRampToValueAtTime(Math.max(40, slide), t + dur);
g.gain.setValueAtTime(gain || 0.08, t);
g.gain.exponentialRampToValueAtTime(0.0001, t + dur);
o.connect(g); g.connect(a.destination);
o.start(t); o.stop(t + dur + 0.02);
} catch (e) {}
}
function noise(dur, gain) {
try {
const a = ac();
const n = a.createBuffer(1, a.sampleRate * dur, a.sampleRate);
const d = n.getChannelData(0);
for (let i = 0; i < d.length; i++) d[i] = (Math.random() * 2 - 1) * (1 - i / d.length);
const s = a.createBufferSource();
s.buffer = n;
const g = a.createGain();
const f = a.createBiquadFilter();
f.type = "highpass";
f.frequency.value = 800;
g.gain.value = gain || 0.15;
s.connect(f); f.connect(g); g.connect(a.destination);
s.start();
} catch (e) {}
}
const keys = new Set();
let pointerX = W / 2;
let pointerInside = false;
window.addEventListener("keydown", (e) => {
if (["ArrowLeft", "ArrowRight", " ", "a", "A", "d", "D"].includes(e.key)) e.preventDefault();
keys.add(e.key);
if (e.key === " " || e.key === "Enter") act();
if (e.key === "p" || e.key === "P") togglePause();
});
window.addEventListener("keyup", (e) => keys.delete(e.key));
canvas.addEventListener("mousemove", (e) => {
const r = canvas.getBoundingClientRect();
pointerX = ((e.clientX - r.left) / r.width) * W;
pointerInside = true;
});
canvas.addEventListener("mouseleave", () => { pointerInside = false; });
canvas.addEventListener("mousedown", (e) => { e.preventDefault(); act(); });
function flash(a) {
flashEl.style.transition = "none";
flashEl.style.opacity = String(a);
requestAnimationFrame(() => {
flashEl.style.transition = "opacity .35s ease";
flashEl.style.opacity = "0";
});
}
let state = "title";
let levelIndex = 0;
let score = 0;
let lives = 3;
let combo = 0;
let shake = 0;
let bricks = [];
let particles = [];
let floaters = [];
let balls = [];
let paddle = { x: W / 2 - 56, w: 112, h: 14, vx: 0 };
let wideTimer = 0;
let slowTimer = 0;
let message = "";
let messageT = 0;
let clearedAnim = 0;
function resetRun() {
score = 0;
lives = 3;
levelIndex = 0;
combo = 0;
loadLevel(0);
syncHud();
}
function brickValue(ch) {
return ch === "S" ? 80 : ch === "C" ? 50 : 20;
}
function brickHp(ch) {
return ch === "S" ? 2 : 1;
}
function loadLevel(i) {
levelIndex = i;
const spec = LEVELS[i];
bricks = [];
const cols = 8;
const margin = 48;
const gap = 8;
const top = 78;
const bw = (W - margin * 2 - gap * (cols - 1)) / cols;
const bh = 24;
spec.rows.forEach((row, r) => {
const cells = row.padEnd(cols, ".").slice(0, cols).split("");
cells.forEach((ch, c) => {
if (ch === ".") return;
const pal = COLORS[ch] || COLORS.R;
bricks.push({
x: margin + c * (bw + gap),
y: top + r * (bh + gap),
w: bw, h: bh,
ch, hp: brickHp(ch), max: brickHp(ch),
color: pal[0], hi: pal[1],
points: brickValue(ch),
alive: true,
});
});
});
wideTimer = 0;
slowTimer = 0;
paddle.w = 112;
serve(false);
message = spec.name;
messageT = 140;
syncHud();
}
function serve(launched) {
balls = [{
x: paddle.x + paddle.w / 2,
y: PADDLE_Y - BALL_R - 2,
vx: launched ? (Math.random() < 0.5 ? -1 : 1) * 4.2 : 0,
vy: launched ? -5.4 : 0,
stuck: !launched,
r: BALL_R,
trail: [],
}];
if (!launched) state = lives === 3 && score === 0 && levelIndex === 0 && overlay.style.display !== "none"
? state : "serve";
}
function launch() {
balls.forEach((b) => {
if (!b.stuck) return;
b.stuck = false;
const dir = (b.x - (paddle.x + paddle.w / 2)) / (paddle.w / 2);
b.vx = 4.2 * (dir || (Math.random() < 0.5 ? -0.4 : 0.4));
b.vy = -5.6;
});
tone(420, 0.08, "square", 0.05, 680);
state = "play";
overlay.style.opacity = "0";
}
function syncHud() {
scoreEl.textContent = String(score);
levelEl.textContent = String(levelIndex + 1);
livesEl.textContent = String(lives);
}
function showOverlay(title, blurb, hint) {
titleEl.textContent = title;
blurbEl.textContent = blurb;
hintEl.textContent = hint;
overlay.style.opacity = "1";
overlay.style.display = "flex";
}
function hideOverlay() {
overlay.style.opacity = "0";
}
function act() {
try { ac(); } catch (e) {}
if (state === "title") {
resetRun();
state = "serve";
showOverlay(LEVELS[0].name, "Level 1 of " + LEVELS.length + ". Launch when you're ready.", "Click or space to launch");
return;
}
if (state === "serve") {
hideOverlay();
launch();
return;
}
if (state === "paused") {
state = "play";
hideOverlay();
return;
}
if (state === "dead" || state === "win") {
resetRun();
state = "serve";
showOverlay(LEVELS[0].name, "Fresh run. Level 1 of " + LEVELS.length + ".", "Click or space to launch");
}
}
function togglePause() {
if (state === "play") {
state = "paused";
showOverlay("Paused", "Take a breath.", "Space to resume");
} else if (state === "paused") {
state = "play";
hideOverlay();
}
}
function burst(x, y, color, n, speed) {
for (let i = 0; i < n; i++) {
const a = Math.random() * Math.PI * 2;
const s = (0.4 + Math.random()) * (speed || 3.2);
particles.push({
x, y,
vx: Math.cos(a) * s,
vy: Math.sin(a) * s - 0.6,
life: 28 + Math.random() * 22,
max: 40,
color,
s: 2 + Math.random() * 3,
});
}
}
function floater(x, y, text, color) {
floaters.push({ x, y, text, color, life: 50, vy: -1.1 });
}
function hitBrick(b, ball) {
if (!b.alive) return;
b.hp -= 1;
shake = Math.min(10, shake + (b.hp <= 0 ? 5 : 2));
if (b.hp > 0) {
tone(180, 0.06, "square", 0.05, 90);
burst(ball.x, ball.y, b.hi, 8, 2);
b.color = "#8d8880";
return;
}
b.alive = false;
combo += 1;
const mult = Math.min(5, 1 + Math.floor((combo - 1) / 3));
const pts = b.points * mult;
score += pts;
syncHud();
burst(b.x + b.w / 2, b.y + b.h / 2, b.hi, 18, 4.2);
floater(b.x + b.w / 2, b.y, "+" + pts + (mult > 1 ? " x" + mult : ""), b.hi);
tone(280 + combo * 28, 0.09, "square", 0.06, 520 + combo * 20);
if (Math.random() < 0.12) spawnPickup(b.x + b.w / 2, b.y + b.h / 2);
if (bricks.every((br) => !br.alive)) levelClear();
}
let pickups = [];
function spawnPickup(x, y) {
const kinds = ["wide", "slow", "multi", "life"];
const kind = kinds[(Math.random() * kinds.length) | 0];
pickups.push({ x, y, vy: 2.1, kind, r: 9 });
}
function applyPickup(p) {
if (p.kind === "wide") {
wideTimer = 600;
paddle.w = 168;
floater(p.x, p.y, "WIDE", "#ffd166");
tone(520, 0.12, "sawtooth", 0.06, 780);
} else if (p.kind === "slow") {
slowTimer = 480;
floater(p.x, p.y, "SLOW", "#4cc9f0");
tone(300, 0.16, "sine", 0.08, 180);
} else if (p.kind === "multi") {
const src = balls[0];
if (src) {
balls.push({ x: src.x, y: src.y, vx: -src.vx || -3, vy: src.vy, stuck: false, r: BALL_R, trail: [] });
balls.push({ x: src.x, y: src.y, vx: src.vx * 0.6 + 2, vy: src.vy * 0.9, stuck: false, r: BALL_R, trail: [] });
}
floater(p.x, p.y, "MULTI", "#ff5a3c");
tone(640, 0.1, "square", 0.06, 900);
} else if (p.kind === "life") {
lives = Math.min(6, lives + 1);
syncHud();
floater(p.x, p.y, "+1 LIFE", "#3ddc97");
tone(660, 0.14, "triangle", 0.08, 880);
}
burst(p.x, p.y, "#fff", 12, 3);
}
function levelClear() {
score += 200 + levelIndex * 50;
syncHud();
flash(0.55);
noise(0.18, 0.2);
tone(523, 0.12, "square", 0.07, 784);
setTimeout(() => tone(659, 0.12, "square", 0.07, 988), 90);
setTimeout(() => tone(784, 0.22, "square", 0.07, 1200), 180);
state = "clear";
clearedAnim = 70;
burst(W / 2, H / 2, "#ffd166", 40, 6);
}
function loseLife() {
lives -= 1;
combo = 0;
syncHud();
shake = 12;
flash(0.35);
noise(0.2, 0.22);
tone(160, 0.25, "sawtooth", 0.07, 60);
if (lives <= 0) {
state = "dead";
showOverlay("Game Over", "Score " + score + ". Level " + (levelIndex + 1) + " of " + LEVELS.length + ".", "Click or space to retry");
return;
}
pickups = [];
wideTimer = 0;
slowTimer = 0;
paddle.w = 112;
serve(false);
state = "serve";
showOverlay("Life lost", lives + (lives === 1 ? " life" : " lives") + " left.", "Click or space to launch");
}
function circleRect(cx, cy, r, rx, ry, rw, rh) {
const nx = Math.max(rx, Math.min(cx, rx + rw));
const ny = Math.max(ry, Math.min(cy, ry + rh));
const dx = cx - nx, dy = cy - ny;
return dx * dx + dy * dy <= r * r;
}
function update() {
if (wideTimer > 0) {
wideTimer--;
if (wideTimer === 0) paddle.w = 112;
}
if (slowTimer > 0) slowTimer--;
const speed = keys.has("Shift") ? 11 : 8.4;
let target = null;
if (keys.has("ArrowLeft") || keys.has("a") || keys.has("A")) target = paddle.x - speed;
else if (keys.has("ArrowRight") || keys.has("d") || keys.has("D")) target = paddle.x + speed;
else if (pointerInside) target = pointerX - paddle.w / 2;
if (target !== null) {
const prev = paddle.x;
paddle.x = Math.max(12, Math.min(W - paddle.w - 12, target));
paddle.vx = paddle.x - prev;
} else {
paddle.vx *= 0.8;
}
if (state === "serve") {
balls.forEach((b) => {
if (b.stuck) {
b.x = paddle.x + paddle.w / 2;
b.y = PADDLE_Y - b.r - 2;
}
});
}
if (state === "play") {
const spdMul = slowTimer > 0 ? 0.62 : 1;
const levelBoost = 1 + levelIndex * 0.045;
balls.forEach((b) => {
if (b.stuck) {
b.x = paddle.x + paddle.w / 2;
b.y = PADDLE_Y - b.r - 2;
return;
}
b.trail.push({ x: b.x, y: b.y });
if (b.trail.length > 10) b.trail.shift();
b.x += b.vx * spdMul * levelBoost;
b.y += b.vy * spdMul * levelBoost;
if (b.x < b.r) { b.x = b.r; b.vx = Math.abs(b.vx); tone(200, 0.04, "square", 0.03); }
if (b.x > W - b.r) { b.x = W - b.r; b.vx = -Math.abs(b.vx); tone(200, 0.04, "square", 0.03); }
if (b.y < b.r + 36) { b.y = b.r + 36; b.vy = Math.abs(b.vy); tone(220, 0.04, "square", 0.03); }
if (b.vy > 0 && circleRect(b.x, b.y, b.r, paddle.x, PADDLE_Y, paddle.w, paddle.h) && b.y < PADDLE_Y + 8) {
const hit = (b.x - (paddle.x + paddle.w / 2)) / (paddle.w / 2);
const sp = Math.hypot(b.vx, b.vy) || 6;
const ang = hit * 1.05;
b.vx = Math.sin(ang) * sp + paddle.vx * 0.18;
b.vy = -Math.abs(Math.cos(ang) * sp);
if (Math.abs(b.vy) < 3.2) b.vy = -3.2;
b.y = PADDLE_Y - b.r - 0.5;
combo = 0;
tone(160, 0.05, "square", 0.05, 240);
burst(b.x, PADDLE_Y, "#f4efe6", 6, 1.6);
}
for (const br of bricks) {
if (!br.alive) continue;
if (!circleRect(b.x, b.y, b.r, br.x, br.y, br.w, br.h)) continue;
const prevX = b.x - b.vx;
const prevY = b.y - b.vy;
const fromLeft = prevX < br.x;
const fromRight = prevX > br.x + br.w;
const fromTop = prevY < br.y;
const fromBot = prevY > br.y + br.h;
if ((fromLeft || fromRight) && !(fromTop || fromBot)) b.vx *= -1;
else if (fromTop || fromBot) b.vy *= -1;
else {
b.vx *= -1;
b.vy *= -1;
}
const cap = 9.2 + levelIndex * 0.3;
const sp = Math.hypot(b.vx, b.vy);
if (sp > cap) { b.vx *= cap / sp; b.vy *= cap / sp; }
if (sp < 4.6) { b.vx *= 4.6 / (sp || 1); b.vy *= 4.6 / (sp || 1); }
hitBrick(br, b);
break;
}
});
balls = balls.filter((b) => b.y < H + 30);
if (balls.length === 0) loseLife();
pickups.forEach((p) => { p.y += p.vy; });
pickups = pickups.filter((p) => {
if (p.y > H + 20) return false;
if (p.y + p.r > PADDLE_Y && p.y - p.r < PADDLE_Y + paddle.h && p.x > paddle.x && p.x < paddle.x + paddle.w) {
applyPickup(p);
return false;
}
return true;
});
}
if (state === "clear") {
clearedAnim--;
if (clearedAnim <= 0) {
if (levelIndex + 1 >= LEVELS.length) {
state = "win";
showOverlay("Cleared", "All " + LEVELS.length + " levels. Score " + score + ".", "Click or space to play again");
tone(880, 0.3, "triangle", 0.08, 1320);
} else {
loadLevel(levelIndex + 1);
state = "serve";
showOverlay(LEVELS[levelIndex].name, "Level " + (levelIndex + 1) + " of " + LEVELS.length + ".", "Click or space to launch");
}
}
}
particles.forEach((p) => {
p.x += p.vx;
p.y += p.vy;
p.vy += 0.06;
p.life--;
});
particles = particles.filter((p) => p.life > 0);
floaters.forEach((f) => { f.y += f.vy; f.life--; });
floaters = floaters.filter((f) => f.life > 0);
if (messageT > 0) messageT--;
shake *= 0.86;
if (shake < 0.2) shake = 0;
}
function roundRect(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 draw() {
const sx = (Math.random() - 0.5) * shake;
const sy = (Math.random() - 0.5) * shake;
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.clearRect(0, 0, W, H);
ctx.save();
ctx.translate(sx, sy);
const g = ctx.createLinearGradient(0, 0, 0, H);
g.addColorStop(0, "#120c1e");
g.addColorStop(1, "#07060d");
ctx.fillStyle = g;
ctx.fillRect(-8, -8, W + 16, H + 16);
ctx.strokeStyle = "rgba(255,255,255,0.04)";
ctx.lineWidth = 1;
for (let x = 40; x < W; x += 40) {
ctx.beginPath(); ctx.moveTo(x, 40); ctx.lineTo(x, H); ctx.stroke();
}
ctx.fillStyle = "rgba(255,255,255,0.05)";
ctx.fillRect(0, 34, W, 2);
for (const br of bricks) {
if (!br.alive) continue;
ctx.fillStyle = br.color;
roundRect(br.x, br.y, br.w, br.h, 5);
ctx.fill();
ctx.fillStyle = br.hi;
roundRect(br.x + 3, br.y + 3, br.w - 6, 5, 2);
ctx.globalAlpha = 0.55;
ctx.fill();
ctx.globalAlpha = 1;
if (br.hp > 1 || br.max > 1 && br.hp < br.max) {
ctx.fillStyle = "rgba(0,0,0,0.25)";
ctx.fillRect(br.x + 8, br.y + br.h - 6, br.w - 16, 2);
}
}
pickups.forEach((p) => {
const col = p.kind === "wide" ? "#ffd166" : p.kind === "slow" ? "#4cc9f0" : p.kind === "life" ? "#3ddc97" : "#ff5a3c";
ctx.fillStyle = col;
ctx.beginPath();
ctx.arc(p.x, p.y, p.r, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = "#07060d";
ctx.font = "bold 11px system-ui";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
const glyph = p.kind === "wide" ? "W" : p.kind === "slow" ? "S" : p.kind === "life" ? "+" : "M";
ctx.fillText(glyph, p.x, p.y + 0.5);
});
const pg = ctx.createLinearGradient(paddle.x, PADDLE_Y, paddle.x, PADDLE_Y + paddle.h);
pg.addColorStop(0, "#fff6ea");
pg.addColorStop(1, "#d7c7b4");
ctx.fillStyle = pg;
ctx.shadowColor = "rgba(255,209,102,0.45)";
ctx.shadowBlur = 16;
roundRect(paddle.x, PADDLE_Y, paddle.w, paddle.h, 7);
ctx.fill();
ctx.shadowBlur = 0;
balls.forEach((b) => {
b.trail.forEach((t, i) => {
ctx.globalAlpha = (i / b.trail.length) * 0.35;
ctx.fillStyle = "#fff";
ctx.beginPath();
ctx.arc(t.x, t.y, b.r * (0.4 + i / b.trail.length * 0.5), 0, Math.PI * 2);
ctx.fill();
});
ctx.globalAlpha = 1;
ctx.fillStyle = "#fff";
ctx.shadowColor = "#ffd166";
ctx.shadowBlur = 18;
ctx.beginPath();
ctx.arc(b.x, b.y, b.r, 0, Math.PI * 2);
ctx.fill();
ctx.shadowBlur = 0;
});
particles.forEach((p) => {
ctx.globalAlpha = Math.max(0, p.life / p.max);
ctx.fillStyle = p.color;
ctx.fillRect(p.x, p.y, p.s, p.s);
});
ctx.globalAlpha = 1;
ctx.textAlign = "center";
floaters.forEach((f) => {
ctx.globalAlpha = Math.max(0, f.life / 50);
ctx.fillStyle = f.color;
ctx.font = "700 16px system-ui";
ctx.fillText(f.text, f.x, f.y);
});
ctx.globalAlpha = 1;
if (messageT > 0 && state === "play") {
ctx.globalAlpha = Math.min(1, messageT / 30);
ctx.fillStyle = "#f4efe6";
ctx.font = "800 28px system-ui";
ctx.fillText(message, W / 2, 200);
ctx.globalAlpha = 1;
}
if (slowTimer > 0 || wideTimer > 0) {
ctx.textAlign = "left";
ctx.font = "12px system-ui";
ctx.fillStyle = "#9a9186";
let lx = 18;
const tags = [];
if (wideTimer > 0) tags.push("WIDE");
if (slowTimer > 0) tags.push("SLOW");
ctx.fillText(tags.join(" · "), 18, H - 16);
}
ctx.restore();
}
overlay.style.transition = "opacity .25s ease";
showOverlay("BREAKOUT", "Clear the bricks. Don't drop the ball. Mouse or arrows to move, click or space to launch.", "Click or press space");
function frame() {
update();
draw();
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
})();
</script>
</body>
</html>