Hamburger breakoutOne-line
GLM 5.3 Flash
by Z.aiReasoningClean
Running live in a sandboxed frame. Click inside it before using the keyboard.
How it went
- Total time
- 4 min 21 s
- Cost
- $0.0065
- Output tokens
- 21,434
- Spent thinking
- 16,029 tokens
- Speed
- 82.6 tok/s
- Console errors
- 0
- First token after
- 1.5 s
- First visible text after
- 3 min 24 s
- Prompt tokens
- 376
- Code size
- 15.1 KB
- Finish reason
- stop
- Served by
- OpenInference
- Model id
z-ai/glm-5.3-flash- Reasoning sent
{"effort":"high"}- Max output
- 64,000 tokens
- Generated
- Nov 10, 2026
What it was asked
The prompt
“i want a breakout game where the paddle is a hamburger and every brick you break explodes into tons of little hearts”
System prompt, sent with every prompt
You are generating a self-contained web demo. It will be rendered automatically in a sandboxed iframe and compared side by side with the output of other AI models given the exact same task. Output rules: - Reply with exactly ONE fenced code block containing the complete file: ```html (or ```svg when the task asks for an SVG). No text before or after the code block. - Everything must be inline: CSS in <style>, JavaScript in <script>. No external files, images, fonts or network requests, except the CDN URLs explicitly allowed by the task. - Do not use localStorage, sessionStorage, cookies, IndexedDB, alert(), confirm(), prompt() or window.open(): they are blocked in the sandbox and will throw. - The page must look right in a 1280×800 viewport and adapt gracefully down to 375px wide. No horizontal scrollbar. - Something meaningful must be visible on first paint: a screenshot is taken automatically about 3 seconds after load, without any interaction. - Everything that makes the demo interesting must be visible within the first seconds: nothing hidden behind a high score, a later level or a long play session. - Games must start in a self-playing demo mode (a simple AI plays) behind a "Click or press a key to start" message, so the game can be judged in action without playing. - The iframe only receives keyboard events after the user clicks it: if the demo uses the keyboard, display a clear "Click or press a key to start" message. - The code must run with zero errors in the console of the latest Chrome.
Code run-1.html · 488 lines
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,user-scalable=no">
<title>Heartbreak Burger — Breakout</title>
<style>
html,body{margin:0;padding:0;height:100%;overflow:hidden;background:#0e0618;}
canvas{display:block;width:100%;height:100%;touch-action:none;cursor:crosshair;}
</style>
</head>
<body>
<canvas id="c"></canvas>
<script>
'use strict';
const cv=document.getElementById('c'),ctx=cv.getContext('2d');
const LW=960,LH=620;
let dpr=1,SC=1,OX=0,OY=0;
function resize(){
dpr=window.devicePixelRatio||1;
const w=window.innerWidth,h=window.innerHeight;
SC=Math.min(w/LW,h/LH);
OX=(w-LW*SC)/2; OY=(h-LH*SC)/2;
cv.width=Math.round(w*dpr); cv.height=Math.round(h*dpr);
cv.style.width=w+'px'; cv.style.height=h+'px';
}
window.addEventListener('resize',resize);
resize();
const clamp=(v,a,b)=>v<a?a:v>b?b:v;
const PALETTE=['#ff5c8a','#ffa94d','#ffe066','#63e6be','#74c0fc','#e599f7'];
const HEARTCOLS=['#ff2e63','#ff5c8a','#ff8fb1','#ffd166','#ffffff','#ff5c8a','#ff5c8a'];
let mode='demo';
let score=0,lives=3,level=1,combo=0,shake=0,speed=5.4;
const paddle={x:LW/2,w:130,vx:0,prevX:LW/2,wideT:0};
const paddleY=LH-64;
const ball={x:paddle.x,y:paddleY-14,vx:0,vy:0,r:8,stuck:true,t:0};
let bricks=[],hearts=[],pops=[],pickups=[],trail=[];
const keys={};
const ambient=[];
for(let i=0;i<14;i++)ambient.push({x:Math.random()*LW,y:Math.random()*LH,s:4+Math.random()*8,vy:0.15+Math.random()*0.35,al:0.08+Math.random()*0.1,c:HEARTCOLS[i%HEARTCOLS.length]});
/* ---------- audio ---------- */
let AC=null;
function initAudio(){ if(AC)return; try{AC=new (window.AudioContext||window.webkitAudioContext)();}catch(e){AC=null;} }
function sfx(f,d,type,v){
if(!AC)return;
try{
const o=AC.createOscillator(),g=AC.createGain();
o.type=type;o.frequency.value=f;
g.gain.setValueAtTime(v,AC.currentTime);
g.gain.exponentialRampToValueAtTime(0.001,AC.currentTime+d);
o.connect(g);g.connect(AC.destination);
o.start();o.stop(AC.currentTime+d);
}catch(e){}
}
/* ---------- shapes ---------- */
function rr(x,y,w,h,r){
ctx.beginPath();
ctx.moveTo(x+r,y);
ctx.arcTo(x+w,y,x+w,y+h,r);
ctx.arcTo(x+w,y+h,x,y+h,r);
ctx.arcTo(x,y+h,x,y,r);
ctx.arcTo(x,y,x+w,y,r);
ctx.closePath();
}
function heartPath(s){
ctx.beginPath();
ctx.moveTo(0,0.4*s);
ctx.bezierCurveTo(-0.6*s,-0.1*s,-0.42*s,-0.6*s,0,-0.22*s);
ctx.bezierCurveTo(0.42*s,-0.6*s,0.6*s,-0.1*s,0,0.4*s);
ctx.closePath();
}
function starPath(s){
ctx.beginPath();
for(let i=0;i<10;i++){
const r=i%2?s*0.45:s;
const a=Math.PI/5*i-Math.PI/2;
ctx[i?'lineTo':'moveTo'](Math.cos(a)*r,Math.sin(a)*r);
}
ctx.closePath();
}
/* ---------- game setup ---------- */
function buildLevel(n){
bricks=[];
const rows=Math.min(4+n,7),cols=10,gap=6,bh=26,top=64;
const bw=(LW-40-(cols-1)*gap)/cols;
for(let r=0;r<rows;r++)for(let c=0;c<cols;c++){
const hp=(r<1&&n>=4)?3:(r<2&&n>=2)?2:1;
bricks.push({x:20+c*(bw+gap),y:top+r*(bh+gap),w:bw,h:bh,hp:hp,max:hp,color:PALETTE[r%PALETTE.length]});
}
}
function resetBall(){
ball.stuck=true;ball.t=0;ball.vx=0;ball.vy=0;
ball.x=paddle.x;ball.y=paddleY-ball.r-2;
trail.length=0;
}
function launch(){
ball.stuck=false;ball.t=0;
const ang=-Math.PI/2+(Math.random()-0.5)*0.7;
ball.vx=Math.cos(ang)*speed;ball.vy=Math.sin(ang)*speed;
}
function startGame(){
mode='play';score=0;lives=3;level=1;combo=0;speed=5.4;
buildLevel(1);resetBall();
}
function spawnHearts(x,y,n){
for(let i=0;i<n;i++){
const a=Math.random()*Math.PI*2,sp=1+Math.random()*4;
hearts.push({x:x,y:y,vx:Math.cos(a)*sp,vy:Math.sin(a)*sp-2.5,rot:Math.random()*6.28,vr:(Math.random()-0.5)*0.3,s:5+Math.random()*9,life:1,dec:0.008+Math.random()*0.012,color:HEARTCOLS[(Math.random()*HEARTCOLS.length)|0]});
}
}
function loseLife(){
if(mode==='demo'){resetBall();return;}
lives--;shake=14;combo=0;
sfx(130,0.35,'sawtooth',0.07);
if(lives<=0){mode='over';}
else resetBall();
}
function hitBrick(b,i,cx,cy){
b.hp--;
if(b.hp<=0){
bricks.splice(i,1);
spawnHearts(cx,cy,22);
const pts=10+combo*5;
score+=pts;
pops.push({x:cx,y:cy-6,txt:'+'+pts,life:1});
combo++;
if(Math.random()<0.09&&pickups.length<3)pickups.push({x:cx,y:cy,vy:1.6,type:Math.random()<0.5?'heart':'star'});
speed=Math.min(8.5,speed+0.05);
sfx(300+Math.min(combo,10)*45,0.07,'square',0.05);
if(bricks.length===0){
level++;
buildLevel(level);
speed=5+level*0.4;
resetBall();
pops.push({x:LW/2,y:LH/2,txt:'LEVEL '+level+'!',life:1.5});
sfx(520,0.2,'sine',0.07);
}
}else{
spawnHearts(cx,cy,8);
score+=5;
sfx(180,0.05,'square',0.04);
}
}
/* ---------- update ---------- */
function update(dt){
if(shake>0)shake=Math.max(0,shake-0.6*dt);
for(const a of ambient){
a.y+=a.vy*dt;a.x+=Math.sin(a.y*0.01)*0.2*dt;
if(a.y>LH+30){a.y=-30;a.x=Math.random()*LW;}
}
for(let i=hearts.length-1;i>=0;i--){
const p=hearts[i];
p.vy+=0.16*dt;p.x+=p.vx*dt;p.y+=p.vy*dt;p.rot+=p.vr*dt;p.life-=p.dec*dt;
if(p.life<=0||p.y>LH+40)hearts.splice(i,1);
}
for(let i=pops.length-1;i>=0;i--){
const p=pops[i];p.y-=0.7*dt;p.life-=0.02*dt;
if(p.life<=0)pops.splice(i,1);
}
for(let i=pickups.length-1;i>=0;i--){
const pk=pickups[i];pk.y+=pk.vy*dt;pk.vy+=0.02*dt;
if(pk.y>paddleY-8&&Math.abs(pk.x-paddle.x)<paddle.w/2+12){
if(pk.type==='heart'){lives=Math.min(5,lives+1);pops.push({x:pk.x,y:pk.y,txt:'+1 LIFE',life:1});sfx(660,0.12,'sine',0.06);}
else{paddle.wideT=480;pops.push({x:pk.x,y:pk.y,txt:'MEGA BURGER!',life:1});sfx(880,0.15,'sine',0.06);}
pickups.splice(i,1);continue;
}
if(pk.y>LH+20)pickups.splice(i,1);
}
if(mode==='over')return;
/* paddle */
if(mode==='demo'){
const target=ball.stuck?LW/2:ball.x+ball.vx*11;
paddle.x+=(target-paddle.x)*0.07*dt;
}else{
if(keys['ArrowLeft']||keys['a']||keys['A'])paddle.x-=9*dt;
if(keys['ArrowRight']||keys['d']||keys['D'])paddle.x+=9*dt;
}
paddle.x=clamp(paddle.x,paddle.w/2+6,LW-paddle.w/2-6);
paddle.vx=clamp((paddle.x-paddle.prevX)*0.5+paddle.vx*0.5,-6,6);
paddle.prevX=paddle.x;
if(paddle.wideT>0)paddle.wideT-=dt;
const targetW=paddle.wideT>0?190:130;
paddle.w+=(targetW-paddle.w)*0.15*dt;
/* ball */
if(ball.stuck){
ball.x=paddle.x;ball.y=paddleY-ball.r-2;
ball.t+=dt;
if(ball.t>1.1)launch();
}else{
ball.x+=ball.vx*dt;ball.y+=ball.vy*dt;
trail.push({x:ball.x,y:ball.y});
if(trail.length>12)trail.shift();
if(ball.x<ball.r+4){ball.x=ball.r+4;ball.vx=Math.abs(ball.vx);}
if(ball.x>LW-ball.r-4){ball.x=LW-ball.r-4;ball.vx=-Math.abs(ball.vx);}
if(ball.y<ball.r+4){ball.y=ball.r+4;ball.vy=Math.abs(ball.vy);}
if(ball.y>LH+30){loseLife();return;}
/* paddle collision */
if(ball.vy>0&&ball.y+ball.r>=paddleY&&ball.y-ball.r<=paddleY+50&&Math.abs(ball.x-paddle.x)<=paddle.w/2+ball.r){
const rel=clamp((ball.x-paddle.x)/(paddle.w/2),-1,1);
const ang=rel*1.05;
ball.vx=Math.sin(ang)*speed;
ball.vy=-Math.cos(ang)*speed;
ball.y=paddleY-ball.r-1;
combo=0;
sfx(240,0.07,'sine',0.05);
}
/* bricks */
for(let i=bricks.length-1;i>=0;i--){
const b=bricks[i];
const cx=clamp(ball.x,b.x,b.x+b.w),cy=clamp(ball.y,b.y,b.y+b.h);
const dx=ball.x-cx,dy=ball.y-cy;
if(dx*dx+dy*dy<=ball.r*ball.r){
if(Math.abs(dx)>Math.abs(dy)){
ball.vx=dx>0?Math.abs(ball.vx):-Math.abs(ball.vx);
ball.x=cx+(dx>0?ball.r+0.2:-ball.r-0.2);
}else{
ball.vy=dy>0?Math.abs(ball.vy):-Math.abs(ball.vy);
ball.y=cy+(dy>0?ball.r+0.2:-ball.r-0.2);
}
hitBrick(b,i,cx,cy);
}
}
}
}
/* ---------- drawing ---------- */
function drawBrick(b){
ctx.fillStyle='rgba(0,0,0,0.35)';
rr(b.x+3,b.y+4,b.w,b.h,8);ctx.fill();
ctx.fillStyle=b.color;
rr(b.x,b.y,b.w,b.h,8);ctx.fill();
ctx.fillStyle='rgba(255,255,255,0.25)';
rr(b.x+3,b.y+3,b.w-6,6,4);ctx.fill();
ctx.save();
ctx.translate(b.x+b.w/2,b.y+b.h/2+2);
ctx.fillStyle='rgba(255,255,255,'+(0.35+0.25*b.hp/b.max)+')';
heartPath(9);ctx.fill();
ctx.restore();
}
function drawPickup(pk){
ctx.save();
const bob=Math.sin(performance.now()/200)*3;
ctx.translate(pk.x,pk.y+bob);
if(pk.type==='heart'){
ctx.shadowColor='#ff5c8a';ctx.shadowBlur=14;
ctx.fillStyle='#ff5c8a';heartPath(13);ctx.fill();
}else{
ctx.shadowColor='#ffd166';ctx.shadowBlur=14;
ctx.fillStyle='#ffd166';starPath(13);ctx.fill();
}
ctx.restore();
}
function drawBurger(x,y,w,tilt){
const k=w/130;
ctx.save();
ctx.translate(x,y);ctx.rotate(tilt);ctx.scale(k,k);
ctx.fillStyle='rgba(0,0,0,0.35)';
rr(-65,44,130,10,5);ctx.fill();
/* bottom bun */
ctx.fillStyle='#f0a24a';
rr(-57,36,114,14,7);ctx.fill();
ctx.fillStyle='rgba(0,0,0,0.12)';
rr(-57,44,114,6,3);ctx.fill();
/* patty */
ctx.fillStyle='#7c4a2d';
rr(-62,24,124,15,7);ctx.fill();
ctx.fillStyle='#5d3220';
rr(-62,32,124,7,3);ctx.fill();
/* cheese */
ctx.fillStyle='#ffd166';
rr(-60,20,120,8,3);ctx.fill();
for(let d=0;d<4;d++){
const dx=-52+d*32;
rr(dx,26,7,9,3);ctx.fill();
}
/* lettuce */
ctx.fillStyle='#6ee7a0';
ctx.beginPath();
for(let i=0;i<9;i++){
const cx=-58+i*14.5;
ctx.moveTo(cx,18);
ctx.arc(cx,18,8,0,Math.PI);
}
ctx.fill();
/* top bun */
ctx.fillStyle='#f7b855';
rr(-65,0,130,18,9);ctx.fill();
ctx.fillStyle='rgba(255,255,255,0.25)';
rr(-60,2,120,5,3);ctx.fill();
/* seeds */
ctx.fillStyle='#ffe9c4';
for(let i=0;i<7;i++){
const sx=-52+i*17.5;
ctx.beginPath();
ctx.ellipse(sx,7+((i%2)?3:0),3,2,0.3,0,7);
ctx.fill();
}
ctx.restore();
}
function drawHUD(){
ctx.textAlign='left';
ctx.fillStyle='#fff';
ctx.font='800 20px system-ui,sans-serif';
ctx.fillText('SCORE '+score,20,32);
ctx.textAlign='center';
ctx.fillText('LEVEL '+level,LW/2,32);
for(let i=0;i<lives;i++){
ctx.save();
ctx.translate(LW-26-i*26,24);
ctx.fillStyle='#ff5c8a';heartPath(11);ctx.fill();
ctx.restore();
}
if(mode==='demo'){
ctx.fillStyle='rgba(255,255,255,0.45)';
ctx.font='700 13px system-ui,sans-serif';
ctx.fillText('DEMO MODE',LW/2,52);
}
}
function drawOverlay(){
if(mode==='play')return;
ctx.save();
ctx.textAlign='center';
if(mode==='demo'){
ctx.fillStyle='rgba(16,6,30,0.55)';
ctx.fillRect(0,0,LW,LH);
const t=performance.now()/1000;
for(let i=0;i<6;i++){
const hx=LW/2+Math.cos(i*1.1+t*0.7)*200;
const hy=340+Math.sin(i*1.7+t*0.9)*60;
ctx.save();
ctx.translate(hx,hy);ctx.globalAlpha=0.5;
ctx.fillStyle=HEARTCOLS[i%HEARTCOLS.length];
heartPath(8+Math.sin(t+i)*3);ctx.fill();
ctx.restore();
}
ctx.globalAlpha=1;
ctx.fillStyle='#fff';
ctx.font='900 64px system-ui,sans-serif';
ctx.fillText('BURGER',LW/2,120);
ctx.fillText('BREAKOUT',LW/2,190);
drawBurger(LW/2,320,220,0);
ctx.font='700 20px system-ui,sans-serif';
ctx.fillStyle='rgba(255,255,255,0.85)';
ctx.fillText('The paddle is a hamburger. Every brick explodes into little hearts.',LW/2,440);
const p=0.6+0.4*Math.sin(performance.now()/300);
ctx.globalAlpha=p;
ctx.fillStyle='#fff';
ctx.font='800 30px system-ui,sans-serif';
ctx.fillText('CLICK OR PRESS ANY KEY TO START',LW/2,560);
ctx.globalAlpha=1;
ctx.fillStyle='rgba(255,255,255,0.6)';
ctx.font='600 15px system-ui,sans-serif';
ctx.fillText('demo — the AI is playing while you wait',LW/2,592);
}else{
ctx.fillStyle='rgba(16,6,30,0.7)';
ctx.fillRect(0,0,LW,LH);
ctx.fillStyle='#ff5c8a';
ctx.font='900 56px system-ui,sans-serif';
ctx.fillText('GAME OVER',LW/2,240);
ctx.fillStyle='#fff';
ctx.font='800 26px system-ui,sans-serif';
ctx.fillText('SCORE '+score,LW/2,300);
const p=0.6+0.4*Math.sin(performance.now()/300);
ctx.globalAlpha=p;
ctx.fillText('CLICK OR PRESS ANY KEY TO TRY AGAIN',LW/2,380);
ctx.globalAlpha=1;
}
ctx.restore();
}
function render(){
ctx.setTransform(1,0,0,1,0,0);
ctx.fillStyle='#0e0618';
ctx.fillRect(0,0,cv.width,cv.height);
ctx.setTransform(dpr*SC,0,0,dpr*SC,dpr*OX,dpr*OY);
ctx.save();
ctx.beginPath();ctx.rect(0,0,LW,LH);ctx.clip();
if(shake>0)ctx.translate((Math.random()-0.5)*shake,(Math.random()-0.5)*shake);
/* background */
const g=ctx.createLinearGradient(0,0,0,LH);
g.addColorStop(0,'#241040');g.addColorStop(1,'#3d1a55');
ctx.fillStyle=g;ctx.fillRect(0,0,LW,LH);
ctx.fillStyle='rgba(255,255,255,0.03)';
for(let i=0;i<16;i++)for(let j=0;j<24;j++)if((i+j)%2)ctx.fillRect(j*40,i*40,40,40);
/* ambient hearts */
for(const a of ambient){
ctx.save();
ctx.globalAlpha=a.al;ctx.translate(a.x,a.y);
ctx.fillStyle=a.c;heartPath(a.s);ctx.fill();
ctx.restore();
}
/* bricks */
for(const b of bricks)drawBrick(b);
/* pickups */
for(const pk of pickups)drawPickup(pk);
/* heart particles */
for(const p of hearts){
ctx.save();
ctx.globalAlpha=Math.min(1,p.life);
ctx.translate(p.x,p.y);ctx.rotate(p.rot);
ctx.fillStyle=p.color;heartPath(p.s);ctx.fill();
ctx.restore();
}
/* ball trail */
for(let i=0;i<trail.length;i++){
ctx.globalAlpha=0.15*(i/trail.length);
ctx.fillStyle='#ff8fb1';
ctx.beginPath();ctx.arc(trail[i].x,trail[i].y,ball.r*(i/trail.length),0,7);ctx.fill();
}
ctx.globalAlpha=1;
/* paddle */
drawBurger(paddle.x,paddleY,paddle.w,clamp(paddle.vx*0.02,-0.14,0.14));
/* ball */
ctx.save();
ctx.shadowColor='#ff5c8a';ctx.shadowBlur=18;
ctx.fillStyle='#fff';
ctx.beginPath();ctx.arc(ball.x,ball.y,ball.r,0,7);ctx.fill();
ctx.restore();
/* score pops */
ctx.textAlign='center';
ctx.font='800 16px system-ui,sans-serif';
for(const p of pops){
ctx.globalAlpha=Math.min(1,p.life);
ctx.fillStyle='#fff';
ctx.fillText(p.txt,p.x,p.y);
}
ctx.globalAlpha=1;
ctx.restore();
drawHUD();
drawOverlay();
}
/* ---------- input ---------- */
function toLogical(e){
const r=cv.getBoundingClientRect();
return {x:(e.clientX-r.left-OX)/SC,y:(e.clientY-r.top-OY)/SC};
}
cv.addEventListener('mousedown',()=>{
initAudio();
if(mode==='demo'||mode==='over')startGame();
else if(ball.stuck)launch();
});
cv.addEventListener('mousemove',e=>{
if(mode==='play'){const p=toLogical(e);paddle.x=p.x;}
});
cv.addEventListener('touchstart',e=>{
e.preventDefault();initAudio();
if(mode==='demo'||mode==='over')startGame();
else if(ball.stuck)launch();
const t=e.touches[0];
if(t&&mode==='play'){const p=toLogical(t);paddle.x=p.x;}
},{passive:false});
cv.addEventListener('touchmove',e=>{
e.preventDefault();
const t=e.touches[0];
if(t&&mode==='play'){const p=toLogical(t);paddle.x=p.x;}
},{passive:false});
window.addEventListener('keydown',e=>{
if(['ArrowLeft','ArrowRight','ArrowUp','ArrowDown',' '].indexOf(e.key)>=0)e.preventDefault();
keys[e.key]=true;
initAudio();
if(mode==='demo'||mode==='over')startGame();
else if(e.key===' '&&ball.stuck)launch();
});
window.addEventListener('keyup',e=>{keys[e.key]=false;});
/* ---------- main loop ---------- */
let last=performance.now();
function frame(now){
const dt=Math.min(2.5,(now-last)/16.667);
last=now;
update(dt);
render();
requestAnimationFrame(frame);
}
buildLevel(1);
resetBall();
requestAnimationFrame(frame);
</script>
</body>
</html>Reasoning trace (16,029 tokens)
Make a breakout game: hamburger paddle, bricks explode into hearts particles. Self-playing demo mode behind "Click or press a key to start". Single HTML file, canvas-based. Fun visual style.
Design: cute/kawaii pastel diner aesthetic. Paddle drawn as hamburger (bun, patty, lettuce, cheese). Bricks as colorful candy blocks. Hearts particles flying with gravity, rotating. Demo AI plays automatically.
Features:
- Canvas game, 1280x800 viewport, responsive.
- Ball physics, brick grid, lives, score, combo.
- Hearts explosion particles with gravity, fade.
- Click/press to start message; demo mode plays behind.
- Restart on win/lose.
Let me write compact but polished code.
Paddle: draw hamburger with bun top (sesame seeds), lettuce wavy, cheese, patty, bottom bun.
Bricks: rounded rects with candy colors and slight gloss.
Hearts: bezier heart path, random colors (pink/red/white), spin.
Demo AI: move paddle toward ball x with slight lag.
Ball: cherry tomato? Just a ball with trail. Maybe a little meatball? Keep simple: white ball with glow trail.
Sound: WebAudio simple beeps — optional, keep tiny. Maybe add small blips via WebAudio (works after user gesture; demo mode before gesture — audio context may be blocked; wrap in try/catch). I'll add subtle sounds after start.
Wallpaper: diner checkerboard background, soft gradient.
Let me code.
Structure:
- canvas full screen, letterboxed game area 960x640? Use canvas sized to window with logical coordinates scaled. Simpler: fixed logical W=960,H=640, scale canvas to fit window preserving aspect, centered.
Actually make it fill viewport: use logical 1280x800? Better: canvas resized to viewport, game field derived. I'll do fixed logical size 960x600 and scale to fit within window, centered with dark backdrop. Screenshot at 1280x800 should look fine.
Game logic:
- bricks: 8 cols x 5 rows, each brick has hp maybe 1-2, colors pastel.
- ball speed increases slightly per brick.
- paddle: width ~110, height ~40 (hamburger).
- collision: ball vs paddle — reflect with angle based on hit position.
- ball vs bricks: AABB, determine side.
- lose ball: lives--, reset ball on paddle. Demo: lives infinite? Demo AI just plays; if it loses, reset ball. Keep lives=3 for player, but in demo mode infinite.
- win: bricks cleared → new level with more rows/speed.
Particles: hearts with vx, vy, gravity, rotation, scale shrink, fade. Spawn ~18 per brick.
Also add floating score text.
Combo text.
HUD: score, lives (hamburger icons? small hearts), level.
Start overlay: "Click or press a key to start", title "HEARTBREAKER BREAKOUT" or "LOVEBURGER". Title: "BURGER BREAKOUT 💘"? Keep it: "HEARTBREAKER — Burger Breakout".
Demo mode: overlay semi-transparent, game plays behind, overlay title + instructions. On click/keypress: start real game.
Fonts: use system fonts, no external. Use "Baloo"? no network. Use system-ui with heavy weight, letter-spacing.
Write code carefully to avoid errors.
Key code:
```js
const LW=960, LH=640;
canvas scaled: ctx.setTransform(scale,0,0,scale,offX,offY) with clearRect.
```
Ball trail: store last positions.
Paddle follow: target x; player uses mouse/touch; demo uses AI.
Let me write it.
Heart draw function:
```js
function drawHeart(x,y,s,rot,color,alpha){
ctx.save();ctx.translate(x,y);ctx.rotate(rot);ctx.scale(s,s);
ctx.globalAlpha=alpha;ctx.fillStyle=color;
ctx.beginPath();
ctx.moveTo(0,3);
ctx.bezierCurveTo(0,-1,-5,-1,-5,-4? ...)
```
Standard heart path:
```
ctx.moveTo(0, 4);
ctx.bezierCurveTo(-6, -2, -3, -8, 0, -4);
ctx.bezierCurveTo(3, -8, 6, -2, 0, 4);
```
Hmm, let me use a known heart path of size ~1:
```
ctx.moveTo(0, 0.5);
ctx.bezierCurveTo(0, 0.4, -0.5, 0.1, -0.5, -0.1);
ctx.bezierCurveTo(-0.5, -0.35, 0, -0.35, 0, -0.1);
ctx.bezierCurveTo(0, -0.35, 0.5, -0.35, 0.5, -0.1);
ctx.bezierCurveTo(0.5, 0.1, 0, 0.4, 0, 0.5);
```
That's a heart with unit size. Scale accordingly. I'll use path with unit coordinates scaled.
Hamburger drawing: paddle width w=110, height h=44 at paddleY. Layers:
- top bun: dome arc, color #f5a623 / #e8a33d, sesame seeds.
- lettuce: green wavy under bun.
- cheese: yellow triangle-ish.
- patty: brown rounded rect.
- bottom bun: rounded rect.
Draw with slight tilt based on paddle velocity? Optional tilt. Keep simple: small tilt when moving.
Bricks: rounded rect, gradient gloss, hp 1-3 with colors: hp3 pink deep, hp2 orange, hp1 mint. Each brick has tiny heart emblem? Bricks could be "gift boxes" or candy. Add small heart on brick.
Collision: circle-rect.
Ball speed: base 5.2 px/frame at 60fps; use dt-based.
Physics: use delta time, dt normalized to 60fps units.
Ball radius 8.
Bricks layout: cols=10, rows=6, brickW = fieldW/cols - gap. Field: margin 30 top for HUD, playfield 960x? Let's define LW=960, LH=620. HUD at top drawn in canvas.
Bricks: startX=20, top=70, brickW=88, gap=6, brickH=26.
10 cols * 88 = 880 + 9 gaps*6=54 + 40 margins = 974. Adjust: cols=10, brickW=86, gap=6 → 860+54+40=954. OK with startX=20? 20+860+54=934, right margin 26. fine.
Paddle y = LH-60.
Level: rows increase; hp pattern depends on level.
Demo AI: paddleX moves toward ball.x predicted with speed 7; also slight error.
Lives: 3 hearts displayed. In demo, show "DEMO" tag.
On game over: show overlay "Game over — click to restart". On win: next level.
Powerups? Could add: some bricks drop hearts that give extra life or expand paddle. Keep scope moderate: add powerup "wide paddle" (star) and "extra life" occasionally. Maybe skip to keep code robust; but it adds interest. I'll add one simple powerup: falling heart = +1 life; falling star = wide paddle for 10s. Actually keep it: falling pickup items: heart (life) and star (multiball?) — multiball is fun. Hmm, multiball complicates. I'll do heart=+1 life, star=+50 score... Let's do heart pickup +1 life and "GIGA" paddle wide. Keep it simple.
Time to write. Keep total under ~600 lines.
Sound: WebAudio blips on brick break (pitch rising with combo), paddle hit, lose life. Create AudioContext on first user gesture. Demo mode: silent until gesture. Fine.
Write the code.
Edge: prevent scroll on touch. touchmove preventDefault.
Mouse move: convert clientX to logical coords.
Implementation plan:
```js
const cv=document.getElementById('c'), ctx=cv.getContext('2d');
let scale=1, ox=0, oy=0;
function resize(){ ... }
window.addEventListener('resize',resize);
```
State object.
Bricks array of {x,y,w,h,hp,maxHp,color}.
Particles array, floats array (score popups), pickups array.
Ball: {x,y,vx,vy,r}. Start stuck to paddle until launch (in demo, auto-launch after 0.8s; in player, launch on click/space? Simpler: launch on click or after 1s auto). I'll have ball stuck to paddle, launched on click/space or automatically after 1.2s in both modes.
Update loop with requestAnimationFrame.
Draw background: gradient + subtle checkerboard + vignette.
Let me write final code now.
Colors:
- bg gradient: #2a1a3a → #1a0f28? Cute diner: warm cream? Let's do a dusk pink: top #ffd9e8 → #ffb3c6? Bright candy bg with dark text HUD. Or dark bg with neon bricks pops. I'll go dark plum background #1b1030 gradient to #2d1b4e, with subtle checkerboard, bricks in bright candy colors, hearts pink/red. Nice contrast.
HUD text: white, font bold.
Title screen: big "BURGER BREAKOUT" with hearts, subtitle "paddle is a burger · bricks are hearts", "Click or press any key to start". Demo playing behind with slight dim overlay.
Collision details:
Ball vs paddle: if ball.vy>0 and ball.y+r >= paddleY and ball.y+r <= paddleY+30 and ball.x within paddleX±w/2+r: reflect: vx = (ball.x - paddleX)/ (w/2) * maxAngle... compute angle = rel * 1.0 (max ~60°), speed preserved. Set ball.y = paddleY - r.
Brick collision: for each alive brick, circle-rect test; if hit, compute overlap on x and y, reflect on smaller penetration axis, damage brick, spawn hearts, particles.
Speed: base 5.4, +0.06 per brick up to 8.5.
Paddle w: 130 base, 180 wide for 8s.
Draw score popup: "heart burst"? just "+100".
Combo: consecutive bricks without paddle hit → multiplier 1..5; show "x3!" text.
Let's write.
Also serve: no console errors. Careful with all variables.
Let me now write the whole file.
```html
<!DOCTYPE html>
<html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>Heartbreak Burger — Breakout</title>
<style>...</style></head>
<body>
<canvas id="c"></canvas>
<script> ... </script>
</body></html>
```
CSS: body margin 0, overflow hidden, background #140a20, canvas display block.
JS full:
```js
'use strict';
const cv=document.getElementById('c'),ctx=cv.getContext('2d');
const LW=960,LH=620;
let SC=1,OX=0,OY=0;
function resize(){
const w=innerWidth,h=innerHeight;
SC=Math.min(w/LW,h/LH);
OX=(w-LW*SC)/2; OY=(h-LH*SC)/2;
cv.width=w*devicePixelRatio; cv.height=h*devicePixelRatio;
cv.style.width=w+'px'; cv.style.height=h+'px';
ctx.setTransform(devicePixelRatio*SC,0,0,devicePixelRatio*SC,devicePixelRatio*OX,devicePixelRatio*OY);
}
```
Hmm, but drawing background outside canvas: canvas covers whole window, background outside game area drawn in screen coords. I'll draw the page bg in CSS and draw the game area rect with border. Fill whole canvas with bg color in identity transform, then set transform for game.
Simplify: in render, first ctx.save(); ctx.setTransform(1,0,0,1,0,0); fill bg; restore; then draw game with transform.
Actually resize sets transform each frame? Set transform each frame in render to be safe.
Game state:
```js
let mode='demo'; // 'demo','play','over'
let score=0,lives=3,level=1,combo=0;
let shake=0;
const paddle={x:LW/2,w:130,vx:0,wideT:0};
let ball={x:0,y:0,vx:0,vy:0,r:8,stuck:true};
let bricks=[],hearts=[],pops=[],pickups=[];
let speed=5.4;
```
buildLevel(n): rows = 4+min(n,3), cols=10.
Brick colors by hp: hp>=3 '#ff5c8a', 2 '#ffb347', 1 '#7ae7c7'. Actually assign per row: row colors cycle.
hp: level 1: mostly 1; rows 0-1 hp2 for level>=2.
Hearts particles: {x,y,vx,vy,rot,vr,s,life,color}.
spawnHearts(x,y,colorbase,n): 20-26 particles, vx random -3.5..3.5, vy -5..1, gravity 0.18, life 1 (decay 0.012), colors from ['#ff5c8a','#ff8fb1','#ff2e63','#ffffff','#ffd166'].
Pops: {x,y,txt,life,vy}.
Pickups: {x,y,vy,type:'heart'|'star'}.
Update:
```js
function update(dt){
// dt ~1 at 60fps
// paddle
if(mode==='demo'){
const target=ball.stuck?LW/2:ball.x+ball.vx*10;
paddle.x+=(target-paddle.x)*0.06*dt;
} else {
// keyboard
if(keys.left) paddle.x-=9*dt;
if(keys.right) paddle.x+=9*dt;
}
paddle.x=clamp(paddle.x,paddle.w/2+8,LW-paddle.w/2-8);
if(paddle.wideT>0)paddle.wideT-=dt;
paddle.w = (paddle.wideT>0?190:130);
// ball
if(ball.stuck){
ball.x=paddle.x; ball.y=paddleY-14;
ball.t+=dt;
if(ball.t>1.1) launch();
} else {
ball.x+=ball.vx*dt; ball.y+=ball.vy*dt;
// walls
if(ball.x<8+r){ball.x=8+r;ball.vx=Math.abs(ball.vx);}
...
if(ball.y<8+r){...}
if(ball.y>LH+20){ loseLife(); }
// paddle
...
// bricks
...
}
particles, pickups, pops update
}
```
Brick collision loop:
```js
for(const b of bricks){
if(b.hp<=0)continue;
const cx=clamp(ball.x,b.x,b.x+b.w), cy=clamp(ball.y,b.y,b.y+b.h);
const dx=ball.x-cx, dy=ball.y-cy;
if(dx*dx+dy*dy<=ball.r*ball.r){
// reflect axis
const ox1=ball.x - (ball.x<b.x? ... )
```
Simplify: compute overlap:
```
const px=(ball.x<b.x+b.w/2)? ...
```
Standard approach: determine penetration depth on each axis:
```js
const overlapX = ball.r - Math.abs(dx);
const overlapY = ball.r - Math.abs(dy);
```
But dx,dy are from closest point; if ball center inside rect, dx=dy=0. Use: if ball center within rect horizontally, dx=0 → vertical hit. Determine reflect axis: if dx===0&&dy===0 (center inside), flip vy. Else if |dx|>|dy| flip vx else flip vy. Then push ball out.
Then hitBrick(b): hp--, spawn hearts (n=22 if destroyed else 8), score+=10*comboMult, combo++, pops, sound, if hp>0 keep, else remove. Also small chance to spawn pickup (heart 8%, star 6%).
Speed up: speed=Math.min(9, speed+0.05).
loseLife(): if mode==='demo' just reset ball (no life loss). Else lives--; shake=12; if lives<=0 gameOver else reset ball.
gameOver(): mode='over', show overlay.
Win: bricks.length===0 → level++, buildLevel, resetBall, speed up.
Pickups: heart → lives++ (cap 5), pop "+1 LIFE"; star → paddle.wideT=480, pop "MEGA BURGER".
Draw:
drawHamburger(x,y,w): center x, top y. Layers heights: total h=46. Let py = paddleY (=LH-64). Draw:
- bottom bun: rounded rect from y+30 to y+42, w-16, color #e8a33d.
- patty: rounded rect y+20..y+32, w-8, color #7a4a2b.
- cheese: yellow #ffd166 trapezoid y+18..y+24 with drips.
- lettuce: green #7ae7c7 wavy, y+12..y+22.
- top bun: dome from y+? top bun occupies y..y+16: arc.
Let me define: top of paddle at yTop. Layers:
- top bun: yTop..yTop+16 (dome: rounded rect with big radius top).
- lettuce: yTop+14..yTop+22 wavy.
- cheese: yTop+18..yTop+26.
- patty: yTop+24..yTop+38.
- bottom bun: yTop+36..yTop+48.
Tilt: rotate by paddle.vx*0.02 clamped.
Sesame seeds: few small ellipses on top bun.
Brick drawing: rounded rect, fill gradient, top gloss line, small heart icon in center (white translucent), border darker.
Bricks colors: rows palette: ['#ff5c8a','#ffa94d','#ffe066','#63e6be','#74c0fc','#e599f7'].
Draw ball: white circle with pink glow, plus trail of previous positions.
Draw hearts particles.
HUD: top bar: "SCORE 0000", lives as small hearts, "LEVEL 1", and in demo mode tag "DEMO MODE — AI playing".
Overlays: draw on canvas (so it's all in one). Demo overlay: dim rect rgba(20,10,35,0.55), title "BURGER BREAKOUT" big, sub hearts emoji drawn? Use text "♥" shapes. Title: two lines "BURGER" "BREAKOUT"? Use "BURGER BREAKOUT ♥". Sub: "The paddle is a hamburger. Every brick explodes into hearts." Then "Click or press any key to start".
Use canvas text with system fonts: `font='900 64px system-ui, sans-serif'`.
Draw a big hamburger illustration on the title screen? The paddle is visible behind; maybe draw a big burger center-left with hearts around. Nice touch: draw a large burger at center of title with floating hearts. I'll draw a decorative burger using drawHamburger scaled.
Draw heart path helper:
```js
function heartPath(s){
ctx.beginPath();
ctx.moveTo(0,0.35*s);
ctx.bezierCurveTo(-0.55*s,-0.15*s,-0.35*s,-0.55*s,0,-0.2*s);
ctx.bezierCurveTo(0.35*s,-0.55*s,0.55*s,-0.15*s,0,0.35*s);
ctx.closePath();
}
```
Check: at (0,0.35) bottom tip; curve to left up to (0,-0.2) top dip; then right. Good.
Input:
- click on canvas: if mode==='demo' → startGame(); if mode==='over' → startGame(); if play → launch ball if stuck.
- keydown: same; arrows/space control in play.
- mousemove: if mode==='play', paddle.x = mouseX logical.
Also touch: touchmove maps to paddle.
startGame(): mode='play', score=0,lives=3,level=1,buildLevel(1),resetBall(),speed=5.4, hide overlay.
Audio:
```js
let AC=null;
function beep(freq,dur,type,vol){ if(!AC)return; try{...}catch(e){} }
function initAudio(){ if(!AC){try{AC=new (window.AudioContext||window.webkitAudioContext)();}catch(e){}} }
```
initAudio on first gesture.
Sounds: brick break: short square blip freq 300+combo*40; paddle: 220 sine; life lost: descending; pickup: two-tone.
Now write everything carefully. Also requestAnimationFrame loop with dt clamp.
Demo AI: paddle.x += (target-paddle.x)*0.08*dt; also add slight wobble so it misses sometimes? Keep it mostly succeeding; if loses ball, reset (no life lost in demo). Fine.
Also demo should reset ball after losing: resetBall().
Score popups: pops.push({x,y,txt,life:1}).
Let me also add screen shake on life lost: shake=10, offset random.
Draw order: bg, bricks, pickups, hearts particles, paddle burger, ball, pops, HUD, overlay.
Overlay drawing: if mode==='demo' or 'over', draw dim + text. For 'over': "GAME OVER" + score + "Click to try again".
Also demo: show "DEMO" pulsing.
Let me write final code now. Careful about rAF loop and dt.
```js
let last=performance.now();
function frame(now){
const dt=Math.min(2.5,(now-last)/16.667); last=now;
update(dt); render();
requestAnimationFrame(frame);
}
```
Write it.
For text rendering crispness: fine.
Now write everything. Also add small "hearts" ambient floating in background? Optional; skip to reduce code. Maybe add a few ambient hearts in bg that drift. Add small array of 14 ambient hearts. Cheap and cute. Yes.
Now, drawing bricks: for nice look, draw shadow, then body, then gloss, then mini heart.
HUD lives: draw hearts at top-left of HUD? HUD top: score left, level center, lives right. HUD height ~40; bricks start y=70.
Bricks: top=64, brickH=26, gap=6, rows=6 max → 6*26+5*6=186, ends at 250. Good.
Ball speed after paddle hit: keep speed, direction from rel.
launch(): angle = -Math.PI/2 + (rand-0.5)*0.6; vx=cos*speed, vy=sin*speed.
Paddle collision: rel = clamp((ball.x-paddle.x)/(paddle.w/2),-1,1); angle = rel*1.05 (max ~60°); vx = Math.sin(angle)*speed; vy = -Math.cos(angle)*speed; ball.y = paddleY - ball.r - 1; combo=0; sound.
paddleY = LH-64.
Ball can go below? lose when ball.y - r > LH.
Let me write drawHamburger:
```js
function drawBurger(x,y,w,tilt){
ctx.save();
ctx.translate(x,y); ctx.rotate(tilt);
const w2=w/2;
// bottom bun
rr(-w2+6,36,w-12,14,7); fill '#f0a24a' (bun) with darker bottom
// patty
rr(-w2+2,24,w-4,15,7); fill '#7c4a2d'; plus darker edge
// cheese
ctx.fillStyle='#ffd166';
path: trapezoid from (-w2+4,26) to (w2-4,26) to (w2-14,34) to (-w2+14,34)? with drips: draw small triangles below.
Actually cheese drips: draw rects hanging.
// lettuce
ctx.fillStyle='#6ee7a0'; scalloped edge: series of arcs.
// top bun
rr(-w2, 0, w, 18, {tl:9,tr:9,bl:0,br:0}) fill '#f7b855'; seeds.
ctx.restore();
}
```
rr helper: rounded rect path.
```js
function rr(x,y,w,h,r){ctx.beginPath();ctx.moveTo(x+r,y);ctx.arcTo(x+w,y,x+w,y+h,r);ctx.arcTo(x+w,y+h,x,y+h,r);ctx.arcTo(x,y+h,x,y,r);ctx.arcTo(x,y,x+w,y,r);ctx.closePath();}
```
Lettuce scallop: draw along y=20: path from left to right with arcs bulging downward. Simplify: draw a series of circles r=6 at y=22 spaced, fill green, plus a rect y=16..22. Then patty drawn over it. Order: lettuce circles behind patty? Draw order: bottom bun, patty, cheese, lettuce, top bun. Lettuce should peek below top bun: draw lettuce blobs at y=18..26, then top bun over y=0..18. So order: bottomBun(y36..50), patty(y24..38), cheese(y20..30 with drips to 34), lettuce blobs(cy=24, r=7), top bun(y0..18). Hmm cheese above patty. Let me just do:
Order bottom→top:
1. bottom bun y=36..50
2. patty y=26..38
3. cheese y=22..30 with drips (triangles) hanging to y=34 over patty
4. lettuce blobs cy=20 (overlapping top bun edge)
5. top bun y=0..18
Total height 50, paddle width 130. Good.
Seeds: 5 ellipses at y=5, x=-w/2+20..+w/2-20, rotate slight, color '#ffe9c4'.
Ball trail: array of {x,y} push each frame, keep 10, draw fading circles.
Draw pickups: heart with slight bob, star (5-point) for mega.
Star path: standard.
OK, also "pops" text render.
Now the ambient hearts: 12 items {x,y,s,vy(0.1..0.3),alpha,color} wrap around.
Also want subtle vignette: radial gradient overlay.
Write final code. Also handle "keys" object: ArrowLeft/Right, a/d.
Also Space to launch.
Demo shouldn't respond to mousemove (it's AI). Fine.
Also handle ball stuck to paddle in demo: paddle at center, ball launches after 1.1s.
Potential issue: when mode==='over', still update demo? Keep bricks frozen, particles still update. In update, if mode==='over', skip ball/paddle logic but update particles. I'll structure: update always particles/pops/pickups; ball/paddle logic only when mode!=='over'.
Also for demo mode, dim overlay should still show the game clearly: overlay alpha 0.5.
Let me count bricks: 10 cols × 5 rows = 50 bricks at level1. Score display 4 digits.
Alright — write it.
One more: "no horizontal scrollbar" — body overflow hidden.
Write the code now, carefully, checking variable names.
Also render function:
```js
function render(){
ctx.setTransform(1,0,0,1,0,0);
ctx.fillStyle='#150b26'; ctx.fillRect(0,0,cv.width,cv.height);
ctx.setTransform(dpr*SC,0,0,dpr*SC,dpr*OX,dpr*OY);
// shake
if(shake>0){ctx.translate((Math.random()-0.5)*shake,(Math.random()-0.5)*shake);}
...
}
```
dpr = window.devicePixelRatio||1.
Clip to game area? When shake, translate may exceed; fine, clip to rect LW×LH. I'll clip: ctx.save(); ctx.beginPath(); ctx.rect(0,0,LW,LH); ctx.clip(); ... draw game; restore. Then draw HUD inside too. Overlay drawn after restore (no clip needed but inside bounds anyway).
Let me write the final HTML.
Also add small CSS for cursor: crosshair? default.
Now write:
```js
const keys={};
addEventListener('keydown',e=>{
if(['ArrowLeft','ArrowRight','ArrowUp','ArrowDown',' '].includes(e.key))e.preventDefault();
keys[e.key]=true; initAudio();
if(mode==='demo'||mode==='over')startGame();
else if(e.key===' '&&ball.stuck)launch();
});
addEventListener('keyup',e=>keys[e.key]=false);
cv.addEventListener('mousedown',e=>{initAudio(); if(mode==='demo'||mode==='over')startGame(); else if(ball.stuck)launch();});
cv.addEventListener('mousemove',e=>{ if(mode==='play'){const r=cv.getBoundingClientRect(); const lx=(e.clientX-r.left)/... }});
```
mouse mapping: lx = (e.clientX - OX)/SC. Since OX/SC are CSS px, fine.
touch:
```js
cv.addEventListener('touchstart',e=>{e.preventDefault();initAudio(); if(mode==='demo'||mode==='over')startGame(); else if(ball.stuck)launch(); const t=e.touches[0]; ...set paddle});
cv.addEventListener('touchmove',e=>{e.preventDefault(); set paddle});
```
Note: mousemove uses clientX; need mapping to logical: lx=(e.clientX-rect.left)/SC - OX/SC? Let's compute: canvas spans full window; logical coords: x=(e.clientX-OX)/SC. Yes.
Now buildLevel:
```js
function buildLevel(n){
bricks=[];
const rows=Math.min(4+n,7), cols=10;
const gap=6, bw=(LW-40-(cols-1)*gap)/cols, bh=26, top=64;
for(let r=0;r<rows;r++)for(let c=0;c<cols;c++){
const hp = (r<2 && n>=2)?2:1;
const x=20+c*(bw+gap), y=top+r*(bh+gap);
bricks.push({x,y,w:bw,h:bh,hp,max:hp,color:PALETTE[r%PALETTE.length]});
}
}
```
PALETTE=['#ff5c8a','#ffa94d','#ffe066','#63e6be','#74c0fc','#e599f7','#f783ac'].
Draw brick with hp bar: draw hp>1 as darker inner? Show hp via small dots or just color intensity. For hp2 bricks, draw heart outline. Simple: draw mini heart centered white alpha 0.35 when hp>=2; full alpha when hp... fine.
hitBrick:
```js
function hitBrick(b){
const multi = combo>=1? 1+Math.min(combo,8)*0.5 : 1;
...
}
```
Simplify: points = 10 * (1+combo). combo increments per brick broken until paddle hit.
```js
b.hp--;
if(b.hp<=0){
bricks.splice(idx,1);
spawnHearts(cx,cy,22);
score+= (10+combo*5);
pops.push({x:b.x+b.w/2,y:b.y,txt:'+'+(10+combo*5),life:1});
combo++;
if(Math.random()<0.09)pickups.push({x:cx,y:cy,vy:1.6,type:Math.random()<0.5?'heart':'star'});
speed=Math.min(8.5,speed+0.05);
sfx(300+Math.min(combo,10)*45,0.06,'square',0.05);
}else{
spawnHearts(cx,cy,8);
score+=5;
sfx(180,0.05,'square',0.04);
}
```
splice: use index loop.
spawnHearts:
```js
function spawnHearts(x,y,n){
for(let i=0;i<n;i++){
const a=Math.random()*Math.PI*2, sp=1+Math.random()*4;
hearts.push({x,y,vx:Math.cos(a)*sp,vy:Math.sin(a)*sp-2.5,rot:Math.random()*6.28,vr:(Math.random()-0.5)*0.3,s:5+Math.random()*9,life:1,dec:0.008+Math.random()*0.01,color:HEARTCOLS[(Math.random()*HEARTCOLS.length)|0]});
}
}
```
HEARTCOLS=['#ff2e63','#ff5c8a','#ff8fb1','#ffd166','#ffffff','#ff5c8a','#ff5c8a'].
Update hearts: vy+=0.16*dt; x+=vx*dt; y+=vy*dt; rot+=vr*dt; life-=dec*dt; remove when life<=0 or y>LH+40.
Draw heart: ctx.globalAlpha=life; fill; heartPath(s); fill.
Also hearts have a slight white outline? skip.
Pops: life-=0.02*dt; y-=0.6*dt; draw text with alpha life.
Pickups update: y+=vy*dt; vy+=0.02*dt; catch if y> paddleY-10 && |x-paddle.x|<paddle.w/2+10 → apply; remove if y>LH+20.
apply pickup: heart → lives=Math.min(5,lives+1), pop "+1 ♥"; star → paddle.wideT=480, pop "MEGA BURGER!".
Now sounds:
```js
function sfx(f,d,type,v){
if(!AC)return;
try{
const o=AC.createOscillator(),g=AC.createGain();
o.type=type;o.frequency.value=f;
g.gain.setValueAtTime(v,AC.currentTime);
g.gain.exponentialRampToValueAtTime(0.001,AC.currentTime+d);
o.connect(g);g.connect(AC.destination);
o.start();o.stop(AC.currentTime+d);
}catch(e){}
}
```
In demo mode before gesture, AC null → silent. Good.
loseLife():
```js
if(mode==='demo'){resetBall();return;}
lives--; shake=14; combo=0; sfx(140,0.3,'sawtooth',0.06);
if(lives<=0){mode='over';}
else resetBall();
```
resetBall(): ball.stuck=true; ball.t=0; speed=5.4? Keep speed at current level base. Reset speed to baseSpeed+level*0.3.
launch(): ball.stuck=false; angle = -PI/2 + (Math.random()-0.5)*0.7; vx=Math.cos(angle)*speed; vy=Math.sin(angle)*speed;
Win check: after splice, if bricks.length===0 → level++; buildLevel(level); resetBall(); speed=5+level*0.4; pops "LEVEL UP!".
render overlay:
```js
function drawOverlay(){
if(mode==='demo'){
ctx.fillStyle='rgba(18,8,32,0.55)'; ctx.fillRect(0,0,LW,LH);
// title
ctx.textAlign='center';
ctx.fillStyle='#fff';
ctx.font='900 68px system-ui,sans-serif';
ctx.fillText('BURGER BREAKOUT',LW/2,150);
// hearts
...
ctx.font='700 20px system-ui';
ctx.fillText('The paddle is a hamburger. Every brick explodes into hearts.',LW/2,210);
// big burger illustration
drawBurger(LW/2, 300, 200, 0); // scaled
// pulsing start
const p=0.5+0.5*Math.sin(performance.now()/400);
ctx.globalAlpha=0.6+0.4*p;
ctx.font='800 30px system-ui';
ctx.fillText('CLICK OR PRESS ANY KEY TO START',LW/2,540);
ctx.globalAlpha=1;
ctx.font='600 16px system-ui'; ctx.fillStyle='rgba(255,255,255,0.7)';
ctx.fillText('DEMO — the AI is playing while you wait',LW/2,580);
}
}
```
drawBurger with scale: my drawBurger assumes w=130 baseline; for w=200 it scales fine since layers are relative to w. But heights fixed. Let me make drawBurger accept w and h scale factor: I'll parameterize: drawBurger(x,y,w,tilt,hs) where hs scales heights. Simpler: drawBurger(x,y,w,tilt) with layer heights proportional to w/130*... hmm. Let me define k = w/130; all layer heights multiplied by k. I'll write drawBurger with internal scale: ctx.translate(x,y); ctx.scale(k,k); then draw with base w=130.
```js
function drawBurger(x,y,w,tilt){
const k=w/130;
ctx.save();ctx.translate(x,y);ctx.rotate(tilt);ctx.scale(k,k);
// base coords: x from -65..65, y 0..50
...
ctx.restore();
}
```
y is top of burger. Good.
Now HUD:
```js
function drawHUD(){
ctx.font='800 20px system-ui';
ctx.fillStyle='#fff';
ctx.textAlign='left';
ctx.fillText('SCORE '+score,20,32);
ctx.textAlign='center';
ctx.fillText('LEVEL '+level,LW/2,32);
// lives
for(let i=0;i<lives;i++){ctx.save();ctx.translate(LW-30-i*26,25);ctx.fillStyle='#ff5c8a';heartPath(12);ctx.fill();ctx.restore();}
if(mode==='demo'){ctx.fillStyle='rgba(255,255,255,0.4)';ctx.font='700 14px';ctx.fillText('DEMO',LW/2,52)?}
}
```
Hmm HUD y=32 fine, bricks start y=64.
Now the demo AI target: ball.x + ball.vx*12 (predictive). Add slight randomness: target = ball.x + ball.vx*10. Fine.
Also in demo, when ball stuck, paddle follows ball.x which is paddle.x — ok.
Handle paddle collision with ball when ball moving down and within expanded paddle area:
```js
if(!ball.stuck && ball.vy>0 && ball.y+ball.r>=paddleY && ball.y+ball.r<=paddleY+26 && Math.abs(ball.x-paddle.x)<=paddle.w/2+ball.r){
const rel=clamp((ball.x-paddle.x)/(paddle.w/2),-1,1);
const ang=rel*1.05;
ball.vx=Math.sin(ang)*speed; ball.vy=-Math.cos(ang)*speed;
ball.y=paddleY-ball.r-1;
combo=0;
sfx(220,0.06,'sine',0.05);
}
```
paddleY = LH-64. burger height 50*k where k=130/130=1 → 50px tall, top at paddleY → bottom at paddleY+50. Collision top at paddleY. Fine.
Ball spawn: resetBall sets ball.x=paddle.x, ball.y=paddleY-14.
Also demo: ball stuck → paddle follows ball.x = paddle.x → no movement; fine.
Edge: ball may hit side walls of bricks etc. fine.
Also add slight "squash" on ball? skip.
Trail: store trail positions each frame when not stuck: trail.push({x,y}); if(trail.length>12)trail.shift(); draw with decreasing alpha.
Ambient hearts: init 12, update: y+=vy*dt; if y>LH+20 reset y=-20, x random. Draw with alpha 0.12.
Now write the render of bg:
```js
// bg
const g=ctx.createLinearGradient(0,0,0,LH);
g.addColorStop(0,'#241040'); g.addColorStop(1,'#3a1750');
ctx.fillStyle=g; ctx.fillRect(0,0,LW,LH);
// checkerboard subtle
ctx.fillStyle='rgba(255,255,255,0.025)';
for(let y=0;y<LH;y+=40)for(let x=(y/40)%2*40...
```
Simpler: draw squares: for(i=0;i<LH/40;i++)for(j=0;j<LW/40;j++) if((i+j)%2) fillRect. 24*16=384 rects — fine.
Then vignette: radial gradient centered, transparent to rgba(0,0,0,0.35).
Also maybe a top border line? Skip.
Draw title-screen decorative hearts around burger: a few hearts floating, static positions with pulse.
Let's finalize. Also draw side walls: thin lines at x=0.. maybe skip.
Let me write the final code now — full file.
Careful: `heartPath` uses ctx path; called with fill.
Check star path:
```js
function starPath(s){
ctx.beginPath();
for(let i=0;i<10;i++){
const r=i%2? s*0.45:s;
const a=Math.PI/5*i - Math.PI/2;
ctx[i?'lineTo':'moveTo'](Math.cos(a)*r, Math.sin(a)*r);
}
ctx.closePath();
}
```
Pickups draw: star → '#ffd166' with glow; heart → '#ff5c8a'.
OK writing final code. Also ensure no errors: define everything before use.
Also: `paddle.tilt` computed from paddle.vx; track prevX to compute vx: paddle.vx=(paddle.x-prevX)/dt smoothed.
Let me write:
```js
let prevX=paddle.x;
in update: paddle.vx=(paddle.x-prevX)*0.5+paddle.vx*0.5; prevX=paddle.x;
tilt = clamp(paddle.vx*0.02, -0.12, 0.12);
```
Now full code. Also add a score counter for demo mode? In demo, HUD shows score too — fine.
Let me write it out.
Also, "bricks" should be drawn with slight 3D: shadow offset 3px dark.
Write code:
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,user-scalable=no">
<title>Heartbreak Burger — Breakout</title>
<style>
html,body{margin:0;padding:0;height:100%;overflow:hidden;background:#0e0618;}
canvas{display:block;width:100%;height:100%;touch-action:none;cursor:crosshair;}
</style>
</head>
<body>
<canvas id="c"></canvas>
<script>
'use strict';
/* ... */
</script>
</body>
</html>
```
Now JS:
```js
const cv=document.getElementById('c'),ctx=cv.getContext('2d');
const LW=960,LH=620;
let dpr=1,SC=1,OX=0,OY=0;
function resize(){
dpr=window.devicePixelRatio||1;
const w=window.innerWidth,h=window.innerHeight;
SC=Math.min(w/LW,h/LH);
OX=(w-LW*SC)/2; OY=(h-LH*SC)/2;
cv.width=Math.round(w*dpr); cv.height=Math.round(h*dpr);
cv.style.width=w+'px'; cv.style.height=h+'px';
}
window.addEventListener('resize',resize);
resize();
const clamp=(v,a,b)=>v<a?a:v>b?b:v;
const PALETTE=['#ff5c8a','#ffa94d','#ffe066','#63e6be','#74c0fc','#e599f7'];
const HEARTCOLS=['#ff2e63','#ff5c8a','#ff8fb1','#ffd166','#ffffff','#ff5c8a','#ff5c8a'];
let mode='demo';
let score=0,lives=3,level=1,combo=0,shake=0,speed=5.4;
const paddle={x:LW/2,w:130,vx:0,prevX:LW/2,wideT:0};
const paddleY=LH-64;
const ball={x:paddle.x,y:paddleY-14,vx:0,vy:0,r:8,stuck:true,t:0};
let bricks=[],hearts=[],pops=[],pickups=[],trail=[];
let keys={};
```
buildLevel, resetBall, launch, spawnHearts, hitBrick, etc.
update function as designed.
Also demo mode: after ball lost, resetBall automatically.
Write update:
```js
function update(dt){
if(shake>0)shake=Math.max(0,shake-0.6*dt);
// ambient hearts
for(const a of ambient){a.y+=a.vy*dt;a.x+=Math.sin(a.y*0.01)*0.2*dt;if(a.y>LH+30){a.y=-30;a.x=Math.random()*LW;}}
// particles
for(let i=hearts.length-1;i>=0;i--){
const p=hearts[i];
p.vy+=0.16*dt;p.x+=p.vx*dt;p.y+=p.vy*dt;p.rot+=p.vr*dt;p.life-=p.dec*dt;
if(p.life<=0||p.y>LH+40)hearts.splice(i,1);
}
for(let i=pops.length-1;i>=0;i--){const p=pops[i];p.y-=0.7*dt;p.life-=0.02*dt;if(p.life<=0)pops.splice(i,1);}
// pickups
for(let i=pickups.length-1;i>=0;i--){
const pk=pickups[i];pk.y+=pk.vy*dt;pk.vy+=0.02*dt;
if(pk.y>paddleY-8&&Math.abs(pk.x-paddle.x)<paddle.w/2+12){
if(pk.type==='heart'){lives=Math.min(5,lives+1);pops.push({x:pk.x,y:pk.y,txt:'+1 LIFE',life:1});sfx(660,0.12,'sine',0.06);}
else{paddle.wideT=480;pops.push({x:pk.x,y:pk.y,txt:'MEGA BURGER!',life:1});sfx(880,0.15,'sine',0.06);}
pickups.splice(i,1);continue;
}
if(pk.y>LH+20)pickups.splice(i,1);
}
if(mode==='over')return;
// paddle
if(mode==='demo'){
const target=ball.stuck?LW/2:ball.x+ball.vx*11;
paddle.x+=(target-paddle.x)*0.07*dt;
}else{
if(keys['ArrowLeft']||keys['a']||keys['A'])paddle.x-=9*dt;
if(keys['ArrowRight']||keys['d']||keys['D'])paddle.x+=9*dt;
}
paddle.x=clamp(paddle.x,paddle.w/2+6,LW-paddle.w/2-6);
paddle.vx=clamp((paddle.x-paddle.prevX)*0.5+paddle.vx*0.5,-6,6);
paddle.prevX=paddle.x;
if(paddle.wideT>0)paddle.wideT-=dt;
const targetW=paddle.wideT>0?190:130;
paddle.w+=(targetW-paddle.w)*0.15*dt;
// ball
if(ball.stuck){
ball.x=paddle.x;ball.y=paddleY-ball.r-2;
ball.t+=dt;
if(ball.t>1.1)launch();
}else{
ball.x+=ball.vx*dt;ball.y+=ball.vy*dt;
trail.push({x:ball.x,y:ball.y});
if(trail.length>12)trail.shift();
if(ball.x<ball.r+4){ball.x=ball.r+4;ball.vx=Math.abs(ball.vx);}
if(ball.x>LW-ball.r-4){ball.x=LW-ball.r-4;ball.vx=-Math.abs(ball.vx);}
if(ball.y<ball.r+4){ball.y=ball.r+4;ball.vy=Math.abs(ball.vy);}
if(ball.y>LH+30){loseLife();return;}
// paddle
if(ball.vy>0&&ball.y+ball.r>=paddleY&&ball.y-ball.r<=paddleY+50&&Math.abs(ball.x-paddle.x)<=paddle.w/2+ball.r){
const rel=clamp((ball.x-paddle.x)/(paddle.w/2),-1,1);
const ang=rel*1.05;
ball.vx=Math.sin(ang)*speed;
ball.vy=-Math.cos(ang)*speed;
ball.y=paddleY-ball.r-1;
combo=0;
sfx(240,0.07,'sine',0.05);
}
// bricks
for(let i=bricks.length-1;i>=0;i--){
const b=bricks[i];
const cx=clamp(ball.x,b.x,b.x+b.w),cy=clamp(ball.y,b.y,b.y+b.h);
const dx=ball.x-cx,dy=ball.y-cy;
if(dx*dx+dy*dy<=ball.r*ball.r){
// reflect
if(Math.abs(dx)>Math.abs(dy))ball.vx=dx>0?Math.abs(ball.vx):-Math.abs(ball.vx);
else ball.vy=dy>0?Math.abs(ball.vy):-Math.abs(ball.vy);
// push out
ball.x+=ball.vx*0.5; ball.y+=ball.vy*0.5;
hitBrick(b,i,cx,cy);
}
}
}
}
```
Wait pushing out: after reflection, ball may still be inside; move by small amount along velocity. Simple approach: ball.x=ball.x+... Let me just move the ball out along the normal: if flipped vx, set ball.x = cx + (dx>0? ball.r+0.1 : -ball.r-0.1)... but dx=0 case. Use: if flipped vx: ball.x = cx + (dx>=0?ball.r+0.2:-ball.r-0.2). Hmm if dx===0 (center aligned horizontally), then |dy|>|dx| branch taken. OK.
Actually simpler: if(Math.abs(dx)>Math.abs(dy)) flip vx and set ball.x = cx + (dx>0?ball.r+0.1:-ball.r-0.1); else flip vy and set ball.y = cy + (dy>0?ball.r+0.1:-ball.r-0.1). But if dx===0 and dy===0 (center inside), both zero; choose flip vy. Fine.
hitBrick(b,i,cx,cy):
```js
function hitBrick(b,i,cx,cy){
b.hp--;
if(b.hp<=0){
bricks.splice(i,1);
spawnHearts(cx,cy,22);
const pts=10+combo*5;
score+=pts;
pops.push({x:cx,y:cy-6,txt:'+'+pts,life:1});
combo++;
if(Math.random()<0.09&&pickups.length<3)pickups.push({x:cx,y:cy,vy:1.6,type:Math.random()<0.5?'heart':'star'});
speed=Math.min(8.5,speed+0.05);
sfx(300+Math.min(combo,10)*45,0.07,'square',0.05);
if(bricks.length===0){
level++;
buildLevel(level);
speed=5+level*0.4;
resetBall();
pops.push({x:LW/2,y:LH/2,txt:'LEVEL '+level+'!',life:1.4});
sfx(520,0.2,'sine',0.07);
}
}else{
spawnHearts(cx,cy,8);
score+=5;
sfx(180,0.05,'square',0.04);
}
}
```
loseLife:
```js
function loseLife(){
if(mode==='demo'){resetBall();return;}
lives--;shake=14;combo=0;
sfx(130,0.35,'sawtooth',0.07);
if(lives<=0){mode='over';}
else resetBall();
}
```
resetBall:
```js
function resetBall(){
ball.stuck=true;ball.t=0;ball.vx=0;ball.vy=0;
ball.x=paddle.x;ball.y=paddleY-ball.r-2;
trail.length=0;
}
```
launch:
```js
function launch(){
ball.stuck=false;ball.t=0;
const ang=-Math.PI/2+(Math.random()-0.5)*0.7;
ball.vx=Math.cos(ang)*speed;ball.vy=Math.sin(ang)*speed;
}
```
startGame:
```js
function startGame(){
mode='play';score=0;lives=3;level=1;combo=0;speed=5.4;
buildLevel(1);resetBall();
}
```
buildLevel:
```js
function buildLevel(n){
bricks=[];
const rows=Math.min(4+n,7),cols=10,gap=6,bh=26,top=64;
const bw=(LW-40-(cols-1)*gap)/cols;
for(let r=0;r<rows;r++)for(let c=0;c<cols;c++){
const hp=(r<2&&n>=2)?2:(r<1&&n>=4)?3:1;
bricks.push({x:20+c*(bw+gap),y:top+r*(bh+gap),w:bw,h:bh,hp,max:hp,color:PALETTE[r%PALETTE.length]});
}
}
```
Wait hp calculation: level1 all hp1; level≥2: rows 0-1 hp2; level≥4: row0 hp3. Fine.
Render:
```js
function render(){
ctx.setTransform(1,0,0,1,0,0);
ctx.fillStyle='#0e0618';
ctx.fillRect(0,0,cv.width,cv.height);
ctx.setTransform(dpr*SC,0,0,dpr*SC,dpr*OX,dpr*OY);
ctx.save();
ctx.beginPath();ctx.rect(0,0,LW,LH);ctx.clip();
if(shake>0)ctx.translate((Math.random()-0.5)*shake,(Math.random()-0.5)*shake);
// bg
const g=ctx.createLinearGradient(0,0,0,LH);
g.addColorStop(0,'#241040');g.addColorStop(1,'#3d1a55');
ctx.fillStyle=g;ctx.fillRect(0,0,LW,LH);
ctx.fillStyle='rgba(255,255,255,0.03)';
for(let i=0;i<16;i++)for(let j=0;j<24;j++)if((i+j)%2)ctx.fillRect(j*40,i*40,40,40);
// ambient
for(const a of ambient){ctx.save();ctx.globalAlpha=a.al;ctx.translate(a.x,a.y);ctx.fillStyle=a.c;heartPath(a.s);ctx.fill();ctx.restore();}
// bricks
for(const b of bricks)drawBrick(b);
// pickups
for(const pk of pickups)drawPickup(pk);
// hearts particles
for(const p of hearts){
ctx.save();ctx.globalAlpha=Math.min(1,p.life);
ctx.translate(p.x,p.y);ctx.rotate(p.rot);
ctx.fillStyle=p.color;heartPath(p.s);ctx.fill();ctx.restore();
}
// trail
for(let i=0;i<trail.length;i++){
ctx.globalAlpha=0.15*(i/trail.length);
ctx.fillStyle='#ff8fb1';
ctx.beginPath();ctx.arc(trail[i].x,trail[i].y,ball.r*(i/trail.length),0,7);ctx.fill();
}
ctx.globalAlpha=1;
// paddle
const tilt=clamp(paddle.vx*0.02,-0.14,0.14);
drawBurger(paddle.x,paddleY,paddle.w,tilt);
// ball
ctx.save();
ctx.shadowColor='#ff5c8a';ctx.shadowBlur=18;
ctx.fillStyle='#fff';
ctx.beginPath();ctx.arc(ball.x,ball.y,ball.r,0,7);ctx.fill();
ctx.restore();
// pops
ctx.textAlign='center';ctx.font='800 16px system-ui,sans-serif';
for(const p of pops){
ctx.globalAlpha=Math.min(1,p.life);
ctx.fillStyle='#fff';ctx.fillText(p.txt,p.x,p.y);
}
ctx.globalAlpha=1;
ctx.restore(); // clip
drawHUD();
drawOverlay();
}
```
drawBrick:
```js
function drawBrick(b){
// shadow
ctx.fillStyle='rgba(0,0,0,0.35)';
rr(b.x+3,b.y+4,b.w,b.h,8);ctx.fill();
ctx.fillStyle=b.color;
rr(b.x,b.y,b.w,b.h,8);ctx.fill();
// gloss
ctx.fillStyle='rgba(255,255,255,0.25)';
rr(b.x+3,b.y+3,b.w-6,6,4);ctx.fill();
// mini heart
ctx.save();ctx.translate(b.x+b.w/2,b.y+b.h/2+2);
ctx.fillStyle='rgba(255,255,255,'+(0.35+0.25*b.hp/b.max)+')';
heartPath(9);ctx.fill();
ctx.restore();
}
```
drawPickup:
```js
function drawPickup(pk){
ctx.save();
const bob=Math.sin(performance.now()/200)*3;
ctx.translate(pk.x,pk.y+bob);
if(pk.type==='heart'){
ctx.shadowColor='#ff5c8a';ctx.shadowBlur=14;
ctx.fillStyle='#ff5c8a';heartPath(13);ctx.fill();
}else{
ctx.shadowColor='#ffd166';ctx.shadowBlur=14;
ctx.fillStyle='#ffd166';starPath(13);ctx.fill();
}
ctx.restore();
}
```
drawBurger:
```js
function drawBurger(x,y,w,tilt){
const k=w/130;
ctx.save();
ctx.translate(x,y);ctx.rotate(tilt);ctx.scale(k,k);
// shadow
ctx.fillStyle='rgba(0,0,0,0.35)';
rr(-65,44,130,10,5);ctx.fill();
// bottom bun
ctx.fillStyle='#f0a24a';
rr(-57,36,114,14,7);ctx.fill();
ctx.fillStyle='rgba(0,0,0,0.12)';
rr(-57,44,114,6,3);ctx.fill();
// patty
ctx.fillStyle='#7c4a2d';
rr(-62,24,124,15,7);ctx.fill();
ctx.fillStyle='#5d3220';
rr(-62,32,124,7,3);ctx.fill();
// cheese
ctx.fillStyle='#ffd166';
rr(-60,20,120,8,3);ctx.fill();
// drips
ctx.beginPath();
ctx.moveTo(-60,26);...
```
Drips: draw 4 small rects hanging from cheese: at x=-50,-20,10,40 width 6 height 6-10 with rounded bottom. Just fillRect with rounded bottom: use rr(x,26,6,9,3). Fine.
```js
for(const dx of [-52,-22,14,44]){rr(dx,26,7,9,3);ctx.fill();}
// lettuce
ctx.fillStyle='#6ee7a0';
ctx.beginPath();
for(let i=0;i<9;i++){
const cx=-58+i*14.5;
ctx.moveTo(cx,20);
ctx.arc(cx,20,8,0,Math.PI);
}
ctx.fill();
```
Hmm moveTo then arc — arcs at cy=20 with r=8, lower half (0..PI) → bulges down to y=28. But that goes below cheese start (20). Lettuce under top bun: blobs at y=16, r=8 → bulge to 24. Then top bun covers y=0..18. Order: draw lettuce after cheese, then top bun. So blobs at cy=18: from 18 to 26. Then top bun covers 0..18; visible lettuce 18-26 peeking. Good.
Then top bun:
```js
ctx.fillStyle='#f7b855';
rr(-65,0,130,18,9);ctx.fill();
// top bun shading
ctx.fillStyle='rgba(255,255,255,0.25)';
rr(-60,2,120,5,3);ctx.fill();
// seeds
ctx.fillStyle='#ffe9c4';
for(let i=0;i<7;i++){
const sx=-52+i*17.5;
ctx.beginPath();
ctx.ellipse(sx,7+((i%2)?3:0),3,2,0.3,0,7);
ctx.fill();
}
ctx.restore();
}
```
rr helper as defined.
drawHUD:
```js
function drawHUD(){
ctx.textAlign='left';
ctx.fillStyle='#fff';
ctx.font='800 20px system-ui,sans-serif';
ctx.fillText('SCORE '+score,20,32);
ctx.textAlign='center';
ctx.fillText('LEVEL '+level,LW/2,32);
for(let i=0;i<lives;i++){
ctx.save();ctx.translate(LW-26-i*26,24);
ctx.fillStyle='#ff5c8a';heartPath(11);ctx.fill();ctx.restore();
}
if(mode==='demo'){
ctx.fillStyle='rgba(255,255,255,0.45)';
ctx.font='700 13px system-ui,sans-serif';
ctx.textAlign='right';
ctx.fillText('DEMO MODE',LW-130,30);
}
}
```
Wait lives hearts at LW-26-i*26 → first at LW-26. Demo text at LW-130 might collide. Fine, put demo text near left? Put at LW/2,52? Actually below level. Put demo tag at LW/2, 52. Hmm bricks start at 64. OK 52 works.
drawOverlay:
```js
function drawOverlay(){
if(mode==='play')return;
ctx.save();
ctx.textAlign='center';
if(mode==='demo'){
ctx.fillStyle='rgba(16,6,30,0.55)';
ctx.fillRect(0,0,LW,LH);
// floating hearts
const t=performance.now()/1000;
for(let i=0;i<6;i++){
const hx=LW/2+Math.cos(i*1.1+t*0.7)*200, hy=250+Math.sin(i*1.7+t*0.9)*60;
ctx.save();ctx.translate(hx,hy);ctx.globalAlpha=0.5;
ctx.fillStyle=HEARTCOLS[i%HEARTCOLS.length];heartPath(8+Math.sin(t+i)*3);ctx.fill();ctx.restore();
}
ctx.globalAlpha=1;
ctx.fillStyle='#fff';
ctx.font='900 64px system-ui,sans-serif';
ctx.fillText('BURGER',LW/2,120);
ctx.fillText('BREAKOUT',LW/2,190);
// big burger
drawBurger(LW/2,320,220,0);
// hearts burst around burger
...
const p=0.6+0.4*Math.sin(performance.now()/300);
ctx.globalAlpha=p;
ctx.fillStyle='#fff';
ctx.font='800 30px system-ui,sans-serif';
ctx.fillText('CLICK OR PRESS ANY KEY TO START',LW/2,560);
ctx.globalAlpha=1;
ctx.fillStyle='rgba(255,255,255,0.6)';
ctx.font='600 15px system-ui,sans-serif';
ctx.fillText('demo — the AI is playing while you wait',LW/2,590);
}else{
ctx.fillStyle='rgba(16,6,30,0.7)';
ctx.fillRect(0,0,LW,LH);
ctx.fillStyle='#ff5c8a';
ctx.font='900 56px system-ui,sans-serif';
ctx.fillText('GAME OVER',LW/2,240);
ctx.fillStyle='#fff';
ctx.font='800 26px system-ui,sans-serif';
ctx.fillText('SCORE '+score,LW/2,300);
const p=0.6+0.4*Math.sin(performance.now()/300);
ctx.globalAlpha=p;
ctx.fillText('CLICK OR PRESS ANY KEY TO TRY AGAIN',LW/2,380);
ctx.globalAlpha=1;
}
ctx.restore();
}
```
Demo title at y=120/190 with burger at 320 (top of burger) — burger height 50*k where k=220/130=1.69 → 84.5 tall. So burger spans 320..405. Then start text at 560. Between 405 and 560 there's space; add subtitle: "The paddle is a hamburger. Every brick explodes into little hearts." at y=430. Good.
Ambient hearts init:
```js
const ambient=[];
for(let i=0;i<14;i++)ambient.push({x:Math.random()*LW,y:Math.random()*LH,s:4+Math.random()*8,vy:0.15+Math.random()*0.35,al:0.08+Math.random()*0.1,c:HEARTCOLS[i%HEARTCOLS.length]});
```
Draw ambient — must be drawn each frame (they're in render bg section). Yes.
heartPath:
```js
function heartPath(s){
ctx.beginPath();
ctx.moveTo(0,0.4*s);
ctx.bezierCurveTo(-0.6*s,-0.1*s,-0.42*s,-0.6*s,0,-0.22*s);
ctx.bezierCurveTo(0.42*s,-0.6*s,0.6*s,-0.1*s,0,0.4*s);
ctx.closePath();
}
```
Check shape: start bottom tip (0,0.4s). Left curve: control (-0.6s,-0.1s) and (-0.42s,-0.6s) ending at (0,-0.22s) top dip. Right curve: control (0.42s,-0.6s),(0.6s,-0.1s) back to (0,0.4s). Looks like a heart. Good.
Frame loop:
```js
let last=performance.now();
function frame(now){
const dt=Math.min(2.5,(now-last)/16.667);
last=now;
update(dt);
render();
requestAnimationFrame(frame);
}
buildLevel(1);
requestAnimationFrame(frame);
```
Input handlers as planned.
Also mousemove:
```js
function toLogical(e){
const r=cv.getBoundingClientRect();
return {x:(e.clientX-r.left-OX)/SC, y:(e.clientY-r.top-OY)/SC};
}
cv.addEventListener('mousemove',e=>{
if(mode==='play'){const p=toLogical(e);paddle.x=p.x;}
});
cv.addEventListener('mousedown',()=>{initAudio();if(mode==='demo'||mode==='over')startGame();else if(ball.stuck)launch();});
cv.addEventListener('touchstart',e=>{
e.preventDefault();initAudio();
if(mode==='demo'||mode==='over')startGame();
else if(ball.stuck)launch();
const t=e.touches[0];if(t&&mode==='play'){const p=toLogical(t);paddle.x=p.x;}
},{passive:false});
cv.addEventListener('touchmove',e=>{
e.preventDefault();
const t=e.touches[0];if(t&&mode==='play'){const p=toLogical(t);paddle.x=p.x;}
},{passive:false});
```
keydown:
```js
window.addEventListener('keydown',e=>{
if(['ArrowLeft','ArrowRight','ArrowUp','ArrowDown',' '].includes(e.key))e.preventDefault();
keys[e.key]=true;
initAudio();
if(mode==='demo'||mode==='over')startGame();
else if(e.key===' '&&ball.stuck)launch();
});
window.addEventListener('keyup',e=>{keys[e.key]=false;});
```
initAudio:
```js
let AC=null;
function initAudio(){
if(AC)return;
try{AC=new (window.AudioContext||window.webkitAudioContext)();}catch(e){AC=null;}
}
```
sfx as planned.
Everything defined. Check no undefined vars: `clamp` defined. `keys` object. `trail` reset. `pops` etc.
One issue: In update, when mode==='over', we return before paddle update — fine, but pickups/pops still update (they run before the return). Good.
In demo, HUD "DEMO MODE" tag at LW/2,52 — but bricks also start at 64; fine.
Another: `ball.t` when stuck — set in resetBall. In update stuck branch: ball.t+=dt; if(ball.t>1.1)launch(). Good.
`pops` for level up: placed center.
Now check `render` trail: `ctx.arc(...,0,7)` — 7 > 2π, fine.
Potential issue: `ctx.textAlign` persist across frames — set as needed.
drawBurger uses `rr` which uses beginPath. rr only defines path; then fill. Good.
One more: paddle collision when ball.vy>0 and ball.y+ball.r>=paddleY — ball.y-ball.r<=paddleY+50 check: if ball already below paddle (deep), it might still trigger. Condition ball.y+ball.r<=paddleY+50 limits. Also ball could pass below paddleY+50 in a frame → then y>LH+30 → lose. Fine.
Also speed can exceed 8.5; vx from paddle uses speed — fine.
Also with dt clamp 2.5 and speed up to 8.5, ball moves ~21px/frame — could tunnel bricks (brick h=26). Acceptable; but at 60fps dt=1, 8.5px/frame fine.
Let me also add: when ball.stuck, paddle.x should be controllable? In play, mousemove works.
Also add launch on click in play — yes.
Now, "no horizontal scrollbar" — overflow hidden on body. Good.
Screenshot at 3s: demo mode playing, overlay visible, burger visible, hearts exploding. Good.
I think the code is complete. Write it out fully.
One more check on update: `if(ball.y>LH+30){loseLife();return;}` — after loseLife, ball resets; return skips rest. Good.
Level up: inside hitBrick — buildLevel then resetBall. But we're inside brick loop iterating with index i; bricks replaced; loop continues with i>=bricks.length? The for loop `for(let i=bricks.length-1;i>=0;i--)` — after splice, bricks.length decreases; loop condition re-checks. After buildLevel, bricks.length resets; loop with current i might be > new length → condition false, exits. Good. Also ball might be inside a new brick region — minor, ok.
Also in demo, after lose, resetBall — ball.stuck with t=0, launches after 1.1s.
Also add a small visual: hearts on top of HUD? no.
Write final file. Also add a subtle "combo" display? skip.
Let me also double check `rr` with negative radius etc. fine.
Also demo AI target: `ball.x+ball.vx*11` — when ball is going up, vx*11 could overshoot. It's fine — AI just moves toward prediction; ball comes back down. It might occasionally miss; that's ok, it resets.
Also make demo paddle clamp: paddle.x clamp handles.
Also, in demo, ball.stuck at start: paddle stays center; ball launches.
Final check on cheese drips: after `ctx.fillStyle='#ffd166'` and rr fill for main cheese, the drips loop uses rr then fill — but fill() with current path from rr — yes each rr call begins new path. Good.
Also `drawBurger` shadow: rr(-65,44,130,10,5) — bottom bun bottom at 50, shadow at 44..54 — ok.
Now write it.