GPT-6 Luna
by OpenAIReasoningClean
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>