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 56 s
Cost
$0.177
Output tokens
17,547
Spent thinking
6,987 tokens
Speed
154.2 tok/s
Console errors
0
First token after
2.1 s
First visible text after
1 min 3 s
Prompt tokens
542
Code size
18.3 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
“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 · 479 lines

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Heartburger Breakout</title>
<style>
  html,body{margin:0;height:100%;overflow:hidden;background:#1a0a22;}
  body{display:flex;align-items:center;justify-content:center;
       background:radial-gradient(ellipse at center,#3a1245 0%,#150720 80%);}
  canvas{display:block;touch-action:none;cursor:pointer;border-radius:10px;
         box-shadow:0 0 60px rgba(255,80,150,.35),0 0 0 2px rgba(255,140,190,.35);}
</style>
</head>
<body>
<canvas id="c"></canvas>
<script>
(() => {
const W = 800, H = 600;
const cv = document.getElementById('c');
const ctx = cv.getContext('2d');
let scale = 1;

function resize() {
  const dpr = Math.min(window.devicePixelRatio || 1, 2);
  const cssW = Math.max(200, Math.min(window.innerWidth, window.innerHeight * 4 / 3));
  const cssH = cssW * 3 / 4;
  cv.style.width = cssW + 'px';
  cv.style.height = cssH + 'px';
  cv.width = Math.round(cssW * dpr);
  cv.height = Math.round(cssH * dpr);
  scale = cv.width / W;
}
window.addEventListener('resize', resize);
resize();

/* ---------- heart sprites ---------- */
const COLORS = ['#ff3d71', '#ff8a3d', '#ffd23d', '#5fd068', '#3dc4f0', '#a66bff', '#ff7eb6', '#ffffff'];
function heartPath(c, s) {
  c.beginPath();
  c.moveTo(0, -s * 0.25);
  c.bezierCurveTo(0, -s * 0.7, -s * 0.6, -s * 0.7, -s * 0.6, -s * 0.2);
  c.bezierCurveTo(-s * 0.6, s * 0.2, -s * 0.2, s * 0.4, 0, s * 0.65);
  c.bezierCurveTo(s * 0.2, s * 0.4, s * 0.6, s * 0.2, s * 0.6, -s * 0.2);
  c.bezierCurveTo(s * 0.6, -s * 0.7, 0, -s * 0.7, 0, -s * 0.25);
  c.closePath();
}
function makeHeart(color) {
  const S = 64, o = document.createElement('canvas');
  o.width = o.height = S;
  const c = o.getContext('2d');
  c.translate(S / 2, S / 2 - 1);
  heartPath(c, 46);
  c.fillStyle = color; c.fill();
  const g = c.createRadialGradient(-8, -12, 2, 0, 0, 30);
  g.addColorStop(0, 'rgba(255,255,255,.65)');
  g.addColorStop(.5, 'rgba(255,255,255,0)');
  g.addColorStop(1, 'rgba(0,0,0,.25)');
  c.fillStyle = g; c.fill();
  c.lineWidth = 1.5; c.strokeStyle = 'rgba(0,0,0,.25)'; c.stroke();
  c.fillStyle = 'rgba(255,255,255,.8)';
  c.beginPath(); c.ellipse(-12, -13, 5, 3, -0.6, 0, 7); c.fill();
  return o;
}
const SPR = COLORS.map(makeHeart);

/* ---------- state ---------- */
let mode = 'demo'; // demo | play | over
let score, lives, level, bricks, ball, parts = [], rings = [], pops = [], shake = 0, T = 0;
const paddle = { x: W / 2, y: H - 62, w: 132, squish: 0, targetX: W / 2 };
const keys = {};
let aiOff = 0;
const bgHearts = [];
for (let i = 0; i < 16; i++) bgHearts.push({ x: Math.random() * W, y: Math.random() * H, s: 14 + Math.random() * 30, v: 12 + Math.random() * 25, c: Math.floor(Math.random() * 7), p: Math.random() * 6 });

const BW = 68, BH = 24, GAP = 6, COLS = 10;
function buildBricks() {
  bricks = [];
  const rows = level >= 3 ? 7 : 6;
  const x0 = (W - (COLS * BW + (COLS - 1) * GAP)) / 2;
  for (let r = 0; r < rows; r++) for (let c = 0; c < COLS; c++) {
    if (level % 3 === 2 && (r + c * 2) % 7 === 0) continue;
    bricks.push({ x: x0 + c * (BW + GAP), y: 74 + r * (BH + GAP), w: BW, h: BH, ci: r % 6,
      hp: r < Math.min(level - 1, 2) ? 2 : 1, flash: 0 });
  }
}
function ballSpeed() { return Math.min(700, 410 + level * 35); }
function resetBall() {
  ball = { x: paddle.x, y: paddle.y - 8, vx: 0, vy: 0, r: 8, stuck: true, t: 0 };
}
function resetGame() {
  score = 0; lives = 3; level = 1;
  buildBricks(); resetBall();
}
function launch() {
  if (!ball.stuck) return;
  const a = (Math.random() - .5) * 0.8, s = ballSpeed();
  ball.vx = Math.sin(a) * s; ball.vy = -Math.cos(a) * s;
  ball.stuck = false;
}
function startPlay() {
  mode = 'play';
  resetGame();
  paddle.targetX = paddle.x;
}

/* ---------- effects ---------- */
function burst(x, y, ci, n) {
  const room = 2600 - parts.length;
  n = Math.min(n, Math.max(0, room));
  for (let i = 0; i < n; i++) {
    const a = Math.random() * Math.PI * 2, sp = 60 + Math.pow(Math.random(), 0.7) * 420;
    const floaty = Math.random() < 0.3;
    parts.push({
      x: x + (Math.random() - .5) * 30, y: y + (Math.random() - .5) * 10,
      vx: Math.cos(a) * sp, vy: Math.sin(a) * sp - 120,
      g: floaty ? -90 : 620, rot: Math.random() * 6, vr: (Math.random() - .5) * 8,
      s: 8 + Math.random() * 16, life: 1.1 + Math.random() * 1.3, max: 0, 
      spr: Math.random() < 0.65 ? SPR[ci] : SPR[Math.random() < .5 ? 6 : 7]
    });
    parts[parts.length - 1].max = parts[parts.length - 1].life;
  }
}
function rain(n) {
  n = Math.min(n, Math.max(0, 2600 - parts.length));
  for (let i = 0; i < n; i++) {
    const l = 2 + Math.random() * 1.5;
    parts.push({ x: Math.random() * W, y: -20 - Math.random() * 200, vx: (Math.random() - .5) * 60, vy: 80 + Math.random() * 160,
      g: 80, rot: Math.random() * 6, vr: (Math.random() - .5) * 4, s: 12 + Math.random() * 18, life: l, max: l,
      spr: SPR[Math.floor(Math.random() * SPR.length)] });
  }
}
function destroyBrick(b) {
  const cx = b.x + b.w / 2, cy = b.y + b.h / 2;
  burst(cx, cy, b.ci, 30 + Math.floor(Math.random() * 10));
  rings.push({ x: cx, y: cy, r: 6, a: 1, ci: b.ci });
  const pts = 10 * level;
  score += pts;
  pops.push({ x: cx, y: cy, t: 0, txt: '+' + pts });
  shake = Math.min(8, shake + 4);
}

/* ---------- update ---------- */
function predictX() {
  const t = (paddle.y - ball.r - ball.y) / ball.vy;
  let x = ball.x + ball.vx * t;
  const m = W - 2 * ball.r, p0 = x - ball.r;
  let p = ((p0 % (2 * m)) + 2 * m) % (2 * m);
  if (p > m) p = 2 * m - p;
  return p + ball.r;
}
function fixAngle() {
  const s = Math.hypot(ball.vx, ball.vy), min = s * 0.3;
  if (Math.abs(ball.vy) < min) {
    ball.vy = (ball.vy < 0 ? -1 : 1) * min;
    const vx = Math.sqrt(s * s - ball.vy * ball.vy);
    ball.vx = (ball.vx < 0 ? -1 : 1) * vx;
  }
}
function loseBall() {
  lives--;
  shake = 10;
  if (lives <= 0) {
    if (mode === 'demo') { resetGame(); }
    else { mode = 'over'; rain(150); }
  } else resetBall();
}

function update(dt) {
  T += dt;
  paddle.squish *= Math.pow(0.001, dt);
  if (mode !== 'over') {
    const hw = paddle.w / 2;
    if (mode === 'demo') {
      let tx;
      if (ball.stuck) tx = W / 2 + Math.sin(T * 1.3) * 250;
      else if (ball.vy > 0) tx = predictX() + aiOff;
      else tx = ball.x * 0.5 + W * 0.25 + aiOff * 0.3;
      const d = tx - paddle.x, m = 950 * dt;
      paddle.x += Math.max(-m, Math.min(m, d));
    } else {
      const dir = (keys.ArrowRight || keys.d || keys.D ? 1 : 0) - (keys.ArrowLeft || keys.a || keys.A ? 1 : 0);
      if (dir) paddle.targetX += dir * 800 * dt;
      paddle.targetX = Math.max(hw, Math.min(W - hw, paddle.targetX));
      paddle.x += (paddle.targetX - paddle.x) * Math.min(1, dt * 28);
    }
    paddle.x = Math.max(hw, Math.min(W - hw, paddle.x));

    if (ball.stuck) {
      ball.x = paddle.x; ball.y = paddle.y - ball.r; ball.t += dt;
      if (ball.t > (mode === 'demo' ? 0.9 : 2.2)) launch();
    } else {
      const sp = Math.hypot(ball.vx, ball.vy);
      const n = Math.max(1, Math.ceil(sp * dt / 4)), st = dt / n;
      for (let i = 0; i < n && !ball.stuck; i++) {
        ball.x += ball.vx * st; ball.y += ball.vy * st;
        const r = ball.r;
        if (ball.x < r) { ball.x = r; ball.vx = Math.abs(ball.vx); }
        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); fixAngle(); }
        // paddle
        if (ball.vy > 0 && ball.y + r >= paddle.y && ball.y - r <= paddle.y + 20 &&
            Math.abs(ball.x - paddle.x) <= paddle.w / 2 + r * 0.6) {
          const rel = Math.max(-1, Math.min(1, (ball.x - paddle.x) / (paddle.w / 2)));
          const a = rel * 1.05, s = ballSpeed();
          ball.vx = Math.sin(a) * s; ball.vy = -Math.cos(a) * s;
          ball.y = paddle.y - r;
          paddle.squish = 1;
          aiOff = (Math.random() - .5) * 90;
          burst(ball.x, paddle.y, 6, 4);
        }
        // bricks
        for (let j = 0; j < bricks.length; j++) {
          const b = bricks[j];
          const px = Math.max(b.x, Math.min(ball.x, b.x + b.w));
          const py = Math.max(b.y, Math.min(ball.y, b.y + b.h));
          const dx = ball.x - px, dy = ball.y - py;
          if (dx * dx + dy * dy < r * r) {
            const cx = b.x + b.w / 2, cy = b.y + b.h / 2;
            const nx = (ball.x - cx) / (b.w / 2), ny = (ball.y - cy) / (b.h / 2);
            if (Math.abs(nx) > Math.abs(ny)) {
              ball.vx = nx > 0 ? Math.abs(ball.vx) : -Math.abs(ball.vx);
              ball.x = nx > 0 ? b.x + b.w + r : b.x - r;
            } else {
              ball.vy = ny > 0 ? Math.abs(ball.vy) : -Math.abs(ball.vy);
              ball.y = ny > 0 ? b.y + b.h + r : b.y - r;
            }
            fixAngle();
            b.hp--;
            if (b.hp <= 0) { bricks.splice(j, 1); destroyBrick(b); }
            else { b.flash = 1; burst(ball.x, ball.y, b.ci, 6); score += 2; shake = Math.min(8, shake + 1.5); }
            break;
          }
        }
        if (ball.y > H + 20) { loseBall(); break; }
      }
      if (bricks.length === 0 && mode !== 'over') {
        level++; buildBricks(); resetBall(); rain(140); score += 100;
      }
    }
  }
  for (const b of bricks) b.flash = Math.max(0, b.flash - dt * 4);
  // particles
  let w = 0;
  for (let i = 0; i < parts.length; i++) {
    const p = parts[i];
    p.life -= dt;
    if (p.life <= 0) continue;
    p.vy += p.g * dt;
    const d = Math.pow(0.35, dt);
    p.vx *= d; if (p.g > 0) p.vy *= Math.pow(0.7, dt); else p.vy *= d;
    p.x += p.vx * dt; p.y += p.vy * dt; p.rot += p.vr * dt;
    if (p.y > H + 40) continue;
    parts[w++] = p;
  }
  parts.length = w;
  for (const r of rings) { r.r += 260 * dt; r.a -= dt * 2.2; }
  rings = rings.filter(r => r.a > 0);
  for (const p of pops) { p.t += dt; p.y -= 40 * dt; }
  pops = pops.filter(p => p.t < 0.9);
  shake *= Math.pow(0.02, dt);
  for (const h of bgHearts) { h.y -= h.v * dt; if (h.y < -40) { h.y = H + 40; h.x = Math.random() * W; } }
}

/* ---------- drawing ---------- */
function lighten(hex, amt) {
  const n = parseInt(hex.slice(1), 16);
  const f = v => Math.round(v + (255 - v) * amt);
  return `rgb(${f(n >> 16)},${f((n >> 8) & 255)},${f(n & 255)})`;
}
function drawBrick(b) {
  const col = COLORS[b.ci];
  const g = ctx.createLinearGradient(0, b.y, 0, b.y + b.h);
  g.addColorStop(0, lighten(col, .45 + b.flash * .4)); g.addColorStop(.5, col); g.addColorStop(1, lighten('#000000'.replace('000000', '000000'), 0) && col);
  ctx.fillStyle = g;
  ctx.beginPath(); ctx.roundRect(b.x, b.y, b.w, b.h, 7); ctx.fill();
  ctx.fillStyle = 'rgba(0,0,0,.18)';
  ctx.beginPath(); ctx.roundRect(b.x, b.y + b.h * .65, b.w, b.h * .35, [0, 0, 7, 7]); ctx.fill();
  ctx.fillStyle = 'rgba(255,255,255,.45)';
  ctx.beginPath(); ctx.roundRect(b.x + 5, b.y + 3, b.w - 10, 5, 3); ctx.fill();
  if (b.hp > 1) { ctx.lineWidth = 2.5; ctx.strokeStyle = '#fff'; ctx.beginPath(); ctx.roundRect(b.x + 1, b.y + 1, b.w - 2, b.h - 2, 6); ctx.stroke(); }
  // little heart emblem
  ctx.globalAlpha = .55;
  ctx.drawImage(SPR[7], b.x + b.w / 2 - 7, b.y + b.h / 2 - 6, 14, 14);
  ctx.globalAlpha = 1;
}

function drawBurger() {
  const sq = paddle.squish;
  const x = paddle.x, y = paddle.y, hw = paddle.w / 2, l = -hw, r = hw;
  ctx.save();
  ctx.translate(x, y + 41);
  ctx.scale(1 + sq * 0.14, 1 - sq * 0.2);
  ctx.translate(0, -41);
  // shadow
  ctx.fillStyle = 'rgba(0,0,0,.25)';
  ctx.beginPath(); ctx.ellipse(0, 44, hw * .9, 4, 0, 0, 7); ctx.fill();
  // bottom bun
  let g = ctx.createLinearGradient(0, 33, 0, 42);
  g.addColorStop(0, '#e8a64a'); g.addColorStop(1, '#b8691f');
  ctx.fillStyle = g;
  ctx.beginPath(); ctx.roundRect(l + 4, 33, hw * 2 - 8, 9, [2, 2, 9, 9]); ctx.fill();
  // patty
  g = ctx.createLinearGradient(0, 25, 0, 34);
  g.addColorStop(0, '#6b3a22'); g.addColorStop(1, '#3d1f10');
  ctx.fillStyle = g;
  ctx.beginPath(); ctx.roundRect(l - 1, 25, hw * 2 + 2, 10, 5); ctx.fill();
  // cheese
  ctx.fillStyle = '#ffcf33';
  ctx.beginPath();
  ctx.moveTo(l + 2, 21); ctx.lineTo(r - 2, 21); ctx.lineTo(r - 2, 26);
  ctx.lineTo(r - 14, 26); ctx.lineTo(r - 20, 32); ctx.lineTo(r - 26, 26);
  ctx.lineTo(l + 18, 26); ctx.lineTo(l + 12, 30); ctx.lineTo(l + 8, 26); ctx.lineTo(l + 2, 26);
  ctx.closePath(); ctx.fill();
  // lettuce
  ctx.fillStyle = '#5cc84a';
  ctx.beginPath(); ctx.moveTo(l - 3, 16);
  for (let i = 0; i <= 26; i++) { const xx = l - 3 + (hw * 2 + 6) * i / 26; ctx.lineTo(xx, 21 + Math.sin(i * 1.3) * 2.6 + 1.5); }
  ctx.lineTo(r + 3, 16); ctx.closePath(); ctx.fill();
  // top bun
  g = ctx.createLinearGradient(0, -4, 0, 17);
  g.addColorStop(0, '#f6bd5e'); g.addColorStop(1, '#d68a2c');
  ctx.fillStyle = g;
  ctx.beginPath(); ctx.moveTo(l, 17); ctx.bezierCurveTo(l, -10, r, -10, r, 17); ctx.closePath(); ctx.fill();
  ctx.fillStyle = 'rgba(255,255,255,.35)';
  ctx.beginPath(); ctx.ellipse(-30, 1, 14, 2.5, -0.3, 0, 7); ctx.fill();
  // seeds
  ctx.fillStyle = '#fff3d0';
  [[-46, 9, .4], [-34, 3, -.3], [-24, 0, .2], [24, 0, -.2], [36, 3, .3], [47, 9, -.4], [0, -1.5, 0]].forEach(s => {
    ctx.beginPath(); ctx.ellipse(s[0], s[1], 3, 1.6, s[2], 0, 7); ctx.fill();
  });
  // face
  const blink = (T % 3.2) < 0.12;
  ctx.fillStyle = '#4a2410';
  if (blink || sq > 0.5) {
    ctx.strokeStyle = '#4a2410'; ctx.lineWidth = 1.8; ctx.lineCap = 'round';
    ctx.beginPath(); ctx.moveTo(-15, 6); ctx.lineTo(-9, 6); ctx.moveTo(9, 6); ctx.lineTo(15, 6); ctx.stroke();
  } else {
    ctx.beginPath(); ctx.arc(-12, 6, 2.4, 0, 7); ctx.arc(12, 6, 2.4, 0, 7); ctx.fill();
  }
  ctx.strokeStyle = '#4a2410'; ctx.lineWidth = 1.8; ctx.lineCap = 'round';
  ctx.beginPath(); ctx.arc(0, 7.5, 4, 0.15 * Math.PI, 0.85 * Math.PI); ctx.stroke();
  ctx.fillStyle = 'rgba(255,90,120,.55)';
  ctx.beginPath(); ctx.ellipse(-22, 10, 4.5, 2.4, 0, 0, 7); ctx.ellipse(22, 10, 4.5, 2.4, 0, 0, 7); ctx.fill();
  ctx.restore();
}

function drawBall() {
  const b = ball;
  const g = ctx.createRadialGradient(b.x, b.y, 2, b.x, b.y, 24);
  g.addColorStop(0, 'rgba(255,150,200,.55)'); g.addColorStop(1, 'rgba(255,150,200,0)');
  ctx.fillStyle = g; ctx.beginPath(); ctx.arc(b.x, b.y, 24, 0, 7); ctx.fill();
  const g2 = ctx.createRadialGradient(b.x - 3, b.y - 3, 1, b.x, b.y, b.r);
  g2.addColorStop(0, '#fff'); g2.addColorStop(1, '#ffb3d1');
  ctx.fillStyle = g2; ctx.beginPath(); ctx.arc(b.x, b.y, b.r, 0, 7); ctx.fill();
}

function txt(s, x, y, size, color, align) {
  ctx.font = `800 ${size}px "Trebuchet MS", system-ui, sans-serif`;
  ctx.textAlign = align || 'center'; ctx.textBaseline = 'middle';
  ctx.lineWidth = Math.max(3, size / 6); ctx.lineJoin = 'round';
  ctx.strokeStyle = 'rgba(60,0,40,.8)'; ctx.strokeText(s, x, y);
  ctx.fillStyle = color || '#fff'; ctx.fillText(s, x, y);
}

function draw() {
  ctx.setTransform(scale, 0, 0, scale, 0, 0);
  const bg = ctx.createLinearGradient(0, 0, 0, H);
  bg.addColorStop(0, '#4a1655'); bg.addColorStop(1, '#1d0a2b');
  ctx.fillStyle = bg; ctx.fillRect(0, 0, W, H);
  for (const h of bgHearts) {
    ctx.globalAlpha = .12;
    ctx.drawImage(SPR[h.c], h.x + Math.sin(T + h.p) * 14 - h.s / 2, h.y - h.s / 2, h.s, h.s);
  }
  ctx.globalAlpha = 1;

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

  for (const b of bricks) drawBrick(b);
  drawBurger();
  if (mode !== 'over') drawBall();

  for (const r of rings) {
    ctx.globalAlpha = Math.max(0, r.a);
    ctx.strokeStyle = COLORS[r.ci]; ctx.lineWidth = 4;
    ctx.beginPath(); ctx.arc(r.x, r.y, r.r, 0, 7); ctx.stroke();
  }
  for (const p of parts) {
    const a = Math.min(1, p.life / 0.5);
    ctx.globalAlpha = a;
    ctx.save();
    ctx.translate(p.x, p.y); ctx.rotate(p.rot * 0.35);
    ctx.drawImage(p.spr, -p.s / 2, -p.s / 2, p.s, p.s);
    ctx.restore();
  }
  ctx.globalAlpha = 1;
  for (const p of pops) {
    ctx.globalAlpha = 1 - p.t / 0.9;
    txt(p.txt, p.x, p.y, 16, '#fff');
  }
  ctx.globalAlpha = 1;
  ctx.restore();

  // HUD
  txt('SCORE ' + score, 20, 28, 20, '#ffd6ea', 'left');
  txt('LEVEL ' + level, W / 2, 28, 20, '#ffd6ea');
  for (let i = 0; i < 3; i++) {
    ctx.globalAlpha = i < lives ? 1 : .22;
    ctx.drawImage(SPR[0], W - 38 - i * 30, 12, 26, 26);
  }
  ctx.globalAlpha = 1;

  // overlays
  const pulse = 0.5 + 0.5 * Math.sin(T * 4);
  if (mode === 'demo' || mode === 'over') {
    ctx.fillStyle = 'rgba(25,5,35,.62)';
    ctx.beginPath(); ctx.roundRect(120, 300, 560, 170, 22); ctx.fill();
    ctx.lineWidth = 2; ctx.strokeStyle = 'rgba(255,140,190,.6)'; ctx.stroke();
    if (mode === 'demo') {
      txt('HEARTBURGER BREAKOUT', W / 2, 345, 38, '#ff9ccb');
      ctx.globalAlpha = 0.55 + pulse * 0.45;
      txt('Click or press a key to start', W / 2, 400, 26, '#fff');
      ctx.globalAlpha = 1;
      txt('Mouse / touch / ← → to move  ·  every brick bursts into hearts', W / 2, 442, 15, '#ffd6ea');
    } else {
      txt('GAME OVER', W / 2, 345, 42, '#ff9ccb');
      txt('Score ' + score + '  ·  Level ' + level, W / 2, 392, 22, '#fff');
      ctx.globalAlpha = 0.55 + pulse * 0.45;
      txt('Click or press a key to play again', W / 2, 438, 22, '#fff');
      ctx.globalAlpha = 1;
    }
  } else if (ball.stuck) {
    ctx.globalAlpha = 0.5 + pulse * 0.5;
    txt('Click / Space to launch', W / 2, H - 110, 18, '#fff');
    ctx.globalAlpha = 1;
  }
}

/* ---------- input ---------- */
function toLogicalX(e) {
  const r = cv.getBoundingClientRect();
  return (e.clientX - r.left) / r.width * W;
}
cv.addEventListener('pointerdown', e => {
  e.preventDefault();
  if (mode !== 'play') { startPlay(); paddle.targetX = toLogicalX(e); }
  else { paddle.targetX = toLogicalX(e); launch(); }
});
window.addEventListener('pointermove', e => {
  if (mode === 'play') paddle.targetX = toLogicalX(e);
});
window.addEventListener('keydown', e => {
  if (['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', ' '].includes(e.key)) e.preventDefault();
  if (['Shift', 'Control', 'Alt', 'Meta', 'Tab'].includes(e.key)) return;
  if (mode !== 'play') { startPlay(); return; }
  keys[e.key] = true;
  if (e.key === ' ' || e.key === 'ArrowUp' || e.key === 'Enter') launch();
});
window.addEventListener('keyup', e => { keys[e.key] = false; });
window.addEventListener('blur', () => { for (const k in keys) keys[k] = false; });

/* ---------- loop ---------- */
resetGame();
// pre-seed a little burst of hearts so the first frames are lively
setTimeout(() => {}, 0);
let last = performance.now();
function frame(now) {
  const dt = Math.min(0.033, (now - last) / 1000);
  last = now;
  update(dt);
  draw();
  requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
})();
</script>
</body>
</html>
Reasoning trace (6,987 tokens)
I'm mapping out a breakout game where the paddle is a hamburger and bricks burst into hearts, plus a self-playing demo that runs behind a "Click or press a key to start" prompt. I'm planning a fixed 800x600 logical canvas that scales to fit the window, with a dark pink-purple gradient background and some subtle floating shapes.

I'm working out the rest of the game's design: the bricks are candy-colored rows, each one bursting into about 25 physics-driven hearts, and the hamburger paddle is built from stacked buns, lettuce, cheese, and patty. I'm also planning the demo-to-player switch, the score and level flow, and capping particles at around 1500.

I'm settling the mechanics now: the ball moves in substeps so collisions stay accurate, brick hits are resolved by checking which axis overlaps more, and the paddle angles the bounce based on where the ball lands. The ball waits on the paddle in play mode until a click or space, and in demo mode it launches on its own after a short delay. I'm also sketching the mode states (demo, play, over) and how pointer and keyboard input move the paddle.

Next I'm pinning down the heart drawing: pre-rendering heart sprites to offscreen canvases in a few colors so drawing a lot of them stays fast, then stamping them with rotation and scale.

I'm working out the heart's bezier outline, the curves that bend up into two lobes and meet at a point below. Once that path is settled, I'm planning to pre-render it into small sprites, one per color, so the many falling hearts can be stamped quickly.

I'm settling the particle behavior now: each brick bursts into hearts launched at random angles, mostly in the brick's own color, with about 30% drifting upward instead of falling. I'm also adding small sparkles and a flash when a brick is hit, and thinking about the brick shading with a gradient and highlight. Next I'm sizing the grid: 10 columns of 68px bricks with 6px gaps inside an 800x600 logical canvas.

I'm sizing the brick grid: 10 columns at 68px with 6px gaps, centered in the 800-pixel width, six rows tall. From level two on, the top rows get two hit points and the ball speeds up with each level. I'm also planning a glossy pink-glow ball and a HUD with score, lives as hearts, and the level, and I'll have the AI paddle track the ball with a bit of noise.

I'm tuning the AI paddle so it predicts where the ball will land, including wall bounces, and adds a bit of sideways offset so its hits vary. I'm capping its speed around 1000 px/s so it looks natural, and I'm sizing the hamburger paddle at 130 by 40 with a hitbox that covers only the top bun.

Writing the hamburger's top bun as a bezier dome, with a gradient fill.

Still writing the hamburger's face and patty details...

Still working out the paddle's squish and bounce: the hit point sets the bounce angle, and the ball gets reflected off the top of the hamburger when it's moving down into it.

I'm settling the collision response: after finding the brick the ball hit, I compare its offset from the brick's center to decide whether to flip the horizontal or vertical velocity, then push the ball out so it doesn't double-count. Screen shake will just be a decaying offset that jitters the canvas, and I'm starting to lay out the game flow for resetting bricks, lives, and score when the ball is lost.

I'm setting up the lose-a-life logic: when lives run out, demo mode just restarts while a real game switches to game over. I'm also planning a level-clear step that rebuilds the bricks, rains hearts down from the top, and keeps the demo running. Last, I'm sketching the start overlay: a dim, translucent panel near y 380 with the title, a pulsing "Click or press a key to start" prompt, and a small movement hint.

I'm settling the keyboard controls now: any key starts the game from demo or game over, space launches the ball in play, and the arrow keys or A/D move the paddle, with the default scrolling suppressed for those keys. Pointer input goes through window events, mapped to canvas coordinates, and only moves the paddle in play mode. The canvas scales to fit the window at a 4:3 ratio and uses the device pixel ratio so it stays sharp.

Now I'm settling the sizing and scaling: the canvas fits the window at a 4:3 ratio, and I'm leaning toward keeping the logical size fixed rather than making height dynamic. Next I'm drafting the background, a gradient with a few faint drifting hearts, and the heart sprite color set.

I'm setting up the sprite helper that draws each heart with a radial gradient and a small highlight, and I'm deciding how particles are stored and capped at around 2500 so the level-clear burst stays manageable.

Now I'm adding particle effects: a radial shockwave ring on each brick break and floating "+10" score popups, with the hearts falling off the bottom of the screen rather than bouncing.

I'm defining the update step, where the paddle is driven by the AI in demo mode, by keys and pointer in play mode, and frozen in game over. The ball stays pinned to the paddle until it's launched.

Next I'm working out the ball's launch: a random angle within about 0.4 radians of straight up, with the velocity split into horizontal and vertical parts. For the AI paddle, while the ball is stuck I'm sweeping it side to side, and once the ball is moving down I'm predicting where it will land, folding that position back in off the walls.

I'm finishing the AI's aim: when the ball is coming down, I offset its target by a random amount each paddle hit so its returns vary, and when it's heading up I drift the paddle toward the middle. Next I'm setting up bricks with a color by row, a hit-point count, and a flash value, and when one is destroyed I'll spawn hearts, a ring, a score popup, and a bit of screen shake.

Still working on the drawing code: the bricks get a rounded shape with a vertical gradient and a top highlight, and two-hit bricks get a white outline. I'm laying out the HUD with the score on the left, the level in the middle, and lives as hearts on the right, with lost lives faded out.

Still working out where the seeds sit on the bun, keeping them clear of the face.

I'm checking where the seeds sit on the bun against the dome curve: the ones near the edges look like they're inside the outline, and the mid-width ones are close to the top edge, so I'll keep them a little lower to stay clear of the face.

Writing the input handling now: pointer coordinates get mapped into the 800-wide canvas, moving the paddle in play mode, and a click starts the game from demo or game over or launches the ball while it's stuck.

Still writing the game logic: I'm wiring up the reset flow and the brick layout, planning for extra rows and two-hit bricks as the level goes up.

Still writing the game loop: I'm stepping the ball in small increments so it can't tunnel through bricks, and checking the walls and the bottom edge for losing a life.

Still writing the rendering setup: I'm handling the canvas scaling, with the window-fit sizing and the device pixel ratio for sharp output, and adding a few final drawing details.

Still writing the page styling: the body is a dark gradient with no margin and centered flex layout, and the canvas gets a soft box-shadow, so on narrow screens it simply fills the width.