Hamburger breakoutOne-line
Claude Haiku 5.5
by AnthropicReasoning1 console error
Running live in a sandboxed frame. Click inside it before using the keyboard.
How it went
- Total time
- 4 min 9 s
- Cost
- $0.029
- Output tokens
- 58,400
- Spent thinking
- 47,378 tokens
- Speed
- 236.8 tok/s
- Console errors
- 1
- First token after
- 1.9 s
- First visible text after
- 3 min 39 s
- Prompt tokens
- 542
- Code size
- 20.5 KB
- Finish reason
- stop
- Served by
- Claude Platform on AWS
- Model id
anthropic/claude-haiku-5.5- Reasoning sent
{"effort":"high"}- Max output
- 128,000 tokens
- Generated
- Nov 10, 2026
Console errors
Caught in the first 3 seconds after load, in a sandboxed iframe like the one above.
Failed to load resource: the server responded with a status of 404 (Not Found)
What it was asked
The prompt
“i want a breakout game where the paddle is a hamburger and every brick you break explodes into tons of little hearts”
System prompt, sent with every prompt
You are generating a self-contained web demo. It will be rendered automatically in a sandboxed iframe and compared side by side with the output of other AI models given the exact same task. Output rules: - Reply with exactly ONE fenced code block containing the complete file: ```html (or ```svg when the task asks for an SVG). No text before or after the code block. - Everything must be inline: CSS in <style>, JavaScript in <script>. No external files, images, fonts or network requests, except the CDN URLs explicitly allowed by the task. - Do not use localStorage, sessionStorage, cookies, IndexedDB, alert(), confirm(), prompt() or window.open(): they are blocked in the sandbox and will throw. - The page must look right in a 1280×800 viewport and adapt gracefully down to 375px wide. No horizontal scrollbar. - Something meaningful must be visible on first paint: a screenshot is taken automatically about 3 seconds after load, without any interaction. - Everything that makes the demo interesting must be visible within the first seconds: nothing hidden behind a high score, a later level or a long play session. - Games must start in a self-playing demo mode (a simple AI plays) behind a "Click or press a key to start" message, so the game can be judged in action without playing. - The iframe only receives keyboard events after the user clicks it: if the demo uses the keyboard, display a clear "Click or press a key to start" message. - The code must run with zero errors in the console of the latest Chrome.
Code run-1.html · 704 lines
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Heartbreakout</title>
<style>
:root { color-scheme: dark; }
* { box-sizing: border-box; }
html, body { margin: 0; height: 100%; overflow: hidden; }
body {
display: flex; align-items: center; justify-content: center;
background: radial-gradient(ellipse at 50% 35%, #3d1234 0%, #1b0716 55%, #0b0309 100%);
font-family: system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
color: #fff5f9;
-webkit-font-smoothing: antialiased;
}
#stage { position: relative; }
canvas {
display: block; border-radius: 14px;
box-shadow: 0 12px 50px rgba(255,64,128,.28), 0 0 0 2px rgba(255,130,170,.2);
touch-action: none; user-select: none; -webkit-user-select: none;
}
#overlay {
position: absolute; left: 50%; top: 60%;
transform: translate(-50%, -50%);
width: min(80%, 300px);
padding: 16px 18px;
text-align: center; line-height: 1.35;
background: rgba(24, 5, 18, .6);
border: 1px solid rgba(255,140,180,.35);
border-radius: 16px;
pointer-events: none;
transition: opacity .35s ease;
}
#overlay.hidden { opacity: 0; }
#title {
margin: 0 0 6px; font-size: 26px; font-weight: 900; letter-spacing: .5px;
color: #ff5c8a; text-shadow: 0 2px 0 #6b0f2b;
}
#sub { margin: 4px 0; font-size: 13px; opacity: .8; }
#prompt {
margin: 8px 0 0; font-size: 16px; font-weight: 800; color: #fff;
animation: pulse 1.3s ease-in-out infinite;
}
@keyframes pulse { 50% { opacity: .4; } }
@media (max-width: 420px) {
#title { font-size: 22px; }
#prompt { font-size: 14px; }
#sub { font-size: 12px; }
}
</style>
</head>
<body>
<div id="stage">
<canvas id="game" aria-label="Heartbreakout: a hamburger paddle breaks heart-shaped bricks"></canvas>
<div id="overlay">
<h1 id="title">Heartbreakout</h1>
<p id="sub">Mouse, touch or ← → to move · click or space to launch</p>
<p id="prompt">Click or press a key to start</p>
</div>
</div>
<script>
(() => {
const W = 480, H = 640;
const PAD = 14, GAP = 4, COLS = 11, BH = 16, BTOP = 86;
const PW = 112, PH = 34, PY = H - 58;
const R = 7.5, TAU = Math.PI * 2;
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
const overlay = document.getElementById('overlay');
const titleEl = document.getElementById('title');
const subEl = document.getElementById('sub');
const promptEl = document.getElementById('prompt');
const BRICK_COLORS = [
['#ff8fb0', '#e0306a'], ['#ffa3c0', '#f0467f'], ['#ff6d98', '#c81d55'],
['#ffb8cc', '#ff5d8c'], ['#ff7aa3', '#d62f66']
];
const HEART_COLORS = ['#ff4d6d', '#ff2e63', '#ff6b9a', '#ff8fb1', '#ffc2d4', '#ff1744', '#ff9ec0', '#ffffff'];
const SEEDS = [[0.16, 5.5], [0.3, 3.5], [0.44, 2.8], [0.58, 3], [0.72, 3.6], [0.86, 5.2]];
const LEFT = ['ArrowLeft', 'a', 'A'];
const RIGHT = ['ArrowRight', 'd', 'D'];
const LAUNCH = [' ', 'ArrowUp', 'w', 'W'];
let dpr = 1;
let mode = 'demo'; // 'demo' | 'play' | 'over'
let lives = 3, score = 0, level = 1;
let bricks = [], bricksTotal = 1, bricksLeft = 1;
let attached = true, attachT = 0, t = 0;
const ball = { x: W / 2, y: PY - R - 1, vx: 0, vy: 0, trail: [] };
const paddle = { x: (W - PW) / 2 };
let mouseX = W / 2, mouseActive = false, keyDir = 0;
let overTimer = 0, levelPause = 0, shake = 0, aiOffset = 0;
let hearts = [];
const bgHearts = [];
let overlayKey = '';
for (let i = 0; i < 14; i++) {
bgHearts.push({
x: Math.random() * W, y: Math.random() * H,
s: 4 + Math.random() * 10, sp: 10 + Math.random() * 22,
a: 0.05 + Math.random() * 0.09, ph: Math.random() * TAU
});
}
const clamp = (v, a, b) => Math.max(a, Math.min(b, v));
function fit() {
dpr = Math.min(window.devicePixelRatio || 1, 2);
let h = Math.min(window.innerHeight - 24, 660);
let w = h * W / H;
const maxW = window.innerWidth - 16;
if (w > maxW) w = maxW;
w = Math.max(200, Math.floor(w));
h = Math.floor(w * H / W);
canvas.style.width = w + 'px';
canvas.style.height = h + 'px';
canvas.width = Math.round(w * dpr);
canvas.height = Math.round(h * dpr);
}
// ---------- Level layouts ----------
function inMask(kind, r, c, rows, cols) {
if (kind === 1) return true; // solid wall
if (kind === 2) { // diamond
return Math.abs(c - (cols - 1) / 2) / ((cols - 1) / 2 + 0.5) +
Math.abs(r - (rows - 1) / 2) / ((rows - 1) / 2 + 0.5) <= 1;
}
// heart (implicit curve (x^2+y^2-1)^3 - x^2 y^3 <= 0)
const x = (c - (cols - 1) / 2) / (cols / 2) * 1.3;
const y = ((rows - 1) / 2 - r) / (rows / 2) * 1.3;
const f = x * x + y * y - 1;
return f * f * f - x * x * y * y * y <= 0;
}
function buildLevel() {
bricks = [];
const kind = (level - 1) % 3;
const rows = kind === 0 ? 9 : kind === 1 ? 5 : 7;
const cols = COLS;
const bw = (W - PAD * 2 - (cols - 1) * GAP) / cols;
for (let r = 0; r < rows; r++) {
for (let c = 0; c < cols; c++) {
if (!inMask(kind, r, c, rows, cols)) continue;
bricks.push({
x: PAD + c * (bw + GAP), y: BTOP + r * (BH + GAP),
w: bw, h: BH, alive: true, c: BRICK_COLORS[r % BRICK_COLORS.length]
});
}
}
bricksTotal = bricks.length;
bricksLeft = bricksTotal;
}
// ---------- Particles ----------
function burst(x, y, n, power) {
for (let i = 0; i < n; i++) {
const a = Math.random() * TAU;
const sp = (60 + Math.random() * 240) * power;
hearts.push({
x, y,
vx: Math.cos(a) * sp,
vy: Math.sin(a) * sp - 60 * power,
life: 0, max: 1.1 + Math.random() * 1.3,
size: 2.5 + Math.random() * 4.5,
rot: Math.random() * TAU, vr: (Math.random() - 0.5) * 8,
col: HEART_COLORS[(Math.random() * HEART_COLORS.length) | 0],
ph: Math.random() * TAU
});
}
if (hearts.length > 1400) hearts.splice(0, hearts.length - 1400);
}
function heartAt(x, y, s, rot) {
ctx.save();
ctx.translate(x, y);
ctx.rotate(rot);
ctx.beginPath();
ctx.moveTo(0, -0.4 * s);
ctx.bezierCurveTo(-0.25 * s, -1.0 * s, -1.0 * s, -0.9 * s, -1.0 * s, -0.2 * s);
ctx.bezierCurveTo(-1.0 * s, 0.35 * s, -0.5 * s, 0.7 * s, 0, 1.0 * s);
ctx.bezierCurveTo(0.5 * s, 0.7 * s, 1.0 * s, 0.35 * s, 1.0 * s, -0.2 * s);
ctx.bezierCurveTo(1.0 * s, -0.9 * s, 0.25 * s, -1.0 * s, 0, -0.4 * s);
ctx.closePath();
ctx.fill();
ctx.restore();
}
function rr(x, y, w, h, r) {
ctx.beginPath();
ctx.moveTo(x + r, y);
ctx.lineTo(x + w - r, y);
ctx.quadraticCurveTo(x + w, y, x + w, y + r);
ctx.lineTo(x + w, y + h - r);
ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
ctx.lineTo(x + r, y + h);
ctx.quadraticCurveTo(x, y + h, x, y + h - r);
ctx.lineTo(x, y + r);
ctx.quadraticCurveTo(x, y, x + r, y);
ctx.closePath();
}
// ---------- Game flow ----------
function attach() {
attached = true;
attachT = t;
ball.vx = 0; ball.vy = 0;
ball.trail = [];
ball.x = paddle.x + PW / 2;
ball.y = PY - R - 1;
}
function ballSpeed() {
const frac = bricksTotal ? 1 - bricksLeft / bricksTotal : 0;
return Math.min(560, 250 + (level - 1) * 12 + frac * 90);
}
function launch() {
if (!attached || levelPause > 0) return;
attached = false;
const sp = ballSpeed();
const a = (Math.random() * 2 - 1) * 0.45;
ball.vx = Math.sin(a) * sp;
ball.vy = -Math.cos(a) * sp;
ball.trail = [];
}
function newGame() {
score = 0; lives = 3; level = 1; levelPause = 0;
buildLevel();
attach();
}
function nextLevel() {
level++;
buildLevel();
attach();
}
function startPlay() {
mode = 'play';
newGame();
setOverlay();
}
function lostBall() {
shake = Math.max(shake, 0.25);
if (lives > 0) lives--;
if (lives <= 0 && mode === 'play') {
mode = 'over';
overTimer = 4.5;
setOverlay();
}
if (lives <= 0 && mode === 'demo') {
newGame();
return;
}
attach();
}
function breakBrick(b) {
b.alive = false;
bricksLeft--;
score += 10;
burst(b.x + b.w / 2, b.y + b.h / 2, 34, 1);
shake = Math.max(shake, 0.12);
if (bricksLeft <= 0 && levelPause <= 0) {
score += 100 * level;
levelPause = 2.4;
attach();
for (let i = 0; i < 8; i++) {
burst(40 + Math.random() * (W - 80), 120 + Math.random() * 340, 30, 1.2);
}
shake = 0.3;
}
}
function onPaddleHit() {
ball.y = PY - R;
const hp = clamp((ball.x - (paddle.x + PW / 2)) / (PW / 2), -1, 1);
const sp = ballSpeed();
const ang = hp * 1.05;
ball.vx = sp * Math.sin(ang);
ball.vy = -sp * Math.cos(ang);
aiOffset = (Math.random() * 2 - 1) * PW * 0.4;
burst(ball.x, PY + 2, 6, 0.5);
}
function predictX() {
const tt = (PY - R - ball.y) / ball.vy;
const x = ball.x + ball.vx * tt;
const L = R, Rr = W - R, span = Rr - L;
let u = (x - L) % (2 * span);
if (u < 0) u += 2 * span;
if (u > span) u = 2 * span - u;
return L + u;
}
function stepBall(s) {
if (attached) return;
ball.x += ball.vx * s;
ball.y += ball.vy * s;
if (ball.x < R) { ball.x = R; ball.vx = Math.abs(ball.vx); }
else if (ball.x > W - R) { ball.x = W - R; ball.vx = -Math.abs(ball.vx); }
if (ball.y < R) { ball.y = R; ball.vy = Math.abs(ball.vy); }
// paddle
if (ball.vy > 0 && ball.y + R >= PY && ball.y + R <= PY + PH &&
ball.x >= paddle.x - R * 0.6 && ball.x <= paddle.x + PW + R * 0.6) {
onPaddleHit();
return;
}
if (ball.y > H + R * 2) { lostBall(); return; }
// bricks
for (const b of bricks) {
if (!b.alive) continue;
const cx = clamp(ball.x, b.x, b.x + b.w);
const cy = clamp(ball.y, b.y, b.y + b.h);
const dx = ball.x - cx, dy = ball.y - cy;
if (dx * dx + dy * dy < R * R) {
const oL = ball.x + R - b.x;
const oR = b.x + b.w - (ball.x - R);
const oT = ball.y + R - b.y;
const oB = b.y + b.h - (ball.y - R);
const m = Math.min(oL, oR, oT, oB);
if (m === oL || m === oR) ball.vx = -ball.vx;
else ball.vy = -ball.vy;
breakBrick(b);
break;
}
}
}
function normalizeSpeed() {
const sp = ballSpeed();
const cur = Math.hypot(ball.vx, ball.vy) || 1;
let vx = ball.vx * sp / cur;
let vy = ball.vy * sp / cur;
const minVy = sp * 0.28;
if (Math.abs(vy) < minVy) {
vy = (vy < 0 ? -1 : 1) * minVy;
vx = (vx < 0 ? -1 : 1) * Math.sqrt(Math.max(0, sp * sp - vy * vy));
}
ball.vx = vx; ball.vy = vy;
}
function aiPaddle(dt) {
let target;
if (attached) {
target = W / 2 + Math.sin(t * 0.9) * 90;
} else if (ball.vy > 0) {
target = predictX() + aiOffset;
} else {
target = W / 2 + Math.sin(t * 0.6) * 70;
}
const desired = clamp(target - PW / 2, 0, W - PW);
const step = clamp(desired - paddle.x, -700 * dt, 700 * dt);
paddle.x += step;
}
function setOverlay() {
const key = mode === 'play' ? 'play' : (mode === 'over' ? 'over|' + score : 'attract');
if (key !== overlayKey) {
overlayKey = key;
if (mode === 'over') {
titleEl.textContent = 'Game over';
subEl.textContent = 'Score ' + score + ' · the burger lives on';
promptEl.textContent = 'Click or press a key to play again';
} else {
titleEl.textContent = 'Heartbreakout';
subEl.textContent = 'Mouse, touch or ← → to move · click or space to launch';
promptEl.textContent = 'Click or press a key to start';
}
}
overlay.classList.toggle('hidden', mode === 'play');
}
// ---------- Update ----------
function update(dt) {
t += dt;
const auto = mode !== 'play';
// paddle
if (auto) {
aiPaddle(dt);
} else if (keyDir) {
paddle.x += keyDir * 640 * dt;
} else if (mouseActive) {
paddle.x += (mouseX - PW / 2 - paddle.x) * Math.min(1, dt * 14);
}
paddle.x = clamp(paddle.x, 0, W - PW);
// level pause
if (levelPause > 0) {
levelPause -= dt;
if (levelPause <= 0) { levelPause = 0; nextLevel(); }
}
// game over timer -> attract mode
if (mode === 'over') {
overTimer -= dt;
if (overTimer <= 0) {
mode = 'demo';
newGame();
setOverlay();
}
}
// ball
if (attached) {
ball.x = paddle.x + PW / 2;
ball.y = PY - R - 1;
if (auto && levelPause <= 0 && t - attachT > 0.7) launch();
} else {
const steps = 3, s = dt / steps;
for (let i = 0; i < steps; i++) stepBall(s);
if (!attached) normalizeSpeed();
}
if (!attached) {
ball.trail.push({ x: ball.x, y: ball.y });
if (ball.trail.length > 12) ball.trail.shift();
}
// particles
for (const h of hearts) {
h.life += dt;
const d = Math.exp(-2.2 * dt);
h.vx *= d;
h.vy = h.vy * d + 120 * dt;
h.x += (h.vx + Math.sin(t * 5 + h.ph) * 14) * dt;
h.y += h.vy * dt;
h.rot += h.vr * dt;
}
hearts = hearts.filter(h => h.life < h.max);
// background drift
for (const b of bgHearts) {
b.y -= b.sp * dt;
b.x += Math.sin(t * 0.8 + b.ph) * 8 * dt;
if (b.y < -20) { b.y = H + 20; b.x = Math.random() * W; }
}
shake = Math.max(0, shake - dt * 0.9);
setOverlay();
}
// ---------- Draw ----------
function drawBurger() {
const x = paddle.x, y = PY, w = PW;
// shadow
ctx.fillStyle = 'rgba(0,0,0,.3)';
ctx.beginPath();
ctx.ellipse(x + w / 2, y + PH + 4, w * 0.46, 4, 0, 0, TAU);
ctx.fill();
// top bun
ctx.fillStyle = '#f2a93b';
ctx.beginPath();
ctx.moveTo(x, y + 11);
ctx.bezierCurveTo(x - 2, y - 2, x + w + 2, y - 2, x + w, y + 11);
ctx.closePath();
ctx.fill();
ctx.fillStyle = 'rgba(255,255,255,.35)';
ctx.beginPath();
ctx.ellipse(x + w * 0.3, y + 4.5, w * 0.14, 1.8, -0.2, 0, TAU);
ctx.fill();
ctx.fillStyle = '#fff6e0';
for (const [f, dy] of SEEDS) {
ctx.beginPath();
ctx.ellipse(x + w * f, y + dy, 2.2, 1.2, 0.5, 0, TAU);
ctx.fill();
}
// lettuce (wavy)
ctx.fillStyle = '#6cd45a';
ctx.beginPath();
ctx.moveTo(x - 3, y + 11);
const n = 8;
for (let i = 0; i < n; i++) {
const x0 = x - 3 + (w + 6) * i / n;
const x1 = x - 3 + (w + 6) * (i + 1) / n;
ctx.quadraticCurveTo((x0 + x1) / 2, y + (i % 2 ? 16.5 : 9.5), x1, y + 11);
}
ctx.lineTo(x + w + 3, y + 14);
ctx.lineTo(x - 3, y + 14);
ctx.closePath();
ctx.fill();
// tomato
ctx.fillStyle = '#e8323c';
rr(x + 1.5, y + 14, w - 3, 4.5, 2.2); ctx.fill();
ctx.fillStyle = 'rgba(255,255,255,.25)';
rr(x + 5, y + 14.8, w * 0.35, 1.2, 0.6); ctx.fill();
// cheese + drips
ctx.fillStyle = '#ffd23f';
ctx.fillRect(x + 1, y + 17.5, w - 2, 3.5);
for (const f of [0.12, 0.3, 0.5, 0.7, 0.88]) {
const len = 2 + ((f * 13) % 3) * 1.2;
rr(x + w * f - 3, y + 19, 6, len, 2.5); ctx.fill();
}
// patty
ctx.fillStyle = '#7b4020';
rr(x + 1, y + 21.5, w - 2, 6.5, 3); ctx.fill();
ctx.fillStyle = 'rgba(0,0,0,.28)';
for (let k = 0; k < 4; k++) ctx.fillRect(x + w * (0.2 + k * 0.18), y + 24, 2.5, 1.5);
// bottom bun
ctx.fillStyle = '#f2a93b';
ctx.beginPath();
ctx.moveTo(x, y + 27.5);
ctx.lineTo(x + w, y + 27.5);
ctx.lineTo(x + w, y + 30);
ctx.quadraticCurveTo(x + w, y + PH, x + w - 5, y + PH);
ctx.lineTo(x + 5, y + PH);
ctx.quadraticCurveTo(x, y + PH, x, y + 30);
ctx.closePath();
ctx.fill();
ctx.fillStyle = 'rgba(255,255,255,.2)';
rr(x + 6, y + 29, w - 12, 1.6, 0.8); ctx.fill();
}
function drawBall() {
for (let i = 0; i < ball.trail.length; i++) {
const p = (i + 1) / ball.trail.length;
ctx.globalAlpha = p * 0.35;
ctx.fillStyle = '#ffd1e0';
ctx.beginPath();
ctx.arc(ball.trail[i].x, ball.trail[i].y, R * p * 0.9, 0, TAU);
ctx.fill();
}
ctx.globalAlpha = 1;
ctx.save();
ctx.shadowColor = 'rgba(255,120,170,.95)';
ctx.shadowBlur = 14;
ctx.fillStyle = '#fff3e2';
ctx.beginPath();
ctx.arc(ball.x, ball.y, R, 0, TAU);
ctx.fill();
ctx.restore();
}
function draw() {
ctx.setTransform(canvas.width / W, 0, 0, canvas.height / H, 0, 0);
ctx.save();
if (shake > 0) {
ctx.translate((Math.random() - 0.5) * shake * 14, (Math.random() - 0.5) * shake * 14);
}
// background
const g = ctx.createLinearGradient(0, 0, 0, H);
g.addColorStop(0, '#2b0b22');
g.addColorStop(1, '#12040d');
ctx.fillStyle = g;
ctx.fillRect(-20, -20, W + 40, H + 40);
// faint drifting hearts
ctx.fillStyle = '#ff4d6d';
for (const b of bgHearts) {
ctx.globalAlpha = b.a;
heartAt(b.x, b.y, b.s, Math.sin(t + b.ph) * 0.4);
}
ctx.globalAlpha = 1;
// bricks
for (const b of bricks) {
if (!b.alive) continue;
const gr = ctx.createLinearGradient(0, b.y, 0, b.y + b.h);
gr.addColorStop(0, b.c[0]);
gr.addColorStop(1, b.c[1]);
ctx.fillStyle = gr;
rr(b.x, b.y, b.w, b.h, 4); ctx.fill();
ctx.fillStyle = 'rgba(255,255,255,.35)';
rr(b.x + 3, b.y + 2, b.w - 6, 3, 1.5); ctx.fill();
}
drawBurger();
drawBall();
// hearts explosion
for (const h of hearts) {
const p = h.life / h.max;
const fade = Math.min(1, (1 - p) * 3.5);
const pop = Math.min(1, h.life * 9);
ctx.globalAlpha = fade;
ctx.fillStyle = h.col;
heartAt(h.x, h.y, h.size * (0.35 + 0.65 * pop) * (1 - 0.3 * p), h.rot);
}
ctx.globalAlpha = 1;
// HUD
ctx.textBaseline = 'top';
ctx.textAlign = 'left';
ctx.fillStyle = 'rgba(255,220,235,.75)';
ctx.font = '700 11px system-ui, sans-serif';
ctx.fillText('SCORE', 16, 14);
ctx.fillStyle = '#fff';
ctx.font = '800 22px system-ui, sans-serif';
ctx.fillText(String(score).padStart(5, '0'), 16, 26);
ctx.textAlign = 'center';
ctx.font = '800 13px system-ui, sans-serif';
if (mode === 'play') {
ctx.fillStyle = '#ffd1e0';
ctx.fillText('LEVEL ' + level, W / 2, 16);
} else {
ctx.fillStyle = '#ff5c8a';
ctx.fillText('DEMO · LEVEL ' + level, W / 2, 16);
}
for (let i = 0; i < 3; i++) {
ctx.fillStyle = i < lives ? '#ff4d6d' : 'rgba(255,255,255,.15)';
heartAt(W - 22 - i * 22, 24, 6, 0);
}
// level clear banner
if (levelPause > 0) {
const a = Math.min(1, (2.4 - levelPause) * 2.5);
ctx.globalAlpha = a;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.font = '900 36px system-ui, sans-serif';
ctx.lineWidth = 8;
ctx.lineJoin = 'round';
ctx.strokeStyle = '#8a0f3a';
ctx.strokeText('LEVEL ' + level + ' CLEAR!', W / 2, H * 0.3);
ctx.fillStyle = '#fff';
ctx.fillText('LEVEL ' + level + ' CLEAR!', W / 2, H * 0.3);
ctx.font = '700 16px system-ui, sans-serif';
ctx.fillStyle = '#ffd1e0';
ctx.fillText('+' + (100 * level) + ' bonus · hearts everywhere', W / 2, H * 0.3 + 34);
ctx.globalAlpha = 1;
}
ctx.restore();
}
// ---------- Input ----------
function toLogicalX(clientX) {
const r = canvas.getBoundingClientRect();
return (clientX - r.left) / r.width * W;
}
window.addEventListener('pointermove', e => {
if (mode === 'play') {
mouseX = toLogicalX(e.clientX);
mouseActive = true;
}
});
window.addEventListener('pointerdown', e => {
if (mode === 'play') {
mouseX = toLogicalX(e.clientX);
mouseActive = true;
} else {
startPlay();
}
launch();
});
window.addEventListener('keydown', e => {
if ([...LEFT, ...RIGHT, ...LAUNCH, 'Spacebar'].includes(e.key)) e.preventDefault();
if (LEFT.includes(e.key)) { keyDir = -1; mouseActive = false; }
else if (RIGHT.includes(e.key)) { keyDir = 1; mouseActive = false; }
if (mode !== 'play') startPlay();
if (LAUNCH.includes(e.key)) launch();
});
window.addEventListener('keyup', e => {
if (LEFT.includes(e.key) && keyDir === -1) keyDir = 0;
if (RIGHT.includes(e.key) && keyDir === 1) keyDir = 0;
});
window.addEventListener('blur', () => { keyDir = 0; });
window.addEventListener('resize', fit);
// ---------- Loop ----------
fit();
newGame();
setOverlay();
let last = performance.now();
function frame(now) {
const dt = Math.min(0.033, Math.max(0, (now - last) / 1000));
last = now;
update(dt);
draw();
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
})();
</script>
</body>
</html>Reasoning trace (47,378 tokens)
I'm working out the breakout game's requirements: a hamburger paddle, bricks that burst into hearts, a self-playing demo behind a start prompt, and a canvas that stays responsive down to 375px with no localStorage. I'm settling on a fixed 480x640 logical canvas that scales to fit the screen. I'm sketching the visuals now: a gradient background, bricks in pastel shades, and a paddle drawn as a hamburger with bun, lettuce, tomato, cheese, and patty. For the ball and hearts, I'm leaning toward a simple white sphere and heart particles that burst outward, drift upward, and fade. I'm working out how the canvas should scale: a fixed 480x640 logical size, with the CSS width capped and the height following a 3:4 aspect ratio. I'm also weighing devicePixelRatio scaling so the drawing stays crisp, and considering a JS resize that fits the canvas to the viewport. I'm settling the paddle's size: its collision box stays small and the drawn hamburger extends above and below it, so the visible burger is taller than the hit area. I'm sizing the paddle's hitbox at 26px tall, with the drawn hamburger layered around it so it reaches roughly 40px from the top bun down to the bottom bun. I'll keep the offsets approximate and let the hit area stay smaller than the visible burger. I'm picking brick colors in soft pinks, corals, and lavenders to echo the hearts, and I'm laying out three levels: rows, a heart-shaped arrangement, and a checkerboard that cycles. I'm setting up the particle system now: each brick spawns around 30 hearts that fly outward at varied speeds and spins, drift and fade over a second or so, with the total capped so performance holds. I'm also considering a small heart puff from the paddle on ball contact. I'm thinking about the walls and lives next: the ball is lost when it drops below the bottom, lives are shown as hearts on the right, and the score and level go top-left and top-right on the canvas. For the demo AI, I'm leaning toward tracking the ball with some lag and error, and maybe predicting where it will land by accounting for wall bounces. I'm sizing the AI's predicted landing spot by projecting the ball's path and bouncing it off the walls, then adding a small random offset on each serve. In demo mode the paddle just tracks the ball, and a click or key press will start the real game with three lives at level one. I'm settling the game-over flow: after a short pause it drops back into demo mode with the start overlay, and any key press restarts the real game. Balls will stay attached to the paddle until a click, space, or up arrow launches them, with demo mode launching on its own after a brief delay. Level clears will show a "Level up!" banner with a large heart burst before the next level starts. I'm laying out the brick grid next: nine columns with a small gap and a fixed row height below the HUD, and I'm weighing whether a heart-shaped pattern fits within seven rows. I'm working out a heart-shaped brick layout, so I'm sketching the rows on a small grid and checking which columns stay symmetric around the center. I'm sketching the heart-shaped brick pattern on an 11-column grid, working out which cells in the top row are filled and which are left empty around the center column. I'm replacing the hand-written heart pattern with a procedural one: a heart equation tested on each grid cell, with 11 columns and 8 rows mapped onto the heart's coordinate range. I'm settling on the heart mask: each grid cell gets tested against the implicit heart equation, with the columns and rows mapped onto its coordinate range. I'm considering nine rows rather than eight, since the grid is coarse and the extra row should make the heart read better. I'm checking the heart mask on a small grid: at the center column every row with a height under 1 falls inside the shape, so I'll verify the spacing and confirm there are enough bricks to read as a heart. I'm checking the heart mask at the center column: rows 1 through 7 fall inside the shape, while the top row dips out, which gives the heart its notch. Toward the outer columns the curve narrows, and at about y=0.5 the test still comes out positive, so I'm working out where the edges of the shape fall. I'm checking the heart mask's edge columns at about x=±0.7: the top lobes come out inside the shape, so that part of the layout holds. Next I'll think about the other levels, starting with a rows pattern for level two. I'm weighing which brick style to use: the level layouts will alternate between a heart shape, a checkerboard or pyramid, and full rows that speed up each cycle. I'm leaning toward pink, candy-like bricks with a highlight so they read as bricks while still matching the hearts. I'm picking the visual details now: a deep plum background, pink frosting-style bricks, and a small glowing cream ball. The hamburger will be drawn from the paddle's left edge and top, so the next step is working out its shape. Still drawing the hamburger: a rounded golden top bun with sesame seeds scattered on it, then a wavy lettuce band next. Still drawing the hamburger's layers: a wavy green lettuce band, then a red tomato strip, a cheese drip, a brown patty with grill marks, and a rounded bottom bun. I'm also checking how the paddle's hit area lines up with the bottom bun, since the ball should bounce off the top of it. I'm deciding the hitbox has to match the drawn bun, so the ball meets the dome instead of passing through it, which means the paddle's top edge sits about 16 pixels above where the bun base is. I'm setting the paddle's hitbox top to match the top of the drawn dome, with the hitbox about 30 pixels tall so the ball meets the bun rather than passing through it. I'm working out the collision check for the paddle: the ball counts as hitting when it's falling and overlaps the paddle's top region. The bounce angle will depend on where the ball lands along the paddle, with the horizontal speed clamped so the ball never flies too flat. I'm setting the game-over flow for demo mode: after a lost game, a fresh demo starts with new bricks, and the start overlay stays up. For the real game, I'm planning the "Game over" message with the score and a short delay before it returns to demo. I'm also adding a small "DEMO" tag so it's clear the paddle is playing itself, and I'm about to work on the brick collision check. I'm now writing the collision check for the bricks: I clamp the ball's center to the brick rectangle and test whether the distance is less than the radius. When it hits, I compare the penetration depth on each side and flip whichever velocity component has the smallest overlap. I'm settling the bricks' collision response: whichever overlap is smallest decides whether the ball flips horizontally or vertically, and I'll stop after the first brick hit each substep. Next I'm sketching how the level-up burst and the ball's speed ramp will work. I'm setting up the particle update for the hearts: they get drag and gravity, a slight wobble, and their alpha fades as their life runs out. I'm settling the particle fade: each heart shrinks over its life, with a quick pop at the start, and the particle list is capped at 1500 by dropping the oldest entries. Next I'm setting up the background gradient. I'm adding a few faint, slowly drifting background hearts for atmosphere. The HUD will show the score top-left, the level centered at the top, and lives as small pink hearts on the right. I'm also weighing whether the start overlay should stay up during demo mode, since a full overlay would hide the demo gameplay, so a smaller banner in the middle may work better. I'm settling the demo overlay as a DOM card over the canvas so the game stays visible behind it, and hiding it once play is active. For the demo itself, I want bricks breaking fast enough that hearts show up within the first three seconds, so the ball launches about 0.8 seconds in and travels from the paddle up to the brick rows. I'm setting the demo to launch the ball after about 0.4 seconds with a random angle, and I'm working out how to fit the canvas to the window: taking the height limited to the screen, deriving the width from the 3:4 ratio, and capping it so it fits the viewport width. I'm settling the canvas fit and layout: the title will go in the start overlay card rather than in a DOM header, so the canvas can use the full height. I'm laying out the input handling now: pointer movement on the canvas sets the paddle target, and the mouse is ignored during the demo. Any click or key press turns the demo off, and a click on the canvas starts the game or launches the ball if it's waiting. I'm mapping out the input handling: keyboard arrows and A/D set the paddle's velocity, space launches the ball, and arrow and space keys get their default scrolling blocked. Touch works through pointer movement on the canvas, and the paddle eases toward the target position in play mode. Next I'm defining the game's state modes and the setup flow for starting a level. I'm defining what happens when a game ends: in play, lives hitting zero moves to a game-over state with a 3.5-second timer, after which it drops back to demo with a fresh setup, and a key or click restarts immediately. In demo, a lost game just resets after about a second. The ball will sit attached to the paddle's center until it's launched. I'm writing the demo AI: it launches the ball on its own and aims the paddle at the predicted landing spot, with a small random offset so it occasionally misses. Now I'm starting the predictX function. I'm writing the level layout function now, picking a different brick pattern for each level, with a heart shape, full rows, and a diamond shape. I'm defining the diamond shape for the level layouts, testing which rows and columns fall inside it, and checking that its corners reduce to a single brick at the top and bottom. Next I'm adding a checkerboard pattern and a full-wall layout for the first level. I'm laying out the brick grid now: the level patterns cycle through hearts, full walls, and diamonds, and each row gets its own pink shade. I'm setting the brick width from the 480-pixel canvas with small padding and gaps, and I'm working out the vertical placement of the rows. I'm sizing the bricks with rounded corners drawn by a custom path, a lighter top stripe, and a slightly darker bottom edge. Each brick that breaks throws out 26 to 34 hearts with random directions and speeds, and they fall under gravity and drag, fading out over one to two seconds. I'm sizing the particle burst at about 30 hearts per brick, with the list capped around 1400 so it stays fast. Score goes up 10 per brick plus a level bonus. When the last brick falls, the game pauses into a level-up banner with a heart fountain, then moves on to the next layout. I'm now working out how the ball's speed scales, with a base speed that rises as bricks break and a level bonus, capped at a maximum. After each collision I'll normalize the velocity, and I need to guard against a near-zero vertical component so the ball doesn't get stuck bouncing sideways. I'm tightening the ball-physics rules: if the vertical speed drops too low, I'll clamp it and renormalize so the ball doesn't skid sideways. Walls reflect the ball, and the paddle deflects it based on where it lands, with the horizontal component scaled from the hit position. I still need to make sure the horizontal speed can't reach zero on a paddle hit. I'm defining the overlay as a DOM element with a title and the game's instructions, and I'm still checking how the demo label and the lives display fit on the canvas. I'm settling on the title "Heartbreakout" with a short start prompt, and the overlay will be a centered card with a light dark backdrop so the game stays visible behind it. The card should stay narrow enough that the demo is still easy to watch. I'm making the overlay a smaller card that sits over the demo, with the game-over text and a restart prompt. Pointer events on the overlay will pass through to the canvas, and clicks or key presses anywhere on the page will start a game, since mouse movement only counts during play. I'm wiring up the mouse and keyboard paddle controls: pointer position on the canvas sets the paddle's target, while the arrow keys move it directly and switch off mouse control. Writing the game's CSS and HTML structure now... Writing the page styles now: a dark radial background, a centered stage, and a canvas with rounded corners and a pink glow. Still writing the overlay styles... Still writing styles... Next I'm setting up the overlay text: the title "Heartbreakout," a start prompt, and a line of controls, with a helper that swaps the message for each mode. Then I'm starting the canvas fit logic, which picks a device pixel ratio capped at 2. Still writing the fit logic: I'm sizing the canvas to the window height, capping the width to the screen, and setting the backing store with the device pixel ratio. Still writing the constants and game state: a fixed padding and brick grid, a paddle size and position, and the variables for mode, lives, score, and the ball. Still writing the level layout: I'm defining a grid of rows and columns for the heart, wall, and diamond patterns, and working out where each brick lands in that grid. Still working through the heart-shaped layout: I'm testing which grid cells fall inside the heart curve, especially near the bottom tip. Still writing the heart test, now checking the bottom rows near the tip, where the curve pulls inward. I'm checking the heart's bottom tip: row 6 sits inside the curve and row 7 falls outside, which is fine. I'll keep nine rows, since the bricks are 16 tall with a 20-pixel spacing. I'm working out the brick counts for the diamond layout, which comes to 41 bricks across its rows. Next I'm setting the brick width and spacing from the 480-pixel canvas. Still writing the brick colors and the speed logic: I'm picking each row's pink from a palette and working on how the ball's speed scales with the bricks left. I'm working out the ball's launch: the speed comes from a base that rises with the level and the share of bricks already broken, and the launch angle is picked at random within a narrow range off vertical. When the ball is still attached, it follows the paddle's center. I'm deciding how the paddle moves in each mode: the demo runs the AI, play uses the keys or mouse, and I still need to work out whether the AI keeps moving during game over. I'm dropping the idea of letting the AI keep playing after a game over, since the overlay text would clash with it. Instead, the AI will run the paddle in both the demo and game-over states, with the ball still moving, and after a few seconds the game resets to a fresh demo. Losing a ball during game over won't cost any lives. Still working through the overlay and mode logic: when lives run out during play, the game drops into a short game-over state, and the auto-play demo resets to a fresh game once that timer ends. I'm deciding that the auto-play should only run in demo and game-over modes, so during a level transition or a live game the mouse controls the paddle. During the level-up pause the ball stays attached to the paddle until the timer ends. Still writing the game loop: I'm setting up the update step with the paddle moving on its own in demo and game-over, following the mouse during play, and letting the ball wait on the paddle until it's launched. Still writing the game loop: the ball's step function handles wall bounces, and I'm working on the paddle collision check next. Still writing the collision check: I'm working out when the ball has reached the paddle's top edge, and how to set its bounce angle from where it lands. Still writing the collision code: the paddle hit sets the bounce angle from where the ball lands, with a little random jitter, and a small burst of hearts comes off the paddle. Still writing the collision code: I'm finding the nearest point on each brick to the ball's center, and when that point falls within the radius, I'm working out which side was hit from the overlap distances. Still writing the collision code: I'm checking which side of the brick the ball hit by its smallest overlap, and flipping the matching velocity component. Once the hits are done, I'll normalize the ball's speed. Still writing the collision code: I'm normalizing the ball's speed toward its target each step, keeping a minimum vertical component so it doesn't get stuck flying flat. Then I'm defining the brick-break handler, which marks the brick dead, drops the brick count, adds to the score, and bursts hearts from its center. Still writing the collision code: once the bricks run out, I'm moving into a level-up state, adding a bonus to the score and bursting a larger wave of hearts. Then I'm weighing whether the pause should be its own flag instead of a mode, so the auto-launch logic doesn't fire too early. Still working out the level pause: the auto-launch will wait until the pause timer ends, and in play mode the player launches the ball by clicking. Still writing the AI paddle: I'm having it aim for the predicted ball position with a random offset when the ball is falling, follow the ball when it's attached, and otherwise drift gently side to side. The paddle moves toward that target at a capped speed each frame. I'm sizing the AI's aim: the offset is kept within the paddle's width, so it always catches the ball. To make the demo feel less perfect, I'm adding an occasional lag to the paddle's movement. Still writing the AI's prediction: the next ball position is found by projecting the ball forward to the paddle's row, then folding that x-coordinate back inside the walls. Still writing the particle burst: I'm defining a function that spawns a set number of hearts at a point, each with a random direction and a color from a pink palette. Still writing the particle burst: each heart gets a random direction, speed, size, spin and pink color, and the list is capped so it doesn't grow without bound. Still writing the heart shape: the particles get a gentle downward pull with a side-to-side wobble, and I'm drawing each heart as a path that gets rotated and faded out over its lifetime. Still writing the heart shape: I'm working out the path with a bottom tip and two rounded lobes, using quadratic curves to build the outline. Still writing the heart shape: I'm working out the outline with a dip at the top center and two rounded lobes, sketching the curves in bezier steps. Still writing the heart shape: I'm checking the curve that comes back from the top dip, which closes the outline. Still writing the heart shape: I'm drawing it as a closed path with a notched top and a rounded bottom tip, then rotating and fading it. Next I'm moving on to the brick drawing, starting with a rounded-rectangle path. Still writing the brick drawing: I'm filling each brick with a vertical gradient from a lighter top to a darker bottom, then laying a glossy white strip across the top edge. Still writing the brick colors: I'm weighing a pink palette for the bricks, a shade or two darker than the hearts, so the bursts still read as cohesive. Still writing the paddle: I'm shaping the top bun as a dome across the paddle's width, starting from the paddle's left edge. Still writing the paddle bun: the top dome's curve height depends on where its control points sit, so I'm nudging them until the peak lands right at the paddle's top edge. Still writing the paddle's lettuce: I'm sketching a wavy band across the width just below the bun, with a series of waves to give it a leafy edge. Still writing the lettuce: I'm drawing a wavy edge just below the bun, alternating the wave height across the paddle's width. Still writing the lettuce: I'm drawing a wavy band under the bun, with the top edge alternating between high and low curves, then closing the bottom with a straight edge. Still writing the paddle: I'm placing a tomato slice just under the lettuce, a thin red strip with a light highlight. Still writing the paddle: I'm adding a yellow cheese layer with drips just under the tomato, then a dark patty with grill stripes. Still writing the paddle: I'm adding the bottom bun with rounded corners, then resizing the paddle's height and moving it up so it sits just above the bottom of the screen. Still writing the bun shape: I'm building the bottom bun with rounded lower corners and a lighter stripe across its top, then adding a dark shadow and a soft glow under the paddle. Still writing the HUD: I'm drawing the background hearts that drift upward and wrap around, then adding a screen shake offset and laying out the score label at the top left. Still writing the HUD: I'm placing the score at the top left, the level label in the center (or a pink "DEMO" tag in demo mode), and up to three small hearts for lives at the top right. Still writing the game loop: I'm defining the overlay update, which hides the overlay during active play and shows it otherwise, with the title and prompt set for either a fresh start or a game over. Still writing the game logic: I'm setting up the new-game and next-level routines, resetting score and lives and building the brick layout before attaching the ball to the paddle. Still writing the game logic: I'm handling the end of a game, where losing the last life switches to a game-over state with a short timer, and the ball is reattached and auto-launched. When that timer runs out, or if the demo runs out of lives, I'm restarting a fresh game. Still writing the input handling: I'm tracking pointer movement during play mode so the paddle follows the mouse or touch. Still writing the keyboard handler: I'm wiring keydown so arrow keys and space are captured, and any key press outside play mode starts a new game. Still writing the keyboard handler: I'm setting the left or right direction on keydown, starting a game if one isn't running, and launching the ball on space or up arrow. Still writing the game loop: I'm handling the input and ticking the frame, so the physics runs at a steady step however fast the screen refreshes. I'm also making sure the start state puts the demo ball on the paddle, with a short wait before it auto-launches. Still writing the game loop: I'm building the update step, which moves the paddle either by the AI in demo mode or by the arrow keys during play. Still writing the game loop: I'm moving the paddle toward the mouse with easing, then stepping the level pause, the game-over timer, and attaching the ball to the paddle with an auto-launch in demo mode. Still writing the game loop: I'm stepping the update in small fixed sub-steps so the ball's physics stays steady, then normalizing its speed. I'm also adding the trail, particles, background hearts, and screen shake, and refreshing the overlay only when its state changes. Still writing the game logic: when a ball falls past the bottom, I'm calling a lost-ball handler that shakes the screen, drops a life, and switches to game over when play runs out, or restarts right away in demo mode. Still writing the game logic: I'm finishing the ball's speed normalization, keeping its vertical component from getting too flat so it doesn't stall along the walls. Still writing the particle update: I'm stepping each heart's lifetime forward and removing the ones that have expired. Still writing the particle physics: I'm applying drag to each heart's velocity and a gentle downward pull, so the hearts settle into a slow, floating drift. Still writing the heart particles: I'm sizing each one with a small random scale, letting it pop in and fade out as its lifetime runs down, then drawing it with its own color. Still writing the draw routine: I'm setting the canvas transform, applying a screen shake offset, and then painting the background, background hearts, bricks, paddle, ball, particles, and HUD in that order. Still writing the draw routine: I'm starting the background gradient and drawing the drifting background hearts with a shared helper that takes position, size, rotation, and color. Still writing the draw routine: I'm finishing the heart helper, which translates and rotates the canvas around each point, traces the heart shape, and fills it, then starting the brick loop by drawing only the live bricks with a vertical gradient. Still writing the draw routine: I'm finishing the brick highlights and moving on to the paddle's shadow, then setting up the ball's glow and trail. Still writing the overlay: I'm deciding when the start or game-over panel should show, keyed to whether the game is in play or over. Still writing the overlay: I'm setting the title, subtitle, and prompt text based on whether the game is over or in demo mode, and toggling the overlay hidden whenever play is active. Still writing the overlay: I'm checking whether the game is in play, showing or hiding the start and game-over panel to match, and keeping its text from being rewritten every frame. Still writing the game code: I'm building out the brick color pairs and the constants for the canvas size, paddle, and ball. Still writing the constants: I'm defining the brick color pairs and the heart palette, then setting up the paddle's seed positions. Still writing the level builder: I'm setting up the brick grid for each level's pattern, with the row count changing by level type. Still writing the brick mask: I'm defining the diamond shape for the level where the bricks taper toward the edges, using the distance from the center on both axes. Still writing the brick mask: I'm checking the heart-shaped layout's row and column math, and working out which bricks in the top row fall inside the shape. I'm checking which bricks in the top row fall inside the heart shape, and the edge cells near the center seem to pass the test. I'm checking the heart-shaped brick layout on the top row, and the center column drops out because the inner curve cuts it away. Moving down to the bottom row and the middle row, the bricks there fall inside the shape the way I expected. Checking the bottom rows of the heart layout, the tip narrows to a single brick at the center of row 7, since the neighboring cells fall outside the curve. Still writing the AI paddle: when the ball is attached I'm having it wander around the center, and when the ball is coming down I'm steering toward its predicted landing spot. Still writing the AI logic: I'm making the paddle's aim wobble a little after each hit, so the ball's angle changes from one return to the next. Still writing the level-up banner: I'm setting its fade-in alpha from the remaining pause time, and drawing "LEVEL n CLEAR!" in bold at the upper-middle of the canvas with a pink outline, plus a bonus line. Still writing the HUD: I'm drawing the score at the top left and the level or demo label centered, and then placing the lives as small hearts along the top right. Still writing the HUD: I'm placing the lives as hearts along the top right, filled for the ones left and faded for the ones lost, and I'm going to pick the level label at top center. Still writing the overlay logic: the level-up banner fades in from its remaining pause time, and the HUD shows the score on the left, the level or demo label at top center, and the lives as hearts on the right. Still writing the page markup: I'm finishing the overlay's title and subtitle lines, then the prompt text, and setting the stage to a relative block that holds the canvas. Still writing the game code: I'm setting up the layout constants, the canvas handles, and the overlay elements, and I'm starting on the brick color pairs. Still writing the constants: I'm defining the brick color pairs and heart palette, and setting up the paddle's seed positions and the initial ball and game state. Still writing the fit function: I'm setting the device pixel ratio and working out the height from the window size. Still writing the fit function: I'm capping the canvas height so it fits the window, then working the width out from the fixed 480x640 ratio and clamping it to the screen width. Still writing the pointer handling: I'm mapping the mouse position to logical canvas coordinates and having a pointer press in play mode launch the ball. Still writing the input handling: I'm setting up the pointer events so mouse and touch move the paddle and a press launches the ball, and I'm defining the keyboard key lists for left, right, and launch. Still writing the key handlers: left and right arrows set the paddle direction, and pressing any key during the demo starts the game before a launch key fires the ball. Still writing the keyboard handling: I'm clearing the paddle's direction on key release and on window blur, and switching off the mouse control whenever the arrow or letter keys are used. Pressing a key in demo mode will start a new game, and the launch keys will then send the ball. Next I'm working out the start and new-game flow, including what happens to the ball when a level is cleared. Still writing the breakout logic: when the last brick falls I'm starting the level-clear pause, awarding the bonus, and re-attaching the ball to the paddle with a burst of hearts. Still writing the game logic: I'm adding a guard so the ball stops moving once it's attached to the paddle, and I'm making sure the update loop checks that before stepping, since a brick collision inside the loop can attach it mid-frame. Still writing the stepBall guard: once the ball is attached it returns early, and I'm now handling the wall bounces and the paddle hit check. Still writing the stepBall collision check: the ball needs to be bouncing off the paddle when its bottom edge reaches the paddle's top and it's within the paddle's horizontal span, and I'm checking that the ball's bottom edge stays within the paddle's height. Still writing the collision code: once the ball passes the lost-ball line, it resets, and for each live brick I check whether the ball's circle overlaps the brick's rectangle, then reflect the ball off whichever side it struck first. Still writing the onPaddleHit function: I'm putting the ball just above the paddle, mapping where it struck to an angle between -1 and 1, and building the new velocity from the current ball speed. Still writing the paddle hit: I'm finishing the new velocity from the hit position, and after that I'll add a bit of random aim offset to the AI. Still writing the draw code: I'm drawing the burger's shadow as a flat ellipse beneath the paddle. Still writing the burger: I'm drawing the bun's top curve and a light highlight across it. Still writing the burger: I'm drawing the flat shadow under the paddle, then the bun's curved top with its highlight, and now I'm placing the sesame seeds across the dome. Still writing the lettuce: I'm setting its green fill and tracing a wavy edge across the top of the patty. Still writing the lettuce: I'm finishing the wavy top edge with its alternating dips, then closing the shape along the bottom of the patty. Still writing the tomato: I'm laying down a red rounded strip under the lettuce and starting a light highlight across it. Still writing the cheese: I'm laying out its yellow base and starting the drips along the edge. Still writing the cheese: I'm drawing a flat yellow slab across the patty and adding drips at spaced intervals along its lower edge. Still writing the burger: I'm laying a flat brown patty under the cheese and adding a few dark stripes across it, then starting the bottom bun. Still writing the bottom bun: I'm closing its rounded corners and setting the bottom edge at the paddle's full height. Still writing the particles: I'm drawing each heart with a fade that tracks its remaining life and a pop-in scale at the start. Still writing the heart shape: I'm setting up the transform, starting the path at the top notch, and drawing the first curve down the left side. Still writing the heart shape: I'm finishing its curves on both sides and closing the path, then filling it. Still writing the HUD: I'm drawing the score label and the zero-padded score number in the top corner. Still writing the HUD: I'm centering the level label at the top, showing it as a demo level when the game isn't in play, and drawing the three lives as hearts in the top-right corner. Still writing the level banner: I'm drawing the "LEVEL CLEAR!" text centered on screen, with its fade-in alpha and a dark outline. Still writing the level banner: I'm adding the bonus line beneath the title, then resetting the alpha once the banner is drawn. Now I'm sizing the level banner so "LEVEL 1 CLEAR!" fits comfortably inside the 480-pixel canvas. The overlay card in demo mode would cover the banner, so I'm moving the banner higher to clear it. I'm weighing where to put the overlay card in demo mode, since centered it would sit over the middle of the play area where the ball travels, and it could also cover the bottom brick rows. I'm considering moving it down toward the gap between the bricks and the paddle instead. Still writing the overlay: I'm setting its top edge to 60% of the canvas so the card sits between the bricks and the paddle, with a dark translucent background so the game stays visible behind it. Still writing the overlay: I'm setting the key that tells it whether the game is over or in attract mode, and when that key changes I'm updating the title, subtitle, and prompt text to match. Now I'm finishing the overlay's key handling so it switches between the game-over and attract text, and I'm deciding that the overlay should also show the prompt in game-over mode. Still writing the game loop: I'm stepping the frame with a clamped delta time and drawing each frame, then queuing the next one. Still writing the game loop: I'm adding the shake decay and the background hearts that drift upward and wrap back to the bottom, then recording the ball's trail while it's in motion and clearing it on attach. Still writing the AI: I'm checking the demo paddle's predicted landing point against the paddle's top edge, and I'll keep the target aligned with the random offset so the ball lands off-center. Still writing the launch: I'm picking a random launch angle and building the ball's starting speed from it. Still writing the click handler: if the game is in demo mode, I'm starting play and launching the ball in the same click, so the first click both begins and sends it off. Next I'm wiring up the click handling: if the game isn't in play, a click starts it and launches the ball right away, and a click during play just launches. The keyboard path does the same for the launch keys, with the arrow keys only starting the game. Still writing the stage logic: I'm checking the overlay sizing at 375px and confirming the canvas fits the screen on both portrait and short landscape layouts. Still writing the update: I'm checking that the normalizing step only runs once the ball is in motion, so it doesn't reset the speed while the ball is still attached to the paddle. Still writing the update loop: I'm checking that the speed normalizing step runs only when the ball is free, and I'm confirming the lost-ball path reattaches it without resetting the game in demo mode. Still writing the stylesheet: I'm setting the dark color scheme, resetting margins, and centering the page with a dark radial gradient behind the game. Still writing the overlay styling: I'm finishing its transform and layout so the card stays centered on its anchor point without blocking clicks. Still writing the stylesheet: I'm finishing the overlay's hidden state, title and subtitle typography, and the pulsing prompt animation. Still writing the stylesheet: I'm dropping the line-height reset on the stage, since the canvas is block-level and won't leave a gap, and I'm checking that the hidden overlay state and pulsing prompt still behave correctly. Still writing the rounded-rectangle helper, drawing its corners with quadratic curves so the bricks and cards come out with soft edges. Still writing the brick and paddle drawing: I'm giving the bricks rounded corners with a gradient fill and a thin highlight strip, and I'm adding the patty stripes on the burger paddle. Still writing the overlay: I'm setting its initial state and making sure it shows in demo mode from the start. I'm also settling on a variable name, using `oR` instead of `or` to stay on the safe side.