GLM 5.3
by Z.aiReasoningClean
Running live in a sandboxed frame. Click inside it before using the keyboard.
How it went
- Total time
- 3 min 45 s
- Cost
- $0.113
- Output tokens
- 31,494
- Spent thinking
- 23,219 tokens
- Speed
- 139.9 tok/s
- Console errors
- 0
- First token after
- 350 ms
- First visible text after
- 3 min 7 s
- Prompt tokens
- 507
- Code size
- 22.3 KB
- Finish reason
- stop
- Served by
- InferenceNet
- Model id
z-ai/glm-5.3- Reasoning sent
{"effort":"high"}- 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 · 652 lines
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width,initial-scale=1"/>
<title>NEON BREAKER — Breakout</title>
<style>
*{margin:0;padding:0;box-sizing:border-box}
html,body{height:100%}
body{
background:
radial-gradient(900px 480px at 50% -8%, #1a2340 0%, rgba(26,35,64,0) 60%),
#07090f;
color:#dfe6f3;
font-family:"Segoe UI",system-ui,-apple-system,"Helvetica Neue",sans-serif;
display:flex;flex-direction:column;align-items:center;justify-content:center;
gap:10px;min-height:100vh;padding:14px;overflow:hidden;
}
header{display:flex;align-items:baseline;gap:14px;user-select:none}
h1{font-size:clamp(15px,2.6vw,22px);letter-spacing:.42em;font-weight:800;color:#eef3ff;text-transform:uppercase}
h1 b{color:#4ecdc4;font-weight:800}
.tag{font-size:11px;letter-spacing:.2em;color:#5b6784;text-transform:uppercase}
#wrap{width:min(960px,100%)}
canvas{
display:block;width:100%;aspect-ratio:960/640;border-radius:14px;background:#0a0d15;
box-shadow:0 0 0 1px #1c2438,0 30px 70px -20px rgba(0,0,0,.85),0 0 90px -30px rgba(78,205,196,.25);
cursor:crosshair;touch-action:none;
}
footer{font-size:11.5px;color:#5b6784;letter-spacing:.05em;text-align:center;line-height:1.7}
footer b{color:#9fb0d0;font-weight:600}
</style>
</head>
<body>
<header><h1>Neon<b>Breaker</b></h1><span class="tag">Level 01</span></header>
<div id="wrap"><canvas id="c" width="960" height="640">Canvas not supported</canvas></div>
<footer>
<b>Mouse</b> or <b>← →</b> to move · catch the capsules: <span style="color:#4ecdc4">wide paddle 10s</span> / <span style="color:#ff9f45">multi-ball ×3</span> · <b>R</b> restarts
</footer>
<script>
'use strict';
const cvs=document.getElementById('c');
const ctx=cvs.getContext('2d');
const W=960,H=640,HUD=54,TAU=Math.PI*2;
const dpr=Math.min(2,window.devicePixelRatio||1);
cvs.width=W*dpr;cvs.height=H*dpr;ctx.setTransform(dpr,0,0,dpr,0,0);
const clamp=(v,a,b)=>v<a?a:v>b?b:v;
const rand=(a,b)=>a+Math.random()*(b-a);
function shade(hex,amt){
const n=parseInt(hex.slice(1),16);
let r=(n>>16)&255,g=(n>>8)&255,b=n&255;
const f=t=>amt>=0?t+(255-t)*amt:t*(1+amt);
return 'rgb('+(f(r)|0)+','+(f(g)|0)+','+(f(b)|0)+')';
}
function rr(x,y,w,h,r){
r=Math.min(r,w/2,h/2);
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();
}
/* ---------------- audio (tiny synth, no assets) ---------------- */
let actx=null;
function ensureAudio(){
if(!actx){try{actx=new (window.AudioContext||window.webkitAudioContext)();}catch(e){}}
if(actx&&actx.state==='suspended')actx.resume();
}
function tone(f,dur,type,vol,slide){
if(!actx||actx.state!=='running')return;
try{
const t=actx.currentTime,o=actx.createOscillator(),g=actx.createGain();
o.type=type||'square';o.frequency.setValueAtTime(f,t);
if(slide)o.frequency.exponentialRampToValueAtTime(Math.max(30,f+slide),t+dur);
g.gain.setValueAtTime((vol||0.12)*0.5,t);
g.gain.exponentialRampToValueAtTime(0.0008,t+dur);
o.connect(g);g.connect(actx.destination);o.start(t);o.stop(t+dur+0.03);
}catch(e){}
}
const sfx={
wall:()=>tone(190,0.05,'triangle',0.10),
paddle:()=>tone(240,0.08,'triangle',0.16),
brick:r=>tone(330+r*45,0.09,'square',0.10),
crack:()=>tone(170,0.08,'square',0.12),
power:()=>{tone(660,0.09,'sine',0.16);setTimeout(()=>tone(990,0.12,'sine',0.16),90);},
lose:()=>tone(320,0.5,'sawtooth',0.14,-250),
win:()=>{[523,659,784,1046].forEach((f,i)=>setTimeout(()=>tone(f,0.18,'triangle',0.15),i*110));}
};
/* ---------------- level ---------------- */
const COLS=12,GAP=6,B_TOP=82,B_H=24;
const B_W=(W-GAP*(COLS+1))/COLS;
const ROW_COLORS=["#ff5e7a","#ff9f45","#ffd166","#7bdc8f","#4ecdc4","#5aa9e6","#a78bfa"];
const PU={wide:{color:'#4ecdc4'},multi:{color:'#ff9f45'}};
const LEVEL=[
"..tttttttt..",
".nnwnnnnmnn.",
"ttnnttnnttnn",
".nmnnwnnnmn.",
"nnttttttttnn",
".nnnnmwnnnn.",
"..nnnnnnnn.."
];
let bricks=[],balls=[],parts=[],pops=[],drops=[];
function initLevel(){
bricks=[];
LEVEL.forEach((row,r)=>{
for(let col=0;col<COLS;col++){
const ch=row[col];
if(ch==='.')continue;
const tough=ch==='t';
const power=ch==='w'?'wide':(ch==='m'?'multi':null);
bricks.push({
x:GAP+col*(B_W+GAP),y:B_TOP+r*(B_H+GAP),w:B_W,h:B_H,
hp:tough?2:1,maxHp:tough?2:1,alive:true,power,
color:ROW_COLORS[r],row:r,seed:(r*31+col*7+3)|0,cracks:null
});
}
});
}
/* ---------------- state ---------------- */
let paddle={x:W/2-56,y:H-46,h:14,wCur:112};
let score=0,lives=3,state='serve',demo=true,serveT=0.9,time=0,playT=0;
let wideT=0,shake=0,demoT=0,keyL=false,keyR=false,mode='mouse',mouseX=W/2;
const stars=[];
for(let i=0;i<70;i++)stars.push({x:rand(6,W-6),y:rand(HUD+8,H-40),r:rand(0.6,1.8),a:rand(0.15,0.5),sp:rand(0.4,1.6),ph:rand(0,TAU)});
function newBall(x,y,vx,vy,sp){return{x,y,vx,vy,r:7,speed:sp,dead:false,trail:[]};}
function launchBall(b){
const a=rand(-0.35,0.35);
b.vx=Math.sin(a)*b.speed;b.vy=-Math.cos(a)*b.speed;
}
function resetServe(){
state='serve';serveT=0.9;
balls=[newBall(paddle.x+paddle.wCur/2,paddle.y-9,0,0,380)];
}
function resetGame(isDemo){
demo=isDemo;score=0;lives=3;playT=0;wideT=0;shake=0;
drops=[];parts=[];pops=[];
initLevel();
paddle.x=W/2-paddle.wCur/2;
resetServe();
}
function loseLife(){
lives--;wideT=0;shake=8;sfx.lose();
if(lives<=0){state='over';demoT=3.2;}
else resetServe();
}
function winGame(){state='win';demoT=3.6;shake=4;sfx.win();}
/* ---------------- cracks (seeded, stable per brick) ---------------- */
function genCracks(br){
let s=br.seed>>>0;
const rnd=()=>{s=(s*1664525+1013904223)>>>0;return s/4294967296;};
const cracks=[],n=3+((rnd()*2)|0);
for(let i=0;i<n;i++){
let x=br.x+br.w*(0.25+rnd()*0.5),y=br.y+br.h*(0.25+rnd()*0.5),a=rnd()*TAU;
const pts=[[x,y]],segs=2+((rnd()*2)|0);
for(let j=0;j<segs;j++){
a+=(rnd()-0.5)*1.6;
const len=5+rnd()*9;
x=clamp(x+Math.cos(a)*len,br.x+3,br.x+br.w-3);
y=clamp(y+Math.sin(a)*len,br.y+3,br.y+br.h-3);
pts.push([x,y]);
}
cracks.push(pts);
}
return cracks;
}
/* ---------------- particles / popups ---------------- */
function burst(br,n,power){
const cols=[br.color,shade(br.color,0.35),'#ffffff'];
const cx=br.x+br.w/2,cy=br.y+br.h/2;
for(let i=0;i<n&&parts.length<400;i++){
const a=rand(0,TAU),v=rand(60,260)*power;
parts.push({
x:cx+rand(-br.w/3,br.w/3),y:cy+rand(-4,4),
vx:Math.cos(a)*v,vy:Math.sin(a)*v-rand(0,80),
grav:420,life:rand(0.4,0.85),max:0.85,size:rand(2,4.5),
color:cols[i%3],rot:rand(0,6),vr:rand(-8,8)
});
}
}
function splash(x){
for(let i=0;i<12;i++)parts.push({
x:x+rand(-6,6),y:H-2,vx:rand(-120,120),vy:rand(-260,-60),
grav:500,life:rand(0.3,0.6),max:0.6,size:rand(2,3.5),
color:'#ff5e7a',rot:0,vr:rand(-6,6)
});
}
function popup(x,y,txt,color){pops.push({x,y,txt,life:1.2,color});}
/* ---------------- gameplay ---------------- */
function hitBrick(br,b){
br.hp--;
b.speed=Math.min(590,b.speed+5);
if(br.hp>0){
br.cracks=genCracks(br);
score+=25;shake=Math.min(10,shake+2.5);
burst(br,5,0.6);sfx.crack();
}else{
br.alive=false;
const val=br.maxHp>1?120:50;
score+=val;
shake=Math.min(10,shake+(br.maxHp>1?3.2:1.6));
burst(br,br.maxHp>1?22:14,1);
popup(br.x+br.w/2,br.y+br.h/2,'+'+val,br.maxHp>1?'#cbd6ee':'#ffe9a8');
if(br.power)drops.push({x:br.x+br.w/2,y:br.y+br.h/2,vy:110,type:br.power});
sfx.brick(br.row);
if(!bricks.some(k=>k.alive))winGame();
}
}
function applyPU(d){
score+=150;sfx.power();
if(d.type==='wide'){
wideT=10;
popup(d.x,paddle.y-18,'WIDE PADDLE','#4ecdc4');
}else{
popup(d.x,paddle.y-18,'MULTI-BALL','#ff9f45');
doMulti();
}
}
function doMulti(){
if(state==='serve'){const b=balls[0];launchBall(b);state='play';}
const base=balls[0];
if(!base)return;
const need=3-balls.length;
if(need<=0){score+=300;popup(base.x,base.y-18,'BONUS +300','#ffd166');return;}
const offsets=[0.4,-0.4,0.8,-0.8];
for(let i=0;i<need;i++){
const a=offsets[i%offsets.length],c=Math.cos(a),s=Math.sin(a);
balls.push({
x:base.x,y:base.y-4,vx:base.vx*c-base.vy*s,vy:base.vx*s+base.vy*c,
r:base.r,speed:base.speed,dead:false,trail:[]
});
}
}
function stepBall(b,sdt){
b.x+=b.vx*sdt;b.y+=b.vy*sdt;
// walls
if(b.x<b.r){b.x=b.r;if(b.vx<0)b.vx=-b.vx;sfx.wall();}
else if(b.x>W-b.r){b.x=W-b.r;if(b.vx>0)b.vx=-b.vx;sfx.wall();}
if(b.y<HUD+b.r){b.y=HUD+b.r;if(b.vy<0)b.vy=-b.vy;sfx.wall();}
if(Math.abs(b.vy)<30)b.vy=(b.vy<=0?-1:1)*30; // safety vs. flat loops
// paddle (angle depends on hit position)
const pw=paddle.wCur;
if(b.vy>0&&b.y+b.r>=paddle.y&&b.y-b.r<=paddle.y+paddle.h&&
b.x>=paddle.x-b.r&&b.x<=paddle.x+pw+b.r){
const rel=clamp((b.x-(paddle.x+pw/2))/(pw/2),-1,1);
const ang=rel*(Math.PI/180*62);
b.vx=Math.sin(ang)*b.speed;b.vy=-Math.cos(ang)*b.speed;
b.y=paddle.y-b.r-0.5;
sfx.paddle();
for(let i=0;i<3;i++)parts.push({x:b.x,y:paddle.y,vx:rand(-60,60),vy:rand(-130,-30),grav:300,life:0.3,max:0.3,size:2,color:'#cfe4ff',rot:0,vr:0});
}
// bricks (tiny sub-steps prevent any pass-through)
for(const br of bricks){
if(!br.alive)continue;
const cx=clamp(b.x,br.x,br.x+br.w),cy=clamp(b.y,br.y,br.y+br.h);
const dx=b.x-cx,dy=b.y-cy;
if(dx*dx+dy*dy<=b.r*b.r){
const ox=(br.w/2+b.r)-Math.abs(b.x-(br.x+br.w/2));
const oy=(br.h/2+b.r)-Math.abs(b.y-(br.y+br.h/2));
if(ox<oy){
if(b.x<br.x+br.w/2){b.x-=ox;b.vx=-Math.abs(b.vx);}
else{b.x+=ox;b.vx=Math.abs(b.vx);}
}else{
if(b.y<br.y+br.h/2){b.y-=oy;b.vy=-Math.abs(b.vy);}
else{b.y+=oy;b.vy=Math.abs(b.vy);}
}
hitBrick(br,b);
break;
}
}
if(b.y-b.r>H+8){b.dead=true;splash(b.x);}
}
function demoAI(dt){
let target=W/2;
if(state==='serve')target=W/2+Math.sin(time*0.7)*140;
else if(balls.length){
let best=balls[0];
for(const b of balls)if(b.y>best.y)best=b;
target=best.x;
}
target+=Math.sin(time*2.3)*16+Math.sin(time*5.1)*7;
const pw=paddle.wCur,des=clamp(target-pw/2,4,W-4-pw),step=560*dt;
paddle.x+=clamp(des-paddle.x,-step,step);
}
function humanPaddle(dt){
const pw=paddle.wCur;
if(mode==='key'){
const sp=680*dt;
if(keyL)paddle.x-=sp;
if(keyR)paddle.x+=sp;
}else{
const des=clamp(mouseX-pw/2,4,W-4-pw);
paddle.x+=(des-paddle.x)*Math.min(1,dt*22);
}
paddle.x=clamp(paddle.x,4,W-4-pw);
}
function update(dt){
time+=dt;
if(state==='play'||state==='serve')playT+=dt;
if(demo)demoAI(dt);else humanPaddle(dt);
const targetW=wideT>0?170:112;
paddle.wCur+=(targetW-paddle.wCur)*Math.min(1,dt*8);
if(wideT>0)wideT-=dt;
if(state==='serve'){
const b=balls[0];
b.x=paddle.x+paddle.wCur/2;b.y=paddle.y-b.r-2;
serveT-=dt;
if(serveT<=0){launchBall(b);state='play';sfx.paddle();}
}else if(state==='play'){
let maxV=0;
for(const b of balls)maxV=Math.max(maxV,Math.abs(b.vx),Math.abs(b.vy));
const steps=Math.max(1,Math.ceil((maxV*dt)/4)),sdt=dt/steps;
for(let s=0;s<steps&&balls.length;s++){
for(const b of balls)if(!b.dead)stepBall(b,sdt);
for(let i=balls.length-1;i>=0;i--)if(balls[i].dead)balls.splice(i,1);
}
if(state==='play'&&!balls.length)loseLife();
}
// falling capsules
if(state==='play'||state==='serve'){
for(let i=drops.length-1;i>=0;i--){
const d=drops[i];
d.y+=d.vy*dt;d.vy=Math.min(240,d.vy+60*dt);
if(d.y+11>=paddle.y&&d.y-11<=paddle.y+paddle.h&&
d.x>paddle.x-10&&d.x<paddle.x+paddle.wCur+10){
applyPU(d);drops.splice(i,1);continue;
}
if(d.y>H+30)drops.splice(i,1);
}
}
for(let i=parts.length-1;i>=0;i--){
const p=parts[i];p.life-=dt;
if(p.life<=0){parts.splice(i,1);continue;}
p.vy+=p.grav*dt;p.x+=p.vx*dt;p.y+=p.vy*dt;p.rot+=p.vr*dt;
}
for(let i=pops.length-1;i>=0;i--){
const p=pops[i];p.life-=dt*1.1;p.y-=32*dt;
if(p.life<=0)pops.splice(i,1);
}
shake=Math.max(0,shake-dt*14);
if(demo&&(state==='over'||state==='win')){
demoT-=dt;
if(demoT<=0)resetGame(true);
}
}
/* ---------------- drawing ---------------- */
function drawBG(){
ctx.fillStyle='#0a0d15';ctx.fillRect(-14,-14,W+28,H+28);
const g=ctx.createRadialGradient(W/2,HUD,40,W/2,HUD,520);
g.addColorStop(0,'rgba(90,169,230,0.10)');g.addColorStop(1,'rgba(0,0,0,0)');
ctx.fillStyle=g;ctx.fillRect(0,HUD,W,H-HUD);
for(const s of stars){
ctx.globalAlpha=s.a*(0.4+0.6*Math.abs(Math.sin(time*s.sp+s.ph)));
ctx.fillStyle='#9fb4dd';
ctx.fillRect(s.x,s.y,s.r,s.r);
}
ctx.globalAlpha=1;
ctx.fillStyle='rgba(78,205,196,0.06)';
ctx.fillRect(0,HUD,2,H-HUD);ctx.fillRect(W-2,HUD,2,H-HUD);
}
function drawWideIcon(cx,cy,s,color){
ctx.strokeStyle=color;ctx.lineWidth=2;ctx.lineCap='round';ctx.lineJoin='round';
ctx.beginPath();
ctx.moveTo(cx-s,cy);ctx.lineTo(cx+s,cy);
ctx.moveTo(cx-s+4,cy-3.5);ctx.lineTo(cx-s,cy);ctx.lineTo(cx-s+4,cy+3.5);
ctx.moveTo(cx+s-4,cy-3.5);ctx.lineTo(cx+s,cy);ctx.lineTo(cx+s-4,cy+3.5);
ctx.stroke();
}
function drawMultiIcon(cx,cy,s,color){
ctx.fillStyle=color;
const pts=[[cx,cy-s*0.8],[cx-s*0.7,cy+s*0.55],[cx+s*0.7,cy+s*0.55]];
for(const p of pts){ctx.beginPath();ctx.arc(p[0],p[1],s*0.34,0,TAU);ctx.fill();}
}
function drawBricks(){
for(const b of bricks){
if(!b.alive)continue;
const x=b.x,y=b.y,w=b.w,h=b.h;
if(b.power){
const pulse=0.5+0.5*Math.sin(time*4+b.x*0.05);
ctx.shadowColor=PU[b.power].color;ctx.shadowBlur=6+7*pulse;
}
const base=b.maxHp>1?'#8fa2c4':b.color;
const g=ctx.createLinearGradient(0,y,0,y+h);
g.addColorStop(0,shade(base,0.38));
g.addColorStop(0.45,base);
g.addColorStop(1,shade(base,-0.34));
rr(x+0.5,y+0.5,w-1,h-1,5);ctx.fillStyle=g;ctx.fill();
ctx.shadowBlur=0;
ctx.strokeStyle='rgba(255,255,255,0.14)';ctx.lineWidth=1;ctx.stroke();
ctx.fillStyle='rgba(255,255,255,0.22)';
rr(x+3,y+2,w-6,3,2);ctx.fill();
if(b.maxHp>1){
ctx.fillStyle='rgba(255,255,255,0.30)';
for(const p of [[x+6,y+6],[x+w-6,y+6],[x+6,y+h-6],[x+w-6,y+h-6]]){
ctx.beginPath();ctx.arc(p[0],p[1],1.6,0,TAU);ctx.fill();
}
}
if(b.cracked){
ctx.fillStyle='rgba(8,10,16,0.30)';rr(x+0.5,y+0.5,w-1,h-1,5);ctx.fill();
ctx.strokeStyle='rgba(10,13,20,0.9)';ctx.lineWidth=1.4;ctx.lineJoin='round';
for(const c of b.cracks){
ctx.beginPath();ctx.moveTo(c[0][0],c[0][1]);
for(let i=1;i<c.length;i++)ctx.lineTo(c[i][0],c[i][1]);
ctx.stroke();
}
}
if(b.power){
ctx.globalAlpha=0.92;
const cx=x+w/2,cy=y+h/2;
if(b.power==='wide')drawWideIcon(cx,cy,8,PU.wide.color);
else drawMultiIcon(cx,cy,8,PU.multi.color);
ctx.globalAlpha=1;
}
}
}
function drawDrops(){
for(const d of drops){
const pu=PU[d.type];
ctx.save();
ctx.translate(d.x,d.y);ctx.rotate(Math.sin(d.y*0.04)*0.12);
ctx.shadowColor=pu.color;ctx.shadowBlur=10;
rr(-19,-11,38,22,10);ctx.fillStyle='#101828';ctx.fill();
ctx.shadowBlur=0;
ctx.strokeStyle=pu.color;ctx.lineWidth=2;ctx.stroke();
if(d.type==='wide')drawWideIcon(0,0,7,pu.color);
else drawMultiIcon(0,0,7,pu.color);
ctx.restore();
}
}
function drawParts(){
for(const p of parts){
ctx.globalAlpha=Math.max(0,p.life/p.max);
ctx.save();
ctx.translate(p.x,p.y);ctx.rotate(p.rot);
ctx.fillStyle=p.color;
ctx.fillRect(-p.size/2,-p.size/2,p.size,p.size);
ctx.restore();
}
ctx.globalAlpha=1;
}
function drawPaddle(){
const x=paddle.x,y=paddle.y,w=paddle.wCur,h=paddle.h;
if(wideT>0){ctx.shadowColor='#4ecdc4';ctx.shadowBlur=12+5*Math.sin(time*6);}
else{ctx.shadowColor='rgba(160,190,255,0.5)';ctx.shadowBlur=7;}
const g=ctx.createLinearGradient(0,y,0,y+h);
g.addColorStop(0,'#f4f8ff');g.addColorStop(0.5,'#c3d4f5');g.addColorStop(1,'#7d97c9');
rr(x,y,w,h,7);ctx.fillStyle=g;ctx.fill();
ctx.shadowBlur=0;
ctx.strokeStyle='rgba(255,255,255,0.35)';ctx.lineWidth=1;ctx.stroke();
ctx.fillStyle='rgba(40,60,110,0.5)';
rr(x+w/2-5,y+3,10,h-6,3);ctx.fill();
}
function drawBalls(){
for(const b of balls){
b.trail.push(b.x,b.y);
if(b.trail.length>14)b.trail.splice(0,2);
for(let i=0;i<b.trail.length;i+=2){
const a=i/b.trail.length;
ctx.globalAlpha=a*0.22;
ctx.fillStyle='#9fe8ff';
ctx.beginPath();ctx.arc(b.trail[i],b.trail[i+1],Math.max(1,b.r*a*0.8),0,TAU);ctx.fill();
}
ctx.globalAlpha=1;
const g=ctx.createRadialGradient(b.x-2,b.y-2,1,b.x,b.y,b.r);
g.addColorStop(0,'#ffffff');g.addColorStop(0.6,'#c9f2ff');g.addColorStop(1,'#5ad0e6');
ctx.shadowColor='rgba(120,220,255,0.8)';ctx.shadowBlur=10;
ctx.fillStyle=g;
ctx.beginPath();ctx.arc(b.x,b.y,b.r,0,TAU);ctx.fill();
ctx.shadowBlur=0;
}
}
function drawPops(){
ctx.textAlign='center';ctx.textBaseline='middle';
ctx.font='700 13px system-ui,Segoe UI,sans-serif';
for(const p of pops){
ctx.globalAlpha=clamp(p.life,0,1);
ctx.shadowColor='rgba(0,0,0,0.8)';ctx.shadowBlur=4;
ctx.fillStyle=p.color;
ctx.fillText(p.txt,p.x,p.y);
}
ctx.globalAlpha=1;ctx.shadowBlur=0;
}
function drawPill(cx,label,frac,color){
const w=124,h=28,x=cx-w/2,y=HUD/2-h/2+4;
rr(x,y,w,h,8);ctx.fillStyle='rgba(255,255,255,0.05)';ctx.fill();
ctx.strokeStyle=color;ctx.lineWidth=1.5;ctx.stroke();
ctx.fillStyle=color;ctx.font='700 11px system-ui,sans-serif';
ctx.textAlign='center';ctx.textBaseline='middle';
ctx.fillText(label,cx,y+10);
ctx.globalAlpha=0.85;
ctx.fillStyle=color;
ctx.fillRect(x+10,y+18,(w-20)*clamp(frac,0,1),3);
ctx.globalAlpha=1;
}
function drawHUD(){
const g=ctx.createLinearGradient(0,0,0,HUD);
g.addColorStop(0,'#101627');g.addColorStop(1,'#0b0f1c');
ctx.fillStyle=g;ctx.fillRect(0,0,W,HUD);
ctx.fillStyle='rgba(78,205,196,0.55)';ctx.fillRect(0,HUD-2,W,2);
ctx.fillStyle='rgba(78,205,196,0.10)';ctx.fillRect(0,HUD-6,W,4);
ctx.textBaseline='middle';
// score
ctx.textAlign='left';
ctx.fillStyle='#5b6784';ctx.font='600 10px system-ui,sans-serif';
ctx.fillText('SCORE',18,17);
ctx.fillStyle='#eef3ff';ctx.font='700 22px Consolas,Menlo,monospace';
ctx.fillText(String(score).padStart(5,'0'),18,38);
// lives
ctx.textAlign='right';
ctx.fillStyle='#5b6784';ctx.font='600 10px system-ui,sans-serif';
ctx.fillText('LIVES',W-18,17);
for(let i=0;i<3;i++){
const cx=W-26-i*22,cy=38;
ctx.beginPath();ctx.arc(cx,cy,7,0,TAU);
if(i<lives){ctx.fillStyle='#ff5e7a';ctx.shadowColor='#ff5e7a';ctx.shadowBlur=8;ctx.fill();ctx.shadowBlur=0;}
else{ctx.strokeStyle='rgba(255,94,122,0.25)';ctx.lineWidth=1.5;ctx.stroke();}
}
// active effect pills
const pills=[];
if(wideT>0)pills.push({label:'WIDE '+Math.ceil(wideT)+'s',frac:wideT/10,color:PU.wide.color});
if(balls.length>1)pills.push({label:'BALLS ×'+balls.length,frac:1,color:PU.multi.color});
pills.forEach((p,i)=>{
const cx=pills.length===1?W/2:W/2+(i===0?-80:80);
drawPill(cx,p.label,p.frac,p.color);
});
}
function fmt(t){
const m=(t/60)|0,s=(t%60)|0;
return String(m).padStart(2,'0')+':'+String(s).padStart(2,'0');
}
function drawOverlays(){
ctx.textAlign='center';ctx.textBaseline='middle';
if(demo&&state!=='over'&&state!=='win'){
const a=0.75+0.25*Math.sin(time*3);
const msg='CLICK OR PRESS ANY KEY TO START';
ctx.font='700 13px system-ui,sans-serif';
const w=ctx.measureText(msg).width+46;
ctx.globalAlpha=a;
rr(W/2-w/2,452,w,30,15);
ctx.fillStyle='rgba(8,12,22,0.85)';ctx.fill();
ctx.strokeStyle='rgba(78,205,196,0.5)';ctx.lineWidth=1;ctx.stroke();
ctx.fillStyle='#eaf6ff';ctx.fillText(msg,W/2,467);
ctx.globalAlpha=0.8;
ctx.font='600 11px system-ui,sans-serif';ctx.fillStyle='#7d8db0';
ctx.fillText('AI IS PLAYING THE DEMO',W/2,496);
ctx.globalAlpha=1;
}
if(state==='serve'&&!demo){
ctx.globalAlpha=0.55+0.45*Math.sin(time*6);
ctx.font='700 12px system-ui,sans-serif';ctx.fillStyle='#cfe0ff';
ctx.fillText('GET READY',paddle.x+paddle.wCur/2,paddle.y-24);
ctx.globalAlpha=1;
}
if(state==='over'||state==='win'){
const win=state==='win';
ctx.fillStyle='rgba(5,8,14,0.78)';ctx.fillRect(0,0,W,H);
ctx.fillStyle=win?'#4ecdc4':'#ff5e7a';
ctx.shadowColor=win?'#4ecdc4':'#ff5e7a';ctx.shadowBlur=30;
ctx.font='800 52px system-ui,sans-serif';
ctx.fillText(win?'LEVEL CLEARED':'GAME OVER',W/2,H/2-72);
ctx.shadowBlur=0;
ctx.fillStyle='#eef3ff';ctx.font='700 26px Consolas,Menlo,monospace';
ctx.fillText('SCORE '+String(score).padStart(5,'0'),W/2,H/2-10);
ctx.fillStyle='#9fb0d0';ctx.font='600 14px system-ui,sans-serif';
ctx.fillText('TIME '+fmt(playT)+' · LIVES LEFT '+Math.max(0,lives),W/2,H/2+22);
ctx.globalAlpha=0.7+0.3*Math.sin(time*4);
ctx.fillStyle=win?'#7be8dd':'#ffb3c0';ctx.font='700 14px system-ui,sans-serif';
ctx.fillText(demo?'RESTARTING DEMO…':'CLICK OR PRESS R TO RESTART',W/2,H/2+70);
ctx.globalAlpha=1;
}
}
function draw(){
ctx.save();
if(shake>0)ctx.translate(rand(-shake,shake)*0.6,rand(-shake,shake)*0.6);
drawBG();
drawBricks();
drawDrops();
drawParts();
drawPaddle();
drawBalls();
drawPops();
ctx.restore();
drawHUD();
drawOverlays();
}
/* ---------------- input ---------------- */
window.addEventListener('pointermove',e=>{
const r=cvs.getBoundingClientRect();
mouseX=(e.clientX-r.left)*W/r.width;
mode='mouse';
});
cvs.addEventListener('pointerdown',e=>{
const r=cvs.getBoundingClientRect();
mouseX=(e.clientX-r.left)*W/r.width;
mode='mouse';
ensureAudio();
if(demo){resetGame(false);return;}
if(state==='over'||state==='win'){resetGame(false);return;}
if(state==='serve')serveT=0;
});
window.addEventListener('keydown',e=>{
if(e.key==='ArrowLeft'||e.key==='ArrowRight'){
e.preventDefault();mode='key';
if(e.key==='ArrowLeft')keyL=true;else keyR=true;
}
if(e.key===' ')e.preventDefault();
ensureAudio();
if(demo){resetGame(false);return;}
if(e.key==='r'||e.key==='R'){resetGame(false);return;}
if(state==='over'||state==='win'){
if(e.key==='Enter'||e.key===' ')resetGame(false);
return;
}
if(state==='serve'&&(e.key===' '||e.key==='ArrowUp'))serveT=0;
});
window.addEventListener('keyup',e=>{
if(e.key==='ArrowLeft')keyL=false;
if(e.key==='ArrowRight')keyR=false;
});
/* ---------------- main loop ---------------- */
resetGame(true);
let last=performance.now();
function frame(now){
const dt=Math.min(0.033,(now-last)/1000);
last=now;
update(dt);
draw();
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
</script>
</body>
</html>Reasoning trace (23,219 tokens)
Let me build a polished Breakout game. Key requirements:
1. Canvas-based, vanilla JS
2. Mouse + arrow key paddle control
3. Angle-based bounce off paddle (center = straight up, edges = sharp)
4. 6+ rows of bricks, mix of 1-hit and 2-hit bricks (2-hit cracks after first hit)
5. Power-up bricks with icons; drop wide paddle (10s) or multi-ball (3 balls)
6. No tunneling — ball must never pass through bricks/paddle. Use sub-stepping / swept collision.
7. Particles on brick break
8. 3 lives, score, game over/victory screens with restart
9. Self-playing demo mode on start with "Click or press a key to start" message
Design: dark polished look, nice typography, glow effects. Let me make it look premium — deep dark background, neon-ish but tasteful palette, subtle vignette, brick colors by row.
For collision robustness: use fixed timestep with sub-steps — split ball movement into small steps so max movement per step < min(brick height, paddle height). Also do proper AABB vs circle collision with penetration resolution.
Brick grid: canvas 900x640 maybe. Let's do internal resolution 960x640, scaled via CSS. Columns 12, rows 7. Brick width ~ (960 - margins)/12.
Brick types:
- normal (1 hit)
- tough (2 hits, cracks after first)
- powerup carriers (marked with icon: "W" for wide, "M" for multi... better icons: arrows for wide ↔, three dots for multi-ball). Draw icons with canvas shapes.
Level layout: define a pattern. 7 rows. Row pattern maybe:
Row 0 (top): tough bricks mostly
Rows alternate. Let me create a layout array with characters:
- '.' empty
- 'n' normal
- 't' tough
- 'w' wide powerup carrier (normal brick carrying wide)
- 'm' multiball carrier
- 'x' tough carrying powerup maybe
Something like:
```
const LEVEL = [
"tttttttttttt",
"nnwmnnnnmw nn",
...
];
```
Let me design 12 cols × 7 rows:
```
row0: "t.t.t.t.t.t." hmm
```
Let me do:
```
[
"..tttttttt..",
"tnnwmmnwwn t",...
```
Careful: each string must be 12 chars. Let me craft:
```
"tttttttttttt",
"nnmnnnnnwnn n" — no spaces allowed except '.'.
```
Layout plan (12 wide, 7 rows):
- Row 0: `..tttttttt..` (tough top row with gaps at edges)
- Row 1: `.nnwnnnnmnn.`
- Row 2: `ttnnttnnttn n`? Hmm 12 chars: `ttnnttnnttn n` — count: t,t,n,n,t,t,n,n,t,t,n,n = "ttnnttnnttnn"
- Row 3: `.nmnnnnnnmn.`? Let's be careful: `.` `n` `m` `n` `n` `n` `n` `n` `n` `m` `n` `.` = ".nmnnnnnnmn."
- Row 4: `nnttttttttn n` → "nntttttttt nn"? count: n,n,t,t,t,t,t,t,t,t,n,n = "nnttttttttnn"
- Row 5: `.nnnnwwnnnn.` (12: . n n n n w w n n n n . )
- Row 6: `..nnnnnnnn..`
Count powerups: row1 has w,m; row3 has m,m; row5 has w,w. That's 5 powerups (2 wide, 3 multi). Good mix. Actually let me balance: row1: `.nnwnnnnmnn.` gives w at index 3, m at index 8. Row 3: `.nmnnwnnnmn.`? Let me just finalize in code and count carefully.
Row colors: gradient by row — top rows tougher (2-hit) colored differently. I'll color tough bricks as steel-gray/blue with rivets, normal bricks in a warm/cool gradient per row.
Crack rendering: after first hit, draw crack lines (procedurally generated per brick with seeded random so it's stable), plus darken.
Power-up drop: falling capsule with icon, caught by paddle → effect. Wide paddle: paddle width × 1.5 for 10s, with timer bar maybe. Multi-ball: spawn 2 extra balls (total 3) from the caught position... Multi-ball "3 balls" — spawn extra balls so total is 3 (or add 2). If more already, keep at... requirement says multi-ball gives 3 balls. I'll spawn extra balls to reach 3 total (or add 2 up to a cap).
Lives: lose life when all balls gone. 3 lives total.
Demo mode: AI plays — paddle follows ball x with some smoothing/lag, auto-restart on game over/victory. Overlay "Click or press a key to start" that dismisses on click/keydown, then player controls.
Keyboard: arrows move paddle. Mouse: paddle follows mouse x within canvas. Also pointer lock not needed.
Rendering polish:
- Dark background with subtle grid/glow
- Ball with trail (fading positions)
- Paddle with rounded rect, glow, gradient
- Particles: colored shards with gravity + fade
- Screen shake on brick break? Subtle, maybe on tough brick / life loss.
- Floating score popups "+50"
- HUD: score, lives (as small ball icons), level, active power-up indicator with countdown bar
- Start overlay, game over screen, victory screen with restart button/press key
Fonts: use system font stack, maybe a Google Font? External network requests allowed only if CDN allowed by task... "No external files, images, fonts or network requests, except the CDN URLs explicitly allowed by the task." The task doesn't allow any CDN. So system fonts only. Use font stack with fallback, e.g., "600 16px 'Segoe UI', system-ui, sans-serif". Fine.
Canvas scaling: fixed internal size 960×640, CSS width 100% max 960. Handle devicePixelRatio? Keep simple: set canvas width/height to 960/640, CSS scales. For crispness could multiply by DPR — let's do DPR-aware: canvas.width = 960*dpr etc., ctx.scale(dpr,dpr). Mouse coords account for CSS scaling via getBoundingClientRect.
Physics details:
- Ball speed constant magnitude (normalize after bounces), speed ~ 6 px/frame at 60fps → use dt-based: speed 420 px/s. Use fixed timestep accumulate or just dt-clamped rAF with substeps.
- Sub-stepping: compute steps = ceil(maxMove / 4). Each substep: move, then check collisions (walls, paddle, bricks). With 4px steps and brick height ~24, no tunneling.
- Paddle collision: only when ball moving downward and ball bottom crosses paddle top; reflect with angle based on offset: angle = offset * maxAngle (e.g., 60°). vx = speed*sin(a), vy = -speed*cos(a).
- Brick collision: circle-AABB: find closest point, if dist < r → collide. Determine bounce axis by comparing penetration. Then push ball out and reflect. Handle corner: reflect based on which axis penetration smaller. Only handle one brick per substep to avoid weirdness, but substeps are tiny so fine.
Ball vs paddle when paddle moves — ball pushed? Just standard reflect.
Bottom: ball lost when y > height + r.
Ball speed increases slightly as bricks break? Maybe +small per brick, capped. Adds polish. Keep cap.
Particles: array, spawn 12-18 on brick break, colored per brick, gravity, fade, rotation for shards (small rects).
Score: normal 50, tough 100 (on final break; 20 for crack?). Powerup catch: +150. Cracked tough hit: +25.
Demo AI: target x = predicted ball landing x (simple: follow ball with max speed, slight offset error). When multiple balls, follow the lowest one. Also occasionally AI misses? No — demo should play well but it's fine if it wins; auto-restart after a pause. Actually to make demo interesting, AI with slight imperfection (target + sin noise) so it sometimes loses a life. Keep it decent though.
Auto-restart in demo mode: after game over/victory, wait 2.5s, reset to demo play.
On user start: reset game fully, lives 3, score 0.
State machine: 'menu-demo' (AI plays), 'playing', 'gameover', 'victory', 'dying' (brief pause after losing ball?). In gameover/victory, if in demo mode auto-restart; if playing for real, show screen with "Click or press R / click button to restart".
Overlay message "Click or press a key to start" shown during demo. On click/keydown → start real game.
Also pause? Not required; skip or add with P/Esc. Keep it simple, maybe skip.
HUD drawn in canvas top bar area. Reserve top ~48px for HUD? Or draw HUD inside canvas top with bricks below. Let's have play field with top HUD strip: score left, lives right, power-up timers center. Bricks start at y=80.
Layout:
- Canvas 960×640.
- HUD: y 0–52.
- Bricks: y from 70, 7 rows × (brickH 24 + gap 6) = 210 → bottom of bricks ~280.
- Paddle y = 600.
- Walls at x=0 and x=960.
Colors: background #0b0e14 → vignette. Rows palette (normal bricks): row-based hues:
- Row colors: ["#ff6b6b","#f39c3d","#ffd166","#4ecdc4"...]. Tough bricks: slate blue "#5c7cfa" with metallic sheen. Powerup carriers: gold border + icon.
Actually let's theme: neon on dark. Normal brick colors per row:
r0 tough: #8ea2c8? Hmm tough rows differ. Let me color normal bricks with a palette per row index, tough with distinct "steel" look.
Palette for rows (normal): ["#ff5e7a","#ff9f45","#ffd166","#7bdc8f","#4ecdc4","#5aa9e6","#a06bff"]. Tough: "#9aa7bd" steel with highlight, and when cracked darker + cracks. Powerup bricks: base color of row but with glowing icon capsule.
Icons:
- Wide: draw "↔" as two arrows or a double-headed arrow.
- Multi: three small circles triangle.
Falling powerup capsule: rounded rect with letter? Draw icon shapes: wide = ↔ arrows; multi = 3 dots. With color: wide = cyan (#4ecdc4), multi = magenta/orange (#ff9f45 or #ff5e7a).
Screen shake: small shake offset applied via ctx.translate, decaying.
Ball trail: keep last N positions, draw fading circles.
Sound? No network; could use WebAudio beeps. Adds polish. Small oscillator blips on bounce/break. WebAudio requires user gesture — start after click; in demo mode before interaction audio context suspended; fine, just guard. Let's add simple sounds with a resume on first gesture. Keep code small. I'll add tiny synth: paddle hit (low blip), brick (higher, pitch based on row), powerup (chime), life lost (down sweep). Guard everything in try/catch.
Let me now write the code carefully.
Structure:
```js
const W=960, H=640;
const HUD_H = 52;
const WALL = 12; // side wall thickness visual
```
Actually walls: play area from 0..W; draw side/top borders inside. Simpler: ball bounces at x=r, x=W-r, y=HUD_H+r (below HUD line) — draw HUD separator line.
Bricks:
```js
const COLS=12, ROWS=7, GAP=6, TOP=76, BRICK_W=(W - GAP*(COLS+1))/COLS, BRICK_H=22;
```
W=960: (960 - 6*13)/12 = (960-78)/12 = 882/12 = 73.5. Good.
Level string rows (12 chars each):
```js
const LEVEL = [
"..tttttttt..",
".nnwnnnnmnn.",
"ttnnttnnttnn",
".nmnnwnnnmn.",
"nnttttttttnn",
".nnnnmwnnnn.",
"..nnnnnnnn..",
];
```
Check lengths:
- "..tttttttt.." = 2+8+2=12 ✓
- ".nnwnnnnmnn." = 1+10+1: chars: . n n w n n n n m n n . = 12 ✓
- "ttnnttnnttnn" = 12 ✓
- ".nmnnwnnnmn." : . n m n n w n n n m n . = 12 ✓
- "nnttttttttnn": n n t t t t t t t t n n = 12 ✓
- ".nnnnmwnnnn." : . n n n n m w n n n n . = 12 ✓
- "..nnnnnnnn.." = 12 ✓
Powerups: w at (1,3), (3,5), (5,6)... count: row1: w1, m1; row3: m2; row5: m1, w1. Totals: wide×2, multi×3. Maybe add one more wide: change row3 to ".nmnnwnnnmn." → that gives w at index 5? chars: . n m n n w n n n m n . — yes w at 5. Wait that's what I wrote: ".nmnnwnnnmn." Let me recount: `.`,`n`,`m`,`n`,`n`,`w`,`n`,`n`,`n`,`m`,`n`,`.` — yes w at 5, m at 2 and 9. So wide×3, multi×3.
Row chars 'n','t','w','m'. 'w'/'m' are normal bricks carrying powerups. Maybe make some carriers tough? Fine as is.
Brick object: {x,y,w,h,type,hp,maxHp,alive,seed,row,col,power}.
Crack drawing: use seeded pseudo-random to draw 3-4 jagged polylines from a random interior point. Precompute crack path points when brick first cracked, store on brick.
Rendering bricks: rounded rect radius 4, gradient fill (lighter top), subtle inner highlight line, border. Tough: steel gradient with two "rivets" (small circles) corners. Cracked: draw cracks in dark color over.
Powerup bricks: draw glowing icon in center: wide icon "⇔" drawn as arrows; multi as 3 dots. Plus pulsing glow (use time).
Falling powerups: {x,y,vy,type,w,h}, vy ~ 120 px/s, rotate slightly? Keep capsule horizontal with icon.
Paddle: base width 110 (wide: 165). Height 14, y=600. Rounded, gradient, glow. Speed for keyboard: 600 px/s. Keyboard & mouse: mouse sets target x directly when moved (track lastInput). Use: if mouse moved recently, paddle follows mouse; arrows override. Implementation: paddle.x controlled by either; keep a flag `control` set by events: on mousemove → mouse mode; on arrow keydown → key mode.
Clamp paddle within [wall+r, W-...].
Ball: {x,y,vx,vy,r=7, speed}. Speed base 380, +2 per brick broken, cap 560. Multi-ball spawn speed same magnitude.
Angle clamp: ensure |vy| >= 0.35*speed after paddle/brick bounces to avoid near-horizontal loops. After brick side bounce vx flips so fine; after paddle: min upward angle component: ensure vy <= -0.28*speed? Paddle edge maxAngle 60° from vertical → cos60=0.5 so vy min = 0.5*speed. Good. Also wall bounces keep vy. Add slight random on top-wall bounce? Not needed.
Ball-brick collision per substep:
```js
for each alive brick (iterate; maybe narrow by grid? fine linear, 84 bricks * substeps ok)
closest point cx,cy clamp
dx,dy; if d2 < r² →
penetration along x = r - |dx| when ball center within...
```
Standard approach: compute overlapX/overlapY:
```js
const ox = (b.w/2 + r) - Math.abs(ball.x - bcx);
const oy = (b.h/2 + r) - Math.abs(ball.y - bcy);
if (ox>0 && oy>0) { collide; if (ox < oy) push x & flip vx else push y & flip vy; }
```
This is AABB-inflated method — robust. Handle only nearest brick if multiple: choose min penetration. Simplify: on first colliding brick in substep, resolve and break. With 4px substeps, fine.
Hit brick: hp--; if hp>0 → cracked state (score+25, small particles, crack). If hp<=0 → alive=false, particles burst, score+= value, maybe drop powerup if type carries, ball speed slight up, shake tiny.
Ball-paddle:
```js
if (ball.vy>0 && ball.y + r >= paddle.y && ball.y - r <= paddle.y + paddle.h && ball.x >= paddle.x - r && ball.x <= paddle.x + pw + r) {
// only if ball center above paddle bottom
const rel = (ball.x - (paddle.x + pw/2)) / (pw/2); // -1..1
const ang = rel * MAX_ANGLE (60° = 1.047 rad)
const sp = ball.speed;
ball.vx = sp*Math.sin(ang); ball.vy = -sp*Math.cos(ang);
ball.y = paddle.y - r;
}
```
MAX_ANGLE maybe 65° → cos65 ≈ 0.42, ok use 60°.
Guard: only when ball.y - r <= paddle.y + paddle.h and ball center y < paddle.y + paddle.h? If ball passed below top edge within substep, snapping back to top is fine since substep small. Condition ball.y + r >= paddle.y catches crossing. Because substeps are 4px, ball can't jump past whole paddle (h=14) in one step... 4 < 14 ok. But also require ball.y < paddle.y + paddle.h + something? If ball is below paddle (missed), it shouldn't bounce back up. With 4px substeps and check `ball.vy > 0 && ball.y + r >= paddle.y && ball.y - ball.y... ` — if ball center already below paddle top by more than h (i.e., ball.y - r > paddle.y + paddle.h), skip. Add condition `ball.y - r < paddle.y + paddle.h`. Then even edge case: ball hits paddle side — treated as top bounce; acceptable and prevents pass-through.
Life lost: when balls array empty → lives--; if lives>0: reset one ball on paddle (serve after short delay, ball follows paddle until click/space or auto-launch after 1s?). Simplify: ball launches automatically after 0.8s from paddle center upward with slight random angle. Or serve attached: ball sits on paddle, launches after 1s or on click/space. In demo, AI just waits. I'll do: after life loss, state 'serve' with timer 1.0s, ball glued to paddle, then auto-launch. Show "Ball ready" small text. Keep it simple: auto-launch.
Game over → overlay screen with final score, "Click or press R to restart". Victory similarly.
Demo mode: `demo=true` initially. AI:
```js
const target = balls.length? lowestBall.x : W/2;
paddle moves toward target at aiSpeed (520 px/s) with small error: err = Math.sin(t*3)*18
```
Also demo collects powerups naturally. When demo loses all lives or wins → show corresponding screen briefly then reset demo automatically after ~3s. Also overlay "Click or press a key to start" pulsing at top center during demo (not blocking view). On any click/keydown → startGame() (demo=false, reset). During demo, also hide? Show semi-transparent.
Restart after game over for real player: click or R key restarts.
Particles: {x,y,vx,vy,life,maxLife,color,size,rot,vr,shape}. Draw as small rounded rects with alpha fade. Gravity 500 px/s², slight drag.
Score popups: {x,y,text,life} floating up.
Powerup timers HUD: active effects list with remaining time bars, drawn at top center.
Wide paddle implementation: paddle.w target = WIDE_W when active; animate width smoothly toward target (nice polish). timer wideTimer 10s.
Multi-ball: on catch, current balls: take first ball position, spawn 2 clones with angles ±25° rotated velocity, cap total 3 (if already 3, add score bonus instead? Just spawn to reach 3). Requirement: gives 3 balls — implement: ensure balls count becomes 3 (spawn clones of a random existing ball with spread). If balls already 3, +500 score and small message.
Ball loss handled when all gone.
Edge: when life lost while multi balls — only when all balls fall.
Screen shake: shake magnitude variable, decay; on tough brick break shake 3, life lost 6.
Sound: WebAudio.
```js
let actx=null; function beep(freq,dur,type,vol){ if(!actx) return; ... }
```
init on first user gesture (click/keydown). In demo before gesture, no sound — fine. Resume in gesture handler.
Now HUD design: dark strip, "SCORE" label small + value large, right: lives as small circles, center: level name "NEON BREAKER" or powerup indicators. Also high score? Not persistent (localStorage banned) — skip.
Background: dark with subtle radial glow at top center, faint grid lines? Draw once? Just draw each frame: fill #0a0d14, radial gradient, then faint diagonal pattern maybe skip. Add subtle starfield dots static (precomputed array) for depth.
Ball trail: store positions each frame (push {x,y}), cap 10, draw circles decreasing alpha/size.
Rendering order: bg, walls glow, bricks, powerups falling, particles, paddle, balls+trails, popups, HUD, overlays.
Text overlays: game over / victory screens: dim overlay + big text + subtext + "Press R / click to restart".
Start overlay (demo): "DEMO — Click or press a key to start" pill at top center below HUD, pulsing.
Let me also handle visibility of everything in first 3 seconds: demo mode playing immediately with ball bouncing — yes.
Fixed timestep: use accumulator with dt=1/120 steps? Simpler: rAF with dt clamp (dt = min(0.033, real)), substeps within movement. Let's do: per frame, steps = Math.ceil(maxDisp / 4) where maxDisp = ball.speed*dt; for each step, move each ball by v*dt/steps and collide. Deterministic enough.
Actually iterate: for each substep: update all balls one substep (move+collide), update paddle? Paddle can move per frame before substeps (paddle collision uses substep). Powerups update per frame. Fine.
Keyboard handling: keydown/keyup set flags; paddle speed in key mode: 640 px/s. Prevent default for arrows to avoid scrolling (page won't scroll anyway).
Mouse: mousemove on canvas → compute x in canvas coords, set paddle target; use direct set (paddle.x = mx - pw/2 clamped). Direct is fine, feels responsive.
Also touch support? Add pointermove covering touch — use pointer events for both. I'll use pointermove on canvas.
Canvas DPR:
```js
const dpr = Math.min(2, window.devicePixelRatio||1);
canvas.width = W*dpr; canvas.height = H*dpr; ctx.setTransform(dpr,0,0,dpr,0,0);
CSS: width:100%; aspect handled.
```
Wrap canvas in a container with max-width 960, centered.
Page layout: body dark, centered column, title above canvas? Keep everything in canvas mostly; page: small header with title "NEON BREAKER" and hint text below canvas about controls. But requirement says the demo compare — make page itself polished: subtle bg, title, canvas with rounded corners + shadow, footer hint line.
Responsive: canvas CSS width min(960px, 100vw - 24px). Height auto via aspect-ratio: 960/640.
Overlay screens drawn inside canvas — fine.
Let me write it.
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Neon Breaker</title>
<style>
:root { color-scheme: dark; }
* { margin:0; padding:0; box-sizing:border-box; }
html,body { height:100%; }
body {
background: radial-gradient(1200px 700px at 50% -10%, #141a2b 0%, #090b11 55%, #05060a 100%);
color:#dfe6f3;
font-family: "Segoe UI", system-ui, -apple-system, sans-serif;
display:flex; flex-direction:column; align-items:center; justify-content:center;
min-height:100vh; padding:16px; gap:12px;
overflow:hidden; /* avoid scrollbars */
}
header { text-align:center; }
h1 { font-size: clamp(18px, 3vw, 26px); letter-spacing: .35em; font-weight:700; color:#eaf0ff; }
h1 span { color:#4ecdc4; }
...
```
Hmm "overflow:hidden" — must adapt down to 375px; page should fit. At 375 wide, canvas is ~351 wide, height ~234. Header + canvas + footer fits 800 height fine.
Let me write full JS now mentally then produce final.
Key constants:
```js
const W=960,H=640, HUD=54, TOP_WALL=HUD+?
```
Top wall for ball: y where bounce happens = HUD + 2? Let's say ball bounces at y = HUD + 10? Actually let bricks start at BRICK_TOP=80. Top boundary for ball = HUD (54). Draw a line at y=HUD separating HUD.
Paddle y = H-44 = 596, h=14. Bottom kill at y > H + 20.
Ball r = 7.
Colors:
```js
const ROW_COLORS = ["#ff5e7a","#ff9f45","#ffd166","#7bdc8f","#4ecdc4","#5aa9e6","#a78bfa"];
```
Tough color base: "#8b9bb8" with darker "#6a7891"; cracked: darker + cracks "#2a3040" strokes.
Hmm rows: my level has tough at rows 0,2(t mixed),4. Colors per row for normals; tough use steel regardless of row. Good.
Score values: normal 50, tough 120 (final), crack 25.
Now write AI:
```js
function demoAI(dt){
// pick ball with greatest y (lowest on screen) among those moving down; else nearest
let target = W/2;
if(balls.length){
let best=null;
for(const b of balls){ if(!best || b.y>best.y) best=b; }
target = best.x;
} else if(state==='serve'){ target = W/2 + Math.sin(t*0.5)*120; }
// add wobble
const err = Math.sin(time*2.2)*14 + Math.sin(time*5.7)*8;
target += err;
const pw = paddleW();
const desired = clamp(target - pw/2, 0, W-pw);
const maxStep = 560*dt;
paddle.x += clamp(desired - paddle.x, -maxStep, maxStep);
}
```
Also AI avoids... fine.
Serve state: after life loss: balls=[newBall glued], serveTimer. During serve, ball follows paddle. When timer done → launch. During demo also fine.
Implement states: 'play' (covers demo and real), 'serve' sub-flag, 'over', 'win'. Actually keep `state` in {'serve','play','over','win'} and `demo` bool separately. On 'over'/'win' in demo → schedule reset after 2.6s. In real → wait for click/R.
startGame(): demo=false; score=0; lives=3; initLevel(); resetServe(); state='serve'.
initLevel builds bricks.
resetServe(): balls=[makeBall at paddle center, glued], serveT=1.0, effects cleared? On life loss, clear powerups falling? Keep falling ones; clear wide effect? I'll clear wide on life loss (fair). Multi irrelevant.
Launch: angle random ±20° from vertical: vx = sp*sin(a), a = (Math.random()*0.6-0.3).
Ball update loop per frame:
```js
let steps = 1;
const maxSpeed = balls.reduce max |v|
steps = Math.max(1, Math.ceil((maxSpeed*dt)/4));
const sub = dt/steps;
for(let s=0;s<steps;s++){
for balls: move & collide walls,paddle,bricks
remove dead balls after loop (splice within)
}
```
Careful removing during iteration — collect after.
Collide order per substep for a ball: walls (x, top), paddle, bricks. Bottom check after.
Brick collision: loop bricks; find best (max penetration? choose the one with min penetration value to resolve properly). Simpler: first with overlap. Because substep tiny, fine.
Wall collide:
```js
if(b.x < r){ b.x=r; b.vx=Math.abs(b.vx); sound }
if(b.x > W-r){...}
if(b.y < HUD + r){ b.y=HUD+r; b.vy=Math.abs(b.vy); }
```
Bottom: if b.y > H + 30 → dead. Also small "fall" effect: draw ball fading below? Just remove; spawn splash particles at bottom when crossing H? Add particles at (x, H) with upward velocities — nice.
Particles update in main frame loop (not substeps).
Brick break:
```js
function hitBrick(brick, ball){
brick.hp--;
if(brick.hp>0){ score+=25; brick.cracked=true; genCracks(brick); small particles(4); sound mid; shake=2 }
else { brick.alive=false; score += brick.maxHp>1?120:50; burst particles; popup; if(brick.power) spawnPower(brick); speedUp(); shake += brick.maxHp>1?3:1.5; sound high }
remaining check → victory
}
```
Victory when no alive bricks → state='win', sound fanfare.
Powerup falling: {x,y,vy:130,type}. Catch: overlap with paddle rect (expanded). Apply:
- wide: wideT = 10; score+=150; popup "WIDE PADDLE".
- multi: score+=150; doMulti(); popup "MULTI-BALL".
Miss: falls below H, remove.
doMulti():
```js
const cur = balls.slice();
while(balls.length<3){
const src = cur[balls.length % cur.length] ...
}
```
Simpler: take base = balls[0]; for i in 0..(3-len): clone base with velocity rotated ±20°, offset slightly. If balls empty (can't be — powerup catch requires ball alive... actually ball could be lost same frame; guard: if balls.length===0 skip multi). Rotation: rotate v by ±0.35 rad alternating.
Clone: {x:base.x, y:base.y - 4, vx rotated, vy, speed}.
Wide paddle width: BASE_PW=112, WIDE_PW=170; animate: paddle.wCur += (target - wCur)*min(1, dt*10).
Paddle draw with glow when wide active (cyan aura) + timer arc? HUD shows timer bar.
HUD content:
- Left: "SCORE" tiny + number (tabular).
- Center-left: "LEVEL 01" maybe. Center: active power pills: "WIDE 7.2s" with bar; "MULTI" indicator.
- Right: lives as 3 dots (filled per remaining life).
Draw HUD: baseline y=54 line with subtle gradient; background slightly different (#0d1120cc).
Popups: text floats.
Overlay draw:
- demo: pill at top-center y~90: rounded rect semi transparent black, text "CLICK OR PRESS A KEY TO START" pulsing alpha.
- over: full dim rgba(4,6,10,.78) + "GAME OVER" big red, "FINAL SCORE 1230", "Click or press R to restart". In demo: "Restarting demo…"
- win: "YOU WIN!" / "LEVEL CLEARED" cyan/gold + stats + restart text.
Input handlers:
```js
function userGesture(){
ensureAudio();
if(demo){ startGame(); return; }
if(state==='over'||state==='win'){ startGame(); }
}
canvas.addEventListener('pointerdown', e=>{ userGesture(); });
window.addEventListener('keydown', e=>{
if(e.key==='ArrowLeft'){ keyL=true; mode='key'; e.preventDefault(); }
...
if(demo && !e.repeat){ startGame(); }
else if((state==='over'||state==='win') && (e.key==='r'||e.key==='R'||e.key==='Enter'||e.key===' ')){ startGame(); }
});
```
Careful: in demo, any key starts. Also arrows set flags — fine.
Pointer move:
```js
canvas.addEventListener('pointermove', e=>{
const rect=canvas.getBoundingClientRect();
const mx = (e.clientX-rect.left)*(W/rect.width);
mode='mouse'; mouseX=mx;
});
```
Paddle update (real play): if mode==='mouse': paddle.x = clamp(mouseX - pw/2,...); smoothing: lerp for nicer feel: paddle.x += (target - paddle.x)*min(1,dt*20). Hmm direct is more precise; use strong lerp 25. If key mode: move by keys speed. Also add mouse only updates when moved — using stored mouseX each frame in mouse mode is fine.
Serve: ball on paddle center: ball.x = paddle.x + pw/2, y = paddle.y - r - 1. Then when serveT<=0 → launch. Also allow early launch on click/space (only in real mode)? Auto-launch after 0.9s anyway; skip early launch to keep clicks for restarts... click during serve in real play: could launch immediately — but pointerdown also triggers userGesture which is fine (demo already exited). Add: if state==='serve' && !demo → launch immediately on pointerdown. Nice.
Screen shake: shakeX/Y random each frame scaled by shake; shake decays *= exp(-8*dt) or shake=max(0,shake-dt*20).
Starfield: 70 dots with random x,y (below HUD), radius, alpha, twinkle via sin(time*speed+phase).
Also decorative bottom "kill line": subtle red glow gradient near bottom? Small.
Ball glow: radial gradient circle.
Now particles count: burst 14 for normal, 22 for tough; velocities radial 60–260; life .5–.9s.
Popups: {x,y,txt,life:1,color} y -= 30*dt; alpha = life.
Cracks generation:
```js
function genCracks(b){
const rnd = mulberry(b.seed);
b.cracks=[];
const n=3+ (rnd()*2|0);
for(i..n){
let px = b.x + b.w*(0.2+rnd()*0.6), py = b.y + b.h*(0.2+rnd()*0.6);
const pts=[[px,py]];
const dir = rnd()*Math.PI*2; let a=dir; let len = 6+rnd()*8;
for seg 2..3: a += (rnd()-0.5)*1.4; px+=Math.cos(a)*len*(...); clamp inside; pts.push
b.cracks.push(pts);
}
}
```
Draw strokes dark rgba(10,12,18,.85), width 1.5.
Tough brick visual: rounded rect with vertical gradient (#93a4c0 → #64748f), top highlight line, 4 rivets? small circles at corners inset 4. When cracked: overlay rgba(0,0,0,.25) + cracks.
Powerup icon on brick: circle glow behind icon; wide icon: draw line with arrowheads both ends (chevrons); multi icon: 3 dots triangle. Color: wide #4ecdc4, multi #ff9f45.
Falling capsule: rounded rect w=34 h=18, bg dark #101726 with colored border glow, icon inside. Slight bob rotation: rotate = sin(y*0.05)*0.15.
Sounds (WebAudio):
```js
function ensureAudio(){ if(!actx){ try{ actx = new (window.AudioContext||window.webkitAudioContext)(); }catch(e){} } if(actx&&actx.state==='suspended') actx.resume(); }
function tone(freq, dur=0.08, type='square', vol=0.15, slide=0){
if(!actx||actx.state!=='running') return;
const t=actx.currentTime;
const o=actx.createOscillator(), g=actx.createGain();
o.type=type; o.frequency.setValueAtTime(freq,t);
if(slide) o.frequency.exponentialRampToValueAtTime(Math.max(30,freq+slide), t+dur);
g.gain.setValueAtTime(vol,t); g.gain.exponentialRampToValueAtTime(0.001,t+dur);
o.connect(g); g.connect(actx.destination); o.start(t); o.stop(t+dur+0.02);
}
```
paddle: tone(220,0.07,'triangle',.2); wall: 180; brick: 300 + row*40; tough crack: 200 square; break: 520; powerup catch: two quick tones (660 then 880 setTimeout); life lost: slide down tone(300,.4,'sawtooth',.2,-240); win: arpeggio.
Demo mode: sounds only if audio running — in demo before gesture, actx null so silent. Good.
Loop:
```js
let last=performance.now();
function frame(now){
let dt=Math.min(0.033,(now-last)/1000); last=now; time+=dt;
update(dt); draw();
requestAnimationFrame(frame);
}
```
update(dt):
- if state 'play' or 'serve':
- paddle control (demo AI or human)
- if serve: ball glued, serveT-=dt, on 0 → launch, state='play'
- if play: substep balls
- if balls empty after: loseLife()
- powerups update & catch
- wide timer
- particles, popups, shake, trail always update
- if demo and (state over/win) demoResetT-=dt → resetLevel keep demo (score reset, lives 3)
loseLife():
```js
lives--;
shake=7; sound;
wideT=0;
if(lives<=0){ state='over'; overT=0; if(demo) demoResetT=3.0; }
else { state='serve'; serveT=1.0; balls=[glued ball]; }
```
Win: state='win'; if demo demoResetT=3.5.
Draw lives icons etc.
Let me also ensure "ball never passes through paddle" — substeps handle it. Also paddle keyboard speed 640*dt could be ~21px per frame moving into ball horizontally... Ball vertical bounce handles. Paddle moving into ball horizontally could overlap but ball reflected upward anyway on next check since condition uses vy>0. If ball moving up and paddle moves into it, ball passes over — acceptable (standard).
One more: bricks vs paddle region fine.
Canvas coordinate for mouse when CSS scaled — handled via rect ratio.
Now HUD & overlays fonts: use `font = '700 20px system-ui, "Segoe UI", sans-serif'`.
Tabular numbers: use font-variant? canvas — just monospace for score: '"Courier New"'? Use font "Consolas, Menlo, monospace" for score digits? Score with system-ui is fine, jitter minor. I'll use monospace stack for score to avoid jitter.
Victory stats: show score and time? Track elapsed play time. Show "TIME 01:23". Nice. timePlay accumulates when state play/serve and !demo? Simpler: track from startGame.
Also show "BEST" not persistent — skip.
Let me write final code. I'll be thorough but compact. Also include particles cap (e.g., 400).
Draw bricks each frame with glow for power ones (pulsing shadowBlur). shadowBlur is expensive; 6 power bricks fine.
Ball draw: shadowBlur 12 cyan-white.
Paddle: gradient #e8f0ff→#9db4e8? Let's do sleek: body gradient from #f5f8ff to #b9c8ea with rounded ends, glow shadow subtle; when wide: cyan border pulse.
Let me write everything.
Also note: the request "wider paddle (10 seconds)" — show countdown.
Bottom danger zone: draw subtle red gradient line at very bottom? Actually the bottom is open (ball falls). Draw thin red glow line at y=H-2? Might confuse (like a wall). Skip; instead when ball lost, particles splash.
Edge case: ball hitting brick from below when glued? not moving; fine.
Speed-up on each break: ball.speed = min(600, speed*1.012)? Use +6 per brick capped 580. All balls share? Each ball has own speed; new clones copy. On speedup, only the ball that broke it. Fine.
Also ensure min |vy| after brick top/bottom bounce? Side bounce flips vx, vy unchanged — could result in vy tiny if launched near horizontal? Launch angle within ±20° of vertical → vy large. Paddle bounce: vy ≥ cos60=0.5 speed. Top wall flip keeps magnitude. Corner reflections keep. So |vy| ≥ 0.5 speed mostly... Actually brick bounce doesn't change magnitude of components. OK, no infinite horizontal loops. Good. But add safety: in wall side bounce, if |vy| < 40, nudge vy sign*40. Cheap insurance.
Popups for powerup catch display at paddle.
Let me now count bricks: rows: r0: 8 tough; r1: 10 (8n+1w+1m); r2: 12 (4t? "ttnnttnnttnn": t at 0,1,4,5,8,9 → 6t,6n); r3: 10 (n×6? ".nmnnwnnnmn." → n at 1,3,4,6,7,8,10 =7n, m×2, w×1); r4: 12 (2n+10t); r5: 10 (8n+m+w); r6: 8n. Total 70 bricks. Tough: 8+6+10=24. Power: 3w, 3m.
Victory check after each break: `if(!bricks.some(b=>b.alive))` win.
Score: max ~ 46*50 + 24*120 + cracks 24*25 + powerups 6*150 ≈ 2300+2880+600+900 = 6680.
Draw HUD "SCORE 000000"? pad 5.
Now, one subtlety: pointermove during demo shouldn't move paddle (AI controls). Fine — human control only when !demo.
Key handling 'r' during play — ignore. Also 'ArrowLeft' etc during demo triggers startGame then also sets flag — good, paddle ready.
Prevent space scroll: preventDefault for arrows & space.
Write the HTML now. Also `<canvas>` fallback text inside.
Let me write draw functions carefully.
roundRect helper: use ctx.roundRect if available else manual. Chrome has it — but write manual helper for safety:
```js
function rr(x,y,w,h,r){ ctx.beginPath(); ctx.moveTo(x+r,y); ... }
```
Just implement manual.
Now brick drawing:
```js
function drawBrick(b){
const x=b.x,y=b.y,w=b.w,h=b.h;
// body
let g;
if(b.maxHp>1){ g=ctx.createLinearGradient(0,y,0,y+h); g.addColorStop(0,'#a7b7d4'); g.addColorStop(.45,'#7c8bab'); g.addColorStop(1,'#5a6a86'); }
else { const c=b.color; g = vertical from lighten to darken }
```
For normal brick, compute gradient: I'll store per-row base color and use two derived shades: top lighter (mix with white 30%), bottom darker. Implement simple: use color with globalAlpha overlays: fill base, then fill top half rgba(255,255,255,.18), bottom rgba(0,0,0,.22) via gradient. Easier: create gradient with same color stops can't lighten easily without color math. Write helper shade(hex, amt) that lightens/darkens.
```js
function shade(hex,amt){ // amt -1..1
const n=parseInt(hex.slice(1),16);
let r=(n>>16)&255,g=(n>>8)&255,b=n&255;
if(amt>=0){ r+= (255-r)*amt; ... } else { r*=(1+amt); }
return `rgb(${r|0},${g|0},${b|0})`;
}
```
Brick draw:
```js
grad = ctx.createLinearGradient(0,y,0,y+h);
grad.addColorStop(0, shade(base,.35));
grad.addColorStop(.5, base);
grad.addColorStop(1, shade(base,-.3));
rr(x+.5,y+.5,w-1,h-1,4); ctx.fillStyle=grad; ctx.fill();
ctx.strokeStyle='rgba(255,255,255,.12)'; stroke;
// top gloss line
ctx.fillStyle='rgba(255,255,255,.25)'; ctx.fillRect(x+3,y+2,w-6,2) rounded-ish fine.
```
Power brick: pulsing glow:
```js
const pulse = .5+.5*Math.sin(time*4 + b.col);
ctx.shadowColor = puColor; ctx.shadowBlur = 8+6*pulse; // on fill
then draw icon.
```
Tough rivets: 4 small circles rgba(255,255,255,.35) at corners inset 5.
Cracked overlay: rgba(15,18,26,.35) fill over; cracks strokes rgba(8,10,14,.9) width 1.4; plus a highlight offset? Fine.
Icon draw functions:
```js
function drawWideIcon(cx,cy,s,color){
ctx.strokeStyle=color; ctx.lineWidth=2; ctx.lineCap='round';
line (cx-s,cy)-(cx+s,cy);
chevrons: left arrowhead at (cx-s,cy): lines to (cx-s+4,cy-3) and (cx-s+4,cy+3); right similarly mirrored.
}
function drawMultiIcon(cx,cy,s,color){
three arcs radius s*0.28 at (cx,cy-s*0.5),(cx-s*0.55,cy+s*0.4),(cx+s*0.55,cy+s*0.4) fill color.
}
```
Falling capsule draw: rounded rect w=36,h=20 centered; bg gradient dark; border color = pu color, lineWidth 2, glow; icon inside.
Paddle draw:
```js
const pw=paddle.wCur, ph=14, x=paddle.x, y=paddle.y;
glow: if wideT>0: shadowColor '#4ecdc4', blur 14+ pulse
grad horizontal: '#dfe9ff' → '#8fb0ff'? Let's do body: rounded rr r=7.
gradient vertical: 0:'#ffffff',0.35:'#cfe0ff'... hmm want visible on dark: light paddle with blue tint.
center notch marker: small darker rounded rect center.
```
Paddle y=594? H=640, paddle.y = 600, h=14 → bottom 614, 26px below. ok.
Ball: white-cyan with radial gradient + trail.
HUD:
```js
ctx.fillStyle='#0b0f1a'; fillRect(0,0,W,HUD);
bottom border: gradient line y=HUD-1..HUD: fill '#2b3answer'
score left at 16, lives right, powerup pills center.
```
Also small level label.
Overlays: dim rect over play area only (below HUD) or full? full canvas fine.
Text drawing helper: function text(str,x,y,size,color,align,weight).
Demo overlay pill: draw at y=86 center:
"▶ DEMO — click or press any key to start". Avoid emoji; triangle char ok? Use "▸"? Just text "DEMO · CLICK OR PRESS ANY KEY TO START" in a pill.
Actually iframe note: keyboard events need click first. Message says click or press a key — fine.
Restart text: "Press R or click to restart".
Let me handle the demo auto-restart function:
```js
function resetDemo(){ demo=true; score=0; lives=3; timePlay=0; initLevel(); resetServe(); }
```
And in update: if(demo && (state==='over'||state==='win')){ demoT-=dt; if(demoT<=0) resetDemo(); }
For real game over: userGesture handles restart (any click/keydown? requirement "restart option" — accept click or R). In userGesture, if !demo && (over/win) → startGame. But careful: pointerdown on canvas during 'over' triggers restart — good.
Also allow clicking a drawn "button"? Text suffices.
Time to write. I'll aim for clean, complete code. Let me draft the full file.
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width,initial-scale=1"/>
<title>NEON BREAKER — Breakout</title>
<style>
*{margin:0;padding:0;box-sizing:border-box}
html,body{height:100%}
body{
background:
radial-gradient(900px 480px at 50% -8%, #1a2340 0%, rgba(26,35,64,0) 60%),
#07090f;
color:#dfe6f3;
font-family:"Segoe UI",system-ui,-apple-system,"Helvetica Neue",sans-serif;
display:flex;flex-direction:column;align-items:center;justify-content:center;
gap:10px;min-height:100vh;padding:14px;
}
header{display:flex;align-items:baseline;gap:12px;user-select:none}
h1{font-size:clamp(15px,2.6vw,22px);letter-spacing:.42em;font-weight:800;color:#eef3ff;text-transform:uppercase}
h1 b{color:#4ecdc4;font-weight:800}
.tag{font-size:11px;letter-spacing:.18em;color:#5b6784;text-transform:uppercase}
#wrap{width:min(960px,100%);}
canvas{
display:block;width:100%;aspect-ratio:960/640;border-radius:14px;
background:#0a0d15;
box-shadow:0 0 0 1px #1c2438, 0 30px 70px -20px rgba(0,0,0,.85), 0 0 90px -30px rgba(78,205,196,.25);
cursor:crosshair; touch-action:none;
}
footer{font-size:11.5px;color:#5b6784;letter-spacing:.06em;text-align:center;line-height:1.7}
footer b{color:#9fb0d0;font-weight:600}
</style>
</head>
<body>
<header><h1>Neon<b>Breaker</b></h1><span class="tag">Level 01</span></header>
<div id="wrap"><canvas id="c" width="960" height="640">Canvas not supported</canvas></div>
<footer><b>Mouse</b> or <b>← →</b> to move · catch <b>capsules</b>: <span style="color:#4ecdc4">wide paddle 10s</span>, <span style="color:#ff9f45">multi-ball ×3</span> · <b>R</b> restarts</footer>
<script>
...
</script>
</body></html>
```
DPR: I'll set width/height attrs in JS with dpr and keep CSS aspect-ratio. Since CSS sets width 100% & aspect-ratio, internal size can be W*dpr.
```js
const W=960,H=640,dpr=Math.min(2,devicePixelRatio||1);
cvs.width=W*dpr;cvs.height=H*dpr;ctx.setTransform(dpr,0,0,dpr,0,0);
```
Now the big script. Let me write it fully, carefully.
```js
'use strict';
const cvs=document.getElementById('c');
const ctx=cvs.getContext('2d');
const W=960,H=640,HUD=54;
const dpr=Math.min(2,window.devicePixelRatio||1);
cvs.width=W*dpr;cvs.height=H*dpr;ctx.setTransform(dpr,0,0,dpr,0,0);
const clamp=(v,a,b)=>v<a?a:v>b?b:v;
const rand=(a,b)=>a+Math.random()*(b-a);
function shade(hex,amt){...}
function rr(x,y,w,h,r){...path}
// ---------- level ----------
const COLS=12, GAP=6, B_TOP=82, B_H=24;
const B_W=(W-GAP*(COLS+1))/COLS;
const ROW_COLORS=["#ff5e7a","#ff9f45","#ffd166","#7bdc8f","#4ecdc4","#5aa9e6","#a78bfa"];
const LEVEL=[
"..tttttttt..",
".nnwnnnnmnn.",
"ttnnttnnttnn",
".nmnnwnnnmn.",
"nnttttttttnn",
".nnnnmwnnnn.",
"..nnnnnnnn..",
];
let bricks=[],balls=[],parts=[],pops=[],drops=[];
let paddle={x:W/2-56,y:H-46,h:14,wCur:112};
let score=0,lives=3,state='serve',demo=true,serveT=1,time=0,playT=0;
let wideT=0,shake=0,demoT=0,keyL=false,keyR=false,mode='mouse',mouseX=W/2;
```
Wait paddle.y=H-46=594.
Power colors: `const PU={wide:{color:'#4ecdc4'},multi:{color:'#ff9f45'}};`
Brick creation:
```js
function initLevel(){
bricks=[];
LEVEL.forEach((row,r)=>{
for(let col=0;col<COLS;col++){
const ch=row[col];
if(ch==='.')continue;
const tough=ch==='t';
const power=ch==='w'?'wide':ch==='m'?'multi':null;
bricks.push({x:GAP+col*(B_W+GAP),y:B_TOP+r*(B_H+GAP),w:B_W,h:B_H,
hp:tough?2:1,maxHp:tough?2:1,alive:true,power,color:ROW_COLORS[r],row:r,
seed:(r*31+col*7+3)|0,cracks:null});
}
});
}
```
Ball:
```js
function newBall(x,y,vx,vy,sp){return {x,y,vx,vy,r:7,speed:sp,trail:[]};}
function launchBall(b){
const a=rand(-0.35,0.35);
b.vx=Math.sin(a)*b.speed;b.vy=-Math.cos(a)*b.speed;
}
```
resetServe:
```js
function resetServe(){
state='serve';serveT=0.9;
balls=[newBall(paddle.x+paddle.wCur/2,paddle.y-8,0,0,baseSpeed())];
}
function baseSpeed(){return 380;}
```
startGame/resetDemo:
```js
function resetGame(isDemo){
demo=isDemo;score=0;lives=3;playT=0;wideT=0;drops=[];parts=[];pops=[];
initLevel();paddle.x=W/2-paddle.wCur/2;resetServe();
}
```
update:
```js
function update(dt){
time+=dt;
if(state==='play'||state==='serve')playT+=dt;
// paddle
if(demo) demoAI(dt); else humanPaddle(dt);
// wide width animation
const target=wideT>0?170:112;
paddle.wCur+=(target-paddle.wCur)*Math.min(1,dt*8);
if(wideT>0)wideT-=dt;
if(state==='serve'){
const b=balls[0];
b.x=paddle.x+paddle.wCur/2;b.y=paddle.y-b.r-2;
serveT-=dt;
if(serveT<=0){launchBall(b);state='play';sfx.paddle();}
} else if(state==='play'){
// substep
let maxV=0;for(const b of balls)maxV=Math.max(maxV,Math.abs(b.vx),Math.abs(b.vy));
const steps=Math.max(1,Math.ceil(maxV*dt/4));
const sdt=dt/steps;
for(let s=0;s<steps;s++){
for(const b of balls){ if(!b.dead) stepBall(b,sdt); }
// remove dead
for(let i=balls.length-1;i>=0;i--) if(balls[i].dead) balls.splice(i,1);
if(!balls.length)break;
}
if(!balls.length) loseLife();
// drops
for(let i=drops.length-1;i>=0;i--){
const d=drops[i];d.y+=d.vy*dt;d.vy=Math.min(240,d.vy+60*dt);
// catch
if(d.y+9>=paddle.y&&d.y-9<=paddle.y+paddle.h&&d.x>paddle.x-10&&d.x<paddle.x+paddle.wCur+10){
applyPU(d);drops.splice(i,1);continue;
}
if(d.y>H+30)drops.splice(i,1);
}
}
// particles
for(let i=parts.length-1;i>=0;i--){const p=parts[i];p.life-=dt;if(p.life<=0){parts.splice(i,1);continue;}p.vy+=p.grav*dt;p.x+=p.vx*dt;p.y+=p.vy*dt;p.rot+=p.vr*dt;}
for(let i=pops.length-1;i>=0;i--){const p=pops[i];p.life-=dt*1.2;p.y-=32*dt;if(p.life<=0)pops.splice(i,1);}
shake=Math.max(0,shake-dt*14);
if(demo&&(state==='over'||state==='win')){demoT-=dt;if(demoT<=0)resetGame(true);}
}
```
Note: drops update only in 'play' — drops frozen during serve; acceptable, but better also update during serve. Move drop update outside. I'll move it to run in both play & serve. Actually catch during serve ok too. Let me restructure: run drops update whenever state play or serve.
stepBall:
```js
function stepBall(b,sdt){
b.x+=b.vx*sdt;b.y+=b.vy*sdt;
// walls
if(b.x<b.r){b.x=b.r;if(b.vx<0)b.vx=-b.vx;sfx.wall();}
else if(b.x>W-b.r){b.x=W-b.r;if(b.vx>0)b.vx=-b.vx;sfx.wall();}
if(b.y<HUD+b.r){b.y=HUD+b.r;if(b.vy<0)b.vy=-b.vy;sfx.wall();}
// safety
if(Math.abs(b.vy)<30&&state==='play'){b.vy=(b.vy<0?-1:1)*30;}
// paddle
if(b.vy>0&&b.y+b.r>=paddle.y&&b.y-b.r<=paddle.y+paddle.h&&b.x>=paddle.x-b.r&&b.x<=paddle.x+paddle.wCur+b.r){
const rel=clamp((b.x-(paddle.x+paddle.wCur/2))/(paddle.wCur/2),-1,1);
const ang=rel*(Math.PI/180*62);
const sp=b.speed;
b.vx=Math.sin(ang)*sp;b.vy=-Math.cos(ang)*sp;
b.y=paddle.y-b.r-0.5;
sfx.paddle();
sparkle small particles at contact? add tiny 3 parts.
}
// bricks
for(const br of bricks){
if(!br.alive)continue;
const cx=clamp(b.x,br.x,br.x+br.w),cy=clamp(b.y,br.y,br.y+br.h);
const dx=b.x-cx,dy=b.y-cy;
if(dx*dx+dy*dy<=b.r*b.r){
// resolve axis by center distance
const ox=(br.w/2+b.r)-Math.abs(b.x-(br.x+br.w/2));
const oy=(br.h/2+b.r)-Math.abs(b.y-(br.y+br.h/2));
if(ox<oy){ if(b.x<br.x+br.w/2){b.x-=ox;b.vx=-Math.abs(b.vx);} else {b.x+=ox;b.vx=Math.abs(b.vx);} }
else { if(b.y<br.y+br.h/2){b.y-=oy;b.vy=-Math.abs(b.vy);} else {b.y+=oy;b.vy=Math.abs(b.vy);} }
hitBrick(br,b);
break;
}
}
// lost
if(b.y-b.r>H+8){
b.dead=true;
splash(b.x);
}
}
```
Hmm wall bounce sound spam per substep — it flips so only once. OK.
One issue: brick loop with `break` after one collision per substep — good.
Also the paddle bounce condition `b.y-b.r<=paddle.y+paddle.h`: if ball center is below paddle bottom - r it skips. With 4px substeps fine.
hitBrick:
```js
function hitBrick(br,b){
br.hp--;
b.speed=Math.min(590,b.speed+5);
if(br.hp>0){
br.cracks=genCracks(br);
score+=25;shake=Math.min(10,shake+2.5);
burst(br,5,0.6);
sfx.crack();
}else{
br.alive=false;
score+=br.maxHp>1?120:50;
shake=Math.min(10,shake+(br.maxHp>1?3.2:1.6));
burst(br,br.maxHp>1?22:14,1);
pops.push({x:br.x+br.w/2,y:br.y+br.h/2,txt:'+'+(br.maxHp>1?120:50),life:1,color:br.maxHp>1?'#cbd6ee':'#ffe9a8'});
if(br.power)drops.push({x:br.x+br.w/2,y:br.y+br.h/2,vy:110,type:br.power});
sfx.brick(br.row);
if(!bricks.some(k=>k.alive)) winGame();
}
}
```
applyPU:
```js
function applyPU(d){
score+=150;sfx.power();
if(d.type==='wide'){
wideT=10;
pops.push({x:d.x,y:paddle.y-16,txt:'WIDE PADDLE',life:1.3,color:'#4ecdc4'});
}else{
pops.push({x:d.x,y:paddle.y-16,txt:'MULTI-BALL',life:1.3,color:'#ff9f45'});
doMulti();
}
}
function doMulti(){
const base=balls[0];
if(!base)return;
let n=3-balls.length;
let k=0;
while(n-->0&&balls.length<6){ // cap
const ang=(k%2?1:-1)*(0.35+0.12*Math.floor(k/2)); hmm
```
Simple: alternate rotate ±20°, ±40°:
```js
const offsets=[0.4,-0.4,0.8,-0.8];
for(let i=0;i<3-balls.length&&i<offsets.length;i++){
const c=Math.cos(offsets[i]),s=Math.sin(offsets[i]);
balls.push({x:base.x,y:base.y,r:base.r,speed:base.speed,
vx:base.vx*c-base.vy*s,vy:base.vx*s+base.vy*c,trail:[]});
}
if(balls.length>=3&¬hing added) bonus...
```
If already 3 balls: score+=300 popup 'BONUS +300'. Handle: `if(3-balls.length<=0){score+=300;pops.push(...'BONUS +300'...)}`.
loseLife:
```js
function loseLife(){
lives--;wideT=0;shake=8;sfx.lose();
if(lives<=0){state='over';demoT=3.2;}
else resetServe();
}
```
winGame: `state='win';demoT=3.5;sfx.win();`
demoAI:
```js
function demoAI(dt){
let target=W/2;
if(state==='serve'){target=W/2+Math.sin(time*0.7)*140;}
else if(balls.length){
let best=balls[0];
for(const b of balls)if(b.y>best.y)best=b;
target=best.x;
}
target+=Math.sin(time*2.3)*16+Math.sin(time*5.1)*7;
const pw=paddle.wCur;
const des=clamp(target-pw/2,4,W-8-pw);
const step=560*dt;
paddle.x+=clamp(des-paddle.x,-step,step);
}
```
humanPaddle:
```js
function humanPaddle(dt){
const pw=paddle.wCur;
if(keyL||keyR){paddle.x+=(keyR-keyL)... }
if(mode==='key'){
const sp=680*dt;
if(keyL)paddle.x-=sp;if(keyR)paddle.x+=sp;
}else{
const des=clamp(mouseX-pw/2,4,W-8-pw);
paddle.x+=(des-paddle.x)*Math.min(1,dt*22);
}
paddle.x=clamp(paddle.x,4,W-8-pw);
}
```
Particles:
```js
function burst(br,n,power){
const cols=[br.color,shade(br.color,.35),'#ffffff'];
const cx=br.x+br.w/2,cy=br.y+br.h/2;
for(let i=0;i<n&&parts.length<400;i++){
const a=rand(0,Math.PI*2),v=rand(60,260)*power;
parts.push({x:cx+rand(-br.w/3,br.w/3),y:cy+rand(-4,4),
vx:Math.cos(a)*v,vy:Math.sin(a)*v-rand(0,80),
grav:420,life:rand(0.4,0.85),max:0.85,size:rand(2,4.5),
color:cols[i%3],rot:rand(0,6),vr:rand(-8,8)});
}
}
function splash(x){
for(let i=0;i<12;i++)parts.push({x:x+rand(-6,6),y:H-2,vx:rand(-120,120),vy:rand(-260,-60),grav:500,life:rand(.3,.6),max:.6,size:rand(2,3.5),color:'#ff5e7a',rot:0,vr:rand(-6,6)});
}
```
genCracks (seeded):
```js
function genCracks(br){
let s=br.seed>>>0;
const rnd=()=>{s=(s*1664525+1013904223)>>>0;return s/4294967296;};
const cracks=[];const n=3+(rnd()*2|0);
for(let i=0;i<n;i++){
let x=br.x+br.w*(0.25+rnd()*0.5),y=br.y+br.h*(0.25+rnd()*0.5);
let a=rnd()*Math.PI*2;
const pts=[[x,y]];
const segs=2+(rnd()*2|0);
for(let j=0;j<segs;j++){
a+=(rnd()-0.5)*1.6;
const len=5+rnd()*9;
x=clamp(x+Math.cos(a)*len,br.x+3,br.x+br.w-3);
y=clamp(y+Math.sin(a)*len,br.y+3,br.y+br.h-3);
pts.push([x,y]);
}
cracks.push(pts);
}
return cracks;
}
```
Sound:
```js
let actx=null;
function ensureAudio(){
if(!actx){try{actx=new (window.AudioContext||window.webkitAudioContext)();}catch(e){}}
if(actx&&actx.state==='suspended')actx.resume();
}
function tone(f,dur,type,vol,slide){
if(!actx||actx.state!=='running')return;
try{
const t=actx.currentTime,o=actx.createOscillator(),g=actx.createGain();
o.type=type||'square';o.frequency.setValueAtTime(f,t);
if(slide)o.frequency.exponentialRampToValueAtTime(Math.max(30,f+slide),t+dur);
g.gain.setValueAtTime((vol||0.12)*0.5,t);
g.gain.exponentialRampToValueAtTime(0.0008,t+dur);
o.connect(g);g.connect(actx.destination);o.start(t);o.stop(t+dur+0.03);
}catch(e){}
}
const sfx={
wall:()=>tone(190,0.05,'triangle',0.10),
paddle:()=>tone(240,0.08,'triangle',0.16),
brick:(r)=>tone(330+r*45,0.09,'square',0.10),
crack:()=>tone(170,0.08,'square',0.12),
power:()=>{tone(660,0.09,'sine',0.16);setTimeout(()=>tone(990,0.12,'sine',0.16),90);},
lose:()=>tone(320,0.5,'sawtooth',0.14,-250),
win:()=>{[523,659,784,1046].forEach((f,i)=>setTimeout(()=>tone(f,0.18,'triangle',0.15),i*110));},
};
```
Draw:
```js
function draw(){
ctx.save();
// shake
if(shake>0)ctx.translate(rand(-shake,shake)*0.6,rand(-shake,shake)*0.6);
drawBG();
drawBricks();
drawDrops();
drawParts();
drawPaddle();
drawBalls();
drawPops();
ctx.restore(); // shake not applied to HUD
drawHUD();
drawOverlays();
}
```
Hmm shake translate then restore before HUD — good. But overlays maybe also unshaken.
drawBG:
```js
function drawBG(){
ctx.fillStyle='#0a0d15';ctx.fillRect(-12,-12,W+24,H+24);
// top glow
let g=ctx.createRadialGradient(W/2,HUD,40,W/2,HUD,520);
g.addColorStop(0,'rgba(90,169,230,0.10)');g.addColorStop(1,'rgba(0,0,0,0)');
ctx.fillStyle=g;ctx.fillRect(0,HUD,W,H-HUD);
// stars
for(const s of stars){
const tw=0.4+0.6*Math.abs(Math.sin(time*s.sp+s.ph));
ctx.globalAlpha=s.a*tw;ctx.fillStyle='#9fb4dd';
ctx.fillRect(s.x,s.y,s.r,s.r);
}
ctx.globalAlpha=1;
// faint side rails
ctx.fillStyle='rgba(78,205,196,0.05)';
ctx.fillRect(0,HUD,2,H-HUD);ctx.fillRect(W-2,HUD,2,H-HUD);
}
```
Stars precomputed: 60 with x in [8,W-8], y in [HUD+10,H-60].
drawBricks:
```js
for(const b of bricks){ if(!b.alive)continue;
const x=b.x,y=b.y,w=b.w,h=b.h;
if(b.power){
const pu=PU[b.power];const pulse=0.5+0.5*Math.sin(time*4+b.x*0.05);
ctx.shadowColor=pu.color;ctx.shadowBlur=6+7*pulse;
}
const base=b.maxHp>1?'#8fa2c4':b.color;
const g=ctx.createLinearGradient(0,y,0,y+h);
g.addColorStop(0,shade(base,0.38));
g.addColorStop(0.45,base);
g.addColorStop(1,shade(base,-0.34));
rr(x+0.5,y+0.5,w-1,h-1,5);ctx.fillStyle=g;ctx.fill();
ctx.shadowBlur=0;
ctx.strokeStyle='rgba(255,255,255,0.14)';ctx.lineWidth=1;ctx.stroke();
// gloss
ctx.fillStyle='rgba(255,255,255,0.22)';
rr(x+3,y+2,w-6,3,2);ctx.fill();
if(b.maxHp>1){
// rivets
ctx.fillStyle='rgba(255,255,255,0.30)';
for(const [rx,ry] of [[x+6,y+6],[x+w-6,y+6],[x+6,y+h-6],[x+w-6,y+h-6]]){
ctx.beginPath();ctx.arc(rx,ry,1.6,0,7);ctx.fill();
}
}
if(b.cracked){
ctx.fillStyle='rgba(8,10,16,0.30)';rr(x+0.5,y+0.5,w-1,h-1,5);ctx.fill();
ctx.strokeStyle='rgba(10,13,20,0.9)';ctx.lineWidth=1.4;ctx.lineJoin='round';
for(const c of b.cracks){
ctx.beginPath();ctx.moveTo(c[0][0],c[0][1]);
for(let i=1;i<c.length;i++)ctx.lineTo(c[i][0],c[i][1]);
ctx.stroke();
}
// chips
ctx.fillStyle='rgba(0,0,0,0.35)';
small notches? skip
}
if(b.power){
const pu=PU[b.power];const cx=x+w/2,cy=y+h/2;
ctx.globalAlpha=0.9;
if(b.power==='wide')drawWideIcon(cx,cy,8,pu.color);else drawMultiIcon(cx,cy,8,pu.color);
ctx.globalAlpha=1;
}
}
```
Icon draws under? Draw after crack overlay so visible — carriers are 1-hit so no cracks. Order fine.
Note shadowBlur with fill also strokes? set shadow before fill; reset after.
drawPaddle:
```js
function drawPaddle(){
const x=paddle.x,y=paddle.y,w=paddle.wCur,h=paddle.h;
if(wideT>0){ctx.shadowColor='#4ecdc4';ctx.shadowBlur=12+5*Math.sin(time*6);}
else {ctx.shadowColor='rgba(160,190,255,0.5)';ctx.shadowBlur=7;}
const g=ctx.createLinearGradient(0,y,0,y+h);
g.addColorStop(0,'#f4f8ff');g.addColorStop(0.5,'#c3d4f5');g.addColorStop(1,'#7d97c9');
rr(x,y,w,h,7);ctx.fillStyle=g;ctx.fill();
ctx.shadowBlur=0;
ctx.strokeStyle='rgba(255,255,255,0.35)';ctx.lineWidth=1;ctx.stroke();
// center notch
ctx.fillStyle='rgba(40,60,110,0.5)';
rr(x+w/2-5,y+3,10,h-6,3);ctx.fill();
}
```
drawBalls:
```js
for(const b of balls){
// trail
b.trail.push(b.x,b.y);
if(b.trail.length>14)b.trail.splice(0,2);
for(let i=0;i<b.trail.length;i+=2){
const a=i/b.trail.length;
ctx.globalAlpha=a*0.25;
ctx.fillStyle='#9fe8ff';
ctx.beginPath();ctx.arc(b.trail[i],b.trail[i+1],b.r*a*0.8,0,7);ctx.fill();
}
ctx.globalAlpha=1;
const g=ctx.createRadialGradient(b.x-2,b.y-2,1,b.x,b.y,b.r);
g.addColorStop(0,'#ffffff');g.addColorStop(0.6,'#c9f2ff');g.addColorStop(1,'#5ad0e6');
ctx.shadowColor='rgba(120,220,255,0.8)';ctx.shadowBlur=10;
ctx.fillStyle=g;ctx.beginPath();ctx.arc(b.x,b.y,b.r,0,7);ctx.fill();
ctx.shadowBlur=0;
}
```
Trail push per frame ok.
drawDrops:
```js
for(const d of drops){
const pu=PU[d.type];
ctx.save();
ctx.translate(d.x,d.y);ctx.rotate(Math.sin(d.y*0.04)*0.12);
ctx.shadowColor=pu.color;ctx.shadowBlur=10;
rr(-19,-11,38,22,10);ctx.fillStyle='#101828';ctx.fill();
ctx.shadowBlur=0;
ctx.strokeStyle=pu.color;ctx.lineWidth=2;ctx.stroke();
ctx.fillStyle=pu.color;
if(d.type==='wide')drawWideIcon(0,0,7,pu.color);else drawMultiIcon(0,0,7,pu.color);
ctx.restore();
}
```
drawParts:
```js
for(const p of parts){
const a=Math.max(0,p.life/p.max);
ctx.globalAlpha=a;
ctx.save();ctx.translate(p.x,p.y);ctx.rotate(p.rot);
ctx.fillStyle=p.color;
ctx.fillRect(-p.size/2,-p.size/2,p.size,p.size);
ctx.restore();
}
ctx.globalAlpha=1;
```
drawPops:
```js
for(const p of pops){
ctx.globalAlpha=clamp(p.life,0,1);
ctx.font='700 13px system-ui,Segoe UI,sans-serif';ctx.textAlign='center';
ctx.fillStyle=p.color;
ctx.shadowColor='rgba(0,0,0,0.8)';ctx.shadowBlur=4;
ctx.fillText(p.txt,p.x,p.y);
ctx.shadowBlur=0;
}
ctx.globalAlpha=1;
```
drawHUD:
```js
function drawHUD(){
const g=ctx.createLinearGradient(0,0,0,HUD);
g.addColorStop(0,'#101627');g.addColorStop(1,'#0b0f1c');
ctx.fillStyle=g;ctx.fillRect(0,0,W,HUD);
ctx.fillStyle='rgba(78,205,196,0.55)';ctx.fillRect(0,HUD-2,W,2);
ctx.fillStyle='rgba(78,205,196,0.12)';ctx.fillRect(0,HUD-6,W,4);
// score
ctx.textAlign='left';ctx.textBaseline='middle';
ctx.fillStyle='#5b6784';ctx.font='600 10px system-ui,sans-serif';ctx.letterSpacing? (canvas letterSpacing supported in Chrome)
ctx.fillText('SCORE',18,17);
ctx.fillStyle='#eef3ff';ctx.font='700 22px Consolas,Menlo,monospace';
ctx.fillText(String(score).padStart(5,'0'),18,38);
// level center-left? show active pills center
let px=W/2;
if(wideT>0){
pill('WIDE',wideT/10,'#4ecdc4', ... )
}
if(balls.length>1) pill('×'+balls.length balls '#ff9f45')
// lives right
ctx.textAlign='right';
ctx.fillStyle='#5b6784';ctx.font='600 10px system-ui,sans-serif';
ctx.fillText('LIVES',W-18,17);
for(let i=0;i<3;i++){
const cx=W-26-i*22, cy=38;
ctx.beginPath();ctx.arc(cx,cy,7,0,7);
if(i<lives){ctx.fillStyle='#ff5e7a';ctx.shadowColor='#ff5e7a';ctx.shadowBlur=8;ctx.fill();ctx.shadowBlur=0;}
else{ctx.strokeStyle='rgba(255,94,122,0.25)';ctx.lineWidth=1.5;ctx.stroke();}
}
}
```
Pills for powerups: draw centered small rounded rects with label and time bar:
```js
function drawPill(cx,label,frac,color){
const w=118,h=26,x=cx-w/2,y=HUD/2-h/2+4;
rr(x,y,w,h,8);ctx.fillStyle='rgba(255,255,255,0.05)';ctx.fill();
ctx.strokeStyle=color;ctx.lineWidth=1.5;ctx.stroke();
ctx.fillStyle=color;ctx.font='700 11px system-ui,sans-serif';ctx.textAlign='center';
ctx.fillText(label,cx,y+11);
// bar
ctx.fillStyle=color;ctx.globalAlpha=0.85;
ctx.fillRect(x+8,y+17,(w-16)*clamp(frac,0,1),3);
ctx.globalAlpha=1;
}
```
Position: wide pill at W/2-70, multi info at W/2+70? If both: wide at W/2-75, balls-count pill at W/2+75. Draw both conditionally with fixed slots: slot1 = W/2-80, slot2=W/2+80.
Multi pill: label 'BALLS ×3', frac 1 (or based on... no timer) — show without bar? Just label. Use frac=1.
drawOverlays:
```js
function drawOverlays(){
ctx.textAlign='center';ctx.textBaseline='middle';
if(demo&&state!=='over'&&state!=='win'){
const a=0.75+0.25*Math.sin(time*3);
ctx.globalAlpha=a;
const msg='CLICK OR PRESS ANY KEY TO START';
ctx.font='700 13px system-ui,sans-serif';
const w=ctx.measureText(msg).width+44;
const x=W/2-w/2,y=B_TOP+8* ... place below bricks? bricks end ~ 82+7*30=292. Place at y=... put pill between bricks and paddle: y=380? paddle at 594. Let's y=430? Actually place near top under HUD: y=HUD+14 overlapping bricks — bad. Place mid: y = 470.
rr(x,470-15,w,30,15);ctx.fillStyle='rgba(8,12,22,0.85)';ctx.fill();
ctx.strokeStyle='rgba(78,205,196,0.5)';ctx.lineWidth=1;ctx.stroke();
ctx.fillStyle='#eaf6ff';ctx.fillText(msg,W/2,470);
ctx.globalAlpha=1;
ctx.font='600 11px system-ui,sans-serif';ctx.fillStyle='#7d8db0';
ctx.fillText('AI IS PLAYING THE DEMO',W/2,470+26); hmm inside? below pill.
}
if(state==='over'||state==='win'){
ctx.fillStyle='rgba(5,8,14,0.78)';ctx.fillRect(0,0,W,H);
const win=state==='win';
ctx.fillStyle=win?'#4ecdc4':'#ff5e7a';
ctx.shadowColor=win?'#4ecdc4':'#ff5e7a';ctx.shadowBlur=30;
ctx.font='800 54px system-ui,sans-serif';
ctx.fillText(win?'LEVEL CLEARED':'GAME OVER',W/2,H/2-70);
ctx.shadowBlur=0;
ctx.fillStyle='#eef3ff';ctx.font='700 26px Consolas,Menlo,monospace';
ctx.fillText('SCORE '+String(score).padStart(5,'0'),W/2,H/2-8);
ctx.fillStyle='#9fb0d0';ctx.font='600 14px system-ui,sans-serif';
ctx.fillText('TIME '+fmt(playT)+' · BRICKS LEFT '+bricks.filter(b=>b.alive).length...
```
Hmm on win bricks left 0. Fine.
Restart text: if demo: 'RESTARTING DEMO…' else: 'CLICK OR PRESS R TO RESTART'. Pulse.
Also small hint on real serve? "GET READY" text during serve near paddle: draw "READY" above paddle fading. Nice touch:
```js
if(state==='serve'&&!demo){ text 'READY' at paddle above }
```
Add subtle.
fmt(t): mm:ss.
Input:
```js
cvs.addEventListener('pointermove',e=>{
const r=cvs.getBoundingClientRect();
mouseX=(e.clientX-r.left)*W/r.width;mode='mouse';
});
cvs.addEventListener('pointerdown',e=>{
const r=cvs.getBoundingClientRect();
mouseX=(e.clientX-r.left)*W/r.width;mode='mouse';
ensureAudio();
if(demo){resetGame(false);return;}
if(state==='over'||state==='win'){resetGame(false);return;}
if(state==='serve'){serveT=0;} // launch immediately
});
window.addEventListener('keydown',e=>{
if(e.key==='ArrowLeft'||e.key==='ArrowRight'){e.preventDefault();mode='key';if(e.key==='ArrowLeft')keyL=true;else keyR=true;}
if(e.key===' ')e.preventDefault();
ensureAudio();
if(demo){resetGame(false);return;}
if(state==='over'||state==='win'){if(e.key==='r'||e.key==='R'||e.key==='Enter'||e.key===' '||e.key.startsWith('Arrow'))resetGame(false);return;}
if(state==='serve'&&(e.key===' '||e.key==='ArrowUp'))serveT=0;
});
window.addEventListener('keyup',e=>{
if(e.key==='ArrowLeft')keyL=false;
if(e.key==='ArrowRight')keyR=false;
});
```
Hmm: pressing arrow in demo starts game AND sets key flag — good.
Note: starting real game on any keydown even before pointer click — allowed ("click or press a key"). But iframe keyboard requires click first — that's environmental; message covers it.
Also: prevent the case where user is in real game and presses R mid-play to restart? Add: if(e.key==='r'||e.key==='R') resetGame(false) during play too. Requirement says restart option on game over screens; mid-game restart is bonus. I'll allow R anytime in real mode.
Init: resetGame(true); rAF loop.
Stars init.
One more check — resetGame(false) sets demo=false. resetGame(true) demo=true.
paddle.x init: W/2-56.
Check B_W: (960-78)/12=73.5. bricks x = 6 + col*(79.5). col 11 → 6+874.5=880.5, +73.5=954 → +GAP=960 ✓.
B_TOP=82, 7 rows*(24+6)=210 → bottom 82+204+24=310. Paddle 594. Good spacing.
Text baseline: set per usage; remember to reset textBaseline. I'll set explicitly everywhere.
ctx.arc end angle: use Math.PI*2 (I wrote 7 shorthand — 7 > 2π fine, but let me use proper TAU const).
letterSpacing on canvas: Chrome supports ctx.letterSpacing = '2px'. Use it for header text in HUD? Keep optional; guard not needed (string assignment harmless). I'll use for the big titles: ctx.letterSpacing='6px' before fillText, then reset to '0px'. Actually to be safe cross-browser, fine — latest Chrome target.
Multi-ball cloning while state==='serve'? drops caught during serve — doMulti with glued ball: clones get vx=vy=0 rotated — still zero, glued? They're separate balls not glued — they'd sit still forever. Guard: only apply multi in state==='play'; if serve, still fine to just add? Simplest: in doMulti, if ball speed ~0, give them launch velocity. Or during serve, delay? Let me make catch during serve possible but doMulti uses base; if base.vx===0&&base.vy===0 → launch clones at spread angles immediately and also state='play'? That prematurely ends serve — acceptable? It'd launch the serve ball. Simpler: during serve, capsule catch still applies wide; for multi: set serveT=0 first then clone after? Hacky.
Cleanest: in applyPU for multi when state==='serve': launch base first (serveT=0 logic inline: launchBall(balls[0]); state='play';), then clones get real velocity. I'll do:
```js
function doMulti(){
if(state==='serve'){const b=balls[0];launchBall(b);state='play';}
...
}
```
Good.
Also during 'over'/'win' the update skips physics — drops frozen, fine.
Check: state 'over' in demo → demoT countdown → resetGame(true) → state 'serve', demo true. Overlay disappears because state != over. ✓.
Also drawOverlays demo pill shows during demo serve/play — yes state!=='over'&&'win'.
Now the launch: vy=-cos(a)*speed with a∈(-0.35,0.35) → vy ≈ -0.94*380=-357, vx ±130. good.
Paddle keyboard speed 680 px/s vs ball 380 — plenty.
Potential bug: in humanPaddle with mode 'key' but no keys pressed → paddle stays. Fine.
Mouse move outside canvas still updates via pointermove only on canvas — attach to window instead for smoother: window.addEventListener('pointermove') and clamp. I'll attach to window but compute relative; only set mode 'mouse' if pointer within canvas horizontal? Setting mouseX clamped is fine. Use window for robustness.
Now HUD pill slots: drawWide pill only if wideT>0 at x=W/2-80; multi pill if balls.length>1 at x=W/2+80. When only one active, center each alone? Slots fixed is fine — actually if only wide active, pill at W/2-80 looks off-center. Compute positions: active list; if one → center W/2; if two → ±80. Implement small array.
fmt time:
```js
function fmt(t){const m=(t/60)|0,s=(t%60)|0;return String(m).padStart(2,'0')+':'+String(s).padStart(2,'0');}
```
Also draw "GET READY" during serve real:
```js
if(state==='serve'&&!demo){
ctx.globalAlpha=0.6+0.4*Math.sin(time*5);
text 'GET READY' at (paddle center, paddle.y-26)
}
```
Also during demo serve show nothing extra.
Let me also double-check no horizontal scrollbar: body overflow hidden? I removed overflow:hidden; with min-height 100vh and small padding, no overflow since widths are %. Set body overflow:hidden to be safe? If viewport very short (like 500px height), content overflows vertically — scrollbar hidden ok. I'll add overflow:hidden on body to guarantee no scrollbars. Hmm but that could cut content on short screens; centering with justify-content:center and canvas aspect fixed width — on 1280×800 fine. I'll use overflow:hidden.
Wait: at 375px width, aspect 960/640 → canvas height = width*2/3 ≈ 340*0.667≈227. Header+canvas+footer ≈ 300px — fits.
At 1280×800: canvas 960×640 + header ~30 + footer ~20 + gaps ≈ 700+ padding — fits 800. Good.
Now write drawWideIcon and drawMultiIcon:
```js
function drawWideIcon(cx,cy,s,color){
ctx.strokeStyle=color;ctx.lineWidth=2;ctx.lineCap='round';ctx.lineJoin='round';
ctx.beginPath();
ctx.moveTo(cx-s,cy);ctx.lineTo(cx+s,cy);
ctx.moveTo(cx-s+4,cy-3.5);ctx.lineTo(cx-s,cy);ctx.lineTo(cx-s+4,cy+3.5);
ctx.moveTo(cx+s-4,cy-3.5);ctx.lineTo(cx+s,cy);ctx.lineTo(cx+s-4,cy+3.5);
ctx.stroke();
}
function drawMultiIcon(cx,cy,s,color){
ctx.fillStyle=color;
const pts=[[cx,cy-s*0.8],[cx-s*0.7,cy+s*0.55],[cx+s*0.7,cy+s*0.55]];
for(const [x,y] of pts){ctx.beginPath();ctx.arc(x,y,s*0.34,0,TAU);ctx.fill();}
}
```
s=8 → dots radius 2.7, spread ok within brick h=24? vertical extent: cy±6.4 → within. Wide icon s=8 → 16 wide, fits 73-wide brick.
rr helper:
```js
function rr(x,y,w,h,r){
r=Math.min(r,w/2,h/2);
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();
}
```
shade:
```js
function shade(hex,amt){
const n=parseInt(hex.slice(1),16);
let r=(n>>16)&255,g=(n>>8)&255,b=n&255;
const f=t=>amt>=0?t+(255-t)*amt:t*(1+amt);
return `rgb(${f(r)|0},${f(g)|0},${f(b)|0})`;
}
```
Substep maxV: use speed magnitude — compute per ball hypot? max(|vx|,|vy|) fine.
Ball trail uses b.trail array push in draw (per frame) — fine.
Score pad: padStart(5).
Also add: paddle contact sparkle — in paddle bounce, push 3 small parts. Optional; add:
```js
for(let i=0;i<3;i++)parts.push({x:b.x,y:paddle.y,vx:rand(-60,60),vy:rand(-120,-30),grav:300,life:0.3,max:0.3,size:2,color:'#cfe4ff',rot:0,vr:0});
```
Victory sound + shake celebration: on win, spawn confetti burst across? Add: for 30 parts from top? Keep simple: big shake=4 and sfx.win.
Double-check balls removal inside substep loop: I splice after each substep's ball loop. `for(const b of balls) stepBall(b)` — if ball dies set b.dead. Then splice. Also break if empty. Then after substeps, if(!balls.length) loseLife(). But loseLife might call resetServe which creates new ball — fine.
Edge: during substep loop state could change to 'over' via loseLife — loseLife called after loop, ok. winGame inside hitBrick during substeps — sets state='win' but substeps continue moving balls... Minor: after win, balls keep moving for rest of frame; next frame state==='win' skips play. But update's `else if(state==='play')` already entered; fine. Also drop catches this frame — ok. But loseLife after win? balls still exist so no. Fine.
Also when winGame triggers, maybe clear remaining balls visually — they keep drawing; fine, frozen next frames.
Check victory also triggers when last brick broken by... only balls break bricks. ✓
Demo AI serve: target moves sinusoidally; after launch ball flies from paddle wherever. Fine.
One concern: AI wobble error ±23 px might cause misses at high speed with paddle 112 wide — occasionally loses life, showing game-over screen in demo, which auto-restarts. Requirement says demo AI plays; occasional miss is okay but let's keep misses rare: error amplitude total ~23px vs half-width 56 — rarely misses. Good.
Demo also should showcase powerups: AI catches most capsules since it tracks lowest ball; capsules fall where brick broken — AI may not be under capsule. It'll catch some. Fine.
Now HUD pill drawing function with textBaseline middle etc. Careful text alignment states — set each time.
Also "NEONBREAKER" header outside canvas.
Let me now also handle `ctx.letterSpacing`: I'll use for big overlay titles: try{ctx.letterSpacing='4px'}catch— assignment won't throw even if unsupported (unknown property just ignored on ctx? Setting nonexistent property is fine in JS). OK.
Write final file. Also remember `textBaseline` default alphabetic — set 'middle' in overlay section.
Let me assemble everything and double check variable names consistent.
Full script order:
1. setup canvas, ctx, constants
2. helpers (clamp, rand, shade, rr, TAU)
3. audio
4. state vars, stars
5. level init
6. ball/serve/launch
7. resetGame, loseLife, winGame
8. hitBrick, applyPU, doMulti, genCracks
9. particles, pops helpers
10. update, demoAI, humanPaddle, stepBall
11. draw functions
12. input
13. main loop + init
Check the update drop-catch condition uses paddle.wCur. ✓
Also drops should also be caught in demo obviously. ✓
I think there might be an issue with `b.dead` balls still being stepped within same substep loop iteration (for...of over live array while marking dead) — stepBall checks `if(!b.dead)`? I wrote that inside substep ball loop. New multi balls pushed during stepBall (hitBrick → drop → not immediate; drops are capsules, not balls — multi only on catch, which happens in update, not substeps). OK, no mutation of balls during substep except dead flags. Good.
Wait — actually doMulti happens in applyPU during update's drop section (outside substeps). ✓
Paddle collision sound fires per substep? Once flipped vy<0, condition b.vy>0 fails. ✓
Now the "GET READY" and demo hint positions.
Also ensure the canvas visible content within first 3 seconds: bricks + demo ball bouncing immediately (serve 0.9s then launches). Maybe reduce initial serve to 0.7s. Also first paint shows bricks & paddle — good.
Let me write out the final HTML.
Also one more requirement check: "Some bricks visibly carry a power-up icon; when broken they drop it" ✓; "catching it with the paddle gives either a wider paddle (10 seconds) or multi-ball (3 balls)" ✓; "particles" ✓; "3 lives, score displayed" ✓; "game over and victory screens with restart" ✓; "at least 6 rows" — 7 ✓; "tougher bricks need 2 hits and visibly crack" ✓; "bounce angle depends on hit position" ✓; "mouse + arrow keys" ✓; "ball never passes through" — substeps ✓.
Demo mode with "Click or press a key to start" ✓.
Final code write-up. I'll be careful with syntax.
Also small detail: `ctx.filter` not used. `shadowBlur` resets after use everywhere — verify each usage resets.
In drawBricks, shadow set only when b.power; after fill, `ctx.shadowBlur=0`. ✓
Let me write it.
Also on window resize nothing needed (CSS handles).
Edge: `String(score).padStart(5,'0')`.
In drawHUD I need textAlign resets: set 'left' for score, 'center' for pills, 'right' for lives label.
Let me write pill label with remaining seconds: wide label `WIDE ${wideT.toFixed(1)}s` — updating decimals jitter fine, use Math.ceil: `WIDE ${Math.ceil(wideT)}s`.
Multi pill: `BALLS ×${balls.length}`.
Positions:
```js
const pills=[];
if(wideT>0)pills.push({label:'WIDE '+Math.ceil(wideT)+'s',frac:wideT/10,color:'#4ecdc4'});
if(balls.length>1)pills.push({label:'BALLS ×'+balls.length,frac:1,color:'#ff9f45'});
pills.forEach((p,i)=>{
const cx = pills.length===1?W/2:W/2+(i===0?-78:78);
drawPill(cx,p.label,p.frac,p.color);
});
```
Alright — also, the header "Level 01" tag. Good.
One thing: ctx state — lineCap etc persist; set as needed.
Write the final answer now. Estimate ~550 lines. Ensure zero console errors: all functions defined before use in runtime (hoisting for function declarations fine).
Star field:
```js
const stars=[];for(let i=0;i<70;i++)stars.push({x:rand(6,W-6),y:rand(HUD+8,H-40),r:rand(0.6,1.8),a:rand(0.15,0.5),sp:rand(0.4,1.6),ph:rand(0,6.28)});
```
drawBG fillRect with negative offsets due to shake translate — I used (-12,-12,W+24,H+24) ✓. HUD drawn after restore, unshaken. But shake translate region includes overlays? drawOverlays after restore ✓.
Pops drawn inside shake — fine.
fmt used in overlays.
Serve glue: ball trail will record same point repeatedly — fine.
Also `mouseX` init W/2; mode 'mouse' default → paddle stays center until move. But keyboard arrows set mode='key'. Fine.
Demo start: resetGame(true) → paddle centered.
One more: on pointerdown when demo → startGame resets; also the same click's pointermove already set mouseX — good.
Also for touch: pointermove/pointerdown work.
In keydown, `e.key.startsWith('Arrow')` — e.key is string, fine.
I'm ready. Also add `textBaseline` reset where needed — I'll set explicit at start of each text-drawing function.
One subtle bug: in stepBall, after brick collision resolution using ox/oy — if ball center inside brick deeply (shouldn't at 4px substeps) it still works.
Another: paddle catch condition uses `b.x>=paddle.x-b.r` — includes r for side hits. Fine.
Ball vs paddle while wide animating shrink — could clip ball inside paddle edges; bounce sets y above anyway.
Final: write the code.
Actually one more consideration about `state==='play'` guard in safety vy nudge — that's for play only, ok.
Also drops during 'over': frozen mid-air, drawn behind overlay dim — fine.
Let me make sure `winGame` also sets `demoT`. ✓
Counting `bricks.filter(b=>b.alive).length` on overlay each frame is fine (70 items).
Write it.