Models 44

Claude Sonnet 5.5

by Anthropicreasoning defaultClean
Open in new window

Running live in a sandboxed frame. Click inside it before using the keyboard.

How it went

Total time
1 min 42 s
Cost
$0.162
Output tokens
16,028
Spent thinking
4,908 tokens
Speed
160.5 tok/s
Console errors
0
First token after
2.2 s
First visible text after
46.5 s
Prompt tokens
793
Code size
19.4 KB
Finish reason
stop
Served by
Claude Platform on AWS
Model id
anthropic/claude-sonnet-5.5
Reasoning sent
model default
Max output
128,000 tokens
Generated
Nov 12, 2026

What it was asked

The prompt
Build a polished Breakout (brick breaker) game in a single HTML file using vanilla JavaScript and a <canvas>. Requirements: - A paddle controlled with the mouse and with the left/right arrow keys. - The bounce angle off the paddle depends on where the ball hits it (center = straight up, edges = sharp angle). - One level with at least 6 rows of bricks, mixing normal bricks and tougher bricks that need 2 hits and visibly crack after the first one. - Some bricks visibly carry a power-up icon; when broken they drop it, and catching it with the paddle gives either a wider paddle (10 seconds) or multi-ball (3 balls). - The ball must never pass through bricks or the paddle. - Particles when a brick breaks. - 3 lives, score displayed. Game over and victory screens with a restart option.
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 · 490 lines

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Breakout</title>
<style>
  html,body{margin:0;height:100%;background:#05060d;overflow:hidden;font-family:system-ui,sans-serif}
  body{display:flex;align-items:center;justify-content:center}
  canvas{display:block;background:#0a0c1a;box-shadow:0 0 60px rgba(80,120,255,.25);border-radius:6px;cursor:none;touch-action:none}
</style>
</head>
<body>
<canvas id="c"></canvas>
<script>
(() => {
const W = 800, H = 600, TOP = 48;
const cv = document.getElementById('c');
const ctx = cv.getContext('2d');
let scale = 1;

function resize() {
  const dpr = window.devicePixelRatio || 1;
  const w = Math.min(window.innerWidth, window.innerHeight * W / H);
  const h = w * H / W;
  cv.style.width = w + 'px';
  cv.style.height = h + 'px';
  cv.width = Math.round(w * dpr);
  cv.height = Math.round(h * dpr);
  scale = cv.width / W;
}
window.addEventListener('resize', resize);
resize();

// ---------- state ----------
const COLS = 10, ROWS = 7, BW = 67, BH = 24, GAP = 5;
const OFFX = (W - (COLS * BW + (COLS - 1) * GAP)) / 2, OFFY = 84;
const HUES = [350, 22, 46, 130, 180, 215, 275];
const PY = 556, PH = 14, BASE_W = 110, WIDE_W = 184, R = 7;

let phase = 'demo';          // demo | play | over | win
let score, lives, balls, bricks, drops, parts, paddle, wideT, hits, speed, shake, time = 0, phaseTime = 0, demoLaunch;
const keys = {};
let mouseActive = false;

const stars = Array.from({length: 60}, () => ({x: Math.random() * W, y: Math.random() * H, s: Math.random() * 1.5 + .3, v: Math.random() * 8 + 3}));

function newBall(stuck) {
  return {x: paddle.x, y: PY - R - 1, vx: 0, vy: 0, stuck: !!stuck, trail: []};
}

function makeBricks() {
  const list = [];
  const pool = [];
  for (let r = 0; r < ROWS; r++) for (let c = 0; c < COLS; c++) pool.push([r, c]);
  const pu = new Map();
  const picks = pool.slice().sort(() => Math.random() - .5).slice(0, 10);
  picks.forEach((p, i) => pu.set(p[0] * COLS + p[1], i % 2 ? 'multi' : 'wide'));
  for (let r = 0; r < ROWS; r++) for (let c = 0; c < COLS; c++) {
    const tough = (r + c) % 3 === 0;
    const crack = [];
    let cx = BW * (.3 + Math.random() * .4), cy = 0;
    crack.push([cx, 0]);
    for (let k = 1; k <= 4; k++) { cx += (Math.random() - .5) * 18; cy = BH * k / 4; crack.push([Math.max(3, Math.min(BW - 3, cx)), cy]); }
    list.push({
      x: OFFX + c * (BW + GAP), y: OFFY + r * (BH + GAP), r, c,
      hue: HUES[r], hp: tough ? 2 : 1, max: tough ? 2 : 1,
      power: pu.get(r * COLS + c) || null, flash: 0, crack, alive: true
    });
  }
  return list;
}

function resetGame() {
  score = 0; lives = 3; hits = 0; speed = 400;
  paddle = {x: W / 2, w: BASE_W};
  wideT = 0; drops = []; parts = []; shake = 0;
  bricks = makeBricks();
  balls = [newBall(true)];
  demoLaunch = 0.9;
}
resetGame();

function startGame() {
  resetGame();
  phase = 'play'; phaseTime = 0;
}

// ---------- input ----------
function toGame(e) {
  const r = cv.getBoundingClientRect();
  return (e.clientX - r.left) / r.width * W;
}
cv.addEventListener('pointermove', e => {
  if (phase === 'play') { paddle.x = clampPaddle(toGame(e)); mouseActive = true; }
});
cv.addEventListener('pointerdown', e => {
  if (phase === 'demo') { startGame(); paddle.x = clampPaddle(toGame(e)); return; }
  if (phase === 'over' || phase === 'win') { if (phaseTime > .5) startGame(); return; }
  if (phase === 'play') { paddle.x = clampPaddle(toGame(e)); launch(); }
});
window.addEventListener('keydown', e => {
  if (['ArrowLeft', 'ArrowRight', 'ArrowUp', ' '].includes(e.key)) e.preventDefault();
  keys[e.key] = true;
  if (phase === 'demo') { startGame(); return; }
  if (phase === 'over' || phase === 'win') { if (phaseTime > .5) startGame(); return; }
  if (e.key === ' ' || e.key === 'ArrowUp' || e.key === 'Enter') launch();
  if (e.key === 'ArrowLeft' || e.key === 'ArrowRight') mouseActive = false;
});
window.addEventListener('keyup', e => { keys[e.key] = false; });

function clampPaddle(x) { return Math.max(paddle.w / 2, Math.min(W - paddle.w / 2, x)); }

function launch() {
  for (const b of balls) if (b.stuck) {
    b.stuck = false;
    const a = (Math.random() < .5 ? -1 : 1) * (0.2 + Math.random() * .2);
    b.vx = speed * Math.sin(a); b.vy = -speed * Math.cos(a);
  }
}

// ---------- helpers ----------
function normalize(b) {
  let m = Math.hypot(b.vx, b.vy) || 1;
  b.vx *= speed / m; b.vy *= speed / m;
  const minY = speed * 0.26;
  if (Math.abs(b.vy) < minY) {
    b.vy = (b.vy < 0 ? -1 : 1) * minY;
    const sx = Math.sqrt(speed * speed - b.vy * b.vy);
    b.vx = (b.vx < 0 ? -1 : 1) * sx;
  }
}

function burst(x, y, hue, n, power) {
  for (let i = 0; i < n; i++) {
    const a = Math.random() * Math.PI * 2, s = (power || 1) * (60 + Math.random() * 200);
    parts.push({x, y, vx: Math.cos(a) * s, vy: Math.sin(a) * s - 40, life: .5 + Math.random() * .6, max: 1.1,
      size: 2 + Math.random() * 4, hue, rot: Math.random() * 6, vr: (Math.random() - .5) * 12});
  }
}

function hitBrick(br) {
  br.hp--; hits++;
  speed = Math.min(560, 400 + hits * 1.6);
  br.flash = .15;
  if (br.hp <= 0) {
    br.alive = false;
    score += br.max === 2 ? 30 : 10;
    burst(br.x + BW / 2, br.y + BH / 2, br.hue, 16, 1);
    shake = Math.min(6, shake + 2.5);
    if (br.power) drops.push({x: br.x + BW / 2, y: br.y + BH / 2, type: br.power, vy: 140});
  } else {
    score += 5;
    burst(br.x + BW / 2, br.y + BH / 2, 0, 7, .7);
    shake = Math.min(6, shake + 1);
  }
}

function applyPower(t) {
  if (t === 'wide') wideT = 10;
  else {
    const src = balls.find(b => !b.stuck) || balls[0];
    let i = 0;
    while (balls.length < 3) {
      const ang = Math.atan2(src.vy, src.vx) + (i++ % 2 ? .4 : -.4);
      const sp = Math.hypot(src.vx, src.vy) || speed;
      balls.push({x: src.x, y: src.y, vx: Math.cos(ang) * sp, vy: Math.sin(ang) * sp, stuck: false, trail: []});
    }
  }
  burst(paddle.x, PY, t === 'wide' ? 205 : 285, 18, 1);
}

// ---------- update ----------
function stepBall(b, dt) {
  b.x += b.vx * dt; b.y += b.vy * dt;
  // walls
  if (b.x < R) { b.x = R; b.vx = Math.abs(b.vx); }
  if (b.x > W - R) { b.x = W - R; b.vx = -Math.abs(b.vx); }
  if (b.y < TOP + R) { b.y = TOP + R; b.vy = Math.abs(b.vy); }
  // paddle
  const hw = paddle.w / 2;
  if (b.vy > 0 && b.y + R >= PY && b.y <= PY + PH * .5 && b.x >= paddle.x - hw - R * .6 && b.x <= paddle.x + hw + R * .6) {
    const off = Math.max(-1, Math.min(1, (b.x - paddle.x) / hw));
    const a = off * 1.12; // ~64deg max
    b.vx = speed * Math.sin(a); b.vy = -speed * Math.cos(a);
    b.y = PY - R;
    burst(b.x, PY, 215, 4, .5);
  }
  // bricks
  let best = null, bd = 1e9, bpx = 0, bpy = 0;
  for (const br of bricks) {
    if (!br.alive) continue;
    const px = Math.max(br.x, Math.min(b.x, br.x + BW));
    const py = Math.max(br.y, Math.min(b.y, br.y + BH));
    const d = Math.hypot(b.x - px, b.y - py);
    if (d < R && d < bd) { bd = d; best = br; bpx = px; bpy = py; }
  }
  if (best) {
    let nx, ny;
    if (bd < 1e-6) { // center inside brick: pick nearest face
      const l = b.x - best.x, r = best.x + BW - b.x, t = b.y - best.y, bt = best.y + BH - b.y;
      const m = Math.min(l, r, t, bt);
      nx = ny = 0;
      if (m === l) { nx = -1; bpx = best.x; bpy = b.y; }
      else if (m === r) { nx = 1; bpx = best.x + BW; bpy = b.y; }
      else if (m === t) { ny = -1; bpy = best.y; bpx = b.x; }
      else { ny = 1; bpy = best.y + BH; bpx = b.x; }
    } else { nx = (b.x - bpx) / bd; ny = (b.y - bpy) / bd; }
    const dot = b.vx * nx + b.vy * ny;
    if (dot < 0) { b.vx -= 2 * dot * nx; b.vy -= 2 * dot * ny; }
    b.x = bpx + nx * (R + .05); b.y = bpy + ny * (R + .05);
    hitBrick(best);
    normalize(b);
  }
}

function updateGame(dt) {
  // paddle control
  if (phase === 'demo') aiControl(dt);
  else {
    const k = (keys.ArrowRight ? 1 : 0) - (keys.ArrowLeft ? 1 : 0);
    if (k) paddle.x += k * 720 * dt;
  }
  paddle.w += ((wideT > 0 ? WIDE_W : BASE_W) - paddle.w) * Math.min(1, dt * 10);
  paddle.x = clampPaddle(paddle.x);
  if (wideT > 0) wideT = Math.max(0, wideT - dt);

  // balls
  for (const b of balls) {
    if (b.stuck) { b.x = paddle.x; b.y = PY - R - 1; b.trail.length = 0; continue; }
    b.trail.push([b.x, b.y]); if (b.trail.length > 9) b.trail.shift();
    const dist = Math.hypot(b.vx, b.vy) * dt;
    const n = Math.max(1, Math.ceil(dist / 3));
    for (let i = 0; i < n; i++) stepBall(b, dt / n);
  }
  balls = balls.filter(b => b.stuck || b.y < H + 20);
  if (balls.length === 0) {
    lives--; shake = 8;
    drops = []; wideT = 0;
    if (lives <= 0) {
      if (phase === 'demo') { resetGame(); return; }
      phase = 'over'; phaseTime = 0;
    } else balls = [newBall(true)];
    demoLaunch = .9;
  }

  // drops
  for (const d of drops) {
    d.y += d.vy * dt;
    if (d.y > PY - 8 && d.y < PY + PH + 10 && Math.abs(d.x - paddle.x) < paddle.w / 2 + 16) {
      d.dead = true; applyPower(d.type); score += 25;
    } else if (d.y > H + 20) d.dead = true;
  }
  drops = drops.filter(d => !d.dead);

  for (const br of bricks) if (br.flash > 0) br.flash -= dt;

  // win
  if (!bricks.some(b => b.alive)) {
    if (phase === 'demo') { resetGame(); return; }
    score += lives * 100;
    phase = 'win'; phaseTime = 0;
    for (let i = 0; i < 8; i++) burst(100 + Math.random() * 600, 150 + Math.random() * 200, Math.random() * 360, 18, 1.4);
  }
}

function aiControl(dt) {
  const st = balls.find(b => b.stuck);
  if (st) { demoLaunch -= dt; if (demoLaunch <= 0) launch(); }
  let target = paddle.x;
  const live = balls.filter(b => !b.stuck);
  let threat = null;
  for (const b of live) if (b.vy > 0 && (!threat || b.y > threat.y)) threat = b;
  const drop = drops.find(d => d.y > 300);
  if (drop && (!threat || threat.y < 380)) target = drop.x;
  else if (threat) target = threat.x + Math.sin(time * 1.7) * paddle.w * .32;
  else if (live[0]) target = live[0].x;
  const d = target - paddle.x, m = 620 * dt;
  paddle.x += Math.max(-m, Math.min(m, d));
}

function update(dt) {
  time += dt; phaseTime += dt;
  for (const s of stars) { s.y += s.v * dt; if (s.y > H) { s.y = 0; s.x = Math.random() * W; } }
  for (const p of parts) { p.life -= dt; p.vy += 520 * dt; p.x += p.vx * dt; p.y += p.vy * dt; p.rot += p.vr * dt; }
  parts = parts.filter(p => p.life > 0);
  shake *= Math.pow(.02, dt);
  if (phase === 'demo' || phase === 'play') updateGame(dt);
  else if (phase === 'win' && Math.random() < dt * 2) burst(100 + Math.random() * 600, 120 + Math.random() * 200, Math.random() * 360, 14, 1.2);
}

// ---------- drawing ----------
function rr(x, y, w, h, r) { ctx.beginPath(); ctx.roundRect(x, y, w, h, r); }

function drawIcon(type, x, y, s) {
  ctx.save(); ctx.translate(x, y); ctx.scale(s, s);
  ctx.strokeStyle = '#fff'; ctx.fillStyle = '#fff'; ctx.lineWidth = 1.8; ctx.lineCap = 'round'; ctx.lineJoin = 'round';
  if (type === 'wide') {
    ctx.beginPath(); ctx.moveTo(-7, 0); ctx.lineTo(7, 0);
    ctx.moveTo(-7, 0); ctx.lineTo(-4, -3); ctx.moveTo(-7, 0); ctx.lineTo(-4, 3);
    ctx.moveTo(7, 0); ctx.lineTo(4, -3); ctx.moveTo(7, 0); ctx.lineTo(4, 3);
    ctx.stroke();
  } else {
    for (const [dx, dy] of [[-4.5, 2.5], [4.5, 2.5], [0, -3.5]]) { ctx.beginPath(); ctx.arc(dx, dy, 2.4, 0, 7); ctx.fill(); }
  }
  ctx.restore();
}

function drawBrick(br) {
  const {x, y} = br;
  const tough = br.max === 2, dmg = tough && br.hp === 1;
  ctx.save();
  ctx.shadowColor = `hsla(${br.hue},90%,60%,.45)`; ctx.shadowBlur = 8;
  const g = ctx.createLinearGradient(0, y, 0, y + BH);
  if (tough && !dmg) { g.addColorStop(0, `hsl(${br.hue},35%,52%)`); g.addColorStop(1, `hsl(${br.hue},40%,26%)`); }
  else if (dmg) { g.addColorStop(0, `hsl(${br.hue},60%,60%)`); g.addColorStop(1, `hsl(${br.hue},65%,40%)`); }
  else { g.addColorStop(0, `hsl(${br.hue},85%,64%)`); g.addColorStop(1, `hsl(${br.hue},80%,42%)`); }
  ctx.fillStyle = g; rr(x, y, BW, BH, 5); ctx.fill();
  ctx.shadowBlur = 0;
  // highlight
  ctx.fillStyle = 'rgba(255,255,255,.22)'; rr(x + 2, y + 2, BW - 4, 5, 3); ctx.fill();
  if (tough) {
    ctx.strokeStyle = dmg ? 'rgba(210,215,230,.55)' : 'rgba(225,230,245,.95)'; ctx.lineWidth = 2;
    rr(x + 1, y + 1, BW - 2, BH - 2, 4); ctx.stroke();
    ctx.fillStyle = 'rgba(230,235,250,.8)';
    if (!dmg) for (const [dx, dy] of [[5, 5], [BW - 5, 5], [5, BH - 5], [BW - 5, BH - 5]]) { ctx.beginPath(); ctx.arc(x + dx, y + dy, 1.3, 0, 7); ctx.fill(); }
  }
  if (dmg) { // cracks
    ctx.save(); ctx.beginPath(); ctx.roundRect(x, y, BW, BH, 5); ctx.clip();
    ctx.strokeStyle = 'rgba(10,10,20,.85)'; ctx.lineWidth = 1.6; ctx.lineJoin = 'round';
    ctx.beginPath(); br.crack.forEach((p, i) => i ? ctx.lineTo(x + p[0], y + p[1]) : ctx.moveTo(x + p[0], y + p[1]));
    const m = br.crack[2]; ctx.moveTo(x + m[0], y + m[1]); ctx.lineTo(x + m[0] + 14, y + m[1] - 4); ctx.lineTo(x + m[0] + 22, y + m[1] + 3);
    ctx.moveTo(x + m[0], y + m[1]); ctx.lineTo(x + m[0] - 13, y + m[1] + 5);
    ctx.stroke();
    ctx.strokeStyle = 'rgba(255,255,255,.35)'; ctx.lineWidth = .8; ctx.translate(1, 1);
    ctx.beginPath(); br.crack.forEach((p, i) => i ? ctx.lineTo(x + p[0], y + p[1]) : ctx.moveTo(x + p[0], y + p[1])); ctx.stroke();
    ctx.restore();
  }
  if (br.power) {
    const cx = x + BW / 2, cy = y + BH / 2;
    ctx.fillStyle = br.power === 'wide' ? '#1d7bff' : '#a43bff';
    ctx.strokeStyle = '#fff'; ctx.lineWidth = 1.5;
    ctx.beginPath(); ctx.arc(cx, cy, 9.5, 0, 7); ctx.fill(); ctx.stroke();
    drawIcon(br.power, cx, cy, .85);
  }
  if (br.flash > 0) { ctx.fillStyle = `rgba(255,255,255,${br.flash * 4})`; rr(x, y, BW, BH, 5); ctx.fill(); }
  ctx.restore();
}

function drawText(t, x, y, size, color, align, weight) {
  ctx.font = `${weight || 700} ${size}px system-ui, sans-serif`;
  ctx.textAlign = align || 'center'; ctx.textBaseline = 'middle';
  ctx.fillStyle = color; ctx.fillText(t, x, y);
}

function draw() {
  ctx.setTransform(scale, 0, 0, scale, 0, 0);
  // background
  const bg = ctx.createLinearGradient(0, 0, 0, H);
  bg.addColorStop(0, '#12183a'); bg.addColorStop(1, '#070812');
  ctx.fillStyle = bg; ctx.fillRect(0, 0, W, H);
  ctx.strokeStyle = 'rgba(120,150,255,.05)'; ctx.lineWidth = 1;
  ctx.beginPath();
  for (let x = 0; x <= W; x += 40) { ctx.moveTo(x, TOP); ctx.lineTo(x, H); }
  for (let y = TOP; y <= H; y += 40) { ctx.moveTo(0, y); ctx.lineTo(W, y); }
  ctx.stroke();
  ctx.fillStyle = '#fff';
  for (const s of stars) { ctx.globalAlpha = .15 + s.s * .2; ctx.fillRect(s.x, s.y, s.s, s.s); }
  ctx.globalAlpha = 1;

  ctx.save();
  if (shake > .2) ctx.translate((Math.random() - .5) * shake, (Math.random() - .5) * shake);

  for (const br of bricks) if (br.alive) drawBrick(br);

  // drops
  for (const d of drops) {
    ctx.save(); ctx.translate(d.x, d.y);
    const c = d.type === 'wide' ? 215 : 280;
    ctx.shadowColor = `hsl(${c},100%,60%)`; ctx.shadowBlur = 14;
    const g = ctx.createLinearGradient(0, -10, 0, 10);
    g.addColorStop(0, `hsl(${c},90%,65%)`); g.addColorStop(1, `hsl(${c},85%,40%)`);
    ctx.fillStyle = g; rr(-18, -10, 36, 20, 10); ctx.fill();
    ctx.shadowBlur = 0; ctx.strokeStyle = '#fff'; ctx.lineWidth = 1.5; rr(-18, -10, 36, 20, 10); ctx.stroke();
    drawIcon(d.type, 0, 0, 1);
    ctx.restore();
  }

  // paddle
  const hw = paddle.w / 2;
  ctx.save();
  const blink = wideT > 0 && wideT < 2 && Math.floor(time * 6) % 2;
  ctx.shadowColor = wideT > 0 ? '#3d9bff' : '#6d8cff'; ctx.shadowBlur = 18;
  const pg = ctx.createLinearGradient(0, PY, 0, PY + PH);
  pg.addColorStop(0, blink ? '#fff' : '#e8efff'); pg.addColorStop(.5, wideT > 0 ? '#3d9bff' : '#7e93ff'); pg.addColorStop(1, '#2c3a9a');
  ctx.fillStyle = pg; rr(paddle.x - hw, PY, paddle.w, PH, 7); ctx.fill();
  ctx.shadowBlur = 0;
  ctx.fillStyle = 'rgba(255,255,255,.4)'; rr(paddle.x - hw + 6, PY + 2, paddle.w - 12, 3, 2); ctx.fill();
  ctx.restore();

  // balls
  for (const b of balls) {
    for (let i = 0; i < b.trail.length; i++) {
      const t = b.trail[i], f = (i + 1) / b.trail.length;
      ctx.fillStyle = `rgba(160,200,255,${f * .25})`;
      ctx.beginPath(); ctx.arc(t[0], t[1], R * f * .9, 0, 7); ctx.fill();
    }
    ctx.save();
    ctx.shadowColor = '#9fd0ff'; ctx.shadowBlur = 16;
    const rg = ctx.createRadialGradient(b.x - 2, b.y - 2, 1, b.x, b.y, R);
    rg.addColorStop(0, '#fff'); rg.addColorStop(1, '#8ec5ff');
    ctx.fillStyle = rg; ctx.beginPath(); ctx.arc(b.x, b.y, R, 0, 7); ctx.fill();
    ctx.restore();
  }

  // particles
  for (const p of parts) {
    const a = Math.max(0, Math.min(1, p.life / p.max * 1.5));
    ctx.save(); ctx.globalAlpha = a; ctx.translate(p.x, p.y); ctx.rotate(p.rot);
    ctx.fillStyle = p.hue === 0 ? '#e8ecff' : `hsl(${p.hue},90%,62%)`;
    ctx.fillRect(-p.size / 2, -p.size / 2, p.size, p.size);
    ctx.restore();
  }
  ctx.restore();

  // HUD
  ctx.fillStyle = 'rgba(8,10,25,.9)'; ctx.fillRect(0, 0, W, TOP);
  ctx.fillStyle = 'rgba(120,150,255,.35)'; ctx.fillRect(0, TOP - 1, W, 1);
  drawText('SCORE', 20, 24, 12, '#7f8fd0', 'left');
  drawText(String(score).padStart(6, '0'), 76, 25, 22, '#fff', 'left');
  drawText('BREAKOUT', W / 2, 25, 18, '#9fb2ff', 'center', 800);
  for (let i = 0; i < 3; i++) {
    const x = W - 30 - i * 26, on = i < lives;
    ctx.save(); ctx.translate(x, 25);
    ctx.fillStyle = on ? '#ff4d6d' : 'rgba(255,255,255,.12)';
    if (on) { ctx.shadowColor = '#ff4d6d'; ctx.shadowBlur = 10; }
    ctx.beginPath();
    ctx.moveTo(0, 8); ctx.bezierCurveTo(-14, -2, -8, -12, 0, -4); ctx.bezierCurveTo(8, -12, 14, -2, 0, 8); ctx.fill();
    ctx.restore();
  }
  if (wideT > 0) {
    ctx.fillStyle = 'rgba(255,255,255,.1)'; rr(W - 220, 18, 90, 10, 5); ctx.fill();
    ctx.fillStyle = '#3d9bff'; rr(W - 220, 18, 90 * wideT / 10, 10, 5); ctx.fill();
    drawText('WIDE', W - 228, 24, 11, '#7fb8ff', 'right');
  }

  // overlays
  if (phase === 'demo') {
    ctx.fillStyle = 'rgba(5,7,20,.6)'; rr(W / 2 - 250, 345, 500, 130, 16); ctx.fill();
    ctx.strokeStyle = 'rgba(130,160,255,.5)'; ctx.lineWidth = 1.5; rr(W / 2 - 250, 345, 500, 130, 16); ctx.stroke();
    drawText('BREAKOUT', W / 2, 382, 36, '#fff', 'center', 900);
    const pulse = .65 + .35 * Math.sin(time * 4);
    drawText('Click or press a key to start', W / 2, 425, 22, `rgba(160,200,255,${pulse})`);
    drawText('Mouse or ← → to move  •  catch power-ups', W / 2, 455, 14, '#8a97c8', 'center', 500);
  } else if (phase === 'play') {
    if (balls.some(b => b.stuck)) {
      const pulse = .6 + .4 * Math.sin(time * 5);
      drawText('Click or press Space to launch', W / 2, 470, 18, `rgba(190,215,255,${pulse})`);
    }
  } else {
    ctx.fillStyle = 'rgba(4,5,15,.72)'; ctx.fillRect(0, TOP, W, H - TOP);
    const win = phase === 'win';
    const k = Math.min(1, phaseTime * 3);
    ctx.save(); ctx.translate(W / 2, 320); ctx.scale(.85 + .15 * k, .85 + .15 * k); ctx.globalAlpha = k;
    ctx.fillStyle = 'rgba(14,18,48,.95)'; rr(-230, -130, 460, 260, 20); ctx.fill();
    ctx.strokeStyle = win ? '#ffd24d' : '#ff4d6d'; ctx.lineWidth = 2; rr(-230, -130, 460, 260, 20); ctx.stroke();
    ctx.shadowColor = win ? '#ffd24d' : '#ff4d6d'; ctx.shadowBlur = 20;
    drawText(win ? 'YOU WIN!' : 'GAME OVER', 0, -78, 48, win ? '#ffd24d' : '#ff4d6d', 'center', 900);
    ctx.shadowBlur = 0;
    drawText('FINAL SCORE', 0, -25, 13, '#8a97c8');
    drawText(String(score), 0, 8, 40, '#fff', 'center', 800);
    ctx.fillStyle = '#4a63ff'; rr(-110, 52, 220, 44, 22); ctx.fill();
    drawText('PLAY AGAIN', 0, 75, 18, '#fff', 'center', 800);
    drawText('Click or press any key', 0, 112, 12, '#8a97c8', 'center', 500);
    ctx.restore();
  }
}

let last = performance.now();
function loop(now) {
  const dt = Math.min(1 / 30, (now - last) / 1000); last = now;
  update(dt);
  draw();
  requestAnimationFrame(loop);
}
requestAnimationFrame(loop);
})();
</script>
</body>
</html>
Reasoning trace (4,908 tokens)
I'm working out how to build a polished single-file Breakout game, and it needs a self-playing demo mode that waits behind a "Click or press a key to start" message. I'm planning a fixed 800x600 logical canvas that scales with CSS to fit its container, with resize handling that accounts for devicePixelRatio, and four states: demo, playing, over, and win.

I'm now settling on the ball and paddle mechanics: the ball stays on the paddle until a click or key press, with an auto-launch after about two seconds as a fallback. For collisions I'm planning small sub-steps of roughly 4px, resolving bricks by the nearest point on each rectangle, and setting the paddle bounce angle from where the ball hits it.

I'm setting the brick layout next: a 10-by-7 grid sized to the 800-pixel width, with some tougher bricks that take two hits and show cracks once damaged. I'm also placing a few power-up bricks that drop capsules for a wider paddle or extra balls.

I'm adding the multi-ball power-up, which keeps spawning balls until there are at least three, and the ball count going to zero costs a life. I'm also planning falling particles with gravity and a slight screen shake, scoring 10 per normal brick and 20 for a tough one, and drawing lives as small circles. The win state triggers when every brick is gone, and clicking or pressing Enter or Space restarts from the game-over screen.

I'm setting the demo's paddle to follow the lowest ball, with a sine offset so it hits at varied angles, and it auto-launches. When a game ends in demo, it restarts on its own shortly after. Mouse movement and arrow keys both move the paddle, with touch handled through pointer events. The ball launches on a click, the space bar, or the up arrow, or else after about 2.5 seconds with a "Click or Space to launch" hint.

I'm settling the start behavior: the first click moves from demo into play, resetting the game with the ball sitting on the paddle, and keyboard input only counts once the user has clicked. I'm skipping sound to avoid complications, and turning to the visual style, with a dark gradient backdrop, a glowing ball with a trail, and a top HUD for score and lives.

Still writing the game code...

Still working through the brick layout, sizing the rows and spacing so the grid ends above the paddle area.

Still writing the collision code, now for the brick hits: for each substep I check which living brick the ball touches by finding the closest point on each rectangle, and I'll resolve the one with the deepest overlap and reflect the velocity off its surface normal.

Still working on the collision code: the paddle bounce is now the focus, so the ball reflects when it's moving down and crosses the paddle's top edge, with the exit angle set by where it hits.

Still writing the game code...

Still writing the game code...

Still writing the game code, now moving on to the input handling: the pointer and keyboard listeners that start the game and restart it from the game-over screen.

Still writing the game code, now covering the demo's paddle logic: it targets the nearest falling ball, with a slight sine offset so it hits at varied angles, and moves at a capped speed.

Still writing the game code...

Still writing the game code, now on the brick flash, the launch hint, and the demo overlay panel.

Still writing the game code, now on the wide-paddle and multi-ball power-up rendering, plus the phase setup for demo, play, over, and win.

Still writing the game code, now on the demo's end-of-game handling: when a life is lost or the bricks are cleared in demo mode, it resets straight away rather than waiting on a timer.

Still writing the game code, now on ball speed normalization and the wall bounces, with the top wall sitting just below the HUD.

Still finishing up the game code...