GLM 5.3
by Z.aiReasoningClean
Running live in a sandboxed frame. Click inside it before using the keyboard.
How it went
- Total time
- 2 min 57 s
- Cost
- $0.068
- Output tokens
- 18,844
- Spent thinking
- 12,956 tokens
- Speed
- 106.7 tok/s
- Console errors
- 0
- First token after
- 376 ms
- First visible text after
- 2 min 21 s
- Prompt tokens
- 507
- Code size
- 15.6 KB
- Finish reason
- stop
- Served by
- InferenceNet
- Model id
z-ai/glm-5.3- Reasoning sent
{"effort":"low"}- Max output
- 943,718 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 · 470 lines
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Neon Breakout</title>
<style>
html,body{margin:0;height:100%;background:#080a12;overflow:hidden;
font-family:system-ui,'Segoe UI',sans-serif;}
body{display:flex;align-items:center;justify-content:center;}
canvas{width:min(940px,96vw,130vh);aspect-ratio:900/660;display:block;
border-radius:10px;cursor:none;
box-shadow:0 24px 70px rgba(0,0,0,.65),0 0 0 1px rgba(120,150,255,.16);}
</style>
</head>
<body>
<canvas id="c" width="900" height="660"></canvas>
<script>
(()=>{
const cv=document.getElementById('c'),ctx=cv.getContext('2d');
const W=900,H=660,HUD=48,PADY=H-42;
const COLS=12,GAP=5,MARG=24,BW=(W-2*MARG-(COLS-1)*GAP)/COLS,BH=26,BTOP=88;
const rowColors=['#ff4d6d','#ff8f51','#ffd23f','#7ee081','#3dd6c3','#4aa8ff','#7b6cff','#c86bff'];
const rowsDef=[
'TTTTTTTTTTTT',
'TTTTTTTTTTTT',
'............',
'T..TT..TT..T',
'............',
'...T....T...',
'............',
'............'];
const powerAt={'2,2':'wide','2,9':'multi','4,5':'multi','4,6':'wide','6,1':'wide','6,10':'multi'};
const stars=Array.from({length:70},()=>({x:Math.random()*W,y:HUD+8+Math.random()*(H-HUD-16),
r:Math.random()*1.3+.3,a:Math.random()*.5+.15}));
const clamp=(v,a,b)=>v<a?a:v>b?b:v;
const rnd=(a,b)=>a+Math.random()*(b-a);
const TAU=Math.PI*2;
let state,score,lives,bricks,remaining,balls,parts,pops,drops,shake,frame,wide,pad,cf;
const keys={l:false,r:false};
let mouseX=null,mouseOn=false;
function newBall(stuck){return{x:pad.x+pad.w/2,y:pad.y-12,vx:0,vy:0,spd:6,stuck,st:0,dead:false};}
function reset(){
score=0;lives=3;parts=[];pops=[];drops=[];shake=0;wide=0;cf=0;frame=0;
pad={x:W/2-55,y:PADY,w:110,tw:110,h:14,pulse:0};
bricks=[];remaining=0;
rowsDef.forEach((rd,row)=>{
for(let c=0;c<COLS;c++){
const ch=rd[c];if(ch!=='T'&&ch!=='.')continue;
const tough=ch==='T';
bricks.push({x:MARG+c*(BW+GAP),y:BTOP+row*(BH+GAP),w:BW,h:BH,row,col:c,
hp:tough?2:1,max:tough?2:1,power:powerAt[row+','+c]||null,flash:0});
remaining++;
}
});
balls=[newBall(true)];
state='playing';
}
function launchBall(b){
const off=rnd(-0.55,0.55);
b.vx=Math.sin(off)*b.spd;b.vy=-Math.cos(off)*b.spd;b.stuck=false;
}
function burst(b,n){
const c=rowColors[b.row];
for(let i=0;i<n;i++)parts.push({x:b.x+b.w/2,y:b.y+b.h/2,
vx:rnd(-3.4,3.4),vy:rnd(-3.6,.8),life:rnd(20,42),max:42,s:rnd(1.5,3.5),c});
}
function hitBrick(b){
b.hp--;b.flash=6;
if(b.hp<=0){
remaining--;const pts=b.max===2?120:60;score+=pts;
burst(b,14);
pops.push({x:b.x+b.w/2,y:b.y+b.h/2,txt:'+'+pts,life:45,color:'#fff',size:14});
if(b.power)drops.push({x:b.x+b.w/2,y:b.y+b.h/2,p:b.power,t:0});
if(remaining<=0)state='win';
}else{score+=20;burst(b,4);}
}
function applyPower(p){
if(p==='wide'){pad.tw=185;wide=600;}
else{
const src=balls.find(b=>!b.stuck)||balls[0];
if(src){
if(src.stuck)launchBall(src);
const count=Math.min(2,6-balls.length);
const base=Math.atan2(src.vx,-src.vy);
for(let i=0;i<count;i++){
const nb=newBall(false);nb.x=src.x;nb.y=src.y-2;nb.spd=src.spd;
const a=base+(i===0?0.55:-0.55);
nb.vx=Math.sin(a)*nb.spd;nb.vy=-Math.cos(a)*nb.spd;
balls.push(nb);
}
}
}
}
function stepBall(b,s){
b.x+=b.vx*s;b.y+=b.vy*s;
const r=8;
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<HUD+r){b.y=HUD+r;b.vy=Math.abs(b.vy);}
// paddle
if(b.vy>0&&b.y+r>=pad.y&&b.y-r<=pad.y+pad.h&&b.x>pad.x-r&&b.x<pad.x+pad.w+r){
b.y=pad.y-r;
const off=clamp((b.x-(pad.x+pad.w/2))/(pad.w/2+r),-1,1);
const ang=off*1.05;
b.spd=Math.min(b.spd+0.05,11);
b.vx=Math.sin(ang)*b.spd;b.vy=-Math.cos(ang)*b.spd;
pad.pulse=6;
}
// bricks
for(const br of bricks){
if(br.hp<=0)continue;
const nx=clamp(b.x,br.x,br.x+br.w),ny=clamp(b.y,br.y,br.y+br.h);
const dx=b.x-nx,dy=b.y-ny,d2=dx*dx+dy*dy;
if(d2<r*r){
if(d2>1e-6){
const d=Math.sqrt(d2),nX=dx/d,nY=dy/d;
const dot=b.vx*nX+b.vy*nY;
if(dot<0){b.vx-=2*dot*nX;b.vy-=2*dot*nY;}
b.x=nx+nX*(r+0.2);b.y=ny+nY*(r+0.2);
}else{b.vy=-b.vy;b.y=br.y-r;}
const minVy=0.3*b.spd;
if(Math.abs(b.vy)<minVy){
const sy=b.vy===0?-1:Math.sign(b.vy);
b.vy=sy*minVy;
b.vx=Math.sign(b.vx||1)*Math.sqrt(Math.max(b.spd*b.spd-b.vy*b.vy,0));
}
hitBrick(br);
break;
}
}
}
function demoAI(f){
let target=W/2,found=false,bestT=1e9;
for(const b of balls){
if(b.stuck)continue;
if(b.vy>0){
const t=(pad.y-b.y)/b.vy;
if(t>=0&&t<bestT){
bestT=t;
let px=b.x+b.vx*t;
while(px<12||px>W-12){if(px<12)px=24-px;if(px>W-12)px=2*(W-12)-px;}
target=px;found=true;
}
}
}
if(!found){let bd=1e9;for(const b of balls){const d=Math.abs(b.x-(pad.x+pad.w/2));if(d<bd){bd=d;target=b.x;}}}
target+=Math.sin(frame*0.03)*7;
pad.x=clamp(pad.x+clamp(target-(pad.x+pad.w/2),-9*f,9*f),8,W-8-pad.w);
}
function update(f){
frame+=f;
if(wide>0){wide-=f;if(wide<=0){wide=0;pad.tw=110;}}
pad.w+=(pad.tw-pad.w)*Math.min(1,0.25*f);
if(pad.pulse>0)pad.pulse-=f;
for(const b of bricks)if(b.flash>0)b.flash-=f;
if(shake>0)shake-=f;
if(state==='demo')demoAI(f);
else if(state==='playing'){
if(keys.l||keys.r){
mouseOn=false;
pad.x=clamp(pad.x+((keys.r?1:0)-(keys.l?1:0))*10*f,8,W-8-pad.w);
}else if(mouseOn&&mouseX!=null){
pad.x=clamp(mouseX-pad.w/2,8,W-8-pad.w);
}
}
const live=state==='playing'||state==='demo';
if(live){
for(const b of balls){
if(b.stuck){
b.x=pad.x+pad.w/2;b.y=pad.y-12;b.st+=f;
if(b.st>(state==='demo'?55:140))launchBall(b);
continue;
}
const n=Math.max(1,Math.ceil((b.spd*f)/2.5)),s=f/n;
for(let i=0;i<n;i++){
stepBall(b,s);
if(b.y-10>H){b.dead=true;break;}
}
}
if(balls.some(b=>b.dead)){
balls=balls.filter(b=>!b.dead);
if(balls.length===0&&state!=='win'&&state!=='gameover'){
lives--;shake=14;
if(lives<=0)state='gameover';
else balls.push(newBall(true));
}
}
for(const d of drops){
d.t+=f;d.y+=2.7*f;
if(d.y+14>pad.y&&d.y-14<pad.y+pad.h&&d.x>pad.x-14&&d.x<pad.x+pad.w+14){
d.got=true;score+=150;applyPower(d.p);
pops.push({x:d.x,y:pad.y-24,txt:d.p==='wide'?'WIDE PADDLE!':'MULTI-BALL!',
life:70,color:d.p==='wide'?'#ffd23f':'#7ee081',size:18});
}
if(d.y>H+30)d.got=true;
}
drops=drops.filter(d=>!d.got);
}
for(const p of parts){p.x+=p.vx*f;p.y+=p.vy*f;p.vy+=0.14*f;p.life-=f;}
parts=parts.filter(p=>p.life>0);
for(const p of pops){p.y-=0.5*f;p.life-=f;}
pops=pops.filter(p=>p.life>0);
if(state==='win'){cf+=f;if(cf>4){cf=0;parts.push({x:rnd(30,W-30),y:HUD+10,
vx:rnd(-1.5,1.5),vy:rnd(1,3),life:60,max:60,s:rnd(2,4),
c:rowColors[Math.floor(rnd(0,8))]});}}
}
function rr(x,y,w,h,r){
ctx.beginPath();
ctx.moveTo(x+r,y);
ctx.arcTo(x+w,y,x+w,y+h,r);ctx.arcTo(x+w,y+h,x,y+h,r);
ctx.arcTo(x,y+h,x,y,r);ctx.arcTo(x,y,x+w,y,r);
ctx.closePath();
}
function drawIcon(cx,cy,r,type,bg,fg){
ctx.beginPath();ctx.arc(cx,cy,r,0,TAU);ctx.fillStyle=bg;ctx.fill();
ctx.fillStyle=fg;
if(type==='wide'){
const l=r*0.85;
ctx.fillRect(cx-l,cy-r*0.12,2*l,r*0.24);
ctx.beginPath();
ctx.moveTo(cx-l,cy);ctx.lineTo(cx-l+r*0.55,cy-r*0.45);ctx.lineTo(cx-l+r*0.55,cy+r*0.45);
ctx.closePath();ctx.fill();
ctx.beginPath();
ctx.moveTo(cx+l,cy);ctx.lineTo(cx+l-r*0.55,cy-r*0.45);ctx.lineTo(cx+l-r*0.55,cy+r*0.45);
ctx.closePath();ctx.fill();
}else{
const dr=r*0.24;
[[0,-r*0.42],[-r*0.45,r*0.3],[r*0.45,r*0.3]].forEach(o=>{
ctx.beginPath();ctx.arc(cx+o[0],cy+o[1],dr,0,TAU);ctx.fill();});
}
}
function drawCracks(b){
const x=b.x,y=b.y,w=b.w,h=b.h,v=(b.col*53%17)/17,v2=(b.row*29%13)/13;
ctx.strokeStyle='rgba(15,18,28,.8)';ctx.lineWidth=1.6;ctx.lineJoin='round';
ctx.beginPath();
ctx.moveTo(x+w*(0.25+v*0.2),y);
ctx.lineTo(x+w*(0.35+v*0.2),y+h*0.4);
ctx.lineTo(x+w*(0.28+v*0.2),y+h*0.7);
ctx.lineTo(x+w*(0.4+v*0.2),y+h);
ctx.moveTo(x+w*(0.35+v*0.2),y+h*0.4);
ctx.lineTo(x+w*0.65,y+h*0.55);
ctx.moveTo(x+w*(0.75+v2*0.15),y+h);
ctx.lineTo(x+w*(0.68+v2*0.15),y+h*0.5);
ctx.lineTo(x+w*(0.8+v2*0.15),y);
ctx.stroke();
ctx.fillStyle='rgba(0,0,0,.18)';rr(x,y,w,h,5);ctx.fill();
}
function drawBrick(b){
const c=rowColors[b.row];
rr(b.x,b.y,b.w,b.h,5);
if(b.max===2){
ctx.fillStyle='#525c70';ctx.fill();
ctx.globalAlpha=0.45;ctx.fillStyle=c;ctx.fill();ctx.globalAlpha=1;
}else{ctx.fillStyle=c;ctx.fill();}
ctx.save();rr(b.x,b.y,b.w,b.h,5);ctx.clip();
const lg=ctx.createLinearGradient(0,b.y,0,b.y+b.h);
lg.addColorStop(0,'rgba(255,255,255,.32)');lg.addColorStop(.5,'rgba(255,255,255,0)');
lg.addColorStop(1,'rgba(0,0,0,.28)');
ctx.fillStyle=lg;ctx.fillRect(b.x,b.y,b.w,b.h);
ctx.restore();
ctx.strokeStyle=b.max===2?'#c9d4e6':'rgba(255,255,255,.35)';
ctx.lineWidth=b.max===2?2:1;
rr(b.x+1,b.y+1,b.w-2,b.h-2,4);ctx.stroke();
if(b.max===2){
ctx.fillStyle='#dfe7f4';
[[6,6],[b.w-6,6],[6,b.h-6],[b.w-6,b.h-6]].forEach(o=>{
ctx.beginPath();ctx.arc(b.x+o[0],b.y+o[1],1.6,0,TAU);ctx.fill();});
}
if(b.hp<b.max)drawCracks(b);
if(b.power)drawIcon(b.x+b.w/2,b.y+b.h/2,10,b.power,'rgba(10,14,24,.72)','#fff');
if(b.flash>0){
ctx.fillStyle='rgba(255,255,255,'+(b.flash/6*0.8)+')';
rr(b.x,b.y,b.w,b.h,5);ctx.fill();
}
}
function draw(){
ctx.save();
if(shake>0)ctx.translate(rnd(-shake,shake)*0.5,rnd(-shake,shake)*0.5);
const g=ctx.createLinearGradient(0,0,0,H);
g.addColorStop(0,'#0c1020');g.addColorStop(1,'#151b31');
ctx.fillStyle=g;ctx.fillRect(-20,-20,W+40,H+40);
for(const s of stars){
ctx.globalAlpha=s.a*(0.6+0.4*Math.sin(frame*0.02+s.x));
ctx.fillStyle='#aebfff';
ctx.beginPath();ctx.arc(s.x,s.y,s.r,0,TAU);ctx.fill();
}
ctx.globalAlpha=1;
// HUD
ctx.fillStyle='rgba(255,255,255,0.06)';ctx.fillRect(0,0,W,HUD);
ctx.fillStyle='rgba(255,255,255,0.14)';ctx.fillRect(0,HUD-1,W,1);
ctx.textBaseline='middle';ctx.textAlign='left';
ctx.font='700 12px system-ui,sans-serif';ctx.fillStyle='rgba(255,255,255,.5)';
ctx.fillText('SCORE',20,15);
ctx.font='800 22px system-ui,sans-serif';ctx.fillStyle='#fff';
ctx.fillText(String(score).padStart(5,'0'),20,37);
for(let i=0;i<3;i++){
const x=W-28-i*26;
ctx.beginPath();ctx.arc(x,25,7,0,TAU);
if(i<lives){ctx.fillStyle='#7fd8ff';ctx.shadowColor='#39c8ff';ctx.shadowBlur=8;ctx.fill();ctx.shadowBlur=0;}
else{ctx.strokeStyle='rgba(255,255,255,.22)';ctx.lineWidth=1.5;ctx.stroke();}
}
ctx.textAlign='center';
ctx.font='800 15px system-ui,sans-serif';ctx.fillStyle='rgba(255,255,255,.35)';
ctx.fillText('N E O N B R E A K O U T',W/2,25);
if(wide>0){
ctx.fillStyle='rgba(255,255,255,.45)';
ctx.font='700 10px system-ui,sans-serif';ctx.fillText('WIDE PADDLE',W/2,36);
ctx.fillStyle='rgba(255,255,255,.12)';ctx.fillRect(W/2-80,41,160,3);
ctx.fillStyle='#ffd23f';ctx.fillRect(W/2-80,41,160*(wide/600),3);
}
// bricks
for(const b of bricks)if(b.hp>0)drawBrick(b);
// drops
for(const d of drops){
const s=13+Math.sin(d.t*0.15)*1.5;
ctx.save();ctx.translate(d.x,d.y);
ctx.shadowColor=d.p==='wide'?'#ffd23f':'#7ee081';ctx.shadowBlur=14;
ctx.fillStyle=d.p==='wide'?'#ffd23f':'#7ee081';
rr(-s,-s,2*s,2*s,6);ctx.fill();ctx.shadowBlur=0;
drawIcon(0,0,s*0.62,d.p,'#10141f',d.p==='wide'?'#ffd23f':'#7ee081');
ctx.restore();
}
// particles
for(const p of parts){
ctx.globalAlpha=Math.max(0,p.life/p.max);
ctx.fillStyle=p.c;
ctx.fillRect(p.x-p.s/2,p.y-p.s/2,p.s,p.s);
}
ctx.globalAlpha=1;
// paddle
const blink=wide>0&&wide<120&&Math.floor(frame/6)%2===0;
ctx.globalAlpha=blink?0.45:1;
const pg=ctx.createLinearGradient(0,pad.y,0,pad.y+pad.h);
pg.addColorStop(0,'#7ff0ff');pg.addColorStop(1,'#2b7bff');
ctx.shadowColor='#39c8ff';ctx.shadowBlur=16;
ctx.fillStyle=pg;rr(pad.x,pad.y,pad.w,pad.h,7);ctx.fill();
ctx.shadowBlur=0;
ctx.fillStyle='rgba(255,255,255,.5)';
rr(pad.x+6,pad.y+2.5,pad.w-12,2,1);ctx.fill();
ctx.globalAlpha=1;
// balls
for(const b of balls){
ctx.shadowColor='#8fd8ff';ctx.shadowBlur=14;
const gr=ctx.createRadialGradient(b.x-2.5,b.y-3,1,b.x,b.y,8);
gr.addColorStop(0,'#ffffff');gr.addColorStop(1,'#bfe4ff');
ctx.fillStyle=gr;ctx.beginPath();ctx.arc(b.x,b.y,8,0,TAU);ctx.fill();
ctx.shadowBlur=0;
}
// popups
for(const p of pops){
ctx.globalAlpha=Math.min(1,p.life/30);
ctx.font='800 '+(p.size||15)+'px system-ui,sans-serif';
ctx.textAlign='center';ctx.fillStyle=p.color;
ctx.fillText(p.txt,p.x,p.y);
}
ctx.globalAlpha=1;
// overlays
const cx=W/2;
if(state==='demo'){
ctx.fillStyle='rgba(8,10,18,0.38)';ctx.fillRect(0,HUD,W,H-HUD);
const tg=ctx.createLinearGradient(cx-220,0,cx+220,0);
tg.addColorStop(0,'#ff4d6d');tg.addColorStop(.5,'#ffd23f');tg.addColorStop(1,'#4aa8ff');
ctx.font='900 56px system-ui,sans-serif';ctx.textAlign='center';ctx.fillStyle=tg;
ctx.shadowColor='rgba(80,160,255,.5)';ctx.shadowBlur=24;
ctx.fillText('NEON BREAKOUT',cx,290);
ctx.shadowBlur=0;
const pu=0.6+0.4*Math.sin(frame*0.08);
ctx.globalAlpha=pu;ctx.font='700 21px system-ui,sans-serif';ctx.fillStyle='#fff';
ctx.fillText('Click or press a key to start',cx,352);
ctx.globalAlpha=1;
ctx.font='600 14px system-ui,sans-serif';ctx.fillStyle='rgba(255,255,255,.55)';
ctx.fillText('Mouse or ← → to move · catch falling power-ups',cx,392);
}else if(state==='gameover'||state==='win'){
ctx.fillStyle='rgba(8,10,16,0.78)';ctx.fillRect(0,HUD,W,H-HUD);
ctx.textAlign='center';
if(state==='gameover'){
ctx.font='900 54px system-ui,sans-serif';ctx.fillStyle='#ff4d6d';
ctx.shadowColor='rgba(255,77,109,.6)';ctx.shadowBlur=22;
ctx.fillText('GAME OVER',cx,300);ctx.shadowBlur=0;
}else{
ctx.font='900 54px system-ui,sans-serif';ctx.fillStyle='#ffd23f';
ctx.shadowColor='rgba(255,210,63,.6)';ctx.shadowBlur=22;
ctx.fillText('LEVEL CLEARED!',cx,300);ctx.shadowBlur=0;
}
ctx.font='700 22px system-ui,sans-serif';ctx.fillStyle='#fff';
ctx.fillText('Score: '+score,cx,356);
const pu=0.6+0.4*Math.sin(frame*0.08);
ctx.globalAlpha=pu;ctx.font='700 19px system-ui,sans-serif';ctx.fillStyle='#7fd8ff';
ctx.fillText('Click or press R to restart',cx,408);
ctx.globalAlpha=1;
}
ctx.restore();
}
function action(){
if(state==='demo'||state==='gameover'||state==='win'){
reset();
}else{
balls.forEach(b=>{if(b.stuck)launchBall(b);});
}
}
cv.addEventListener('pointermove',e=>{
const r=cv.getBoundingClientRect();
mouseX=(e.clientX-r.left)*W/r.width;
mouseOn=true;
});
cv.addEventListener('pointerdown',action);
window.addEventListener('keydown',e=>{
if(e.key==='ArrowLeft'||e.key==='ArrowRight'||e.key===' ')e.preventDefault();
if(e.key==='ArrowLeft')keys.l=true;
if(e.key==='ArrowRight')keys.r=true;
if(state==='demo'&&(e.key==='ArrowLeft'||e.key==='ArrowRight')){action();return;}
if(e.key===' '){
if(state==='playing')balls.forEach(b=>{if(b.stuck)launchBall(b);});
else action();
return;
}
if(e.key==='r'||e.key==='R'||e.key==='Enter'){
if(state==='gameover'||state==='win')action();
return;
}
if(state==='demo')action();
});
window.addEventListener('keyup',e=>{
if(e.key==='ArrowLeft')keys.l=false;
if(e.key==='ArrowRight')keys.r=false;
});
reset();state='demo';
let last=performance.now();
function tick(t){
let f=Math.min((t-last)/16.667,2.5);last=t;
if(f<=0)f=0.001;
update(f);draw();
requestAnimationFrame(tick);
}
requestAnimationFrame(tick);
})();
</script>
</body>
</html>Reasoning trace (12,956 tokens)
Let me build a polished Breakout game. Plan carefully.
Requirements:
- Canvas breakout, vanilla JS
- Paddle: mouse + arrow keys
- Bounce angle depends on hit position
- One level, ≥6 rows, normal bricks + 2-hit bricks that crack
- Power-up bricks with visible icons; drop power-up; catch = wide paddle (10s) or multiball (3 balls)
- No pass-through (use swept collision or sub-stepping)
- Particles on brick break
- 3 lives, score, game over/victory screens with restart
- Demo mode AI plays behind "Click or press a key to start"
- 1280×800 viewport, responsive down to 375px, no horizontal scrollbar
- First paint visible ~3s screenshot
Design: dark neon aesthetic, retro but polished. Use fixed logical canvas size, scale via CSS.
Let me write the code.
Structure:
- Full-page dark background, canvas centered with subtle glow.
- Canvas logical size: 900×640 or so. Fit within viewport: use CSS max-width/height.
Game logic:
Constants:
- W=900, H=640
- Brick grid: cols=12, rows=8, brick w = (W - 2*margin)/cols... Let's do cols 12, brickW ~ 66, gap 6.
Rows: 8 rows. Row types: top rows tougher (2 hits), some bricks carry power-ups.
Brick: {x,y,w,h,hp,maxHp,power:null|'wide'|'multi'}
Power-up icons drawn on brick: 'W' for wide (icon like ↔ arrows), 'M' for multi (three dots). Draw simple glyphs.
Ball physics: sub-step movement. Move ball in small steps (e.g., divide movement into steps of max 4px) to avoid tunneling. At each step, check paddle collision and brick collision (AABB vs circle).
Paddle collision: angle based on hit offset: angle = offset * maxAngle (e.g., 60°). speed constant-ish. Ball speed increases slightly over time / per brick.
Only allow paddle bounce when ball moving down and ball bottom >= paddle top.
Brick collision: find brick at ball position, determine side of collision, reflect. Circle-AABB: compute closest point, if dist < r → collision; normal from closest point. Reflect velocity along normal. Reduce hp; if 0 → break: particles, maybe drop powerup, score.
Bricks stored in grid for lookup: brickAt(col,row). Use grid lookup: compute col/row from ball pos, check neighbors? Simpler: loop over alive bricks each sub-step — 96 bricks × steps fine performance-wise. But better: compute candidate cells. I'll just loop all bricks; with ~8 substeps × 96 bricks = 768 checks per frame per ball — fine.
Lives: 3. When ball(s) all fall below bottom → lose life. If multi-ball active, only lose life when all balls gone.
Power-ups: falling squares with icon, caught by paddle → apply. Wide paddle: 10s timer. Multi-ball: spawn 2 extra balls from an existing ball position with angle variations.
Particles: array, gravity, fade.
Score: normal brick 50, tough 50 first hit + 100 break? Say 100 total for tough, 50 normal. Display score & lives top HUD drawn on canvas.
Screens: start overlay (demo mode running behind), game over, victory with "Click or press R to restart" / button. Restart via click or key R / Enter.
Demo mode: AI moves paddle toward ball's predicted x (nearest descending ball). Show "Click or press a key to start" overlay. On first click/keydown → start real game (reset).
State machine: 'demo', 'playing', 'gameover', 'win'. In demo, AI plays; also demo plays automatically so screenshot shows action.
Mouse control: canvas mousemove → paddle x (map client coords to canvas coords accounting for scaling).
Keyboard: arrows move paddle continuously; keydown/keyup flags.
Handle canvas scaling: canvas CSS width 100% max sizes; compute scale = canvas.getBoundingClientRect().width / W.
Responsive: container with aspect-ratio; canvas width 100%.
No console errors. Everything inline.
Let me also ensure ball never passes through paddle: sub-steps of ≤3px plus paddle collision check per sub-step, plus clamp: if ball below paddle top and moving down and overlapping x-range and within paddle vertical band → snap on top. Edge case: ball hits paddle side — treat as reflect angle anyway (still fine, angle formula handles it since offset ±1). Cap offset so ball goes up.
Also ensure ball doesn't get stuck horizontally: enforce min vertical velocity component (|vy| ≥ 0.35*speed? enforce min angle ~ 20° from horizontal).
Brick crack: draw cracks — when hp < maxHp, draw crack lines and reduce brightness. I'll pre-generate crack pattern per brick (random seed) or draw fixed zigzag cracks. Simple: two crack polylines based on brick position.
Level layout (8 rows × 12 cols). Tough bricks in top 2-3 rows. Power-ups scattered: maybe 6 power bricks: 3 wide, 3 multi.
Colors per row: hue gradient. Tough bricks: different look (steel color with rivets?). Let's do rows colors: row hue from 190→330 etc. Tough: use white/steel border, darker fill with hatch.
HUD: top strip inside canvas? Draw score/lives above play area. I'll make canvas include HUD area at top (height 46) and walls.
Layout: W=900, H=660. HUD top 44px. Bricks start y=70. Rows 8 × (bH=24 + gap 6) → 8*30 = 240 → bricks y 70..310. Paddle at y=H-40. Fine.
Ball radius 8. Paddle w=110 (wide: 170), h=14.
Speed: base 6.2, +0.15 per 10 bricks? Increase slightly with score/time, cap ~10.
Sub-stepping: steps = ceil(speed*dtframes... ) — use fixed timestep accumulation: I'll use per-frame with dt normalized to 60fps, and substeps = ceil(maxDisp/3).
Ball serve: attached to paddle, launch on click/space after life loss? Simpler: after losing a life, ball resets attached to paddle and launches automatically after 1s or on click. In demo, auto-launch. In playing, launch on click/space or after 2s auto. Let's do: ball stuck to paddle with "click to launch" hint; also auto-launch after 1.5s so demo flows.
Actually keyboard events only after click — game starts after click anyway, so space works.
Game over: dark overlay with text "GAME OVER", score, "Click or press R to restart". Victory similar with confetti-ish particles.
Restart resets everything, state 'playing' (since user already interacted).
Pause? Not required. Skip.
Write code now.
```js
const W=900,H=660;
```
Bricks: cols=12, margin side=24, gap=5, brickW=(W-2*24 - (cols-1)*gap)/cols = (900-48-55)/12 = 797/12 ≈ 66.4. brickH=26. top offset 78.
Rows = 8. Row 0,1 tough (hp2), rows 2-7 normal. Maybe also row 2 has some tough mixed. Let's define pattern per row: rows 0-1 all tough, rows 2-7: tough at certain columns for variety? Requirement: "at least 6 rows of bricks, mixing normal bricks and tougher bricks". I'll do rows 0-1 tough, rows 2,3 mixed (alternating?), keep clean: row pattern strings maybe. Use array of row defs:
row 0: all tough
row 1: all tough
row 2: normal
row 3: normal with tough at cols 0,5,6,11? Let's craft pattern strings of 12 chars: 'T'=tough,'.'=normal.
r0: 'TTTTTTTTTTTT'
r1: 'TTTTTTTTTTTT'
r2: '............'
r3: 'T..T....T..T'
r4: '............'
r5: '...T....T...'
r6: '............'
r7: '............'
Power-ups: choose fixed positions: e.g., (row2,col2,'wide'), (row2,col9,'multi'), (row4,col4,'multi'), (row4,col7,'wide'), (row6,col1,'wide'), (row6,col10,'multi'). Store on brick.
Colors: row hue: h = 200 + row*18? Let's do array of fill colors: from hot to cool. Row 0 top: '#ff5c7a'? Let's do: rowColors = ['#ff4d6d','#ff9551','#ffd23f','#7ee081','#3fd2c7','#4aa8ff','#7b6cff','#c86bff'] — rainbow. Tough bricks: darker steel with metallic sheen; maybe keep row color but with steel overlay + border and rivets. Hmm, to distinguish: tough bricks have metallic silver-gray base with colored top edge? Simpler: tough brick base color = row color but desaturated + white border + inner bevel; after crack visible cracks. Let me make tough = lighter/steel: fill '#8d99ae'-ish with row-colored stripe. I'll do: tough brick fill = mix of gray and row color via drawing row color at low alpha over gray. Fine.
Draw brick: rounded rect radius 4, gradient highlight top, border. If tough: double border, small rivet dots corners. If power: icon in center: wide = '↔' arrows drawn as two triangles + line; multi = three small circles. If cracked: crack lines dark.
Power-up drop entity: rounded square 26px, pulsing, icon, falls at 2.6 px/frame, slight sway. On paddle overlap → apply, score +150, flash text.
Effects:
- Wide paddle: paddle.targetW = 170; timer wideTimer=10s (600 frames). Blink last 2s.
- Multi: keep existing balls, add 2 with angles ±25° from an existing ball. Cap total balls maybe 6.
Ball loss: filter balls y > H+r. If balls.length===0: lives--; if lives>0 reset ball on paddle (sticky, auto-launch timer). else gameover.
Win: all bricks hp<=0 → victory. If multi-powerup dropped after last brick... drop spawns only when brick breaks; if all broken → win immediately; power-up falling no longer matters. Fine.
Paddle draw: rounded, cyan gradient, glow. Wide: draw extension.
Background: dark navy with subtle grid/stars. Pre-render stars onto offscreen? Just draw static background each frame with cached pattern via createPattern from offscreen canvas — nice. Or just gradient + a few glow circles. I'll draw gradient each frame + faint moving starfield? Keep simple: gradient + subtle vignette.
Screen shake on life lost: small.
HUD: score left, lives right (draw small ball icons), center: high? Also show active power-up timer bar when wide active.
Messages overlay: state-based. Use DOM overlay divs or canvas text? Canvas text simpler for demo overlay. But "Click or press a key to start" message: draw on canvas with pulsing. Buttons: "click anywhere" works.
Input:
- mousemove on canvas → paddle target x (only in playing; in demo AI controls).
- click: if state demo → startGame(); if playing & ball stuck → launch; if gameover/win → restart;
- keydown: same triggers; arrows set flags; 'r' restart when over; space launch.
- Prevent arrows scrolling: preventDefault.
AI (demo): target = ball with lowest y among moving down (predict x with simple reflection off walls? just use ball.x + ball.vx * ((paddleY - ball.y)/|vy|) with wall bounce reflection — do simple: predicted = ball.x; if vy>0 predicted = ball.x + vx*timeToPaddle, reflect within [r, W-r]). Move paddle toward predicted with easing, add slight error so it occasionally misses? No—demo should look good; let it mostly succeed but to show lives? Keep it competent.
Frame loop: requestAnimationFrame with dt clamp; use dt in frames f = dt/16.67 clamped [0.25?, 2]. Substeps: n = ceil(maxSpeed*f/3).
Collision helper per ball per substep:
```
ball.x += vx*s; ball.y += vy*s;
// walls
if x<r → x=r, vx=abs(vx); if x>W-r → mirror; if y<topY+r → y=..., vy=abs(vy)
// paddle
if vy>0 && y+r >= padY && y - r <= padY+padH && x > padX-r && x < padX+padW+r:
if y+r <= padY + padH + something? just place y = padY - r; compute offset=(x-(padX+padW/2))/(padW/2+r) clamp ±1; angle = offset*maxAngle (60°) from vertical; speed preserved; vx = sin*spd, vy = -cos*spd.
also add slight paddle movement influence? skip.
// bricks: loop alive bricks; circle-rect test with closest point; if hit:
determine axis: compare penetration dx vs dy: overlapX = r - |x - cx clamp|... compute:
let nx = clamp(x, b.x, b.x+b.w), ny = clamp(y,b.y,b.y+b.h); dx=x-nx, dy=y-ny; d2=dx*dx+dy*dy;
if d2 < r*r: hit. normal = (dx,dy)/d (if inside, d=0 → use velocity reversal... handle center-in-brick: push up).
reflect: if |dx|>|dy| → vx = -vx (and push x out) else vy=-vy... Use:
if d>0: n=(dx/d,dy/d); v·n reflect; move ball to nx+ n*(r+0.1).
else: reflect vy, push to top.
hitBrick(b); break; // one brick per substep
```
Since substeps ≤3px and brick h 26, no tunneling.
Also ensure ball can't get trapped bouncing horizontally among bricks — min |vy| enforcement after brick hits: if |vy| < 0.25*spd → set to 0.25 sign preserved, renormalize? Enforce at launch/reflect only. Also top wall: y>HUD.
Hit brick:
```
b.hp--; flash timer b.flash=6;
if hp<=0: alive=false; score += (b.max===2?120:60); spawnParticles; maybe spawnPower(b); remaining--; if remaining===0 win.
else score+=20 (chip points); small particles few.
```
Particles: pos, vel random, color from brick, life, size, gravity 0.15.
Score popups: floating text "+60"? Nice touch; add small array.
Lives display: hearts/balls icons top-right.
Fonts: use canvas font with system stack: `600 20px 'Segoe UI', system-ui, sans-serif` — fine, no external fonts allowed? "No external files, fonts" — system-ui fine.
Demo overlay: semi-dark? Requirement: game visible behind message. Draw translucent bar at bottom? I'll draw dimmed vignette + centered title "BREAKOUT" + "Click or press a key to start". Demo runs fully behind (bricks regenerate each restart).
When user clicks to start: reset to fresh level, state playing.
Victory: particles celebration + overlay; click/R restarts → state 'playing' fresh.
Edge: mouse coordinates with canvas CSS scaled: rect = canvas.getBoundingClientRect(); x=(e.clientX-rect.left)*W/rect.width.
Resize: canvas CSS: width:min(96vw, calc((100vh - 40px) * 900/660))? Simpler: container flex center; canvas style: max-width:96vw; max-height:90vh; width:auto? Use: canvas { width: min(96vw, 122vh*... ) } Let's compute: aspect = 900/660 ≈ 1.3636. CSS: width: min(940px, 96vw, 128vh); hmm need height fit too: width limited by height: maxW_by_h = (96vh)*1.3636 ≈ 130.9vh. So width: min(940px, 96vw, 131vh). Good.
375px wide: canvas 96vw ≈ 360px wide → fine, no horizontal scrollbar.
Write it.
Also draw "WIDE 10s" timer bar under HUD when active.
Also small detail: paddle glow trail? Keep scope reasonable.
Let me write the full file.
Key handling nuance: keydown on window; for start message we need "press a key" — window keydown works without focus? In sandbox iframe, keyboard events require the iframe focused via click first. We display message; if user presses key without focus it won't register, but click also works. Fine.
Restart on 'r' or Enter or click when gameover/win.
Auto-launch stuck ball: stuckTimer counts; launch after 90 frames in demo always; in playing after 150 frames or click/space.
When ball stuck, position follows paddle.
Lives: draw as small paddle icons? circles.
Now code:
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>NEON BREAKOUT</title>
<style>
html,body{margin:0;height:100%;background:#0a0c14;overflow:hidden;
font-family:system-ui,'Segoe UI',sans-serif;}
body{display:flex;align-items:center;justify-content:center;}
canvas{width:min(940px,96vw,131vh);aspect-ratio:900/660;border-radius:10px;
box-shadow:0 20px 60px rgba(0,0,0,.6),0 0 0 1px rgba(120,150,255,.18);
cursor:none; display:block;}
</style>
</head>
<body>
<canvas id="c" width="900" height="660"></canvas>
<script>
...
</script>
</body>
</html>
```
cursor:none since paddle follows mouse — good.
JS details:
```
const cv=document.getElementById('c'),ctx=cv.getContext('2d');
const W=900,H=660,HUD=48,TOPY=HUD+2; // play top wall y = HUD
let state='demo', score=0, lives=3, bricks=[], remaining=0,
balls=[], parts=[], pops=[], drops=[], shake=0, frame=0,
wide=0, // frames of wide remaining
pad={x:W/2-55,w:110,h:14,y:H-42,tw:110},
keys={left:false,right:false}, mouseX=null;
```
Pad width animates toward target (tw) for smooth widen/shrink.
Ball: {x,y,vx,vy,spd,stuck}
makeBall(x,y,vx,vy,spd).
resetLevel(): build bricks, balls=[stuck ball], drops=[], parts=[], pops=[], score=0,lives=3, wide=0.
Bricks build:
```
const COLS=12,GAP=5,MARG=24,BW=(W-2*MARG-(COLS-1)*GAP)/COLS,BH=26,BTOP=86;
const rowsDef=[ 'TTTTTTTTTTTT','TTTTTTTTTTTT','............','T..TT..TT..T','............','...T....T...','............','............' ];
```
Check row3: 'T..TT..TT..T' length: T,.,.,T,T,.,.,T,T,.,.,T = 12 ✓. row5 '...T....T...' = 3+1+4+1+3=12 ✓.
Power positions:
```
const powers=[{r:2,c:2,p:'wide'},{r:2,c:9,p:'multi'},{r:4,c:5,p:'multi'},{r:4,c:6,p:'wide'},{r:6,c:1,p:'wide'},{r:6,c:10,p:'multi'}];
```
rowColors array 8 entries.
brick={x,y,w:BW,h:BH,hp,max,power,row}
speed base: BASE=6; each brick broken: spd increase small: ball.spd += 0.02? Apply per ball on paddle hit: spd = min(spd+0.03, 11). Also global difficulty: min( base + brokenCount*0.012, ...). Simpler: on paddle bounce ball.spd=min(ball.spd+0.04,11). New balls inherit.
launch(ball): angle random ±35°, vy negative:
```
const a=(Math.random()*0.9-0.45)+ (-Math.PI/2); // around up
vx=Math.cos(a)*spd...
```
Actually angle from vertical: vx=sin(off)*spd, vy=-cos(off)*spd with off in [-0.6,0.6].
Paddle reflect max angle 60° (1.047 rad).
Min vy enforcement after brick bounce: if |vy|<0.28*spd: vy = (vy<0?-1:1)*0.28*spd; renormalize vx to keep spd: vx = sign(vx)*sqrt(spd²-vy²). Apply after brick hits and wall hits? Just brick hits and occasionally.
Update loop with dt normalization f = clamp(dtms/16.667, 0.2, 2.5).
substeps: total displacement per frame = spd*f up to ~27; substeps n = Math.ceil((spd*f)/2.5) per ball. Loop balls outer, substeps inner — but paddle/other interactions fine.
Paddle update before balls: keyboard: pad.x += (right-left)*9*f; mouse: if mouseX!=null → pad.x = mouseX - pad.w/2 eased? Direct is fine but demo AI also sets pad target. Use pad.tx target:
- playing: if mouse moved recently, pad.tx from mouse; keys override by movement.
Simplify: in playing: if keys pressed → move by keys and set mouseX=null (so keys take over until mouse moves). Else if mouseX!=null → pad target = mouseX - w/2.
- demo: AI target.
pad.x += (target-pad.x)*min(1,0.35*f) for AI demo; for mouse direct follow feels better: pad.x = clamp(mouseX - w/2). I'll do direct for mouse, and mark mouseActive=true until key pressed.
Clamp pad.x in [6, W-6-pad.w].
Wide timer: wide -= f (frames); when hits 0 → pad.tw=110.
pad.w += (pad.tw-pad.w)*0.2*f clamp.
Ball stuck: x=pad center, y=pad.y-10; stuckTimer+=f; auto launch: demo always at 60; playing at 150.
Losing: after moving balls, balls = balls.filter(b=>b.y < H+20). if length 0:
```
lives--; shake=12;
if lives<=0 → state='gameover'
else spawn stuck ball
```
Drops update: y+=2.6*f; catch if intersects paddle rect (y+13>pad.y && y-13<pad.y+pad.h && x within pad.x-13..pad.x+pad.w+13): apply. Remove if y>H+30.
applyPower(p):
```
if p==='wide': pad.tw=180; wide=600; pops.push('WIDE PADDLE!')
else: multiball
```
multiball: take first ball; if balls.length>=6 skip? spawn up to total 3 (or add 2). If ball stuck? If stuck just keep. Spawn 2 clones rotated ±0.5 rad.
pops: floating texts {x,y,txt,life,color}.
Particles update: vy+=0.12*f... use per-frame units with f.
Drawing:
bg: fill gradient #0b0e1a→#141a2e; subtle stars precomputed array of {x,y,r,a} drawn each frame (cheap, ~60 stars). Plus faint horizontal line at HUD.
HUD: score text left "SCORE 0000"? Use padded. Lives: circles right. Center: "NEON BREAKOUT" small? Put power timer center.
Bricks: for each alive: draw. flash: brighter overlay when b.flash>0.
Rounded rect helper.
Brick draw:
```
const col=rowColors[b.row];
if tough: base '#5a6478'?
```
Design decision: normal brick: colored fill, top highlight (rgba white .25 line), bottom shadow. Tough: steel gradient (#9aa4b5→#5b6577) with colored 4px stripe? Actually give tough bricks the row color but with a metallic frame: fill row color darkened + 2px white-ish inner stroke + corner rivets. Simplest polished approach:
normal: fillStyle = color; draw rounded rect fill; gradient overlay: create linear gradient top rgba(255,255,255,.28) → transparent → rgba(0,0,0,.25). stroke rgba(255,255,255,.25) lineWidth1.
tough: same but base = desaturated: use color with extra dark overlay, plus stroke '#e8ecf4' width 2, plus 4 rivet dots. After first hit (hp==1): draw cracks: dark polyline from random-ish seed. Use deterministic seed: use (b row,col) to offset crack pattern. Cracks: lines: from left edge mid to center to top edge, jagged. Also overlay dark rgba(0,0,0,.35) to look damaged.
Power icon on brick: white circle bg rgba(255,255,255,.85) r=9 at center? Draw glyph:
- wide: '↔' as text? Canvas text '↔' may render — it's a common unicode char, should be fine: ctx.fillText('↔'). Multi: three dots '⁂'? Use drawn shapes: wide: left+right triangles; multi: 3 small circles arranged triangle. Drawn shapes safer & crisper. Do drawn shapes in white with dark ring.
Drop powerup: rounded square 26, color: wide='#ffd23f', multi='#7ee081'; icon same glyphs; pulsing glow: shadowBlur.
Ball: white core with cyan glow: shadowColor '#7fd8ff', shadowBlur 12, circle r 8, plus inner highlight. Trail: draw previous positions? Keep simple: glow suffices; maybe motion trail: store last positions... skip; add subtle: draw semi-transparent circle at x-vx*2.
Paddle: rounded rect gradient cyan→blue, top highlight, glow shadow. When wide active & <2.5s remaining blink alpha.
Overlay drawing (state):
- demo: bottom-center translucent panel? Draw title big center: "NEON BREAKOUT" with gradient text, "Click or press any key to start" pulsing, small hint line "Mouse / ← → to move · Catch power-ups". Dim bricks? no—keep visible.
- gameover: dark overlay rgba(6,8,14,.78) full play area + "GAME OVER" red, score, "Click or press R to restart".
- win: overlay + "YOU WIN!" gold + score + restart. Confetti particles spawned each few frames during win.
Shake: ctx.translate random within shake amplitude, shake decays.
Top wall: balls bounce at y=HUD+r.
Also walls: left x=0? play area 0..W full width; margins only for bricks. ok.
Score pops drawn rising.
Let me also add: when ball hits paddle, tiny squash animation: pad.pulse=6 → draw slightly taller. Nice.
Now write final code carefully, watch for errors.
Also `document` keydown: check e.key. preventDefault for arrows and space.
Demo AI:
```
function demoAI(f){
// pick ball moving down with max y (closest to paddle) else any ball
let target=W/2, best=-1e9;
for(const b of balls){ if(b.stuck){target=pad.x+pad.w/2; break;}
let ty=b.y + ... predicted if vy>0: t=(pad.y-b.y)/b.vy; px=b.x+b.vx*t; reflect in [10,W-10]:
while(px<10||px>W-10){ if(px<10)px=20-px; if(px>W-10)px=2*(W-10)-px; }
const val = b.vy>0 ? 1e6-... choose ball with vy>0 largest y? choose smallest time.
}
}
```
Implement: candidates vy>0; pick min t; if none, follow nearest ball x. Move: pad.x += clamp(target-(pad.x+pad.w/2), -8*f, 8*f). Add error: target += Math.sin(frame*0.01)*8 for liveliness? keep small.
Demo also collects powerups naturally.
Now the auto behaviors in demo: stuck launch after 60 frames.
Edge: multi-ball in demo → AI handles prediction with multiple; picks most urgent (min t). Good.
Victory check in hitBrick.
Write final HTML. Also console-clean.
Let me now write everything concretely.
Crack drawing:
```
function drawCracks(b){
ctx.strokeStyle='rgba(10,12,20,.75)';lw 1.5;
const s=(b.row*7+b.col*13)%... deterministic
pattern: begin at (x + w*0.3, y) line to (x+w*0.45,y+h*0.5) line to (x+w*0.35,y+h);
second: (x+w*0.7, y+h) → (x+w*0.6,y+h*0.45) → (x+w*0.75,y);
plus small branch.
}
```
Add slight per-brick variation via pseudo-random from index.
Particles spawn on break: 14 particles brick color, on chip: 4.
pops: {x,y,txt,life:60,color}.
Win confetti: in draw loop when state==='win', spawn particle every 3 frames from top random.
Now, code:
```js
(() => {
const cv=document.getElementById('c'),ctx=cv.getContext('2d');
const W=900,H=660,HUD=48,PADY=H-42;
const COLS=12,GAP=5,MARG=24,BW=(W-2*MARG-(COLS-1)*GAP)/COLS,BH=26,BTOP=88;
const rowColors=['#ff4d6d','#ff8f51','#ffd23f','#7ee081','#3dd6c3','#4aa8ff','#7b6cff','#c86bff'];
const rowsDef=['TTTTTTTTTTTT','TTTTTTTTTTTT','............','T..TT..TT..T','............','...T....T...','............','............'];
const powerAt={'2,2':'wide','2,9':'multi','4,5':'multi','4,6':'wide','6,1':'wide','6,10':'multi'};
const stars=Array.from({length:70},()=>({x:Math.random()*W,y:HUD+Math.random()*(H-HUD),r:Math.random()*1.3+.3,a:Math.random()*.5+.15}));
let state,score,lives,bricks,remaining,balls,parts,pops,drops,shake,frame,wide,pad,keys={l:false,r:false},mouseX=null,mouseOn=false;
const clamp=(v,a,b)=>v<a?a:v>b?b:v;
const rnd=(a,b)=>a+Math.random()*(b-a);
```
pad object: {x,y:PADY,w,tw,h:14,pulse:0}
reset():
```
function reset(){
state='playing';score=0;lives=3;parts=[];pops=[];drops=[];shake=0;wide=0;
pad={x:W/2-55,y:PADY,w:110,tw:110,h:14,pulse:0};
bricks=[];remaining=0;
rowsDef.forEach((rd,row)=>{ for(let c=0;c<COLS;c++){ if(rd[c]!=='T'&&rd[c]!=='.')continue; // always '.' or 'T'
const tough=rd[c]==='T';
const b={x:MARG+c*(BW+GAP),y:BTOP+row*(BH+GAP),w:BW,h:BH,row,col:c,hp:tough?2:1,max:tough?2:1,power:powerAt[row+','+c]||null,flash:0};
bricks.push(b); if(tough)... remaining++ counts all bricks;
}});
balls=[newBall(true)];
}
function newBall(stuck){return {x:pad?pad.x+pad.w/2:W/2,y:PADY-12,vx:0,vy:0,spd:6,stuck,st:0};}
```
remaining = bricks.length (all must break).
Demo start: reset(); state='demo'.
launchBall(b):
```
const off=rnd(-0.55,0.55); b.vx=Math.sin(off)*b.spd; b.vy=-Math.cos(off)*b.spd; b.stuck=false;
```
hitBrick(b):
```
b.hp--; b.flash=6;
if(b.hp<=0){ remaining--; score+=b.max===2?120:60; burst(b,14);
if(b.power)drops.push({x:b.x+b.w/2,y:b.y+b.h/2,p:b.power,t:0});
if(remaining<=0){state='win';}
} else { score+=20; burst(b,4,true); }
```
burst(b,n,small): particles with color rowColors[b.row].
Substep ball move:
```
function stepBall(b,s){ // s = substep scale (fraction of frame)
b.x+=b.vx*s; b.y+=b.vy*s;
const r=8;
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<HUD+r){b.y=HUD+r;b.vy=Math.abs(b.vy);}
// paddle
if(b.vy>0&&b.y+r>=pad.y&&b.y-r<=pad.y+pad.h&&b.x>pad.x-r&&b.x<pad.x+pad.w+r){
b.y=pad.y-r;
const off=clamp((b.x-(pad.x+pad.w/2))/(pad.w/2+r),-1,1);
const ang=off*1.05; // 60deg
b.spd=Math.min(b.spd+0.05,11);
b.vx=Math.sin(ang)*b.spd; b.vy=-Math.cos(ang)*b.spd;
pad.pulse=6;
}
// bricks
for(const br of bricks){ if(br.hp<=0)continue;
const nx=clamp(b.x,br.x,br.x+br.w),ny=clamp(b.y,br.y,br.y+br.h);
const dx=b.x-nx,dy=b.y-ny,d2=dx*dx+dy*dy;
if(d2<r*r){
if(d2>0.0001){const d=Math.sqrt(d2),nX=dx/d,nY=dy/d;
const dot=b.vx*nX+b.vy*nY;
if(dot<0){b.vx-=2*dot*nX;b.vy-=2*dot*nY;}
b.x=nx+nX*(r+0.2);b.y=ny+nY*(r+0.2);
} else { b.vy=-b.vy; b.y=br.y-r; }
// enforce min vertical
const minVy=0.3*b.spd;
if(Math.abs(b.vy)<minVy){const sy=b.vy===0?-1:Math.sign(b.vy);b.vy=sy*minVy;const vx2=b.spd*b.spd-b.vy*b.vy;b.vx=Math.sign(b.vx||1)*Math.sqrt(Math.max(vx2,0));}
hitBrick(br);
break;
}
}
}
```
Wait: reflect when ball moving away could re-collide — fine.
One issue: after breaking brick behind, ball continues — fine.
Note minVy enforcement could cause slight speed change; renormalizing vx keeps speed = spd. ok.
update(f):
```
function update(f){
frame+=f;
// paddle width
if(wide>0){wide-=f;if(wide<=0){wide=0;pad.tw=110;}}
pad.w+=(pad.tw-pad.w)*Math.min(1,0.25*f);
if(pad.pulse>0)pad.pulse-=f;
// paddle control
if(state==='demo'){demoAI(f);}
else if(state==='playing'){
let target=null;
if(keys.l||keys.r){ mouseOn=false; target=pad.x+((keys.r?1:0)-(keys.l?1:0))*10*f; pad.x=clamp(target,8,W-8-pad.w); }
else if(mouseOn&&mouseX!=null){ pad.x=clamp(mouseX-pad.w/2,8,W-8-pad.w); }
}
// stuck balls
for(const b of balls){ if(b.stuck){ b.x=pad.x+pad.w/2; b.y=pad.y-12; b.st+=f;
const lim=state==='demo'?55:140;
if(b.st>lim)launchBall(b);
}}
// balls
for(const b of balls){ if(b.stuck)continue;
const n=Math.max(1,Math.ceil((b.spd*f)/2.5));
const s=f/n;
for(let i=0;i<n;i++){ if(b.dead)break; stepBall(b,s); if(b.y-b.spd>H+30){b.dead=true;break;} }
}
// count dead
if(balls.some(b=>b.dead)){
balls=balls.filter(b=>!b.dead);
if(balls.length===0&&state!=='win'){
lives--; shake=14;
if(lives<=0){state='gameover';}
else{ const nb=newBall(true); nb.x=pad.x+pad.w/2; balls.push(nb); }
}
}
```
Wait: dead flag also if state changed to win mid-step — fine.
Hmm bug: newBall uses pad... pad defined. ok.
Drops:
```
for(const d of drops){ d.t+=f; d.y+=2.7*f;
if(d.y+14>pad.y&&d.y-14<pad.y+pad.h&&d.x>pad.x-14&&d.x<pad.x+pad.w+14){
d.got=true; score+=150; applyPower(d.p);
pops.push({x:d.x,y:pad.y-20,txt:d.p==='wide'?'WIDE PADDLE':'MULTI-BALL',life:70,color:d.p==='wide'?'#ffd23f':'#7ee081'});
}
if(d.y>H+30)d.got=true;
}
drops=drops.filter(d=>!d.got);
```
applyPower:
```
function applyPower(p){
if(p==='wide'){pad.tw=185;wide=600;}
else{ // multi
const src=balls.find(b=>!b.stuck)||balls[0];
if(src){ if(src.stuck)launchBall(src);
const count=Math.min(2,6-balls.length);
for(let i=0;i<count;i++){const nb=newBall(false);nb.x=src.x;nb.y=src.y;nb.spd=src.spd;
const a=Math.atan2(src.vx,-src.vy)+ (i===0?0.5:-0.5); nb.vx=Math.sin(a)*nb.spd; nb.vy=-Math.cos(a)*nb.spd; balls.push(nb);}
}
}
}
```
atan2(src.vx, -src.vy): angle from up. rotate ±0.5. Good.
Particles/pops update:
```
for(const p of parts){p.x+=p.vx*f;p.y+=p.vy*f;p.vy+=0.14*f;p.life-=f;}
parts=parts.filter(p=>p.life>0);
for(const p of pops){p.y-=0.5*f;p.life-=f;}
pops=pops.filter(p=>p.life>0);
for(const b of bricks)if(b.flash>0)b.flash-=f;
if(shake>0)shake-=f;
if(state==='win'&&frame%4<1... use accumulator: winConfetti timer.
```
Confetti: spawn each ~4 frames: use a counter cf+=f; if(cf>4){cf=0; parts.push(...)} in update when state==='win'.
Demo AI:
```
function demoAI(f){
let target=W/2,bestT=1e9,found=false;
for(const b of balls){
if(b.stuck){continue;}
if(b.vy>0){const t=(pad.y-b.y)/b.vy; if(t>=0&&t<bestT){bestT=t;
let px=b.x+b.vx*t;
while(px<12||px>W-12){if(px<12)px=24-px;if(px>W-12)px=2*(W-12)-px;}
target=px;found=true;}}
}
if(!found){let bd=1e9;for(const b of balls){const d=Math.abs(b.x-(pad.x+pad.w/2));if(d<bd){bd=d;target=b.x;}}}
target+=Math.sin(frame*0.03)*6;
const cx=pad.x+pad.w/2;
pad.x=clamp(pad.x+clamp(target-cx,-9*f,9*f),8,W-8-pad.w);
}
```
Note: demo shouldn't be perfect so viewers see lives? Doesn't matter; competent demo fine. The sin wobble gives small error.
Draw:
```
function draw(){
ctx.save();
if(shake>0)ctx.translate(rnd(-shake,shake)*0.5,rnd(-shake,shake)*0.5);
// bg
const g=ctx.createLinearGradient(0,0,0,H);g.addColorStop(0,'#0c1020');g.addColorStop(1,'#151b31');
ctx.fillStyle=g;ctx.fillRect(-20,-20,W+40,H+40);
for(const s of stars){ctx.globalAlpha=s.a*(0.6+0.4*Math.sin(frame*0.02+s.x));ctx.fillStyle='#aebfff';ctx.beginPath();ctx.arc(s.x,s.y,s.r,0,7);ctx.fill();}
ctx.globalAlpha=1;
// hud
ctx.fillStyle='rgba(255,255,255,0.06)';ctx.fillRect(0,0,W,HUD);
ctx.fillStyle='rgba(255,255,255,0.12)';ctx.fillRect(0,HUD-1,W,1);
ctx.font='700 20px system-ui,Segoe UI,sans-serif';ctx.textBaseline='middle';
ctx.fillStyle='#e8ecf8';ctx.textAlign='left';ctx.fillText('SCORE',20,16);
ctx.font='800 26px system-ui...';ctx.fillStyle='#fff';ctx.fillText(String(score).padStart(5,'0'),20,40?)
```
Hmm layout HUD 48 high: line1 labels y 16, values y 36. Left: SCORE; right: lives circles; center: title small + power bar.
Lives: draw 3 circles top-right: filled for remaining, hollow dim for lost.
Center: 'NEON BREAKOUT' small letterspaced? ctx letterSpacing not universal. Just text center, alpha .5. Plus wide timer bar: if wide>0: bar width prop across center bottom of HUD: rect at y=HUD-6 width (wide/600)*200 centered, color gold, label 'WIDE'.
Bricks: drawBrick(b).
Drops, balls, paddle, particles, pops.
Overlays per state (drawn last):
demo overlay:
```
ctx.fillStyle='rgba(8,10,18,0.45)';ctx.fillRect(0,HUD,W,H-HUD);
title: gradient text 'NEON BREAKOUT' 54px center y=270;
'Click or press a key to start' pulsing 18px y=330;
hints y=370 smaller: 'Mouse or ← → arrows · Catch ⬌ power-ups'? avoid emoji glyphs; use text: 'Mouse / Arrow keys to move — catch dropped power-ups'.
```
Hmm rgba overlay dims the game; but requirement: message behind which AI plays — dimming 0.45 ok, still visible. Maybe lighter 0.35.
gameover overlay: rgba(10,6,14,.8) over play area; 'GAME OVER' red 52px; 'Score: X' ; 'Click or press R to restart'.
win: 'LEVEL CLEARED!' gold; score; restart text.
Balls draw:
```
for(const b of balls){
ctx.shadowColor='#8fd8ff';ctx.shadowBlur=14;
const gr=ctx.createRadialGradient(b.x-2,b.y-3,1,b.x,b.y,8);
gr.addColorStop(0,'#fff');gr.addColorStop(1,'#bfe4ff');
ctx.fillStyle=gr;ctx.beginPath();ctx.arc(b.x,b.y,8,0,7);ctx.fill();
ctx.shadowBlur=0;
}
```
radial gradient inside shadowed circle ok.
Paddle draw:
```
const px=pad.x,pw=pad.w,ph=pad.h+(pad.pulse>0?2:0);
rounded rect px,pad.y-(ph-pad.h)... keep y fixed: draw at pad.y- (pulse? keep same). Just draw rect(pad.x,pad.y,pw,pad.h).
gradient vertical '#7ff0ff'→'#2b7bff'; shadowColor '#39c8ff' blur 16.
top highlight white line.
```
Blink when wide ending: if wide>0&&wide<120&&Math.floor(frame/6)%2 → alpha .5.
Bricks draw:
```
function drawBrick(b){
const c=rowColors[b.row];
rr(b.x,b.y,b.w,b.h,5);
if(b.max===2){ // tough: steel base + color tint
ctx.fillStyle='#525c70';ctx.fill();
ctx.fillStyle=c; ctx.globalAlpha=0.45; rr fill; globalAlpha=1;
} else {ctx.fillStyle=c;ctx.fill();}
// gloss
const gg=ctx.createLinearGradient(b.y... ) use save/clip:
ctx.save();rr(...);ctx.clip();
const lg=ctx.createLinearGradient(0,b.y,0,b.y+b.h);
lg.addColorStop(0,'rgba(255,255,255,.32)');lg.addColorStop(.5,'rgba(255,255,255,0)');lg.addColorStop(1,'rgba(0,0,0,.28)');
ctx.fillStyle=lg;ctx.fillRect(b.x,b.y,b.w,b.h);ctx.restore();
// border
ctx.strokeStyle=b.max===2?'#c9d4e6':'rgba(255,255,255,.35)';ctx.lineWidth=b.max===2?2:1; rr(...);ctx.stroke();
if(b.max===2){ rivets: 4 dots '#dfe7f4' at corners inset 5 }
if(b.hp<b.max){cracks}
if(b.power) drawIcon(b.x+b.w/2,b.y+b.h/2,10,b.power,'#fff');
if(b.flash>0){ctx.fillStyle='rgba(255,255,255,'+(b.flash/6*0.8)+')';rr fill}
}
```
rr helper path only:
```
function rr(x,y,w,h,r){ctx.beginPath();ctx.moveTo(x+r,y);ctx.arcTo(x+w,y,x+w,y+h,r);ctx.arcTo(x+w,y+h,x,y+h,r);ctx.arcTo(x,y+h,x,y,r);ctx.arcTo(x,y,x+w,y,r);ctx.closePath();}
```
drawIcon(cx,cy,s,type,color):
- ring: circle stroke color, r=s, alpha bg dark rgba(10,14,24,.55) fill circle.
- wide: line horizontal + two arrowheads (triangles).
- multi: three dots r=2.2 at triangle offsets (0,-3.5),(-3.5,2.5),(3.5,2.5)... scale with s/10.
Drops draw: rounded square 26 with color bg, dark icon glyph, glow pulse.
```
function drawDrop(d){
const s=13+Math.sin(d.t*0.15)*1.5;
ctx.save();ctx.translate(d.x,d.y);
ctx.shadowColor=d.p==='wide'?'#ffd23f':'#7ee081';ctx.shadowBlur=14;
ctx.fillStyle=d.p==='wide'?'#ffd23f':'#7ee081';
rr(-s,-s,2*s,2*s,6);ctx.fill();ctx.shadowBlur=0;
drawIcon(0,0,s*0.62,d.p,'#10141f');
ctx.restore();
}
```
Wait drawIcon(cx,cy,s...) draws ring+glyph with given color; for drop: glyph dark on bright. Ring: I'll design drawIcon to draw: dark filled circle (bg) + colored glyph. For brick (colorful bg) dark circle ring looks good. For drop (bright square) dark circle bg with... circle bg should be bright then glyph dark. Let drawIcon take (bgColor, fgColor).
drawIcon(cx,cy,r,type,bg,fg):
```
ctx.beginPath();ctx.arc(cx,cy,r,0,7);ctx.fillStyle=bg;ctx.fill();
ctx.strokeStyle='rgba(255,255,255,.5)'? skip.
ctx.fillStyle=fg;
if(type==='wide'){
const l=r*0.9;
ctx.fillRect(cx-l,cy-1.2,2*l,2.4);
// arrowheads
tri(cx-l,cy, -1): path (cx-l,cy),(cx-l+5? use r*0.5)
left: moveTo(cx-l, cy), lineTo(cx-l+r*0.55, cy-r*0.42), lineTo(cx-l+r*0.55, cy+r*0.42)
right mirrored.
}else{
dots r*0.22 at offsets: (0,-r*0.42),(-r*0.45,r*0.3),(r*0.45,r*0.3)
}
```
Cracks:
```
function drawCracks(b){
ctx.strokeStyle='rgba(15,18,28,.8)';ctx.lineWidth=1.6;ctx.lineJoin='round';
const x=b.x,y=b.y,w=b.w,h=b.h,v=(b.col*53%17)/17, v2=(b.row*29%13)/13;
ctx.beginPath();
ctx.moveTo(x+w*(0.25+v*0.2),y);
ctx.lineTo(x+w*(0.35+v*0.2),y+h*0.4);
ctx.lineTo(x+w*(0.28+v*0.2),y+h*0.7);
ctx.lineTo(x+w*(0.4+v*0.2),y+h);
ctx.moveTo(x+w*(0.35+v*0.2),y+h*0.4);
ctx.lineTo(x+w*0.65,y+h*0.55);
ctx.moveTo(x+w*(0.75+v2*0.15),y+h);
ctx.lineTo(x+w*(0.68+v2*0.15),y+h*0.5);
ctx.lineTo(x+w*(0.8+v2*0.15),y);
ctx.stroke();
// damage overlay
ctx.fillStyle='rgba(0,0,0,.18)';rr(b.x,b.y,b.w,b.h,5);ctx.fill();
}
```
Particles draw: rects or circles, alpha = life/max.
pops draw: text with alpha fade, font 700 16px, stroke shadow.
Score popups + also "+60" per brick? Only powerup messages plus small "+pts" for tough breaks maybe. Add small pop for every brick: {txt:'+'+pts}. Could clutter; add for tough breaks only? Add for all with small font at brick pos — it's a nice juice. I'll add for all bricks with life 40.
Also draw ball speed increase? skip.
Input handlers:
```
cv.addEventListener('mousemove',e=>{
const r=cv.getBoundingClientRect();
mouseX=(e.clientX-r.left)*W/r.width; mouseOn=true;
});
cv.addEventListener('mousedown',()=>action());
window.addEventListener('keydown',e=>{
if(['ArrowLeft','ArrowRight',' '].includes(e.key))e.preventDefault();
if(e.key==='ArrowLeft')keys.l=true;
if(e.key==='ArrowRight')keys.r=true;
if(state==='playing'&&e.key===' ')balls.forEach(b=>{if(b.stuck)launchBall(b);});
if(e.key==='r'||e.key==='R'||e.key==='Enter'||e.key===' ')action();
else if any key → action() too (demo start on any key).
});
```
Simplify: in keydown: after arrow handling, call action() for any key not arrow, and for arrows also if state demo start. action():
```
function action(){
if(state==='demo'||state==='gameover'||state==='win'){ reset(); state='playing'; ... reset sets state playing; for demo we want a fresh game too (demo already broke bricks). Yes reset() fresh.
} else { // playing: launch stuck balls
balls.forEach(b=>{if(b.stuck)launchBall(b);});
}
}
```
But reset() sets state='playing' internally — fine.
Note after gameover/win restart, mouseOn might be true with old mouseX — paddle jumps to mouse; acceptable.
Also keyup clears keys.
Main loop:
```
let last=performance.now();
function tick(t){
let f=Math.min((t-last)/16.667,2.5); last=t; if(f<=0)f=0.001; clamp min? f=Math.max(f,0.1)? if tab inactive dt big — clamped 2.5.
update(f); draw();
requestAnimationFrame(tick);
}
reset(); state='demo'; balls... reset builds; then state='demo'.
requestAnimationFrame(tick);
```
reset(): set everything and state='playing'; then I call reset(); state='demo'; to begin.
During gameover/win, update still runs particles, confetti; balls frozen? Let balls keep moving? On gameover overlay, balls list empty anyway. On win, balls may still exist — freeze them: in update, only move balls if state==='playing'||state==='demo'. Also drops freeze. paddle AI frozen. OK add guards.
Also brick flash decay etc always.
One more: in 'win', remaining 0, ball may still be under overlay moving—guard stops it.
Font stacks: '700 20px system-ui, "Segoe UI", sans-serif'. Canvas font shorthand with fallbacks fine.
textBaseline set per use.
HUD lives icons:
```
for(let i=0;i<3;i++){const x=W-30-i*26;ctx.beginPath();ctx.arc(x,24,7,0,7);
if(i<lives){ctx.fillStyle='#7fd8ff';ctx.shadowColor... just fill}
else{ctx.strokeStyle='rgba(255,255,255,.25)';stroke}
}
```
Order: fill rightmost = current life. Fine: i<lives filled.
Center HUD: 'NEON BREAKOUT' at W/2, y 24, alpha .35, letterSpacing manual: draw with spacing: ctx.letterSpacing='4px' supported in Chrome. Use try? Just set ctx.letterSpacing='4px' — Chrome supports; if not, harmless (older ignored? Setting unknown property on ctx is fine—it's a real property; unsupported browsers ignore assignment? Assigning to nonexistent property just adds it, no error). ok.
Wide bar: if wide>0: text small 'WIDE' + bar at center y 40: width = 160*(wide/600).
Victory confetti colors from rowColors.
Now also ensure "something meaningful visible in 3s": demo running with AI playing immediately. Yes, ball auto-launches at 55 frames.
Potential bug: in reset(), pad recreated; balls=[newBall(true)] uses new pad. Good. But newBall referenced pad before assignment order: create pad first. Yes.
In gameover: keep last state visible behind overlay (bricks remain). balls empty. draw fine.
Edge: click on canvas triggers mousedown → action; also 'click or press a key'. Good. Touch: add touchstart? Bonus: cv.addEventListener('touchmove'...) skip—desktop judged. Add pointer events? Use pointermove instead of mousemove to cover both:
```
cv.addEventListener('pointermove',e=>{...})
cv.addEventListener('pointerdown',e=>{action()})
```
pointermove gives clientX too. Good.
cursor:none might hide cursor in demo — but user needs to see message; paddle indicates position. Keep cursor:none during playing only? Simple: keep none; fine.
Score pops small: pops.push({x,y,txt:'+'+pts,life:45,color:'#fff',size:14}) and powerup msgs size 18.
draw pops:
```
ctx.globalAlpha=Math.min(1,p.life/30);ctx.font='800 '+(p.size||15)+'px ...';ctx.textAlign='center';ctx.fillStyle=p.color;ctx.fillText(p.txt,p.x,p.y);ctx.globalAlpha=1;
```
Careful: pop for brick at brick center.
Now assemble full file. Also make sure no unused undefined vars. Let me write it out.
Also HUD center title maybe move to top-left with logo. Layout: left: "SCORE" label + value. right: lives. center: "NEON BREAKOUT" faint. Good.
Ball trail juice: skip.
Also add paddle drawn glow behind. ok.
Let me also double check brick region: BTOP=88, 8 rows × 31 = 248 → bottom 336. Paddle y=618. Ball space plenty.
Demo dim overlay covering bricks region only? Full play area with 0.35 alpha + title. Good.
Write final answer.
One more check: `ctx.arc(x,y,r,0,7)` 7 > 2π ok.
stepBall brick loop `break` after one brick per substep — good.
Wide timer in frames uses f-based decrement with real time — good (600 frames ≈ 10s).
Also requirement: "The ball must never pass through bricks or the paddle" — substeps of ≤2.5px with r=8 and brick h=26 guarantee resolution. Paddle check every substep with position snap. Good.
Check stepBall paddle: also guard ball not below paddle (if ball.y - r > pad.y+pad.h skip — condition b.y-r<=pad.y+pad.h covers).
Ball could hit paddle from below after passing? vy>0 required and y+r>=pad.y and y-r<=pad.y+h. If ball is below paddle entirely (y-r>pad.y+h) no bounce — falls out. ok.
Now write it.