Models 44

GPT-6 Luna

by OpenAIReasoningClean
Open in new window

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

How it went

Total time
46.8 s
Cost
$0.0033
Output tokens
6,558
Spent thinking
0 tokens
Speed
149 tok/s
Console errors
0
First token after
2.8 s
First visible text after
2.8 s
Prompt tokens
508
Code size
19.3 KB
Finish reason
stop
Served by
OpenAI
Model id
openai/gpt-6-luna
Reasoning sent
{"effort":"none"}
Max output
64,000 tokens
Generated
Nov 10, 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 · 461 lines

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Breakout — Neon Arcade</title>
<style>
  * { box-sizing: border-box; }
  :root {
    --bg: #080d19;
    --panel: rgba(14, 22, 40, .86);
    --muted: #8895b2;
    --white: #f1f5ff;
    --cyan: #4ae5ff;
    --pink: #ff5c9c;
    --gold: #ffcf66;
  }
  html, body { width:100%; height:100%; margin:0; overflow:hidden; }
  body {
    font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
    color:var(--white);
    background:
      radial-gradient(ellipse at 50% -8%, #192b4b 0%, #0c1425 42%, #070b15 100%);
    display:flex; align-items:center; justify-content:center;
  }
  .shell { width:min(1120px, calc(100vw - 40px)); }
  header {
    display:flex; justify-content:space-between; align-items:flex-end;
    margin:0 2px 15px; gap:20px;
  }
  .brand { display:flex; align-items:center; gap:13px; }
  .mark {
    width:42px; height:42px; border:1px solid rgba(74,229,255,.38);
    background:linear-gradient(145deg,rgba(74,229,255,.19),rgba(74,229,255,.03));
    border-radius:13px; display:grid; place-items:center;
    box-shadow:0 0 25px rgba(74,229,255,.12);
  }
  .mark svg { width:23px; height:23px; }
  h1 { font-size:20px; letter-spacing:.13em; margin:0; font-weight:800; }
  .subtitle { color:var(--muted); font-size:11px; letter-spacing:.16em; margin-top:3px; }
  .hud { display:flex; gap:9px; }
  .stat {
    min-width:94px; padding:8px 13px 7px; border:1px solid rgba(132,158,204,.16);
    background:rgba(16,25,44,.72); border-radius:10px;
  }
  .stat-label { color:#7e8eaf; font-size:9px; letter-spacing:.17em; text-transform:uppercase; }
  .stat-value { margin-top:2px; font-size:16px; font-weight:750; letter-spacing:.04em; }
  #livesValue { color:#ff729f; letter-spacing:3px; }
  .board {
    position:relative; overflow:hidden; border-radius:17px;
    border:1px solid rgba(116,160,218,.23);
    background:#080f1d;
    box-shadow:0 24px 80px rgba(0,0,0,.48), 0 0 55px rgba(52,111,190,.09);
  }
  canvas { width:100%; height:auto; display:block; aspect-ratio: 1120 / 650; }
  .overlay {
    position:absolute; inset:0; display:flex; align-items:center; justify-content:center;
    background:linear-gradient(180deg,rgba(5,10,20,.28),rgba(5,10,20,.61));
    transition:opacity .25s;
  }
  .overlay.hidden { opacity:0; pointer-events:none; }
  .modal {
    width:min(430px, calc(100% - 36px)); text-align:center; padding:28px 30px 25px;
    border-radius:18px; border:1px solid rgba(128,174,228,.25);
    background:linear-gradient(145deg,rgba(18,31,54,.95),rgba(9,17,32,.94));
    box-shadow:0 20px 80px rgba(0,0,0,.45),0 0 45px rgba(74,229,255,.08);
  }
  .eyebrow { color:var(--cyan); letter-spacing:.22em; font-size:10px; font-weight:800; }
  .modal h2 { font-size:29px; letter-spacing:.08em; margin:8px 0; }
  .modal p { color:#a8b6d0; font-size:13px; line-height:1.55; margin:6px 0 19px; }
  .start-button {
    border:0; border-radius:9px; padding:12px 24px; color:#07111c; font-weight:850;
    letter-spacing:.12em; text-transform:uppercase; cursor:pointer;
    background:linear-gradient(90deg,#53e7ff,#72ffc8);
    box-shadow:0 5px 24px rgba(74,229,255,.23);
  }
  .start-button:hover { filter:brightness(1.08); transform:translateY(-1px); }
  .hint { margin-top:13px; color:#73829e; font-size:10px; letter-spacing:.06em; }
  footer {
    display:flex; justify-content:space-between; align-items:center; color:#72809d;
    font-size:10px; letter-spacing:.08em; margin:12px 3px 0;
  }
  .controls { display:flex; gap:17px; }
  .key { border:1px solid rgba(126,151,192,.28); border-radius:4px; padding:2px 5px; color:#bcc9df; }
  .legend { display:flex; gap:15px; }
  .legend span { display:flex; align-items:center; gap:6px; }
  .dot { width:7px; height:7px; border-radius:50%; background:#40d9ff; }
  .dot.tough { background:#ffb45f; }
  @media (max-width:600px) {
    .shell { width:calc(100vw - 22px); }
    header { align-items:center; margin-bottom:10px; }
    .mark { width:35px; height:35px; }
    h1 { font-size:15px; }
    .subtitle { font-size:8px; }
    .hud { gap:5px; }
    .stat { min-width:59px; padding:6px 7px; }
    .stat-label { font-size:7px; }
    .stat-value { font-size:13px; }
    #livesValue { letter-spacing:1px; }
    footer { font-size:8px; }
    .legend { gap:8px; }
    .controls { gap:9px; }
  }
</style>
</head>
<body>
<div class="shell">
  <header>
    <div class="brand">
      <div class="mark" aria-hidden="true">
        <svg viewBox="0 0 24 24" fill="none"><path d="M4 7h7M13 7h7M4 12h5M11 12h9M4 17h9M15 17h5" stroke="#4ae5ff" stroke-width="2" stroke-linecap="round"/><circle cx="12" cy="12" r="2.4" fill="#f1f5ff"/></svg>
      </div>
      <div><h1>BREAKOUT</h1><div class="subtitle">NEON ARCADE · SECTOR 01</div></div>
    </div>
    <div class="hud">
      <div class="stat"><div class="stat-label">Score</div><div class="stat-value" id="scoreValue">00000</div></div>
      <div class="stat"><div class="stat-label">Lives</div><div class="stat-value" id="livesValue">♥ ♥ ♥</div></div>
      <div class="stat"><div class="stat-label">Bricks</div><div class="stat-value" id="brickValue">42</div></div>
    </div>
  </header>

  <main class="board">
    <canvas id="game" width="1120" height="650" aria-label="Breakout game"></canvas>
    <div class="overlay" id="overlay">
      <div class="modal">
        <div class="eyebrow" id="modalEyebrow">READY PLAYER ONE</div>
        <h2 id="modalTitle">BREAK THE GRID</h2>
        <p id="modalText">Clear the neon wall. Tough bricks take two hits — catch the falling power-ups for an edge.</p>
        <button class="start-button" id="startButton">Start game</button>
        <div class="hint">CLICK OR PRESS A KEY TO START</div>
      </div>
    </div>
  </main>
  <footer>
    <div class="controls"><span><b class="key">←</b> <b class="key">→</b> Move</span><span>Mouse · Touch</span></div>
    <div class="legend"><span><i class="dot"></i> Standard</span><span><i class="dot tough"></i> Reinforced</span></div>
  </footer>
</div>
<script>
(() => {
  "use strict";
  const canvas = document.getElementById("game");
  const ctx = canvas.getContext("2d");
  const W = 1120, H = 650;
  const scoreEl = document.getElementById("scoreValue");
  const livesEl = document.getElementById("livesValue");
  const brickEl = document.getElementById("brickValue");
  const overlay = document.getElementById("overlay");
  const titleEl = document.getElementById("modalTitle");
  const eyebrowEl = document.getElementById("modalEyebrow");
  const textEl = document.getElementById("modalText");
  const startButton = document.getElementById("startButton");

  const palette = ["#46dfff", "#42f0c5", "#9df064", "#ffd05f", "#ff9a61", "#ff6196"];
  const brickDefs = [];
  const cols = 7, rows = 6;
  const brickW = 126, brickH = 25, gapX = 17, gapY = 13;
  const gridW = cols * brickW + (cols - 1) * gapX;
  const gridX = (W - gridW) / 2;
  const gridY = 88;
  const paddle = { x: W / 2, y: H - 53, w: 118, h: 14, target: W / 2, wideUntil: 0 };
  let balls = [], drops = [], particles = [], score = 0, lives = 3, remaining = 0;
  let running = false, ended = false, last = 0, keyLeft = false, keyRight = false;
  let demo = true, frame = 0;

  function makeBricks() {
    brickDefs.length = 0;
    for (let r = 0; r < rows; r++) {
      for (let c = 0; c < cols; c++) {
        // Mix in reinforced bricks, with glowing power-up markers on selected bricks.
        const tough = ((r * 3 + c * 5) % 7 === 0) || (r === 1 && c % 3 === 1);
        const power = ((r * 7 + c * 3) % 11 === 2) ? (c % 2 ? "wide" : "multi") : null;
        brickDefs.push({
          x:gridX + c * (brickW + gapX), y:gridY + r * (brickH + gapY),
          w:brickW, h:brickH, hp:tough ? 2 : 1, maxHp:tough ? 2 : 1,
          color:palette[r], power, alive:true
        });
      }
    }
    remaining = brickDefs.length;
  }

  function resetGame() {
    score = 0; lives = 3; drops = []; particles = []; paddle.w = 118;
    paddle.x = W/2; paddle.target = W/2; paddle.wideUntil = 0;
    makeBricks();
    balls = [{x:W/2, y:H-84, vx:3.25, vy:-5.1, r:7, trail:[]}];
    updateHud();
    running = true; ended = false; last = performance.now();
    overlay.classList.add("hidden");
  }
  function updateHud() {
    scoreEl.textContent = String(score).padStart(5, "0");
    livesEl.textContent = Array.from({length:3}, (_,i)=>i<lives ? "♥" : "·").join(" ");
    brickEl.textContent = String(remaining).padStart(2,"0");
  }
  function showEnd(win) {
    running = false; ended = true;
    eyebrowEl.textContent = win ? "SECTOR CLEARED" : "SIGNAL LOST";
    titleEl.textContent = win ? "VICTORY!" : "GAME OVER";
    textEl.textContent = win
      ? `You cleared the grid with ${String(score).padStart(5,"0")} points. Ready for another run?`
      : `Final score: ${String(score).padStart(5,"0")}. The grid is waiting for a rematch.`;
    startButton.textContent = "Play again";
    overlay.classList.remove("hidden");
  }
  function start() {
    if (!running) resetGame();
  }

  function addParticles(x,y,color,count=12) {
    for (let i=0;i<count;i++) {
      const a=Math.random()*Math.PI*2, speed=1+Math.random()*4;
      particles.push({x,y,vx:Math.cos(a)*speed,vy:Math.sin(a)*speed,
        life:.45+Math.random()*.5,max:.95,size:2+Math.random()*3,color});
    }
  }
  function dropPower(brick) {
    if (brick.power) drops.push({x:brick.x+brick.w/2,y:brick.y+brick.h/2,vy:1.7,kind:brick.power,spin:0});
  }
  function applyPower(kind) {
    if (kind === "wide") {
      paddle.w = 175; paddle.wideUntil = performance.now()+10000;
      addParticles(paddle.x,paddle.y-8,"#55f2ce",18);
    } else {
      const source = balls[0];
      if (source) {
        const speed = Math.hypot(source.vx,source.vy);
        const angle = Math.atan2(source.vy,source.vx);
        for (const delta of [-.42,.42]) {
          balls.push({x:source.x,y:source.y,vx:Math.cos(angle+delta)*speed,vy:Math.sin(angle+delta)*speed,r:7,trail:[]});
        }
      }
      addParticles(paddle.x,paddle.y-8,"#d191ff",18);
    }
  }

  function circleRect(ball, rect) {
    const nx = Math.max(rect.x, Math.min(ball.x, rect.x + rect.w));
    const ny = Math.max(rect.y, Math.min(ball.y, rect.y + rect.h));
    const dx = ball.x - nx, dy = ball.y - ny;
    return dx*dx + dy*dy <= ball.r*ball.r ? {nx,ny,dx,dy} : null;
  }
  function collideBrick(ball, brick) {
    const hit = circleRect(ball, brick);
    if (!hit) return false;
    const overlapX = ball.r - Math.abs(hit.dx);
    const overlapY = ball.r - Math.abs(hit.dy);
    if (overlapX < overlapY) {
      ball.x += Math.sign(hit.dx || ball.vx) * overlapX;
      ball.vx = -ball.vx;
    } else {
      ball.y += Math.sign(hit.dy || ball.vy) * overlapY;
      ball.vy = -ball.vy;
    }
    brick.hp--;
    if (brick.hp <= 0) {
      brick.alive = false; remaining--; score += 100;
      addParticles(brick.x+brick.w/2,brick.y+brick.h/2,brick.color,18);
      dropPower(brick);
      updateHud();
      if (remaining === 0) showEnd(true);
    } else {
      score += 25;
      addParticles(brick.x+brick.w/2,brick.y+brick.h/2,"#ffe6a0",6);
      updateHud();
    }
    return true;
  }

  function update(dt, now) {
    if (!running) return;
    const step = Math.min(dt, .034);
    if (keyLeft) paddle.target -= 620*step;
    if (keyRight) paddle.target += 620*step;
    if (demo) {
      const targetBall = balls[0];
      if (targetBall && targetBall.vy > 0) {
        const predict = targetBall.x + targetBall.vx * Math.max(0,(paddle.y-targetBall.y)/Math.max(1,targetBall.vy));
        paddle.target = predict + Math.sin(now*.0012)*18;
      } else paddle.target = W/2 + Math.sin(now*.0008)*220;
    }
    paddle.target = Math.max(paddle.w/2,Math.min(W-paddle.w/2,paddle.target));
    paddle.x += (paddle.target-paddle.x)*Math.min(1,step*13);
    if (paddle.wideUntil && now > paddle.wideUntil) { paddle.w=118; paddle.wideUntil=0; }

    for (let i=balls.length-1;i>=0;i--) {
      const b=balls[i];
      b.trail.push({x:b.x,y:b.y}); if (b.trail.length>9) b.trail.shift();
      b.x += b.vx*step*60; b.y += b.vy*step*60;
      if (b.x-b.r<16) { b.x=16+b.r; b.vx=Math.abs(b.vx); }
      if (b.x+b.r>W-16) { b.x=W-16-b.r; b.vx=-Math.abs(b.vx); }
      if (b.y-b.r<17) { b.y=17+b.r; b.vy=Math.abs(b.vy); }

      const rect={x:paddle.x-paddle.w/2,y:paddle.y-paddle.h/2,w:paddle.w,h:paddle.h};
      if (b.vy>0 && circleRect(b,rect)) {
        b.y=rect.y-b.r-.1;
        const hit=(b.x-paddle.x)/(paddle.w/2);
        const angle=hit*1.12;
        const speed=Math.min(8.2,Math.max(6.1,Math.hypot(b.vx,b.vy)*1.012));
        b.vx=Math.sin(angle)*speed;
        b.vy=-Math.cos(angle)*speed;
        addParticles(b.x,rect.y,"#55e8ff",5);
      }
      for (const brick of brickDefs) {
        if (brick.alive && collideBrick(b,brick)) break;
      }
      if (b.y-b.r>H+5) balls.splice(i,1);
    }
    if (balls.length===0 && running) {
      lives--;
      updateHud();
      if (lives<=0) showEnd(false);
      else balls=[{x:paddle.x,y:paddle.y-27,vx:(Math.random()-.5)*4,vy:-5.3,r:7,trail:[]}];
    }
    for (let i=drops.length-1;i>=0;i--) {
      const d=drops[i]; d.y+=d.vy*step*60; d.spin+=step*3;
      if (d.y+10>=paddle.y-paddle.h/2 && d.y-10<=paddle.y+paddle.h/2 &&
          d.x>=paddle.x-paddle.w/2-8 && d.x<=paddle.x+paddle.w/2+8) {
        applyPower(d.kind); drops.splice(i,1);
      } else if (d.y>H+15) drops.splice(i,1);
    }
    for (let i=particles.length-1;i>=0;i--) {
      const p=particles[i]; p.x+=p.vx*step*60; p.y+=p.vy*step*60; p.vy+=.045;
      p.life-=step; if(p.life<=0) particles.splice(i,1);
    }
  }

  function roundedRect(x,y,w,h,r) {
    ctx.beginPath(); ctx.roundRect(x,y,w,h,r);
  }
  function drawBackground(now) {
    ctx.clearRect(0,0,W,H);
    const grad=ctx.createLinearGradient(0,0,0,H);
    grad.addColorStop(0,"#0b1528"); grad.addColorStop(.56,"#091322"); grad.addColorStop(1,"#070d18");
    ctx.fillStyle=grad; ctx.fillRect(0,0,W,H);
    // Subtle arcade grid and ambient lights
    ctx.save();
    ctx.strokeStyle="rgba(94,139,197,.045)"; ctx.lineWidth=1;
    for(let x=28;x<W;x+=40){ctx.beginPath();ctx.moveTo(x,0);ctx.lineTo(x,H);ctx.stroke();}
    for(let y=20;y<H;y+=40){ctx.beginPath();ctx.moveTo(0,y);ctx.lineTo(W,y);ctx.stroke();}
    const glow=ctx.createRadialGradient(W/2,230,5,W/2,230,470);
    glow.addColorStop(0,"rgba(42,115,178,.10)"); glow.addColorStop(1,"rgba(20,55,100,0)");
    ctx.fillStyle=glow;ctx.fillRect(0,0,W,H);
    ctx.restore();
    ctx.fillStyle="rgba(128,169,216,.32)";
    ctx.fillRect(16,17, W-32,1);
  }

  function drawBricks() {
    for(const b of brickDefs) {
      if(!b.alive) continue;
      ctx.save();
      ctx.shadowBlur=13;ctx.shadowColor=b.color+"66";
      roundedRect(b.x,b.y,b.w,b.h,6);
      const g=ctx.createLinearGradient(0,b.y,0,b.y+b.h);
      g.addColorStop(0,b.color);g.addColorStop(1,b.color+"aa");
      ctx.fillStyle=g;ctx.fill();
      ctx.shadowBlur=0;
      ctx.strokeStyle="rgba(255,255,255,.34)";ctx.lineWidth=1;ctx.stroke();
      ctx.fillStyle="rgba(255,255,255,.18)";
      roundedRect(b.x+4,b.y+3,b.w-8,3,2);ctx.fill();
      if(b.maxHp===2 && b.hp===1) {
        // Clearly visible fracture after the first hit.
        ctx.strokeStyle="rgba(35,26,34,.75)";ctx.lineWidth=2;
        ctx.beginPath();ctx.moveTo(b.x+b.w*.57,b.y+4);ctx.lineTo(b.x+b.w*.49,b.y+11);
        ctx.lineTo(b.x+b.w*.57,b.y+15);ctx.lineTo(b.x+b.w*.45,b.y+22);ctx.stroke();
        ctx.strokeStyle="rgba(255,248,218,.72)";ctx.lineWidth=.8;
        ctx.beginPath();ctx.moveTo(b.x+b.w*.57,b.y+4);ctx.lineTo(b.x+b.w*.49,b.y+11);ctx.lineTo(b.x+b.w*.57,b.y+15);ctx.stroke();
      }
      if(b.power) {
        const cx=b.x+b.w-14,cy=b.y+12.5;
        ctx.beginPath();ctx.arc(cx,cy,9,0,Math.PI*2);
        ctx.fillStyle="rgba(7,14,26,.72)";ctx.fill();
        ctx.strokeStyle=b.power==="wide"?"#75ffce":"#db9aff";ctx.lineWidth=1;ctx.stroke();
        ctx.fillStyle=b.power==="wide"?"#a3ffde":"#e6c2ff";
        ctx.font="bold 12px ui-sans-serif,system-ui";ctx.textAlign="center";ctx.textBaseline="middle";
        ctx.fillText(b.power==="wide"?"↔":"✦",cx,cy+.3);
      }
      ctx.restore();
    }
  }
  function draw() {
    const now=performance.now();
    drawBackground(now);
    drawBricks();
    // Falls
    for(const d of drops) {
      ctx.save();ctx.translate(d.x,d.y);ctx.rotate(Math.sin(d.spin)*.12);
      const color=d.kind==="wide"?"#72ffce":"#d49cff";
      ctx.shadowColor=color;ctx.shadowBlur=17;
      roundedRect(-12,-12,24,24,7);ctx.fillStyle="rgba(19,28,51,.95)";ctx.fill();
      ctx.strokeStyle=color;ctx.lineWidth=1.5;ctx.stroke();
      ctx.fillStyle=color;ctx.font="bold 15px ui-sans-serif,system-ui";ctx.textAlign="center";ctx.textBaseline="middle";
      ctx.fillText(d.kind==="wide"?"↔":"✦",0,1);ctx.restore();
    }
    // Particle burst
    for(const p of particles) {
      ctx.globalAlpha=Math.max(0,p.life/p.max);ctx.fillStyle=p.color;
      ctx.beginPath();ctx.arc(p.x,p.y,p.size*Math.min(1,p.life*2),0,Math.PI*2);ctx.fill();
    }
    ctx.globalAlpha=1;
    // Paddle aura and paddle body
    const pw=paddle.w;
    ctx.save();ctx.shadowColor="#4ae5ff";ctx.shadowBlur=24;
    const pg=ctx.createLinearGradient(paddle.x-pw/2,paddle.y,paddle.x+pw/2,paddle.y);
    pg.addColorStop(0,"#42b8ff");pg.addColorStop(.5,"#e3ffff");pg.addColorStop(1,"#47e3ff");
    roundedRect(paddle.x-pw/2,paddle.y-paddle.h/2,pw,paddle.h,7);ctx.fillStyle=pg;ctx.fill();
    ctx.shadowBlur=0;ctx.fillStyle="rgba(255,255,255,.72)";
    roundedRect(paddle.x-pw*.22,paddle.y-paddle.h/2+2,pw*.44,3,2);ctx.fill();ctx.restore();
    // Balls and their comet tails
    for(const b of balls) {
      for(let i=0;i<b.trail.length;i++) {
        const t=b.trail[i],a=(i+1)/b.trail.length*.22;
        ctx.beginPath();ctx.arc(t.x,t.y,b.r*(i+1)/b.trail.length,0,Math.PI*2);
        ctx.fillStyle=`rgba(82,225,255,${a})`;ctx.fill();
      }
      ctx.save();ctx.shadowColor="#a4f8ff";ctx.shadowBlur=19;
      const bg=ctx.createRadialGradient(b.x-2,b.y-2,1,b.x,b.y,b.r+2);
      bg.addColorStop(0,"#ffffff");bg.addColorStop(.45,"#cefcff");bg.addColorStop(1,"#45dfff");
      ctx.beginPath();ctx.arc(b.x,b.y,b.r,0,Math.PI*2);ctx.fillStyle=bg;ctx.fill();ctx.restore();
    }
    // Bottom warning rail
    ctx.fillStyle="rgba(74,229,255,.18)";ctx.fillRect(16,H-17,W-32,1);
  }

  function loop(now) {
    const dt=(now-last)/1000 || 0;last=now;
    update(dt,now);draw();
    requestAnimationFrame(loop);
  }
  function pointerMove(e) {
    const rect=canvas.getBoundingClientRect();
    const scale=W/rect.width;
    paddle.target=(e.clientX-rect.left)*scale;
    paddle.target=Math.max(paddle.w/2,Math.min(W-paddle.w/2,paddle.target));
  }
  canvas.addEventListener("pointermove",pointerMove);
  canvas.addEventListener("pointerdown",()=>{ if(!running) start(); });
  window.addEventListener("keydown",e=>{
    if(["ArrowLeft","ArrowRight"," "].includes(e.key)) e.preventDefault();
    if(e.key==="ArrowLeft") {keyLeft=true;demo=false;}
    if(e.key==="ArrowRight") {keyRight=true;demo=false;}
    if(!running) { demo=false; start(); }
    if(ended && (e.key==="Enter"||e.key===" ")) start();
  });
  window.addEventListener("keyup",e=>{
    if(e.key==="ArrowLeft") keyLeft=false;
    if(e.key==="ArrowRight") keyRight=false;
  });
  startButton.addEventListener("click",()=>{demo=false;start();});
  makeBricks();
  updateHud();
  // Self-playing preview: visible action immediately, while the start prompt remains clear.
  balls=[{x:W/2,y:H-84,vx:3.1,vy:-5.2,r:7,trail:[]}];
  running=true;
  requestAnimationFrame(t=>{last=t;requestAnimationFrame(loop);});
})();
</script>
</body>
</html>