Hamburger breakoutOne-line
Mistral Large 4
by MistralReasoningClean
Running live in a sandboxed frame. Click inside it before using the keyboard.
How it went
- Total time
- 7 min 20 s
- Cost
- $0.091
- Output tokens
- 39,329
- Spent thinking
- 29,486 tokens
- Speed
- 90.9 tok/s
- Console errors
- 0
- First token after
- 7.3 s
- First visible text after
- 5 min 20 s
- Prompt tokens
- 372
- Code size
- 25.5 KB
- Finish reason
- stop
- Served by
- Mistral
- Model id
mistralai/mistral-large-4-0- Reasoning sent
model default- Max output
- 262,144 tokens
- Generated
- Nov 12, 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 · 621 lines
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Burger Breakout ♥</title>
<style>
:root{--pink:#d6336c;}
*{box-sizing:border-box;margin:0;padding:0}
html,body{height:100%}
body{
font-family:ui-rounded,"Hiragino Maru Gothic ProN","Segoe UI",system-ui,-apple-system,sans-serif;
background:linear-gradient(160deg,#fff6ec,#ffe4ef 55%,#ffd9e0);
color:#5b2b3a;min-height:100%;display:flex;justify-content:center;
padding:16px 12px 26px;overflow-x:hidden;-webkit-user-select:none;user-select:none;
}
.wrap{width:100%;max-width:900px}
header{text-align:center;margin-bottom:8px}
h1{font-size:clamp(26px,4.6vw,38px);color:var(--pink);letter-spacing:.5px;
text-shadow:0 2px 0 rgba(255,255,255,.9);display:flex;align-items:center;justify-content:center;gap:10px;flex-wrap:wrap}
h1 .bsvg{width:44px;height:40px;filter:drop-shadow(0 3px 3px rgba(214,51,108,.3))}
h1 .heart{color:#ff4d6d;text-shadow:0 0 12px rgba(255,77,109,.55)}
.tag{color:#a86b7d;font-size:clamp(12px,2vw,14.5px);margin-top:2px}
.hud{display:flex;align-items:center;gap:18px;flex-wrap:wrap;background:rgba(255,255,255,.7);
border:2px solid #fff;border-radius:16px;padding:8px 16px;margin:10px 0;box-shadow:0 6px 18px rgba(214,51,108,.14)}
.stat{display:flex;flex-direction:column;line-height:1.05}
.stat span{font-size:9.5px;letter-spacing:2.2px;color:#c08497;font-weight:800}
.stat b{font-size:21px;color:var(--pink);font-variant-numeric:tabular-nums}
#score{display:inline-block}
#score.pop{animation:pop .3s ease}
@keyframes pop{40%{transform:scale(1.45);color:#ff2e63}}
#lives{letter-spacing:3px;font-size:19px}
#lives .on{color:#ff4d6d;text-shadow:0 0 8px rgba(255,77,109,.45)}
#lives .off{color:#e7ccd6}
.badge{margin-left:auto;display:flex;align-items:center;gap:7px;background:var(--pink);color:#fff;
font-size:10.5px;font-weight:800;letter-spacing:1.4px;padding:7px 12px;border-radius:999px;
box-shadow:0 4px 10px rgba(214,51,108,.35);animation:bob 2s ease-in-out infinite;white-space:nowrap}
.badge .dot{width:8px;height:8px;border-radius:50%;background:#fff;animation:pulse 1s infinite}
@keyframes pulse{50%{opacity:.25}}
@keyframes bob{50%{transform:translateY(-3px)}}
.stage{position:relative;border-radius:20px;overflow:hidden;border:3px solid #fff;background:#fff;
box-shadow:0 20px 45px rgba(214,51,108,.28),0 4px 12px rgba(0,0,0,.08)}
canvas{display:block;width:100%;height:auto;touch-action:none}
.overlay{position:absolute;inset:0;z-index:5;display:flex;align-items:center;justify-content:center;
background:rgba(255,238,245,.38);backdrop-filter:blur(3px);-webkit-backdrop-filter:blur(3px);transition:opacity .35s ease}
.overlay.hidden{opacity:0;pointer-events:none}
.card{background:rgba(255,255,255,.88);border:2px solid #fff;border-radius:22px;padding:22px 30px;text-align:center;
max-width:min(430px,88%);box-shadow:0 14px 34px rgba(214,51,108,.32);animation:bob 2.6s ease-in-out infinite}
.card .bsvg{width:62px;height:56px}
.card h2{color:var(--pink);font-size:clamp(17px,3vw,23px);margin:6px 0}
.card p{color:#9c5f74;font-size:clamp(11.5px,2vw,13.5px);line-height:1.5}
.card .bigscore b{color:var(--pink);font-size:20px}
.card .bh{color:#ff4d6d}
.hint{text-align:center;margin-top:10px;color:#b98296;font-size:clamp(10.5px,1.9vw,12.5px)}
.hint b{color:var(--pink)}
@media (max-width:480px){.hud{gap:12px;padding:7px 12px}.badge{margin-left:0}.card{padding:16px 18px}}
</style>
</head>
<body>
<div class="wrap">
<header>
<h1><span id="tb"></span>Burger Breakout <span class="heart">♥</span></h1>
<p class="tag">The paddle is a hamburger — and every brick explodes into a shower of hearts!</p>
</header>
<div class="hud">
<div class="stat"><span>SCORE</span><b id="score">0</b></div>
<div class="stat"><span>LEVEL</span><b id="level">1</b></div>
<div class="stat"><span>LIVES</span><b id="lives"></b></div>
<div class="badge" id="badge"><span class="dot"></span>DEMO · CLICK TO TAKE OVER</div>
</div>
<div class="stage" id="stage">
<canvas id="game"></canvas>
<div class="overlay" id="overlay"><div class="card" id="card"></div></div>
</div>
<p class="hint" id="hint">A demo is playing — <b>click or press any key</b> to grab the paddle. Move with mouse, ← → or touch.</p>
</div>
<script>
(()=>{
"use strict";
const canvas=document.getElementById('game');
const ctx=canvas.getContext('2d');
const stage=document.getElementById('stage');
const overlay=document.getElementById('overlay');
const card=document.getElementById('card');
const scoreEl=document.getElementById('score');
const levelEl=document.getElementById('level');
const livesEl=document.getElementById('lives');
const badge=document.getElementById('badge');
const hintEl=document.getElementById('hint');
const tb=document.getElementById('tb');
const W=900,H=560;
const clamp=(v,a,b)=>v<a?a:v>b?b:v;
const rand=(a,b)=>a+Math.random()*(b-a);
/* ---------- inline burger icon (SVG) ---------- */
const BURGER='<svg viewBox="0 0 64 58" xmlns="http://www.w3.org/2000/svg" class="bsvg">'
+'<ellipse cx="32" cy="52" rx="25" ry="5" fill="#e8a558"/>'
+'<rect x="9" y="42" width="46" height="9" rx="4.5" fill="#7c4a22"/>'
+'<path d="M13 37h38v5h-5l-2.5 6-2.5-6h-9l-2.5 7-2.5-7H13z" fill="#ffd43b"/>'
+'<path d="M8 33q4-7 8 0t8 0 8 0 8 0 8 0 8 0v4H8z" fill="#6cc24a"/>'
+'<path d="M6 31a26 17 0 0 1 52 0z" fill="#f0b269"/>'
+'<ellipse cx="20" cy="21" rx="2.4" ry="1.4" fill="#fff3d0" transform="rotate(-18 20 21)"/>'
+'<ellipse cx="32" cy="17" rx="2.4" ry="1.4" fill="#fff3d0"/>'
+'<ellipse cx="44" cy="21" rx="2.4" ry="1.4" fill="#fff3d0" transform="rotate(18 44 21)"/>'
+'<ellipse cx="26" cy="26" rx="2.2" ry="1.3" fill="#fff3d0" transform="rotate(-8 26 26)"/>'
+'<ellipse cx="38" cy="26" rx="2.2" ry="1.3" fill="#fff3d0" transform="rotate(8 38 26)"/>'
+'<circle cx="26" cy="29.5" r="1.8" fill="#4a2c12"/><circle cx="38" cy="29.5" r="1.8" fill="#4a2c12"/>'
+'<path d="M28 32.5q4 3.4 8 0" stroke="#4a2c12" stroke-width="1.6" fill="none" stroke-linecap="round"/>'
+'</svg>';
tb.innerHTML=BURGER;
/* ---------- audio (WebAudio, no assets) ---------- */
let AC=null;
function initAudio(){
if(!AC){ try{ AC=new (window.AudioContext||window.webkitAudioContext)(); }catch(e){ AC=null; } }
if(AC&&AC.state==='suspended'){ AC.resume().catch(()=>{}); }
}
function tone(f0,f1,dur,type,vol){
if(!AC||AC.state!=='running')return;
try{
const t=AC.currentTime,o=AC.createOscillator(),g=AC.createGain();
o.type=type||'sine';
o.frequency.setValueAtTime(Math.max(f0,1),t);
o.frequency.exponentialRampToValueAtTime(Math.max(f1,1),t+dur);
g.gain.setValueAtTime(vol||0.12,t);
g.gain.exponentialRampToValueAtTime(0.0001,t+dur);
o.connect(g); g.connect(AC.destination);
o.start(t); o.stop(t+dur+0.05);
}catch(e){}
}
/* ---------- background (pre-rendered) ---------- */
let bg=null;
function makeBG(){
bg=document.createElement('canvas'); bg.width=W*2; bg.height=H*2;
const b=bg.getContext('2d'); b.scale(2,2);
const g=b.createLinearGradient(0,0,0,H);
g.addColorStop(0,'#fff8ef'); g.addColorStop(0.6,'#ffe7f0'); g.addColorStop(1,'#ffd9e5');
b.fillStyle=g; b.fillRect(0,0,W,H);
b.fillStyle='rgba(255,110,155,0.08)';
const st=54; let row=0;
for(let y=st*0.7;y<H;y+=st){ row++;
for(let x=st/2+(row%2)*st/2;x<W;x+=st){ b.beginPath(); b.arc(x,y,7,0,7); b.fill(); }
}
const rg=b.createRadialGradient(W/2,40,20,W/2,40,340);
rg.addColorStop(0,'rgba(255,255,255,0.55)'); rg.addColorStop(1,'rgba(255,255,255,0)');
b.fillStyle=rg; b.fillRect(0,0,W,140);
b.fillStyle='rgba(255,255,255,0.4)'; b.fillRect(0,H-24,W,24);
b.strokeStyle='rgba(230,120,160,0.35)'; b.lineWidth=2; b.setLineDash([10,8]);
b.beginPath(); b.moveTo(0,H-24); b.lineTo(W,H-24); b.stroke(); b.setLineDash([]);
}
/* ---------- game state ---------- */
let state='demo', paused=false, gameOver=false;
let score=0, lives=3, level=1, combo=0, keyDir=0;
let bricks=[], hearts=[], sparks=[], texts=[], banners=[];
let shake=0, time=0, serveTimer=0, ambientT=0, clearTO=null;
const paddle={x:W/2,y:H-54,w:120,h:38,targetX:W/2,squash:0,blink:0,blinkT:2.2};
const ball={x:W/2,y:0,vx:0,vy:0,r:9,stuck:true,spin:0};
const PAL=[['#ff8fab','#e5486d'],['#ffc078','#e8590c'],['#ffe08a','#f08c00'],['#b2f2bb','#37b24d'],['#a5d8ff','#1c7ed6'],['#d0bfff','#7048e8'],['#fcc2d7','#d6336c']];
const HEART_COLORS=['#ff4d6d','#ff6b8a','#ff8fab','#ffc2d1','#e8455f','#ff2e63','#fff0f3','#ff5c8a'];
function buildLevel(){
bricks=[];
const cols=10, rows=Math.min(3+level,7);
const m=48, gap=8, top=86;
const bw=(W-m*2-(cols-1)*gap)/cols, bh=22;
for(let r=0;r<rows;r++) for(let c=0;c<cols;c++){
const hp=(level>=3&&r<2)?2:(level>=6&&r<3?2:1);
const pal=PAL[(r*3+c+level)%PAL.length];
bricks.push({x:m+c*(bw+gap),y:top+r*(bh+gap),w:bw,h:bh,hp:hp,maxHp:hp,alive:true,
fill:hp>1?pal[1]:pal[0],edge:pal[1]});
}
}
function ballSpeed(){ return Math.min(650,335+(level-1)*38); }
function resetBall(){
ball.stuck=true; ball.vx=0; ball.vy=0;
ball.x=paddle.x; ball.y=paddle.y-ball.r-3; serveTimer=0.9;
}
function resetRun(){
score=0; lives=3; level=1; combo=0;
hearts.length=0; sparks.length=0; texts.length=0; banners.length=0;
paddle.x=W/2; paddle.targetX=W/2;
buildLevel(); resetBall(); serveTimer=0.8; updateHUD();
}
function launch(){
const a=-Math.PI/2+rand(-0.55,0.55), sp=ballSpeed();
ball.vx=Math.cos(a)*sp; ball.vy=Math.sin(a)*sp;
ball.stuck=false; tone(520,880,0.1,'sine',0.1);
}
/* ---------- particles ---------- */
function explode(x,y,n,spread,power){
for(let i=0;i<n;i++){
if(hearts.length>650) hearts.splice(0,50);
const a=rand(0,Math.PI*2), sp=rand(90,power);
const life=rand(1.2,2.4);
hearts.push({x:x+rand(-spread,spread), y:y+rand(-spread*0.6,spread*0.6),
vx:Math.cos(a)*sp, vy:Math.sin(a)*sp-rand(40,160),
rot:rand(0,6.28), vr:rand(-9,9), s:rand(4.5,10.5),
life:life, max:life, color:HEART_COLORS[(Math.random()*HEART_COLORS.length)|0], g:rand(480,700)});
}
const ns=Math.floor(n*0.35);
for(let i=0;i<ns;i++){
if(sparks.length>250) sparks.splice(0,40);
sparks.push({x:x,y:y,vx:rand(-260,260),vy:rand(-320,120),life:rand(.3,.7),max:.7,s:rand(1.5,3.5)});
}
}
function addText(x,y,text,size,color){ texts.push({x:x,y:y,text:text,size:size,color:color,life:1.1,vy:-46}); }
/* ---------- gameplay ---------- */
function hitBrick(b){
b.hp--;
if(b.hp<=0){
b.alive=false; combo++;
const pts=10+Math.min(combo,12)*2; score+=pts;
addText(b.x+b.w/2,b.y+b.h/2,'+'+pts,17,'#d6336c');
if(combo>=3) addText(b.x+b.w/2,b.y-8,'combo x'+combo,13,'#ff8fab');
explode(b.x+b.w/2,b.y+b.h/2,30+((Math.random()*14)|0),b.w/2,430);
shake=Math.min(shake+3.2,9);
tone(480+Math.random()*260,150,0.13,'square',0.11);
tone(1300,650,0.08,'sine',0.07);
}else{
score+=5; addText(b.x+b.w/2,b.y,'+5',13,'#e8590c');
explode(b.x+b.w/2,b.y+b.h/2,10,b.w/2,260);
tone(300,150,0.09,'square',0.08);
}
popScore(); updateHUD();
if(bricks.every(b=>!b.alive)) levelCleared();
}
function levelCleared(){
if(clearTO) return;
ball.stuck=true; ball.vx=ball.vy=0;
banners.push({text:'LEVEL '+level+' CLEAR! ♥',life:1.5,max:1.5,size:46});
explode(W/2,H/3,70,160,520);
[523,659,784,1047].forEach((f,i)=>setTimeout(()=>tone(f,f*1.01,0.18,'triangle',0.12),i*90));
clearTO=setTimeout(()=>{
clearTO=null; level++; buildLevel(); resetBall(); serveTimer=1.2; updateHUD();
},1100);
}
function loseLife(){
lives--; updateHUD();
shake=Math.min(shake+7,10);
explode(ball.x,H-24,30,14,400);
tone(360,70,0.5,'sawtooth',0.15);
if(lives<=0){
gameOver=true; setCard('over');
if(state==='demo'){
setTimeout(()=>{ if(gameOver&&state==='demo'){ gameOver=false; resetRun(); hideOverlay(); } },3000);
}
}else{ resetBall(); serveTimer=1.3; }
}
function stepBall(sdt){
ball.x+=ball.vx*sdt; ball.y+=ball.vy*sdt;
if(ball.x<ball.r+4){ ball.x=ball.r+4; ball.vx=Math.abs(ball.vx); tone(200,160,0.05,'square',0.05); }
else if(ball.x>W-ball.r-4){ ball.x=W-ball.r-4; ball.vx=-Math.abs(ball.vx); tone(200,160,0.05,'square',0.05); }
if(ball.y<ball.r+4){ ball.y=ball.r+4; ball.vy=Math.abs(ball.vy); tone(200,160,0.05,'square',0.05); }
if(ball.y>H+26){ ball.stuck=true; loseLife(); return; }
if(ball.vy>0 && ball.y+ball.r>=paddle.y && ball.y-ball.r<=paddle.y+paddle.h+12 && Math.abs(ball.x-paddle.x)<=paddle.w/2+ball.r){
const off=clamp((ball.x-paddle.x)/(paddle.w/2),-1,1);
const ang=-Math.PI/2+off*1.05;
let sp=Math.hypot(ball.vx,ball.vy)*1.012; sp=Math.min(sp,ballSpeed()+70);
ball.vx=Math.cos(ang)*sp; ball.vy=Math.sin(ang)*sp;
ball.y=paddle.y-ball.r-1;
combo=0; paddle.squash=1; shake=Math.min(shake+1.2,5);
tone(230,440,0.09,'triangle',0.15);
return;
}else if(ball.y>paddle.y-2 && ball.y<paddle.y+paddle.h+4 && Math.abs(ball.x-paddle.x)<paddle.w/2){
ball.y=paddle.y-ball.r-1; ball.vy=-Math.abs(ball.vy||200);
}
for(const b of bricks){
if(!b.alive) 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){
if(Math.abs(dx)>Math.abs(dy)){
ball.vx=(dx>=0?1:-1)*Math.abs(ball.vx);
ball.x=cx+(dx>=0?ball.r+0.5:-ball.r-0.5);
}else{
ball.vy=(dy>=0?1:-1)*Math.abs(ball.vy);
ball.y=cy+(dy>=0?ball.r+0.5:-ball.r-0.5);
}
hitBrick(b); break;
}
}
}
/* ---------- update ---------- */
function update(dt){
time+=dt;
if(state==='demo'){
const closing=ball.vy>0&&!ball.stuck;
let err=closing?clamp((paddle.y-ball.y)*0.11,-52,52):0;
err+=Math.sin(time*1.9)*5;
paddle.targetX=clamp(ball.x+err,paddle.w/2+6,W-paddle.w/2-6);
}
if(keyDir!==0){ paddle.x+=keyDir*740*dt; paddle.targetX=paddle.x; }
else{
const maxSpd=state==='demo'?Math.min(760,560+level*35):1500;
const dx=paddle.targetX-paddle.x;
paddle.x+=clamp(dx,-maxSpd*dt,maxSpd*dt);
}
paddle.x=clamp(paddle.x,paddle.w/2+5,W-paddle.w/2-5);
paddle.squash=Math.max(0,paddle.squash-dt*4.5);
paddle.blinkT-=dt; if(paddle.blinkT<=0){ paddle.blink=0.13; paddle.blinkT=rand(1.8,4.2); }
paddle.blink=Math.max(0,paddle.blink-dt);
if(ball.stuck){
ball.x=paddle.x; ball.y=paddle.y-ball.r-3;
serveTimer-=dt;
if(serveTimer<=0&&!clearTO) launch();
}else{
const sp=Math.hypot(ball.vx,ball.vy);
const steps=Math.min(8,Math.max(1,Math.ceil(sp*dt/(ball.r*0.8))));
for(let i=0;i<steps;i++){ stepBall(dt/steps); if(ball.stuck) break; }
ball.spin+=dt*9;
}
for(let i=hearts.length-1;i>=0;i--){
const p=hearts[i]; p.life-=dt;
if(p.life<=0){ hearts.splice(i,1); continue; }
if(p.amb){
p.y+=p.vy*dt; p.x+=(p.vx+Math.sin(time*2.5+p.seed)*14)*dt;
if(p.y<-24) hearts.splice(i,1);
continue;
}
p.vy+=p.g*dt; p.x+=p.vx*dt; p.y+=p.vy*dt; p.rot+=p.vr*dt;
if(p.y>H-14&&p.vy>0){ p.y=H-14; p.vy=-p.vy*0.42; p.vx*=0.72; p.vr*=0.8; }
}
for(let i=sparks.length-1;i>=0;i--){
const s=sparks[i]; s.life-=dt;
if(s.life<=0){ sparks.splice(i,1); continue; }
s.vy+=900*dt; s.x+=s.vx*dt; s.y+=s.vy*dt;
}
for(let i=texts.length-1;i>=0;i--){ const t=texts[i]; t.life-=dt; t.y+=t.vy*dt; if(t.life<=0) texts.splice(i,1); }
for(let i=banners.length-1;i>=0;i--){ const b=banners[i]; b.life-=dt; if(b.life<=0) banners.splice(i,1); }
shake=Math.max(0,shake-dt*20);
ambientT-=dt;
if(ambientT<=0){
ambientT=rand(0.35,0.7);
if(hearts.length<640) hearts.push({amb:true,x:rand(20,W-20),y:H+14,vx:rand(-14,14),
vy:rand(-46,-26),rot:rand(0,6.28),vr:rand(-2,2),s:rand(3.5,7),life:rand(5,8),max:8,
color:HEART_COLORS[(Math.random()*HEART_COLORS.length)|0],seed:rand(0,9)});
}
}
/* ---------- drawing ---------- */
function rr(x,y,w,h,r){
ctx.beginPath();
if(ctx.roundRect){ ctx.roundRect(x,y,w,h,r); }
else{
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.32*s);
ctx.bezierCurveTo(-0.62*s,-0.28*s,-0.32*s,-0.95*s,0,-0.42*s);
ctx.bezierCurveTo(0.32*s,-0.95*s,0.62*s,-0.28*s,0,0.32*s);
ctx.closePath();
}
function drawBricks(){
for(const b of bricks){
if(!b.alive) continue;
rr(b.x,b.y,b.w,b.h,6); ctx.fillStyle=b.fill; ctx.fill();
rr(b.x+2,b.y+2,b.w-4,7,4); ctx.fillStyle='rgba(255,255,255,0.30)'; ctx.fill();
rr(b.x,b.y,b.w,b.h,6); ctx.strokeStyle=b.edge; ctx.globalAlpha=0.55; ctx.lineWidth=2; ctx.stroke(); ctx.globalAlpha=1;
ctx.save(); ctx.translate(b.x+b.w/2,b.y+b.h/2+1); ctx.globalAlpha=0.18;
ctx.strokeStyle='#7a2b45'; ctx.lineWidth=1.4; heartPath(4.6); ctx.stroke(); ctx.restore();
if(b.hp<b.maxHp){
ctx.strokeStyle='rgba(90,30,50,0.55)'; ctx.lineWidth=1.6; ctx.beginPath();
ctx.moveTo(b.x+b.w*0.25,b.y+3); ctx.lineTo(b.x+b.w*0.42,b.y+b.h-4); ctx.lineTo(b.x+b.w*0.36,b.y+b.h*0.6);
ctx.moveTo(b.x+b.w*0.68,b.y+4); ctx.lineTo(b.x+b.w*0.6,b.y+b.h-3); ctx.stroke();
}
}
}
function drawHearts(){
for(const p of hearts){
const a=p.amb?clamp(p.life/2,0,0.5):clamp(p.life/0.45,0,1);
if(a<=0) continue;
const pop=p.amb?1:Math.min(1,(p.max-p.life)*6);
ctx.save(); ctx.globalAlpha=a; ctx.translate(p.x,p.y); ctx.rotate(p.rot);
ctx.scale(p.s*pop,p.s*pop);
ctx.fillStyle=p.color; heartPath(1); ctx.fill();
ctx.restore();
}
}
function drawSparks(){
ctx.save(); ctx.globalCompositeOperation='lighter';
for(const s of sparks){
ctx.globalAlpha=clamp(s.life/s.max,0,1);
ctx.fillStyle='#fff6d8';
ctx.beginPath(); ctx.arc(s.x,s.y,s.s,0,Math.PI*2); ctx.fill();
}
ctx.restore(); ctx.globalAlpha=1;
}
function drawPaddle(){
const px=paddle.x, py=paddle.y, w=paddle.w, h=paddle.h;
ctx.fillStyle='rgba(120,40,70,0.15)';
ctx.beginPath(); ctx.ellipse(px,py+h+7,w/2,6,0,0,Math.PI*2); ctx.fill();
ctx.save();
ctx.translate(px,py+h);
const sq=paddle.squash;
ctx.scale(1+sq*0.14,1-sq*0.24);
ctx.translate(-px,-(py+h));
/* bottom bun */
rr(px-w/2+3,py+30,w-6,8,4); ctx.fillStyle='#e8a558'; ctx.fill();
/* patty */
rr(px-w/2,py+21,w,10,5); ctx.fillStyle='#7c4a22'; ctx.fill();
ctx.strokeStyle='rgba(60,30,10,0.35)'; ctx.lineWidth=1.4; ctx.beginPath();
ctx.moveTo(px-w/2+8,py+25); ctx.lineTo(px+w/2-8,py+25);
ctx.moveTo(px-w/2+8,py+28); ctx.lineTo(px+w/2-8,py+28); ctx.stroke();
/* cheese + drips */
ctx.fillStyle='#ffd43b';
ctx.fillRect(px-w*0.44,py+15.5,w*0.88,6.5);
for(const d of [-0.3,0,0.3]){
const cx=px+d*w;
ctx.beginPath(); ctx.moveTo(cx-4.5,py+22); ctx.lineTo(cx+4.5,py+22); ctx.lineTo(cx,py+29); ctx.closePath(); ctx.fill();
}
/* lettuce */
ctx.fillStyle='#6cc24a';
ctx.beginPath();
const lw=w+10, x0=px-lw/2, base=py+16, n=9, seg=lw/n;
ctx.moveTo(x0,base+2);
for(let k=0;k<n;k++){ ctx.quadraticCurveTo(x0+(k+0.5)*seg,base+(k%2?-3.5:6.5),x0+(k+1)*seg,base+2); }
ctx.lineTo(x0+lw,base+5); ctx.lineTo(x0,base+5); ctx.closePath(); ctx.fill();
/* top bun */
const g=ctx.createLinearGradient(0,py-4,0,py+18);
g.addColorStop(0,'#f7c37e'); g.addColorStop(1,'#e2954a');
ctx.fillStyle=g;
ctx.beginPath(); ctx.ellipse(px,py+17,w/2,16,0,Math.PI,0); ctx.closePath(); ctx.fill();
/* sesame seeds */
ctx.fillStyle='#fff3d0';
for(const o of [-0.66,-0.44,-0.2,0.02,0.24,0.48,0.68]){
const sx=px+o*w*0.46;
const sy=py+17-14*Math.sqrt(Math.max(0,1-o*o*0.9))-1;
ctx.save(); ctx.translate(sx,sy); ctx.rotate(o*0.6);
ctx.beginPath(); ctx.ellipse(0,0,2.4,1.4,0,0,Math.PI*2); ctx.fill(); ctx.restore();
}
/* face */
const ey=py+10;
ctx.fillStyle='#4a2c12';
if(paddle.blink>0){
ctx.strokeStyle='#4a2c12'; ctx.lineWidth=2; ctx.beginPath();
ctx.moveTo(px-15,ey); ctx.lineTo(px-9,ey);
ctx.moveTo(px+9,ey); ctx.lineTo(px+15,ey); ctx.stroke();
}else{
ctx.beginPath(); ctx.arc(px-12,ey,2.6,0,Math.PI*2); ctx.arc(px+12,ey,2.6,0,Math.PI*2); ctx.fill();
}
ctx.strokeStyle='#4a2c12'; ctx.lineWidth=1.8;
ctx.beginPath(); ctx.arc(px,ey+2,5,Math.PI*0.15,Math.PI*0.85); ctx.stroke();
ctx.fillStyle='rgba(255,120,150,0.45)';
ctx.beginPath(); ctx.arc(px-21,ey+3,3.2,0,Math.PI*2); ctx.arc(px+21,ey+3,3.2,0,Math.PI*2); ctx.fill();
ctx.restore();
}
function drawBall(){
const b=ball;
if(b.stuck){
const p=clamp(1-serveTimer/1.2,0,1);
ctx.strokeStyle='rgba(214,51,108,'+(0.5+0.3*Math.sin(time*6))+')';
ctx.lineWidth=3;
ctx.beginPath(); ctx.arc(b.x,b.y,b.r+6,-Math.PI/2,-Math.PI/2+p*Math.PI*2); ctx.stroke();
}
ctx.save(); ctx.translate(b.x,b.y); ctx.rotate(b.spin*0.4);
const g=ctx.createRadialGradient(-b.r*0.35,-b.r*0.4,b.r*0.2,0,0,b.r);
g.addColorStop(0,'#ff8787'); g.addColorStop(0.6,'#f03e3e'); g.addColorStop(1,'#c92a2a');
ctx.fillStyle=g; ctx.beginPath(); ctx.arc(0,0,b.r,0,Math.PI*2); ctx.fill();
ctx.fillStyle='rgba(255,255,255,0.85)';
ctx.beginPath(); ctx.arc(-b.r*0.35,-b.r*0.42,b.r*0.26,0,Math.PI*2); ctx.fill();
ctx.fillStyle='#2f9e44'; ctx.fillRect(-1.6,-b.r-4,3.2,5);
ctx.save(); ctx.translate(2.5,-b.r-3); ctx.rotate(-0.5);
ctx.beginPath(); ctx.ellipse(0,0,3.4,1.6,0,0,Math.PI*2); ctx.fill(); ctx.restore();
ctx.restore();
}
function drawTexts(){
ctx.textAlign='center'; ctx.textBaseline='middle';
for(const t of texts){
const a=clamp(t.life/0.4,0,1);
ctx.globalAlpha=a;
ctx.font='800 '+t.size+'px ui-rounded,"Segoe UI",system-ui,sans-serif';
ctx.lineWidth=3; ctx.strokeStyle='rgba(255,255,255,0.85)';
ctx.strokeText(t.text,t.x,t.y);
ctx.fillStyle=t.color; ctx.fillText(t.text,t.x,t.y);
}
ctx.globalAlpha=1;
}
function drawBanners(){
for(const b of banners){
const k=1-b.life/b.max, sc=1+Math.sin(k*Math.PI)*0.15;
ctx.save();
ctx.globalAlpha=clamp(b.life/0.4,0,1);
ctx.translate(W/2,H*0.38); ctx.scale(sc,sc);
ctx.textAlign='center'; ctx.textBaseline='middle';
ctx.font='900 '+b.size+'px ui-rounded,"Segoe UI",system-ui,sans-serif';
ctx.lineWidth=8; ctx.strokeStyle='rgba(255,255,255,0.9)';
ctx.strokeText(b.text,0,0);
ctx.fillStyle='#d6336c'; ctx.fillText(b.text,0,0);
ctx.restore();
}
}
function render(){
const s=canvas.width/W;
ctx.setTransform(s,0,0,s,0,0);
ctx.clearRect(0,0,W,H);
if(bg) ctx.drawImage(bg,0,0,W,H);
ctx.save();
if(shake>0.15) ctx.translate(rand(-shake,shake)*0.5,rand(-shake,shake)*0.5);
drawBricks();
drawSparks();
drawHearts();
drawPaddle();
drawBall();
drawTexts();
drawBanners();
ctx.restore();
}
/* ---------- HUD / overlay ---------- */
function updateHUD(){
scoreEl.textContent=score;
levelEl.textContent=level;
let lh='';
for(let i=0;i<3;i++) lh+= i<lives?'<span class="on">♥</span>':'<span class="off">♥</span>';
livesEl.innerHTML=lh;
}
function popScore(){ scoreEl.classList.remove('pop'); void scoreEl.offsetWidth; scoreEl.classList.add('pop'); }
function setCard(kind){
if(kind==='start'){
card.innerHTML=BURGER+'<h2>Click or press any key to start!</h2>'
+'<p>The burger is playing itself in demo mode — smash some bricks and watch them burst into hearts. Click anytime to take the paddle!</p>';
}else if(kind==='pause'){
card.innerHTML='<h2>Paused</h2><p>Press <b>P</b> or click to keep smashing</p>';
}else if(kind==='over'){
card.innerHTML='<h2>Game over <span class="bh">♥</span></h2>'
+'<p class="bigscore">Score <b>'+score+'</b> · Level '+level+'</p>'
+'<p>Click or press any key to play again</p>';
}
overlay.classList.remove('hidden');
}
function hideOverlay(){ overlay.classList.add('hidden'); }
function onTakeControl(){
hideOverlay();
badge.style.display='none';
hintEl.innerHTML='You control the burger — <b>mouse / ← → / touch</b> · <b>P</b> pause · <b>R</b> restart';
}
function togglePause(){
if(gameOver) return;
paused=!paused;
if(paused) setCard('pause'); else hideOverlay();
}
/* ---------- input ---------- */
window.addEventListener('keydown',e=>{
if(e.key==='ArrowLeft'||e.key==='ArrowRight'||e.key===' '){ e.preventDefault(); }
initAudio();
const k=e.key;
if(gameOver){ gameOver=false; resetRun(); state='play'; onTakeControl(); return; }
if(state==='demo'){
state='play'; onTakeControl();
if(k==='ArrowLeft') keyDir=-1; else if(k==='ArrowRight') keyDir=1;
return;
}
if(paused){ if(k==='p'||k==='P'||k==='Escape'||k===' '||k==='Enter') togglePause(); return; }
if(k==='ArrowLeft') keyDir=-1;
else if(k==='ArrowRight') keyDir=1;
else if(k==='p'||k==='P'||k==='Escape') togglePause();
else if(k==='r'||k==='R') resetRun();
});
window.addEventListener('keyup',e=>{
if(e.key==='ArrowLeft'&&keyDir===-1) keyDir=0;
else if(e.key==='ArrowRight'&&keyDir===1) keyDir=0;
});
window.addEventListener('pointermove',e=>{
const r=canvas.getBoundingClientRect();
if(r.width>0) paddle.targetX=clamp((e.clientX-r.left)/r.width*W,paddle.w/2+5,W-paddle.w/2-5);
});
stage.addEventListener('pointerdown',e=>{
initAudio();
if(gameOver){ gameOver=false; resetRun(); state='play'; onTakeControl(); return; }
if(paused){ togglePause(); return; }
if(state==='demo'){ state='play'; onTakeControl(); }
const r=canvas.getBoundingClientRect();
if(r.width>0) paddle.targetX=clamp((e.clientX-r.left)/r.width*W,paddle.w/2+5,W-paddle.w/2-5);
});
document.addEventListener('visibilitychange',()=>{
if(document.hidden&&!paused&&!gameOver&&state==='play') togglePause();
});
/* ---------- sizing ---------- */
function resize(){
const cssW=stage.clientWidth||W;
const dpr=Math.min(2,window.devicePixelRatio||1);
canvas.width=Math.max(320,Math.round(cssW*dpr));
canvas.height=Math.round(canvas.width*H/W);
makeBG();
}
window.addEventListener('resize',resize);
/* ---------- boot & loop ---------- */
buildLevel();
resetBall();
serveTimer=0.7;
updateHUD();
setCard('start');
resize();
let last=performance.now();
function frame(now){
let dt=(now-last)/1000; last=now;
if(dt>0.05) dt=0.05;
if(!paused) update(dt);
render();
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
})();
</script>
</body>
</html>Reasoning trace (29,486 tokens)
The user wants a Breakout game where:
- Paddle is a hamburger
- Bricks explode into tons of little hearts when broken
Requirements:
- Single self-contained HTML file
- Inline CSS/JS
- Must be self-playing demo mode (AI plays) behind "Click or press a key to start" message
- Must look good at 1280×800 and adapt to 375px
- No external resources (no external images/fonts — I'll draw everything with canvas)
- No localStorage, etc.
- Visible action within first 3 seconds — so the demo AI should start playing automatically (self-playing), with an overlay message "Click or press a key to start" that the user can dismiss to take control. Actually the rule says: "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 runs automatically with AI, and there's a message overlay saying click/key to take control. When clicked, player controls with mouse/keyboard.
Let me design:
Canvas-based breakout:
- Canvas fills a nice area, responsive. Use devicePixelRatio scaling.
- Paddle: draw a hamburger — bun top (sesame seeds), lettuce (wavy green), cheese, patty, bun bottom. Draw with canvas paths, maybe with slight squash animation when hitting ball.
- Ball: maybe a little heart? Or a cherry/tomato? Let's make the ball a shiny heart or a meatball? A heart ball fits the theme. Or a ketchup drop. Let's do a glossy red heart ball... Actually hearts come from bricks. Ball could be a little cherry tomato. Cute. Let's do a tomato ball with highlight.
- Bricks: rows of bricks colored like... maybe each brick is a small burger ingredient? Simpler: colorful bricks with rounded corners, some look like mini ingredients. When broken, spawn 20-40 little hearts that fly out with physics (gravity, rotation, fade), plus maybe a few sparkles.
- Hearts drawn as classic heart path with rotation, varying colors (pink, red, white), with slight glow.
- Score, lives, level display. HUD drawn in DOM above canvas or on canvas. Let's do DOM HUD for crispness: score, lives (hearts), level.
- Walls, bounce sounds? Audio via WebAudio (allowed, it's inline). Sounds on brick break: a little pop. Keep it optional and guard with AudioContext resume on first gesture. Since demo runs automatically, audio context may be suspended until gesture; handle gracefully (try to resume on click).
- AI demo mode: paddle follows ball x with some smoothing and slight error; when overlay clicked or key pressed, switch to player control (mouse move / arrow keys / touch). Overlay says "Click or press any key to play!" and fades out on interaction. Game keeps running in demo mode.
- Levels: when all bricks cleared, next level with more rows/speed, regenerate bricks.
- Lives: 3, ball lost → lose a life, reset ball on paddle. Game over state with restart (click). Win? Endless levels.
- Particles: hearts array with position, velocity, rotation, angular velocity, size, color, life. Update with gravity, bounce off floor slightly or just fade. "Tons of little hearts" — spawn like 30+ per brick, plus combo multiplier. Also floating score text "+10".
- Screen shake on brick break? Small.
- Background: warm kitchen gradient, maybe subtle pattern (checkered tablecloth?). Draw subtle polka dots. Keep performance fine.
Layout:
- Page: centered column, title "♥ Burger Breakout ♥", HUD (score, lives, level), canvas, hint text. Canvas aspect maybe 16:10, max width ~960px, height responsive. At 1280×800 viewport: canvas ~ 960×600 fits with header. At 375px: canvas width ~ 95vw, height scaled by aspect ratio. Use CSS aspect-ratio and JS resize to match backing store.
Implementation details:
- Use requestAnimationFrame with delta time.
- Game states: 'demo' (AI), 'play' (player), plus overlay visible in demo; 'gameover' overlay.
- Actually keep it simple: mode = 'demo' | 'play'. Overlay div positioned over canvas with semi-transparent backdrop and message "🍔 Click or press any key to start!" — but no emoji? Emoji is fine (not external resource). Hmm, "No external files, images, fonts" — emoji are system fonts, allowed. But to be safe with rendering, I can use text hearts "♥" (unicode, fine).
Wait — should the overlay block view? "behind a 'Click or press a key to start' message" — so message is overlaid but game visible behind, playing via AI. Make overlay semi-transparent with blur so game visible. On click/keydown: mode='play', overlay fades out. Also a small "Demo mode — click to take control" pill that remains? Keep simple: overlay hides; a small hint at bottom "Move mouse or ← → to play · Demo AI is controlling until you click".
Controls in play mode: mousemove over canvas sets paddle target; arrow keys move paddle; touch move. Also click canvas when ball stuck? Ball always launches automatically after a short delay (serve countdown) or on click. Let's auto-serve after 1s with a countdown ring; in demo AI serves automatically too.
Paddle hamburger drawing: width ~110, height ~34. Layers:
- Top bun: half-ellipse, tan (#e8a85c → gradient), sesame seeds (small white ellipses rotated).
- Lettuce: wavy green strip below bun.
- Cheese: yellow with drips (triangles hanging).
- Patty: brown rounded rect.
- Bottom bun: tan rounded rect.
Add eyes? Cute face on the bun? Maybe two eyes and smile on top bun to make it a character — fun. Add little arms? Keep face: eyes blink occasionally. Squash when ball hits (scaleY bounce).
Ball: tomato — red circle with green stem leaf on top, highlight. Cute.
Bricks: rows x cols depending on level. Colors pastel rainbow. Each brick has hp 1 (maybe higher levels some bricks hp 2 with crack overlay). Rounded rect with slight bevel (lighter top edge). Maybe brick pattern: some bricks drawn as tiny hearts? The explosion is hearts, so bricks could be plain candy-like bricks. Add subtle shine.
Heart particle drawing: path using bezier:
```
ctx.moveTo(0, size*0.35);
ctx.bezierCurveTo(0,0, -size,0? ...)
```
Standard heart:
```
function heartPath(ctx, s){
ctx.beginPath();
ctx.moveTo(0, s*0.3);
ctx.bezierCurveTo(-s, -s*0.3, -s*0.5, -s, 0, -s*0.35);
ctx.bezierCurveTo(s*0.5, -s, s, -s*0.3, 0, s*0.3);
ctx.closePath();
}
```
Something like that. Fill with color, maybe stroke darker. Rotate.
Particles update: vy += gravity*dt; x += vx*dt; rot += vr*dt; life -= dt; alpha = life/maxLife; scale pop-in. Remove when life<=0 or y > floor + margin. Also bounce off side walls? Just let them fall past; canvas clips. Maybe bounce once on bottom for extra juice: if y > floorY, vy = -vy*0.4, y = floorY. Fun.
Cap particles: if hearts.length > 600, remove oldest.
Combo: consecutive brick breaks without ball touching paddle? Simpler: score per brick = 10 * level? Add floating text.
Screen shake: shake magnitude decays; apply translate in render.
Background: gradient from #2b1a3a? Let's do warm: top #ff9a8b-ish? Kitchen vibe: soft cream to peach with subtle checker at bottom? I'll do vertical gradient #fff3e0 → #ffe0ec (peach to pink), plus faint polka dots drawn once to an offscreen canvas pattern, or just draw dots each frame (cheap: pre-render to offscreen canvas at resize).
HUD: DOM elements above canvas: Score, Level, Lives (hearts ♥♥♥ red/gray), and "DEMO" badge when in demo mode. Style with a nice font stack (system fonts: 'Trebuchet MS', 'Comic Sans MS'? Use "Baloo"? Not available offline; use system-ui rounded: font-family: ui-rounded, 'Hiragino Maru Gothic ProN', 'Quicksand', system-ui... fallback fine). Use bold system-ui.
Title: "Burger Breakout" with heart. Subtitle: "Smash the bricks — they burst into hearts!".
Overlay: absolute over canvas, centered card: "🍔 Click or press any key to start!" plus "The AI is playing a demo — take over anytime!" Semi transparent so game visible. pointer-events: auto; on click → start play. Also keydown anywhere → start. After start, hide overlay (display none) but show small floating hint bottom-right: "Demo mode — click to take control" when in demo? Actually after taking control, hint: "← → or mouse to move · P pause". Keep minimal.
Pause: 'P' or Esc toggles pause; show "Paused" overlay. Also visibilitychange auto-pause. Nice but keep code manageable.
Game over: overlay "Game Over! Score: X — Click to play again". Click resets.
Level clear: brief "Level N cleared! ♥" banner then next level.
Let me structure code:
HTML:
```
<!DOCTYPE html>
<html><head><meta charset><meta viewport><title>Burger Breakout</title><style>...</style></head>
<body>
<div class="wrap">
<header>
<h1>🍔 Burger <span>Breakout</span> ♥</h1>
<p class="tag">Every brick bursts into a shower of hearts!</p>
</header>
<div class="hud">
<div class="stat">SCORE <b id="score">0</b></div>
<div class="stat">LEVEL <b id="level">1</b></div>
<div class="stat lives" id="lives">♥♥♥</div>
<div class="badge" id="demoBadge">DEMO · AI PLAYING</div>
</div>
<div class="stage" id="stage">
<canvas id="game"></canvas>
<div class="overlay" id="overlay">
<div class="card">
<div class="big">🍔</div>
<h2>Click or press any key to start!</h2>
<p>A demo is playing behind this — grab the paddle anytime.</p>
</div>
</div>
</div>
<p class="hint">🖱 Move mouse / ← → keys / touch to move the burger paddle · P to pause</p>
</div>
<script>...</script>
</body></html>
```
CSS: body gradient background (kitchen-y), wrap max-width 980px, center. Canvas display block width 100%, aspect-ratio 8/5? Let's use game logical size 900×560 (ratio ~1.607). Use CSS: .stage { position:relative; } canvas { width:100%; height:auto; aspect-ratio: 900/560; border-radius: 18px; box-shadow...; background: gradient; touch-action:none; }
JS:
- const canvas, ctx; let W=900, H=560 logical. resize(): set canvas.width = clientWidth * dpr etc., and scale factor = canvas.width / W? Simpler: keep logical coordinates and setTransform(dpr*scale...). Approach: canvas CSS width 100%; we set canvas.width = cssW*dpr; ctx.setTransform(canvas.width/W, 0,0, canvas.height/H, 0,0) each frame start (after resize set canvas.height = canvas.width * H/W). Since aspect-ratio CSS keeps ratio, canvas.height = width*H/W. Then drawing in logical coords works. Pointer coords: (e.clientX-rect.left)/rect.width*W.
Game objects:
- paddle: {x, y=H-46, w:118, h:36, targetX, squash:0, blink: timer}
- ball: {x,y,vx,vy,r:9, stuck:true, speed}
- bricks: array {x,y,w,h,hp,maxHp,color,alive}
- hearts: particle array
- texts: floating score texts
- shake: 0
Levels: level n: rows = 4 + min(n,4)? cols = 10. Brick area top margin 70. brick w = (W - 2*margin - (cols-1)*gap)/cols with margin 40, gap 8. Speed = 320 + (n-1)*40 capped 620. Ball speed base ~ 340.
Brick colors palette: ['#ff6b81','#ffb36b','#ffe066','#8ce99a','#66d9e8','#b197fc','#f783ac'] pastel-ish with darker stroke. Maybe level-based hue rotation.
Row hp: for level>=3, top rows hp 2. Color darker when hp2.
Brick drawing: rounded rect (use roundRect if available — Chrome yes; fallback manual). Add top highlight line, and if hp<maxHp draw crack lines.
Collision: ball vs bricks — circle-rect: find closest point, if dist < r → reflect based on side (compare overlap on x vs y). Standard: compute overlap; reflect vy if hit from top/bottom else vx. Break brick → explode.
Explosion function brickBurst(b): spawn n = 26 + rand*10 hearts at brick center with random velocities (angle all around, speed 120-420), sizes 5-11, colors from heart palette ['#ff4d6d','#ff758f','#ff8fab','#ffc2d1','#ff0054','#e63950','#ffffff'], life 1.2-2.2s, gravity 500, rotation. Plus 6 sparkle dots (small circles) maybe. Plus floating text "+10". shake += 3 (cap 8). sound pop.
Also occasionally a bigger heart? fine.
Ball-paddle collision: if ball moving down and within paddle rect (with rounded top), reflect with angle based on hit position: offset = (ball.x - paddle.x)/(paddle.w/2) clamped [-1,1]; angle = -90° + offset*60°; speed maintain/increase slightly. Set ball.y above paddle. paddle.squash=1. sound boing. Combo reset? Add "rally" count shown maybe.
Walls: left/right/top bounce; bottom → lose life.
Serve: ball.stuck = true, ball follows paddle; countdown 1.2s then launch at angle up random ±30°. In demo mode AI moves paddle; serve auto.
AI: paddle.targetX = ball.x + sin(t)*small error; when ball moving down and close, track precisely; also add slight lag: target = ball.x + (level-based error). Simple: paddle.x lerps toward ball.x with max speed ~ 520 + level*30. If ball stuck, follow ball.x. To make it look decent, AI speed slightly less than ball speed so occasionally misses → lives lost, game over → auto restart demo after 2s. That's fine and shows game over flow. Actually to keep demo alive, AI speed = max(ballSpeed*0.9, 500)? Ball speed up to 620; AI 560+level*40. It'll mostly catch. Add small aim error that decreases when ball close to paddle: err = clamp(dist*0.15, -40, 40). Good.
Player input: mousemove on canvas (and window) → paddle.targetX = pointer logical x. Keys: left/right set keyDir; in update, if keyDir, paddle.x += dir*speed*dt (speed 640), else lerp toward targetX: paddle.x += (targetX - x) * min(1, dt*18) with clamp. Simpler: in play mode, paddle.x moves toward targetX at up to 900 px/s. Mouse sets targetX continuously. Keys set targetX too (targetX += dir*700*dt). Unified: velocity approach. Let's do: desired = targetX; dx = desired - x; step = clamp(dx, -maxSpd*dt, maxSpd*dt); x += step. maxSpd: demo 640, play 1000.
Pause: toggle paused; overlay card changes to "Paused — press P to resume"? Keep separate small overlay reuse: show overlay with different text, mode remains. Manage: state = 'demo' | 'play'; paused bool; gameOver bool. Overlay shown when: (state==='demo' && !started) || paused || gameOver. Content set accordingly. Click behavior: if gameOver → reset game, state='play', hide overlay. else if demo → state='play', hide overlay. else if paused → unpause.
Hmm but in demo the overlay says "click to start" — after clicking, state='play'. If user wants to go back to demo? Add key 'D' to return to demo? Optional; skip, but maybe pressing 'R' restarts. Keep: R restarts run (state stays). Fine.
Sounds: WebAudio: function beep(freq, dur, type, vol, slide). pop sound on brick: quick noise? Use osc square 600→200 Hz 0.08s + noise? Keep simple osc. paddle hit: triangle 220→330. wall: short 180. lose life: descending saw. level clear: arpeggio. Create ctx lazily on first user gesture (click/keydown) — wrap in try. In demo before gesture, ctx suspended; just skip playing if ctx.state !== 'running'.
HUD update: score text, level, lives as hearts: render '♥'.repeat(lives) colored; use spans for gray when lost. Simpler: textContent with color all red; lost lives show '♡'.
Demo badge: show when state==='demo': "DEMO — CLICK TO TAKE CONTROL". Hide in play.
Canvas render:
- clear; draw bg (pre-rendered offscreen pattern canvas sized W,H at logical res, drawn with drawImage; since transform scales, offscreen at W×H logical fine, maybe ×2 for sharpness — just W×H with dpr? The transform scales it up; slight blur acceptable, or render offscreen at W*dpr... keep simple: draw gradient + dots directly each frame? Dots ~ 60 circles per frame is fine. Or pre-render once at 1800×1120 and drawImage scaled — sharp. Do that.)
- apply shake translate.
- draw bricks.
- draw hearts particles (behind ball? in front of bricks, behind paddle fine—draw after bricks).
- draw paddle hamburger.
- draw ball.
- draw floating texts.
- if ball.stuck draw aim hint? small.
- vignette? skip.
Hamburger drawing detail (paddle at x center, y top = paddle.y):
Let w=118, h=36. Layers from top:
- top bun: height 14: path: moveTo(x-w/2, y+14) quadratic to top arc: use ellipse: ctx.ellipse(x, y+14, w/2, 12, 0, PI, 0) fill bun gradient (#f2b268 top → #e09a4e). Actually draw half-ellipse: save, begin path, ellipse(x, y+13, w/2, 13, 0, Math.PI, 0), closePath (line across bottom), fill.
- seeds: 7 small ellipses white-ish (#fff3d6) rotated random-ish (precomputed offsets), positioned on bun arc: for i, sx = x + offset[i]*(w/2*0.8), sy = y + 13 - 13*sqrt(1-(offset^2)) roughly; draw ellipse rx 2.2 ry 1.3 rotated.
- face on bun: two eyes (dark circles) at x±12, y+6; smile arc. Blink: every 3-4s eyes become lines for 0.12s. Cute.
- lettuce: y+14..y+19: wavy strip: fill green #7bc950; path with quadratic waves across width, slightly wider than bun (w+6).
- cheese: y+19..y+24: yellow #ffd43b rectangle width w*0.9 with 3 drips (triangles down to y+28).
- patty: y+24..y+31: brown #8d5a2b rounded rect width w, rx 4; darker grill lines? two darker lines.
- bottom bun: y+31..y+37: tan #e8a85c rounded rect width w*0.96 rx 5.
Total height ~37. Squash: when squash>0, scale y around bottom: ctx.translate(x, y+h); ctx.scale(1+squash*0.15, 1-squash*0.25); translate back. squash decays *= exp.
Draw shadow under paddle: ellipse dark alpha.
Ball tomato: circle r9 red gradient (#ff5252 → #d63031), highlight small white circle offset, stem: green small rect + two leaves at top. Rotate slightly by velocity? Keep stem up; add spin rotation = x*0.05. Fine: rotate ctx by ball.spin.
Brick roundRect: ctx.roundRect available in Chrome 99+. Use fallback helper.
Collision ball-brick: iterate alive bricks; closest point cx=clamp(ball.x, b.x, b.x+b.w), cy=clamp(ball.y,...); dx=ball.x-cx, dy=ball.y-cy; if dx*dx+dy*dy < r*r → hit. Determine side: if |dx|>|dy| reflect vx else vy (approx). To be safer compute previous position; simple approach ok at these speeds with dt clamp (dt max 0.033, ball speed ≤ 700 → 23px per frame; brick h ~22 — could tunnel. Use substeps: move ball in 2 substeps per frame when speed high. I'll implement physics with substeps = ceil(speed*dt / (r)) maybe 3. Simpler: perform collision check after moving in small steps: steps = Math.ceil(dist/r/1) cap 6. Implement moveBall with substepping: for each substep move ball by v*dt/steps, check wall collisions and paddle and bricks each substep. Good.)
Brick break scoring: 10*hp? score += 10 (×combo?). Add multiplier: combo increments per brick without paddle touch, displayed "xN". score += 10*combo? Could balloon; fine: 10 * min(combo,8)? Keep score += 10 + (combo>4? 5:0)? Simplify: score += 10 * level? Let's do points = 10 * hp * (1 + floor(combo/5))? Overcomplicating. points = 10 * combo capped at 50? I'll do: combo++ on brick; points = 10 + (combo-1)*2 capped 30; on paddle hit combo=0. Show combo text when ≥3: "COMBO x3".
Lives: 3. On bottom: lives--, update HUD, sound, hearts burst red at bottom? spawn 12 hearts at ball pos falling. If lives<=0 → gameOver: state stays, overlay shows game over card with score, "Click to play again". Demo auto-restart? If gameOver in demo mode, after 2.5s auto reset to demo continue? Could be nice for screenshot; but overlay blocks view. Let's auto-restart after 3s even in demo (reset level 1, lives 3, hide overlay, state demo). In play mode, wait for click.
Level clear: when no alive bricks: level++, rebuild bricks, increase ball speed, brief banner text (big center "LEVEL 2 ♥" fading) via texts with big font, sound arpeggio, ball stuck re-serve.
Initial: build level 1, state='demo', overlay visible, ball stuck, countdown to auto-serve so action starts within 3s. Screenshot at ~3s: bricks + hearts flying likely if AI breaks one quickly. Ball speed 340, bricks start y~80, ball serve upward... time to first brick ~ (H-46-9-80)/340 ≈ 1.2s + serve delay 1.0s → first explosion ~2.5s. Good, within 3s likely. Make serve delay 0.6s to be safe. Also spawn a few ambient hearts floating at start? Could add gentle idle hearts drifting up from bottom for charm even before first break — nice touch: every 0.4s spawn 1 small heart rising slowly with low alpha in demo? Might clutter; add subtle: ambient hearts rising from bottom edge every 0.5s, life long, alpha 0.5, no gravity (float up). Cute background. Cap.
Resize handling: ResizeObserver on stage or window resize → fit(). Keep logical W,H constant; canvas.width = cssW*dpr; canvas.height = cssW*dpr*H/W. ctx.setTransform each frame: s = canvas.width/W; ctx.setTransform(s,0,0,s,0,0).
Pointer events: on canvas pointermove → update target from clientX. pointerdown → treat as click (start play / restart). Also touch-action none. Keys: keydown: if overlay visible & not gameover → start play (any key) — but if key is 'P' maybe pause instead; spec: "Click or press a key to start". So any key starts play and hides overlay; also handle arrows preventDefault to avoid scroll. After started: ArrowLeft/Right set keyDir; 'p' toggles pause; 'r' restart.
Careful: keydown when state demo → start play AND apply the key (e.g., space serves immediately). Fine.
Mouse move also should work when overlay hidden — attach to window mousemove, compute only if over canvas region? Just use clientX relative to canvas rect always; moving mouse anywhere moves paddle — acceptable and convenient.
Auto demo restart after game over: setTimeout 2500 → if still gameOver && state==='demo' → resetRun(keep state demo), hide overlay.
Now HUD lives hearts: use text '♥' colored #ff4d6d with text-shadow; lost: '♡' gray. innerHTML with spans.
Score pop animation on change: add class 'pop' retrigger.
Banner texts array: {x,y,text,life,maxLife,size,color}. Draw centered with alpha.
Let me also add subtle CRT? no.
Colors/background: page bg: linear-gradient(160deg,#fff8f0,#ffe9f3 60%,#ffe3d9). Stage canvas bg offscreen: vertical gradient #fff6ec → #ffd9e8, plus polka dots rgba(255,120,160,0.08) radius 6 spaced 46 offset rows; plus a "table" strip at bottom? The playfield includes floor at bottom (ball lost below). Draw a dashed line? Just let hearts bounce at y=H-8.
Border of playfield: rounded rect stroke inside canvas? Canvas has CSS border-radius + overflow hidden via CSS (canvas { border-radius } clips drawing? Canvas content isn't clipped by border-radius unless overflow hidden on wrapper with same radius — set .stage{border-radius:18px; overflow:hidden}). Yes wrap canvas in .stage with overflow hidden and radius; canvas block. Overlay inside stage too.
Add top "ceiling" decoration? skip.
Performance: hearts capped 500; each heart path with bezier ×2 — fine.
Sound design (WebAudio):
```
let AC=null; function audio(){ if(!AC){ try{AC=new (window.AudioContext||window.webkitAudioContext)();}catch(e){} } if(AC&&AC.state==='suspended'){AC.resume();} return AC; }
function tone(f0,f1,dur,type='sine',vol=0.15){ const ac=audio(); if(!ac||ac.state!=='running')return; const o=ac.createOscillator(), g=ac.createGain(); o.type=type; const t=ac.currentTime; o.frequency.setValueAtTime(f0,t); o.frequency.exponentialRampToValueAtTime(Math.max(f1,1),t+dur); g.gain.setValueAtTime(vol,t); g.gain.exponentialRampToValueAtTime(0.0001,t+dur); o.connect(g).connect(ac.destination); o.start(t); o.stop(t+dur+0.02); }
```
Brick: tone(520+rand*200, 180, 0.12,'square',0.12) + tone(1200,600,0.06,'sine',0.08). Paddle: tone(240,420,0.09,'triangle',0.18). Wall: tone(200,160,0.05,'square',0.07). Lose: tone(400,80,0.4,'sawtooth',0.15). Clear: notes [523,659,784,1047] staggered via setTimeout — but setTimeout fine. Serve: tone(600,900,0.08,'sine',0.1).
Now write update(dt):
```
if paused or overlayBlocking? game runs even with overlay (demo). if gameOver: still update particles (hearts fall) but not ball/bricks. Actually keep updating particles & paddle idle.
paddle update: desired target; if keys dir active (play mode) target += dir*720*dt.
clamp paddle.x to [w/2+6, W-w/2-6].
ball: if stuck: ball.x = paddle.x + aimOffset? ball.x=paddle.x; ball.y = paddle.y - r - 2; serveTimer -= dt; if <=0 launch.
else substep movement.
bricks collision → break.
hearts update; texts update; shake decay; squash decay; blink timer; ambient spawn timer.
```
Ball launch angle: in demo: toward random brick-ish: angle = -PI/2 + (rand-0.5)*1.2. Play: same.
Ball speed: base = 340 + (level-1)*35, cap 640. On paddle hit, speed *= 1.02 cap.
Ball-brick reflect: compute normal: if |dx| > |dy|: vx = sign(dx)*|vx|... else vy = sign(dy)*|vy|. Add slight random spin. Break brick: alive=false, burst, score, combo, sound, shake, check level clear (aliveCount==0 → nextLevel after short delay? immediate with banner + brief slow-mo? just immediate rebuild after 0.8s pause: set state 'levelclear' timer, freeze ball stuck). Implement: levelClearTimer = 0.9; during it, skip ball/brick updates but show banner; then rebuild.
Simplify: on clear → banner "LEVEL N CLEAR! ♥", sound, setTimeout(()=>{level++; buildLevel(); ball.stuck=true; serveTimer=1; }, 900). During timeout ball keeps moving? Set ball.stuck=true immediately and vx=vy=0 to avoid weirdness; hearts continue. Good.
Paddle collision detail: if ball.vy>0 and ball.y+r >= paddle.y and ball.y-r <= paddle.y+paddle.h+6 and |ball.x-paddle.x| <= paddle.w/2 + r: compute offset = clamp((ball.x-paddle.x)/(paddle.w/2),-1,1); ang = -PI/2 + offset*1.05; speed=hypot; ball.vx=cos(ang)*speed; ball.vy=sin(ang)*speed; ball.y = paddle.y - r - 1; combo=0; squash=1; sound.
Walls: x<r → x=r, vx=|vx|; x>W-r similar; y<r → vy=|vy|. y > H+20 → lose life.
Substep:
```
const dist = Math.hypot(ball.vx,ball.vy)*dt; const steps = Math.min(6, Math.max(1, Math.ceil(dist/(ball.r*0.9))));
for(s=0;s<steps;s++){ ball.x+=vx*dt/steps; ... wall checks; paddle check; brick check (break on first hit, break out) }
```
Brick check function returns true if any brick hit (to stop substeps? continue fine).
Hearts particle: {x,y,vx,vy,rot,vr,s,life,maxLife,color,bounce}
update: vy += 620*dt; x+=vx*dt; y+=vy*dt; rot+=vr*dt; life-=dt; if y>H-10 && vy>0 { y=H-10; vy*=-0.45; vx*=0.7; } — bounce on floor line. alpha = min(1, life/0.5). scale pop: sc = min(1, (maxLife-life)*6+0.3)? Use easeOutBack-ish: sc = 1 + 0.4*sin? Keep: sc = Math.min(1, (maxLife-life)*5) * ... plus shrink at end: multiply by min(1, life/0.4).
Draw heart: save translate rotate scale; path; fill; small white highlight? skip highlight for perf, maybe lighter stroke. Add shadowBlur? expensive; skip. Use globalAlpha.
Colors hearts palette warm: ['#ff4d6d','#ff6b8a','#ff8fab','#ffc2d1','#e8455f','#ff2e63','#fff0f3'].
Ambient hearts: {ambient:true, vy negative small, no gravity, alpha 0.35, s small, drift vx sine}. Spawn at bottom random x, rise. Remove when y< -20 or life end. Implement as same array with g=0 flag.
Floating texts: {x,y,vy:-40,life:1,text,size,color} draw with font bold `${size}px ui-rounded, system-ui` fillText center, alpha fade.
Combo display: if combo>=3 draw near paddle top "x3 COMBO!" small — or as floating text each brick: `+${pts}` and when combo>=3 also "COMBO x"+combo. Fine.
Build level:
```
function buildLevel(){ bricks=[]; const cols=10, rows=Math.min(4+level,8); const m=46, gap=8, top=78; const bw=(W-2*m-(cols-1)*gap)/cols; const bh=22; const pal = palette rotated by level; for r,c: hp = (level>=3 && r<2)?2:1 (also level>=5 r<3?2); color = pal[(r+level)%pal.length]; if hp>1 color darker shade — precompute pairs: each palette entry {fill, edge}. hp2 → use edge color as fill? Simpler: two palettes per row: normal & strong. strong colors more saturated/darker.
brick={x:m+c*(bw+gap), y:top+r*(bh+gap), w:bw,h:bh,hp,maxHp:hp,alive:true, hue offset for shimmer}
}
```
Add slight per-brick wobble? no.
Draw brick: rounded rect radius 6; fill color; top bevel: lighter overlay rect height 6 alpha .35 white; bottom edge darker stroke. If hp<maxHp: draw crack: two lines. Also tiny shine dot. Also maybe draw tiny heart embossed on brick? cute: draw small heart outline alpha 0.25 center — reinforces theme. Yes! draw mini heart stroke on each brick, alpha 0.18.
roundRect helper:
```
function rr(x,y,w,h,r){ ctx.beginPath(); if(ctx.roundRect){ctx.roundRect(x,y,w,h,r);} else { manual } }
```
Paddle hamburger face blink: blinkT timer: every 2.8+rand s set blink=0.12. eyes drawn as circles r2.6 dark #5b3a1e at (x-13, y+7),(x+13,y+7) or lines when blinking. Smile: arc from x-6,y+11 to x+6 radius 6, 0.15PI..0.85PI stroke.
Cheese drips: at offsets [-w*0.3, 0, w*0.3]: triangle from (cx-5,y+24) to (cx+5,y+24) to (cx, y+30). fill cheese color. Draw cheese base rect y+19 h6 w*0.92 centered.
Lettuce wave: path: start (x-w/2-4, y+18); for segments across: quadraticCurveTo alternating control y+15 / y+21... simpler: build with small arcs: for i in 0..n: x0... Use: ctx.moveTo(x-w/2-3, y+19); then loop k=0..8: cx = x-w/2-3 + (k+0.5)*step; ctx.quadraticCurveTo(cx, y+(k%2? 15.5:22), x-w/2-3+(k+1)*step, y+19); then line down to y+21 and back? Lettuce as filled blob: after top wave, lineTo(right, y+22), lineTo(left, y+22), close. Fill #76c043 with darker stroke? Just fill, plus second layer lighter #8ed05a offset? Keep one fill + stroke darker.
Order drawing paddle: shadow, bottom bun, patty, cheese+drips, lettuce, top bun, seeds, face. Wait layering: top bun on top visually (higher y). Draw from bottom up: bottom bun (y+31..37), patty (24..31), cheese (19..24 + drips to 29), lettuce (wave ~15..22) drawn after cheese so it overlaps top of cheese? Lettuce should be above cheese (closer to top bun). Visual stack top→bottom: bun, lettuce, cheese, patty, bun. Painter's order bottom-most first: bottom bun, patty, cheese, lettuce, top bun. Lettuce wave spanning y 14..22 overlapping cheese top — draw after cheese. Good.
Squash transform around paddle center-bottom.
Ball stuck indicator: draw small pulsing ring / "serve" arrow? draw 3 dots above paddle? Keep countdown ring: circle arc around ball showing serveTimer fraction. Simple: alpha pulse.
Overlay card style: glassmorphism: background rgba(255,255,255,0.72), backdrop-filter blur(6px), border-radius 20px, padding, box-shadow, text center. Big hamburger emoji? Emoji rendering in headless Chrome: color emoji available usually on Linux? In many sandboxes, emoji may render as monochrome or tofu. Risk: 🍔 might show as box. Safer: draw a small inline SVG hamburger? SVG inline is allowed (it's inline, no external). I can inline a tiny SVG hamburger icon in the card and title. Let's create simple SVG burger: layers rects/ellipses. That guarantees visuals. Also hearts '♥' unicode — usually available in default fonts (DejaVu has ♥ U+2665). In canvas fillText for combo, hearts fine. I'll use SVG for burger icon in overlay/title, and '♥' text for lives (canvas-drawn hearts for particles anyway).
Actually title: "Burger Breakout" with small inline SVG burger before it. Overlay card: bigger SVG burger + text.
SVG burger (48×40):
- top bun: path M4,18 a20,12 0 0 1 40,0 z fill #f0b269, seeds circles.
- lettuce: path wave fill green.
- cheese: rect yellow with drips.
- patty: rect brown rx.
- bottom bun: rect tan rx.
Write compact.
HUD badge demo: pulsing dot.
Now sizes: W=900,H=560. Paddle y = H-52. Brick top 78.
At 1280×800 viewport: wrap max-width 960; header ~90px; hud ~40; stage 960×(560/900*960=597) = 597 → total ~ 90+40+597+hint 30 = 757 < 800 ✓. Set wrap max-width 920 to be safe → stage 573, total ~730. Good.
Mobile 375: stage width ~ 375-24=351 → height 218. Playable. HUD wraps. Font sizes with clamp.
Edge: dpr cap 2.
Prevent horizontal scrollbar: body overflow-x hidden.
Let me now also handle "first paint meaningful": overlay + demo running with bricks visible, hearts ambient rising — yes visible immediately.
Write the code carefully.
Detailed code plan:
```js
(() => {
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
const stage = document.getElementById('stage');
const W=900, H=560;
let dpr=1;
function resize(){
const cssW = stage.clientWidth;
dpr = Math.min(2, window.devicePixelRatio||1);
canvas.width = Math.round(cssW*dpr);
canvas.height = Math.round(cssW*dpr*H/W);
canvas.style.height = (cssW*H/W)+'px';
makeBG();
}
window.addEventListener('resize', resize);
```
CSS: canvas {width:100%; display:block} and we set style.height explicitly (aspect-ratio also set; either fine).
BG offscreen:
```
let bg=null;
function makeBG(){
bg=document.createElement('canvas'); bg.width=W*2; bg.height=H*2;
const b=bg.getContext('2d'); b.scale(2,2);
const g=b.createLinearGradient(0,0,0,H);
g.addColorStop(0,'#fff7ee'); g.addColorStop(.55,'#ffe9f1'); g.addColorStop(1,'#ffdbe4');
b.fillStyle=g; b.fillRect(0,0,W,H);
// polka dots
b.fillStyle='rgba(255,110,150,0.07)';
const step=52;
for(let y=step/2;y<H;y+=step) for(let x=((y/step)%2? step/2:0)+step/2; x<W; x+=step){ b.beginPath(); b.arc(x,y,7,0,7); b.fill(); }
// soft top glow
...
}
```
arc end angle 7 > 2π fine.
State vars:
```
let state='demo'; // 'demo' | 'play'
let paused=false, gameOver=false, levelClear=false;
let score=0, lives=3, level=1, combo=0;
let paddle, ball, bricks=[], hearts=[], texts=[], banners=[];
let shake=0, serveTimer=0, ambientT=0, time=0;
```
resetRun(): score=0;lives=3;level=1;combo=0; buildLevel(); resetBall(); updateHUD();
resetBall(): ball={x:paddle.x,y:paddle.y-12,vx:0,vy:0,r:9,stuck:true,spin:0}; serveTimer=0.8;
paddle init: {x:W/2,y:H-52,w:118,h:38,targetX:W/2,squash:0,blink:0,blinkT:2};
buildLevel as planned. palettes:
```
const PAL=[['#ff8fab','#e5486d'],['#ffc078','#e8590c'],['#ffec99','#f08c00'],['#b2f2bb','#2f9e44'],['#a5d8ff','#1971c2'],['#d0bfff','#6741d9'],['#fcc2d7','#c2255c']];
```
strong = edge color as fill with darker stroke.
Input:
```
let keyDir=0;
window.addEventListener('keydown', e=>{
if(['ArrowLeft','ArrowRight',' '].includes(e.key)) e.preventDefault();
audio();
if(gameOver){ if(e.key) { startPlay(); resetRun(); } return; }
if(paused){ if(e.key==='p'||e.key==='P'||e.key==='Escape'){togglePause();} else startPlay(); return; }
if(state==='demo'){ startPlay(); }
if(e.key==='ArrowLeft') keyDir=-1;
if(e.key==='ArrowRight') keyDir=1;
if(e.key==='p'||e.key==='P') togglePause();
if(e.key==='r'||e.key==='R'){ resetRun(); state='play'; hideOverlay(); }
});
window.addEventListener('keyup', e=>{ if(e.key==='ArrowLeft'&&keyDir===-1)keyDir=0; if(e.key==='ArrowRight'&&keyDir===1)keyDir=0; });
canvas.addEventListener('pointermove', e=>{ const r=canvas.getBoundingClientRect(); paddle.targetX=(e.clientX-r.left)/r.width*W; });
window.addEventListener('pointermove', same for window? Use canvas only + also window mousemove to be forgiving: attach to window and compute if within rect expanded. I'll attach to window pointermove and compute always (paddle follows mouse x even outside—fine).
stage.addEventListener('pointerdown', e=>{ audio(); if(gameOver){resetRun(); state='play'; hideOverlay(); gameOver=false;} else if(paused){togglePause();} else if(state==='demo'){startPlay();} });
```
startPlay(): state='play'; hideOverlay(); demoBadge hidden; hint text change.
hideOverlay(): overlay.classList.add('hidden').
showOverlay(html?) set card content.
Pause: paused=true; show overlay with "Paused" card (no auto AI). On resume hide.
Game over: gameOver=true; show overlay gameover card; if state==='demo' setTimeout(()=>{ if(gameOver&&state==='demo'){ gameOver=false; resetRun(); hideOverlay(); } },2600);
Overlay visibility control: overlay element with .hidden class (opacity 0, pointer-events none, transition). Card content swapped via function setCard(type).
Loop:
```
let last=performance.now();
function frame(now){ let dt=(now-last)/1000; last=now; dt=Math.min(dt,0.033); time+=dt; if(!paused) update(dt); render(); requestAnimationFrame(frame); }
requestAnimationFrame(frame);
```
Even when paused, render (frozen scene) — fine.
update(dt):
```
// paddle
let desired=paddle.targetX;
if(state==='play'&&keyDir!==0){ desired=paddle.x+keyDir*760*dt*60? }
```
Hmm mixing: if keyDir, move directly: paddle.x += keyDir*720*dt and also targetX=paddle.x. Else approach target: maxSpd = state==='demo'? 620+level*20 : 1400; dx=desired-paddle.x; step=clamp(dx,-maxSpd*dt,maxSpd*dt); paddle.x+=step. AI: desired = ball.stuck? ball.x : ball.x + aimErr; aimErr decays toward 0 when ball near paddle: err target = clamp((paddle.y-ball.y)*0.12, -46,46) * (ball.vy>0?1:0.3) + noise. Let me:
```
if(state==='demo'){ const closing = ball.vy>0; let err = closing? clamp((paddle.y-ball.y)*0.10, -50, 50) : 0; err += Math.sin(time*1.7)*6; paddle.targetX = ball.x + err; }
```
Demo maxSpd = 560 + level*30 (cap 760). Ball max speed cap 620 — AI mostly catches; sometimes misses at high speed → life lost → adds drama. OK.
clamp paddle.x.
squash decay: paddle.squash*=Math.pow(0.001,dt)? use paddle.squash=Math.max(0,paddle.squash-dt*4). blink: paddle.blinkT-=dt; if<0 {paddle.blink=0.13; paddle.blinkT=2+Math.random()*2.5;} paddle.blink=max(0,blink-dt).
ball:
```
if(ball.stuck){ ball.x=paddle.x; ball.y=paddle.y-ball.r-3; serveTimer-=dt; if(serveTimer<=0) launch(); }
else {
const sp=Math.hypot(ball.vx,ball.vy); const steps=Math.min(8,Math.max(1,Math.ceil(sp*dt/(ball.r*0.8))));
for(let i=0;i<steps;i++){ stepBall(dt/steps); if(ball.stuck) break; }
ball.spin+=dt*8;
}
```
stepBall(sdt): move; walls; paddle; bricks.
launch(): ang=-Math.PI/2+(Math.random()-0.5)*1.1; sp=ballSpeed(); ball.vx=Math.cos(ang)*sp; ball.vy=Math.sin(ang)*sp; ball.stuck=false; tone serve.
ballSpeed(): Math.min(640, 330+(level-1)*35 + (levelClear?0:0)) maybe + combo*2? keep.
Walls in stepBall:
```
ball.x+=vx*sdt; ball.y+=vy*sdt;
if(ball.x<ball.r+4){ball.x=ball.r+4; ball.vx=Math.abs(ball.vx); tone(190,150,.05,'square',.06);}
if(ball.x>W-ball.r-4){...}
if(ball.y<ball.r+4){ball.y=ball.r+4; ball.vy=Math.abs(ball.vy); tone(...)}
if(ball.y>H+24){ loseLife(); }
```
Paddle:
```
if(ball.vy>0 && ball.y+ball.r>=paddle.y && ball.y-ball.r<=paddle.y+paddle.h+8 && Math.abs(ball.x-paddle.x)<=paddle.w/2+ball.r){
const off=clamp((ball.x-paddle.x)/(paddle.w/2),-1,1);
const ang=-Math.PI/2+off*1.05;
let sp=Math.hypot(ball.vx,ball.vy)*1.015; sp=Math.min(sp, ballSpeed()+60);
ball.vx=Math.cos(ang)*sp; ball.vy=Math.sin(ang)*sp;
ball.y=paddle.y-ball.r-1; combo=0; paddle.squash=1; shake=Math.min(shake+1.5,6);
tone(230,430,.09,'triangle',.16);
}
```
Bricks:
```
for(const b of bricks){ if(!b.alive) 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){
if(Math.abs(dx)>Math.abs(dy)) ball.vx=Math.sign(dx||1)*Math.abs(ball.vx); else ball.vy=Math.sign(dy||1)*Math.abs(ball.vy);
hitBrick(b); break;
}
}
```
hitBrick(b): b.alive=false; b.hp--; if(b.hp>0){ // cracked, respawn heart small? just re-alive with crack: b.alive=true; burst small 8 hearts; tone lower; } else { explode big }. Wait hp handling: on hit, hp--; if hp<=0 → destroyed (full explosion, points) else cracked (small puff). Points only on destroy? give 5 on crack. Implement:
```
function hitBrick(b){
b.hp--;
if(b.hp<=0){ b.alive=false; combo++; const pts=10+Math.min(combo,10)*2; score+=pts; addText(b.x+b.w/2,b.y,'+'+pts,18,'#e5486d'); if(combo>=3) addText(b.x+b.w/2,b.y-22,'COMBO x'+combo,14,'#ff8fab'); explode(b); tone(500+Math.random()*250,160,.12,'square',.12); tone(1400,700,.07,'sine',.08); shake=Math.min(shake+3,9); }
else { explodeSmall(b); tone(300,140,.08,'square',.09); score+=5; addText(...,'+5',13,...); }
updateHUD();
if(bricks.every(x=>!x.alive)) levelCleared();
}
```
explode(b): n=28+rand*14 hearts centered with random vel: ang random, sp 140+rand*380; s 5+rand*7; life 1.1+rand*1.3; colors random; vr (rand-0.5)*12; g=620; bounce true. Also 8 sparkles: store as hearts with sparkle flag? separate array sparks {x,y,vx,vy,life,color:'#fff'} drawn as small circles, gravity, no bounce. Add.
Cap: if hearts.length>650 splice oldest 100.
levelCleared(): levelClear=true (pause ball): ball.stuck=true; vx=vy=0; banner('LEVEL '+level+' CLEAR! ♥', ...); arpeggio tones; setTimeout(()=>{ if(gameOver) return; level++; buildLevel(); resetBall(); serveTimer=1.1; levelClear=false; updateHUD(); },1000); updateHUD level shows level+1? show level+1 after rebuild; during banner show current. Also big heart burst from center: explodeAt(W/2,H/2, 60 hearts).
loseLife(): lives--; updateHUD(); explodeAt(ball.x, H-30, 24 hearts red); tone(380,70,.45,'sawtooth',.16); shake=8; if(lives<=0){ gameOver=true; showCard('over'); if(state==='demo'){ setTimeout(()=>{ if(gameOver&&state==='demo'){ gameOver=false; resetRun(); hideOverlay(); } },2800);} } else { resetBall(); serveTimer=1.2; }
updateHUD(): scoreEl.textContent=score; levelEl.textContent=level; livesEl.innerHTML = hearts spans: for i<3: i<lives? '<span class="on">♥</span>':'<span class="off">♥</span>' with .off color #d8c3cc. demoBadge style display.
Particles update:
```
for(let i=hearts.length-1;i>=0;i--){ const p=hearts[i]; p.life-=dt; if(p.life<=0){hearts.splice(i,1);continue;} if(p.amb){ p.y+=p.vy*dt; p.x+=p.vx*dt+Math.sin(time*3+p.seed)*12*dt; if(p.y<-20) hearts.splice(i,1); continue;} p.vy+=p.g*dt; p.x+=p.vx*dt; p.y+=p.vy*dt; p.rot+=p.vr*dt; if(p.y>H-12&&p.vy>0){p.y=H-12;p.vy*=-p.vy*0.42;p.vx*=0.72;} }
sparks similar.
texts: y+=vy*dt; life-=dt; remove.
shake*=Math.pow(0.02,dt) → shake=max(0,shake-dt*22)? use shake=Math.max(0,shake - dt*18) plus jitter each frame ±shake.
ambient: ambientT-=dt; if<=0 {ambientT=0.45; spawn ambient heart at random x bottom: {amb:true,x:rand*W,y:H+10,vx:(rand-.5)*20,vy:-(30+rand*40),s:4+rand*5,life:6+rand*3,...}} cap ambient count maybe.
```
Render:
```
function render(){
const s=canvas.width/W; ctx.setTransform(s,0,0,s,0,0);
ctx.clearRect(0,0,W,H);
ctx.drawImage(bg,0,0,W,H);
ctx.save();
if(shake>0.2) ctx.translate((Math.random()-.5)*shake,(Math.random()-.5)*shake);
drawBricks(); drawSparks(); drawHearts(); drawTexts? texts above; drawPaddle(); drawBall(); drawBanners();
ctx.restore();
}
```
Order: bricks, hearts (in front), paddle, ball, texts/banners on top.
drawBricks: for alive: rr fill color; bevel: fillStyle rgba(255,255,255,.28) rr(x,y,w,6, top only — approximate with rect) — use rr(x+2,y+2,w-4,7,4) clip? simpler: ctx.fillRect? rounded top only hard; just draw rounded full with gradient? Use linear gradient per brick: create each frame? 80 bricks × gradient fine. Simpler: fill base color; then overlay white alpha .22 rounded rect (x+2,y+2,w-4,8,r=4); then stroke bottom: strokeStyle edge color lineWidth 2 rr stroke. Mini heart emboss: ctx.globalAlpha=.16; stroke heart path centered scaled 4; restore alpha. If hp<maxHp draw crack lines: two strokes across.
Perf fine.
drawHeartPath(x,y,s,rot):
```
ctx.save(); ctx.translate(x,y); ctx.rotate(rot); ctx.scale(s,s);
ctx.beginPath();
ctx.moveTo(0,0.32);
ctx.bezierCurveTo(-0.55,-0.05,-0.42,-0.55,0,-0.22);
ctx.bezierCurveTo(0.42,-0.55,0.55,-0.05,0,0.32);
ctx.closePath(); ctx.fill(); ctx.restore();
```
Check shape: classic heart: top dip at (0,-0.22)? Let me define standard:
```
moveTo(0, 0.3)
bezierCurveTo(0, 0.1, -0.5, 0.1? ...)
```
Common heart path (unit):
M0,0.3 C -0.6,-0.3 -0.3,-0.9 0,-0.4 C 0.3,-0.9 0.6,-0.3 0,0.3 Z — hmm let me verify: start bottom point (0,0.3). Curve to left lobe: control (-0.6,-0.3) and (-0.3,-0.9) end (0,-0.4) — that makes left side bulge up-left ending at center dip (0,-0.4). Then mirror: control (0.3,-0.9),(0.6,-0.3) end (0,0.3). Yes that yields heart with dip at top center (0,-0.4), lobes up to ~-0.7.
So:
```
ctx.moveTo(0,0.32);
ctx.bezierCurveTo(-0.62,-0.28,-0.32,-0.95,0,-0.42);
ctx.bezierCurveTo(0.32,-0.95,0.62,-0.28,0,0.32);
ctx.closePath();
```
Good. Scale s means heart spans ~1.2s tall.
drawPaddle: shadow ellipse at (x, y+h+6) rx w/2 alpha .12 black. squash transform:
```
ctx.save(); ctx.translate(paddle.x, paddle.y+paddle.h); const sq=paddle.squash; ctx.scale(1+sq*0.12, 1-sq*0.22); ctx.translate(-paddle.x, -(paddle.y+paddle.h));
// draw layers with local coords using px=paddle.x, py=paddle.y, w=paddle.w
```
Bottom bun: rr(px-w/2+3, py+30, w-6, 8, 4) fill '#e9a860';
patty: rr(px-w/2, py+22, w, 10, 5) fill '#7a4a21'; grill lines: darker strokes 3 vertical? horizontal lines: two lines across at py+25,py+28 alpha .3 '#5b3413'.
cheese: fillRect(px-w*0.44, py+16, w*0.88, 7) '#ffd43b'; drips: triangles at px-w*0.3, px, px+w*0.3: moveTo(cx-5,py+23) lineTo(cx+5,py+23) lineTo(cx,py+29+rand? fixed) fill.
lettuce: wave path across width w+8 centered at py+15:
```
ctx.fillStyle='#69c23f'; ctx.beginPath(); const lw=w+8; const x0=px-lw/2; ctx.moveTo(x0,py+17); const n=9; for(let k=0;k<n;k++){ const seg=lw/n; ctx.quadraticCurveTo(x0+(k+0.5)*seg, py+(k%2? 11:22), x0+(k+1)*seg, py+17);} ctx.lineTo(x0+lw, py+20); ctx.lineTo(x0, py+20); ctx.closePath(); ctx.fill();
```
Hmm wave amplitude: control y alternating 11 and 22 around base 17 gives scalloped top edge; then bottom edge straight at 20 → lettuce strip with wavy top. Good. Add darker green stroke? optional.
top bun:
```
const g=ctx.createLinearGradient(0,py-2,0,py+16); g.addColorStop(0,'#f6c078'); g.addColorStop(1,'#e59a4e');
ctx.fillStyle=g; ctx.beginPath(); ctx.ellipse(px, py+16, w/2, 15, 0, Math.PI, 0); ctx.closePath(); ctx.fill();
```
ellipse from PI to 0 draws top half (upper arc) — with closePath chord at py+16. Yes.
seeds: precomputed offsets [-0.62,-0.38,-0.12,0.12,0.38,0.62] plus [ -0.25, 0.25 ] higher row: draw at (px+off*w/2*0.92, py+16-15*Math.sqrt(Math.max(0,1-off*off*0.85)) ... approximate: y = py+16 - 13*Math.sqrt(1-(off*0.95)**2) - 1. For off 0.62 → sqrt(1-0.347)=0.807→ y=py+16-10.5-1. ok. Seeds: ellipse rx2.3 ry1.3 rotate off*0.5, fill '#fff3d0'.
face: eyes at (px-12, py+9),(px+12,py+9): if blink>0 draw line else circle r2.6 '#4a2c12'; smile: stroke arc (px, py+11) r5 from 0.15π to 0.85π, lineWidth 1.8 '#4a2c12'. Blush: two pink circles alpha .5 at (px-20,py+12),(px+20,py+12) r3 '#ff9eb0' alpha .5.
restore.
Ball:
```
ctx.save(); ctx.translate(ball.x,ball.y); ctx.rotate(ball.spin*0.3);
radial gradient circle r: g center (-r*0.3,-r*0.3): '#ff7b7b'→'#e03131'→'#c92a2a'; fill circle.
highlight: white alpha .8 circle at (-r*0.35,-r*0.4) r*0.28.
stem: at top (0,-r): small green rect + leaf: fillStyle '#2f9e44'; fillRect(-1.5,-r-4,3,5); leaf ellipse at (2.5,-r-3) rx3 ry1.5 rotate -0.5.
restore.
If stuck: draw pulsing ring: ctx.strokeStyle rgba(255,77,109, alpha) arc r+6+sin(time*6)*2.
```
Banners: {text,life,max,size} center W/2, H*0.4; font bold size px; fillStyle '#e5486d' with white stroke? draw with shadow. alpha = life/max.
Texts: font bold size 'ui-rounded, "Segoe UI", system-ui, sans-serif'.
Serve countdown: if ball.stuck draw small text? draw arc ring around ball: progress = 1 - serveTimer/0.8. stroke arc from -π/2. fine.
Overlay card HTML set by JS:
```
function setCard(kind){
let html='';
if(kind==='start') html=`<div class="burger">${BURGER_SVG}</div><h2>Click or press any key to start!</h2><p>The burger AI is playing a demo behind this card — take over whenever you like.</p>`;
if(kind==='pause') html=`<h2>Paused</h2><p>Press P or click to resume</p>`;
if(kind==='over') html=`<h2>Game over! 💔</h2>...` — emoji 💔 maybe tofu; use ♥ text styled. `<h2>Game over</h2><p class="score">Score: <b>${score}</b> · Level ${level}</p><p>Click or press any key to play again</p>`;
overlayCard.innerHTML=html; overlay.classList.remove('hidden');
}
```
BURGER_SVG string.
Emoji in title/hint: risk of tofu in headless Linux Chrome without emoji fonts. Avoid emoji entirely; use ♥ (DejaVu Sans has U+2665, and canvas fillText hearts are drawn paths anyway). For HUD lives use ♥ text — DejaVu includes it; most Linux has DejaVu. Safe enough. Title uses inline SVG burger + text + ♥ span colored.
Hint text: "Move with mouse, ← → keys, or touch · P pause · R restart".
Demo badge: "● DEMO — click to take control" pulsing.
Now CSS:
```
*{box-sizing:border-box;margin:0;padding:0}
html,body{height:100%}
body{font-family:ui-rounded,'Segoe UI',system-ui,-apple-system,sans-serif; background:linear-gradient(160deg,#fff6ec 0%,#ffe4ef 55%,#ffd9e0 100%); min-height:100%; display:flex; justify-content:center; padding:18px 12px 28px; overflow-x:hidden; color:#5b2b3a;}
.wrap{width:100%; max-width:940px;}
header{text-align:center; margin-bottom:10px;}
h1{font-size:clamp(26px,4.5vw,40px); letter-spacing:1px; color:#d6336c; text-shadow:0 2px 0 #fff;}
h1 .bicon{width:1.1em;height:1.1em;vertical-align:-0.18em;}
h1 .heart{color:#ff4d6d;}
.tag{color:#a86b7d; font-size:clamp(13px,2vw,15px); margin-top:2px;}
.hud{display:flex; align-items:center; gap:10px; flex-wrap:wrap; background:rgba(255,255,255,.65); border:2px solid rgba(255,255,255,.9); box-shadow:0 4px 14px rgba(214,51,108,.12); border-radius:14px; padding:8px 14px; margin-bottom:10px; backdrop-filter:blur(4px);}
.stat{font-size:12px; letter-spacing:1.5px; color:#b06a80; font-weight:700;}
.stat b{display:block? inline} — make stat as column: label small, value big.
```
Let's structure stat: `<div class="stat"><span>SCORE</span><b id="score">0</b></div>` with span block tiny, b block font 20 color #d6336c.
lives: `<div class="stat lives"><span>LIVES</span><b id="lives"></b></div>` b font-size 20 letter-spacing 2px; .on color #ff4d6d text-shadow; .off color #e3cdd6.
badge: margin-left:auto; background:#d6336c; color:#fff; font-size:11px; font-weight:800; letter-spacing:1px; padding:6px 10px; border-radius:999px; display:flex; gap:6px; align-items:center; .dot pulsing animation. When hidden: display none.
.stage{position:relative; border-radius:20px; overflow:hidden; box-shadow:0 18px 40px rgba(214,51,108,.25), 0 4px 10px rgba(0,0,0,.08); border:3px solid #fff; background:#fff;}
canvas{display:block; width:100%; touch-action:none; cursor:none? keep default; }
.overlay{position:absolute; inset:0; display:flex; align-items:center; justify-content:center; background:rgba(255,240,246,.35); backdrop-filter:blur(3px); transition:opacity .35s; z-index:5;}
.overlay.hidden{opacity:0; pointer-events:none;}
.card{background:rgba(255,255,255,.85); border-radius:22px; padding:26px 34px; text-align:center; box-shadow:0 12px 30px rgba(214,51,108,.3); border:2px solid #fff; max-width:min(420px, 86%); animation:bob 2.4s ease-in-out infinite;}
.card h2{color:#d6336c; font-size:clamp(18px,3vw,24px); margin:8px 0 6px;}
.card p{color:#9c5f74; font-size:clamp(12px,2vw,14px); line-height:1.45;}
.card .burger svg{width:64px;height:56px;}
@keyframes bob{0%,100%{transform:translateY(0)}50%{transform:translateY(-6px)}}
.hint{text-align:center; margin-top:10px; color:#b98296; font-size:clamp(11px,1.8vw,13px);}
.score-pop animation on #score when changes: add class via JS: scoreEl.classList.remove('pop'); void offsetWidth; add. .pop{animation:pop .25s} @keyframes pop{50%{transform:scale(1.35)}} — b is inline; make display:inline-block.
```
Media small: hud gap smaller; card padding smaller.
BURGER_SVG: viewBox 0 0 64 56:
```
<svg viewBox="0 0 64 56" ...>
<ellipse cx="32" cy="50" rx="26" ry="5" fill="#e9a860"/>
<rect x="8" y="40" width="48" height="9" rx="4.5" fill="#7a4a21"/>
<path d="M10 40 L54 40 L54 36 L10 36 Z" fill="#ffd43b"/> cheese with drips:
better cheese: <path d="M12 34 h40 v4 h-6 l-2 5 -2-5 h-8 l-2 6 -2-6 h-12 z" fill="#ffd43b"/>
lettuce: <path d="M8 32 q4 -6 8 0 q4 -6 8 0 q4 -6 8 0 q4 -6 8 0 q4 -6 8 0 q4 -6 8 0 v3 h-48 z" fill="#69c23f"/>
top bun: <path d="M6 30 a26 16 0 0 1 52 0 z" fill="#f0b269"/>
seeds: ellipses fill #fff3d0.
face: eyes circles, smile path.
</svg>
```
Let me craft coordinates properly later in code.
Actually simpler to reuse same drawing for canvas; SVG for DOM icon only.
Now overlay initial: setCard('start') at boot; state='demo'; badge visible.
Also "Click or press a key to start" must be clear — yes h2 exactly that text.
One more requirement: "The iframe only receives keyboard events after the user clicks it" — we show message ✓.
Potential issue: keydown handler starting play on ANY key including 'Tab' — fine, preventDefault only arrows/space.
Also when overlay hidden and user presses key, nothing needed.
Pointerdown on stage also triggers when clicking overlay (overlay covers stage; pointerdown listener on stage will still fire due to bubbling since overlay is child of stage). Good — attach to stage.
Also add pointerdown → audio() resume.
visibilitychange: if hidden && !paused && state==='play' → togglePause(true). Simple: document.addEventListener('visibilitychange', ()=>{ if(document.hidden && !paused && !gameOver) togglePause(); });
togglePause(): paused=!paused; if(paused) setCard('pause') else hideOverlay(). But if state demo and paused... overlay shown with pause card; on resume hide. Edge: in demo, user pressing P: first keydown starts play (state demo → startPlay) then also toggles pause? My handler: if(state==='demo') startPlay(); then later if key==='p' togglePause → would immediately pause. Reorder: handle pause keys first: if key p/P/Escape: if state demo → startPlay() then togglePause()? That pauses right after start — acceptable? User pressed P intending pause in demo... ambiguous. Simpler: in demo mode, any key starts play and does NOT trigger other actions (return after startPlay, except apply arrow dir). Implement:
```
window.addEventListener('keydown', e=>{
if(['ArrowLeft','ArrowRight',' '].includes(e.key)) e.preventDefault();
initAudio();
const k=e.key;
if(gameOver){ resetRun(); gameOver=false; state='play'; hideOverlay(); return; }
if(state==='demo'){ state='play'; onTakeControl(); if(k==='ArrowLeft')keyDir=-1; if(k==='ArrowRight')keyDir=1; return; }
if(paused){ if(k==='p'||k==='P'||k==='Escape'||k==='Enter'||k===' ') togglePause(); return; }
if(k==='ArrowLeft')keyDir=-1; else if(k==='ArrowRight')keyDir=1;
else if(k==='p'||k==='P'||k==='Escape') togglePause();
else if(k==='r'||k==='R'){ resetRun(); }
});
```
onTakeControl(): hideOverlay(); badge hide; hint text update to "You're in control! ← → / mouse / touch · P pause · R restart".
pointerdown on stage:
```
stage.addEventListener('pointerdown', e=>{
initAudio();
if(gameOver){ gameOver=false; resetRun(); state='play'; onTakeControl(); return; }
if(paused){ togglePause(); return; }
if(state==='demo'){ state='play'; onTakeControl(); }
const r=canvas.getBoundingClientRect(); paddle.targetX=(e.clientX-r.left)/r.width*W;
});
```
window pointermove: always update targetX (works even before click — but paddle is AI in demo; updating targetX harmless since demo overrides targetX each frame). In play mode follows mouse globally. ok.
touch: pointermove covers touch drag with touch-action none.
Now explodeAt(x,y,n,big): push hearts.
Also when ball lost, hearts burst at bottom.
Banner function: banners.push({text, life:1.6, max:1.6, size:44}).
Level clear also spawn celebration hearts across top.
updateHUD pop.
Let me now write final code fully, carefully checking syntax.
Also ensure no horizontal scrollbar: body overflow-x hidden; wrap max-width 100%.
Canvas logical W=900 H=560; stage width = wrap width (max 940 minus padding? wrap width 100% max 940). At 1280 viewport: body padding 12 → wrap 940 → stage 940 → canvas height 940*560/900=585. Header ~ h1 40 + tag 20 + margins ~ 75; hud ~ 58; hint ~ 24; paddings 18+28 → total ≈ 75+58+585+24+46+10 ≈ 798. Slightly over 800? Tight. Reduce max-width to 900 → canvas h 560 exactly (900*560/900). Total ≈ 75+58+560+24+46 ≈ 763 ✓. Set wrap max-width 900px.
At 375: wrap = 375-24=351 → canvas h=218. fine.
Write HUD stat CSS: .stat{display:flex;flex-direction:column;line-height:1.1} .stat span{font-size:10px;letter-spacing:2px;color:#c08497;font-weight:800} .stat b{font-size:20px;color:#d6336c;font-variant-numeric:tabular-nums}
Lives b: letter-spacing 3px; font-size 18px.
Now the JS in detail. I'll write it out.
```js
(()=>{
"use strict";
const canvas=document.getElementById('game');
const ctx=canvas.getContext('2d');
const stage=document.getElementById('stage');
const overlay=document.getElementById('overlay');
const card=document.getElementById('card');
const scoreEl=document.getElementById('score');
const levelEl=document.getElementById('level');
const livesEl=document.getElementById('lives');
const badge=document.getElementById('badge');
const hintEl=document.getElementById('hint');
const W=900,H=560;
const clamp=(v,a,b)=>v<a?a:v>b?b:v;
const rand=(a,b)=>a+Math.random()*(b-a);
// audio
let AC=null;
function initAudio(){ if(!AC){ try{ AC=new (window.AudioContext||window.webkitAudioContext)(); }catch(e){ AC=null; } } if(AC&&AC.state==='suspended'){ AC.resume().catch(()=>{}); } }
function tone(f0,f1,dur,type,vol){ if(!AC||AC.state!=='running')return; try{ const t=AC.currentTime; const o=AC.createOscillator(),g=AC.createGain(); o.type=type||'sine'; o.frequency.setValueAtTime(Math.max(f0,1),t); o.frequency.exponentialRampToValueAtTime(Math.max(f1,1),t+dur); g.gain.setValueAtTime(vol||0.12,t); g.gain.exponentialRampToValueAtTime(0.0001,t+dur); o.connect(g); g.connect(AC.destination); o.start(t); o.stop(t+dur+0.05);}catch(e){} }
// background
let bg=null;
function makeBG(){ bg=document.createElement('canvas'); bg.width=W*2; bg.height=H*2; const b=bg.getContext('2d'); b.scale(2,2);
const g=b.createLinearGradient(0,0,0,H); g.addColorStop(0,'#fff8ef'); g.addColorStop(0.6,'#ffe7f0'); g.addColorStop(1,'#ffd9e5'); b.fillStyle=g; b.fillRect(0,0,W,H);
b.fillStyle='rgba(255,110,155,0.08)'; const st=54; let row=0;
for(let y=st*0.7;y<H;y+=st){ row++; for(let x=((row%2)?st*0.5:st)+ (row%2?0:0); x<W; x+=st){ ... } }
```
Hmm polka offset: for row parity shift x by st/2. Let me: `for(let x=st/2 + (row%2)*st/2; x<W; x+=st)`. ok.
Also a soft radial glow top center: radial gradient white alpha .5 at (W/2, 60) r 300. And bottom "table" strip: fillRect(0,H-26,W,26) rgba(255,255,255,.35)? plus dashed line at H-26? Keep: subtle.
Actually hearts bounce at H-12; a table strip at bottom looks nice: b.fillStyle='rgba(255,255,255,0.4)'; b.fillRect(0,H-24,W,24); b.strokeStyle='rgba(230,120,160,0.35)'; dashed line at H-24: b.setLineDash([10,8]); line from 0,H-24 to W,H-24.
Game state:
```
let state='demo', paused=false, gameOver=false;
let score=0, lives=3, level=1, combo=0;
let bricks=[], hearts=[], sparks=[], texts=[], banners=[];
let shake=0, time=0, serveTimer=0, ambientT=0, clearTO=null;
const paddle={x:W/2,y:H-54,w:120,h:38,targetX:W/2,squash:0,blink:0,blinkT:2.2};
const ball={x:W/2,y:0,vx:0,vy:0,r:9,stuck:true,spin:0};
const PAL=[['#ff8fab','#e5486d'],['#ffc078','#e8590c'],['#ffe08a','#f08c00'],['#b2f2bb','#37b24d'],['#a5d8ff','#1c7ed6'],['#d0bfff','#7048e8'],['#fcc2d7','#d6336c']];
const HEART_COLORS=['#ff4d6d','#ff6b8a','#ff8fab','#ffc2d1','#e8455f','#ff2e63','#fff0f3','#ff5c8a'];
```
buildLevel():
```
function buildLevel(){
bricks=[];
const cols=10, rows=Math.min(3+level,7);
const m=48, gap=8, top=86;
const bw=(W-m*2-(cols-1)*gap)/cols, bh=22;
for(let r=0;r<rows;r++) for(let c=0;c<cols;c++){
const hp=(level>=3&&r<Math.min(2,rows))?2:(level>=6&&r<3?2:1);
const pal=PAL[(r*3+c+level)%PAL.length];
bricks.push({x:m+c*(bw+gap),y:top+r*(bh+gap),w:bw,h:bh,hp,maxHp:hp,alive:true,fill:hp>1?pal[1]:pal[0],edge:pal[1]});
}
}
```
rows for level1 = 4. good.
resetBall(): ball.stuck=true; ball.vx=ball.vy=0; ball.x=paddle.x; ball.y=paddle.y-ball.r-3; serveTimer=0.9;
resetRun(): score=0;lives=3;level=1;combo=0;bricks/hearts/etc clear; paddle.x=W/2;paddle.targetX=W/2; buildLevel(); resetBall(); updateHUD();
ballSpeed(): Math.min(650, 335+(level-1)*38);
launch(): const a=-Math.PI/2+rand(-0.55,0.55); const sp=ballSpeed(); ball.vx=Math.cos(a)*sp; ball.vy=Math.sin(a)*sp; ball.stuck=false; tone(520,880,0.1,'sine',0.1);
explode(x,y,n,spread,power):
```
function explode(x,y,n,spread,power){
for(let i=0;i<n;i++){
if(hearts.length>640) hearts.splice(0, hearts.length-640+1);
const a=rand(0,Math.PI*2), sp=rand(90,power);
hearts.push({x:x+rand(-spread,spread), y:y+rand(-spread*0.6,spread*0.6), vx:Math.cos(a)*sp, vy:Math.sin(a)*sp-rand(40,160), rot:rand(0,6.28), vr:rand(-9,9), s:rand(4.5,10.5), life:rand(1.2,2.4), max:2.4, color:HEART_COLORS[(Math.random()*HEART_COLORS.length)|0], g:rand(480,700)});
}
for(let i=0;i<Math.floor(n*0.35);i++){ sparks.push({x,y,vx:rand(-260,260),vy:rand(-320,120),life:rand(.3,.7),max:.7,s:rand(1.5,3.5),color:'#fff6d8'}); }
}
```
vy upward bias: sin(a)*sp - extra → hearts fly up-ish then fall. good.
explodeAt for loseLife: explode(ball.x, H-20, 26, 10, 380).
hitBrick(b):
```
function hitBrick(b){
b.hp--;
if(b.hp<=0){
b.alive=false; combo++;
const pts=10+Math.min(combo,12)*2; score+=pts;
addText(b.x+b.w/2, b.y+b.h/2, '+'+pts, 17, '#d6336c');
if(combo>=3) addText(b.x+b.w/2, b.y-6, 'combo x'+combo, 13, '#ff8fab');
explode(b.x+b.w/2, b.y+b.h/2, 30+((Math.random()*14)|0), b.w/2, 430);
shake=Math.min(shake+3.2, 9);
tone(480+Math.random()*260,150,0.13,'square',0.11);
tone(1300,650,0.08,'sine',0.07);
} else {
score+=5; addText(b.x+b.w/2,b.y,'+5',13,'#e8590c');
explode(b.x+b.w/2, b.y+b.h/2, 10, b.w/2, 260);
tone(300,150,0.09,'square',0.08);
}
popScore(); updateHUD();
if(bricks.every(b=>!b.alive)) levelCleared();
}
```
levelCleared():
```
function levelCleared(){
if(clearTO) return;
ball.stuck=true; ball.vx=ball.vy=0;
banners.push({text:'LEVEL '+level+' CLEAR!', life:1.5, max:1.5, size:46});
explode(W/2, H/3, 70, 160, 520);
[523,659,784,1047].forEach((f,i)=>setTimeout(()=>tone(f,f*1.01,0.18,'triangle',0.12), i*90));
clearTO=setTimeout(()=>{
clearTO=null; level++; buildLevel(); resetBall(); serveTimer=1.2; updateHUD();
},1100);
}
```
loseLife():
```
function loseLife(){
lives--; updateHUD(); shake=Math.min(shake+7,10);
explode(ball.x, H-24, 30, 14, 400);
tone(360,70,0.5,'sawtooth',0.15);
if(lives<=0){ gameOver=true; setCard('over'); if(state==='demo'){ setTimeout(()=>{ if(gameOver&&state==='demo'){ gameOver=false; resetRun(); hideOverlay(); } },3000); } }
else { resetBall(); serveTimer=1.3; }
}
```
update(dt) full:
```
function update(dt){
time+=dt;
// paddle
if(state==='demo'){
const closing=ball.vy>0&&!ball.stuck;
let err=closing? clamp((paddle.y-ball.y)*0.11,-52,52):0;
err+=Math.sin(time*1.9)*5;
paddle.targetX=clamp(ball.x+err, paddle.w/2+6, W-paddle.w/2-6);
}
if(keyDir!==0){ paddle.x+=keyDir*740*dt; paddle.targetX=paddle.x; }
else {
const maxSpd=state==='demo'? Math.min(760,560+level*35) : 1500;
const dx=paddle.targetX-paddle.x;
paddle.x+=clamp(dx,-maxSpd*dt,maxSpd*dt);
}
paddle.x=clamp(paddle.x,paddle.w/2+5,W-paddle.w/2-5);
paddle.squash=Math.max(0,paddle.squash-dt*4.5);
paddle.blinkT-=dt; if(paddle.blinkT<=0){paddle.blink=0.13;paddle.blinkT=rand(1.8,4.2);}
paddle.blink=Math.max(0,paddle.blink-dt);
// ball
if(ball.stuck){ ball.x=paddle.x; ball.y=paddle.y-ball.r-3; serveTimer-=dt; if(serveTimer<=0) launch(); }
else{
const sp=Math.hypot(ball.vx,ball.vy);
const steps=Math.min(8,Math.max(1,Math.ceil(sp*dt/(ball.r*0.8))));
for(let i=0;i<steps;i++){ stepBall(dt/steps); if(ball.stuck) break; }
ball.spin+=dt*9;
}
// particles
for(let i=hearts.length-1;i>=0;i--){ const p=hearts[i]; p.life-=dt;
if(p.life<=0){hearts.splice(i,1);continue;}
if(p.amb){ p.y+=p.vy*dt; p.x+=(p.vx+Math.sin(time*2.5+p.seed)*14)*dt; if(p.y<-24){hearts.splice(i,1);} continue; }
p.vy+=p.g*dt; p.x+=p.vx*dt; p.y+=p.vy*dt; p.rot+=p.vr*dt;
if(p.y>H-14&&p.vy>0){ p.y=H-14; p.vy=-p.vy*0.42; p.vx*=0.72; p.vr*=0.8; }
}
for(let i=sparks.length-1;i>=0;i--){ const s=sparks[i]; s.life-=dt; if(s.life<=0){sparks.splice(i,1);continue;} s.vy+=900*dt; s.x+=s.vx*dt; s.y+=s.vy*dt; }
for(let i=texts.length-1;i>=0;i--){ const t=texts[i]; t.life-=dt; t.y+=t.vy*dt; if(t.life<=0)texts.splice(i,1); }
for(let i=banners.length-1;i>=0;i--){ const b=banners[i]; b.life-=dt; if(b.life<=0)banners.splice(i,1); }
shake=Math.max(0,shake-dt*20);
// ambient hearts
ambientT-=dt;
if(ambientT<=0){ ambientT=rand(0.35,0.7);
if(hearts.length<640) hearts.push({amb:true,x:rand(20,W-20),y:H+14,vx:rand(-14,14),vy:rand(-46,-26),rot:rand(0,6.28),vr:rand(-2,2),s:rand(3.5,7),life:rand(5,8),max:8,color:HEART_COLORS[(Math.random()*HEART_COLORS.length)|0],seed:rand(0,9)});
}
}
```
stepBall(sdt):
```
function stepBall(sdt){
ball.x+=ball.vx*sdt; ball.y+=ball.vy*sdt;
if(ball.x<ball.r+4){ball.x=ball.r+4;ball.vx=Math.abs(ball.vx);tone(200,160,0.05,'square',0.05);}
else if(ball.x>W-ball.r-4){ball.x=W-ball.r-4;ball.vx=-Math.abs(ball.vx);tone(200,160,0.05,'square',0.05);}
if(ball.y<ball.r+4){ball.y=ball.r+4;ball.vy=Math.abs(ball.vy);tone(200,160,0.05,'square',0.05);}
if(ball.y>H+26){ ball.stuck=true; loseLife(); return; }
// paddle
if(ball.vy>0&&ball.y+ball.r>=paddle.y&&ball.y-ball.r<=paddle.y+paddle.h+10&&Math.abs(ball.x-paddle.x)<=paddle.w/2+ball.r){
const off=clamp((ball.x-paddle.x)/(paddle.w/2),-1,1);
const ang=-Math.PI/2+off*1.05;
let sp=Math.hypot(ball.vx,ball.vy)*1.012; sp=Math.min(sp, ballSpeed()+70);
ball.vx=Math.cos(ang)*sp; ball.vy=Math.sin(ang)*sp;
ball.y=paddle.y-ball.r-1;
combo=0; paddle.squash=1; shake=Math.min(shake+1.2,5);
tone(230,440,0.09,'triangle',0.15);
return;
}
// bricks
for(const b of bricks){ if(!b.alive)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){
if(Math.abs(dx)>Math.abs(dy)) ball.vx=(dx>=0?1:-1)*Math.abs(ball.vx);
else ball.vy=(dy>=0?1:-1)*Math.abs(ball.vy);
// nudge out
if(Math.abs(dx)>Math.abs(dy)) ball.x=cx+(dx>=0?ball.r+0.5:-ball.r-0.5); else ball.y=cy+(dy>=0?ball.r+0.5:-ball.r-0.5);
hitBrick(b); break;
}
}
}
```
Note: after loseLife, ball.stuck=true → outer loop breaks. But loseLife may set gameOver; fine. However if lives>0, resetBall sets stuck; the outer for checks ball.stuck and breaks. But ball.y>H+26 → we set stuck then loseLife resets position anyway.
Edge: ball.vy could be ~0 horizontal; paddle condition requires vy>0; if ball moving horizontally at paddle level it passes through — rare; add condition: if ball within paddle x-range and y overlapping → handle regardless of vy sign: change condition to (ball.vy>0 || (ball.y+ball.r>=paddle.y && ball.y-ball.r<=paddle.y+paddle.h+10))... simpler: keep vy>0 but also clamp: if ball.y > paddle.y && ball.y < paddle.y+paddle.h && |dx|<w/2+r → treat as hit with vy positive. Let me change condition to:
```
if(ball.y+ball.r>=paddle.y && ball.y-ball.r<=paddle.y+paddle.h+10 && Math.abs(ball.x-paddle.x)<=paddle.w/2+ball.r && ball.vy>0){...}
```
plus safety: else if(ball.y>paddle.y-2 && ball.y<paddle.y+paddle.h && Math.abs(ball.x-paddle.x)<paddle.w/2){ push up: ball.y=paddle.y-ball.r-1; ball.vy=-Math.abs(ball.vy); } — add as safety.
Render functions:
```
function rr(x,y,w,h,r){ ctx.beginPath(); if(ctx.roundRect){ctx.roundRect(x,y,w,h,r);} else { 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.32*s); ctx.bezierCurveTo(-0.62*s,-0.28*s,-0.32*s,-0.95*s,0,-0.42*s); ctx.bezierCurveTo(0.32*s,-0.95*s,0.62*s,-0.28*s,0,0.32*s); ctx.closePath(); }
```
render():
```
function render(){
const s=canvas.width/W;
ctx.setTransform(s,0,0,s,0,0);
ctx.clearRect(0,0,W,H);
if(bg) ctx.drawImage(bg,0,0,W,H);
ctx.save();
if(shake>0.15) ctx.translate(rand(-shake,shake)*0.5, rand(-shake,shake)*0.5);
drawBricks();
drawSparks();
drawHearts();
drawPaddle();
drawBall();
drawTexts();
drawBanners();
ctx.restore();
}
```
drawBricks:
```
function drawBricks(){
for(const b of bricks){ if(!b.alive)continue;
rr(b.x,b.y,b.w,b.h,6); ctx.fillStyle=b.fill; ctx.fill();
rr(b.x+2,b.y+2,b.w-4,7,4); ctx.fillStyle='rgba(255,255,255,0.30)'; ctx.fill();
rr(b.x,b.y,b.w,b.h,6); ctx.strokeStyle=b.edge; ctx.globalAlpha=0.55; ctx.lineWidth=2; ctx.stroke(); ctx.globalAlpha=1;
// embossed heart
ctx.save(); ctx.translate(b.x+b.w/2,b.y+b.h/2+1); ctx.globalAlpha=0.18; ctx.strokeStyle='#7a2b45'; ctx.lineWidth=1.4; heartPath(4.6); ctx.stroke(); ctx.restore();
if(b.hp<b.maxHp){ ctx.strokeStyle='rgba(90,30,50,0.55)'; ctx.lineWidth=1.6; ctx.beginPath(); ctx.moveTo(b.x+b.w*0.25,b.y+3); ctx.lineTo(b.x+b.w*0.42,b.y+b.h-4); ctx.lineTo(b.x+b.w*0.36,b.y+b.h*0.6); ctx.moveTo(b.x+b.w*0.68,b.y+4); ctx.lineTo(b.x+b.w*0.6,b.y+b.h-3); ctx.stroke(); }
}
}
```
drawHearts:
```
function drawHearts(){
for(const p of hearts){
const a=p.amb? clamp(p.life/2,0,0.5) : clamp(p.life/0.45,0,1);
if(a<=0)continue;
const pop=p.amb?1: Math.min(1,(p.max-p.life)*6);
ctx.save(); ctx.globalAlpha=a; ctx.translate(p.x,p.y); ctx.rotate(p.rot); ctx.scale(p.s*pop,p.s*pop);
ctx.fillStyle=p.color; heartPath(1); ctx.fill();
ctx.restore();
}
}
```
Wait heartPath(s) multiplies inside by s; calling heartPath(1) then scaling via transform — good.
drawSparks: small circles fillStyle color alpha life/max, maybe additive: ctx.globalCompositeOperation='lighter' for sparks — nice glow. Use it.
drawPaddle: as planned. Let me write concrete numbers with py=paddle.y, px=paddle.x, w=paddle.w, h=paddle.h.
```
function drawPaddle(){
const px=paddle.x, py=paddle.y, w=paddle.w, h=paddle.h;
// shadow
ctx.fillStyle='rgba(120,40,70,0.15)';
ctx.beginPath(); ctx.ellipse(px, py+h+7, w/2, 6, 0, 0, Math.PI*2); ctx.fill();
ctx.save();
ctx.translate(px, py+h);
const sq=paddle.squash;
ctx.scale(1+sq*0.14, 1-sq*0.24);
ctx.translate(-px, -(py+h));
// bottom bun
rr(px-w/2+3, py+30, w-6, 8, 4); ctx.fillStyle='#e8a558'; ctx.fill();
// patty
rr(px-w/2, py+21, w, 10, 5); ctx.fillStyle='#7c4a22'; ctx.fill();
ctx.strokeStyle='rgba(60,30,10,0.35)'; ctx.lineWidth=1.4;
ctx.beginPath(); ctx.moveTo(px-w/2+8,py+25); ctx.lineTo(px+w/2-8,py+25); ctx.moveTo(px-w/2+8,py+28); ctx.lineTo(px+w/2-8,py+28); ctx.stroke();
// cheese
ctx.fillStyle='#ffd43b';
ctx.fillRect(px-w*0.44, py+15.5, w*0.88, 6.5);
const drips=[-0.3,0,0.3];
for(const d of drips){ const cx=px+d*w; ctx.beginPath(); ctx.moveTo(cx-4.5,py+22); ctx.lineTo(cx+4.5,py+22); ctx.lineTo(cx,py+22+rand? no—fixed 7); ctx.closePath(); ctx.fill(); }
```
Drip length fixed 7 → to py+29 overlapping patty top — fine (drawn before lettuce/bun? cheese drawn after patty so drip covers patty top — ok visually).
```
// lettuce
ctx.fillStyle='#6cc24a';
ctx.beginPath();
const lw=w+10, x0=px-lw/2, base=py+16, n=9, seg=lw/n;
ctx.moveTo(x0, base+2);
for(let k=0;k<n;k++){ ctx.quadraticCurveTo(x0+(k+0.5)*seg, base+(k%2? -3.5: 6.5), x0+(k+1)*seg, base+2); }
ctx.lineTo(x0+lw, base+5); ctx.lineTo(x0, base+5); ctx.closePath(); ctx.fill();
```
Wave: control y alternates above/below base+2 → scallops. ok.
```
// top bun
const g=ctx.createLinearGradient(0,py-4,0,py+18);
g.addColorStop(0,'#f7c37e'); g.addColorStop(1,'#e2954a');
ctx.fillStyle=g;
ctx.beginPath(); ctx.ellipse(px, py+17, w/2, 16, 0, Math.PI, 0); ctx.closePath(); ctx.fill();
// seeds
ctx.fillStyle='#fff3d0';
const offs=[-0.66,-0.44,-0.2,0.02,0.24,0.48,0.68];
for(const o of offs){ const sx=px+o*w*0.46; const sy=py+17-14*Math.sqrt(Math.max(0,1-o*o*0.9))-1; ctx.save(); ctx.translate(sx,sy); ctx.rotate(o*0.6); ctx.beginPath(); ctx.ellipse(0,0,2.4,1.4,0,0,Math.PI*2); ctx.fill(); ctx.restore(); }
// face
const ey=py+10;
ctx.fillStyle='#4a2c12';
if(paddle.blink>0){ ctx.strokeStyle='#4a2c12'; ctx.lineWidth=2; ctx.beginPath(); ctx.moveTo(px-15,ey); ctx.lineTo(px-9,ey); ctx.moveTo(px+9,ey); ctx.lineTo(px+15,ey); ctx.stroke(); }
else { ctx.beginPath(); ctx.arc(px-12,ey,2.6,0,Math.PI*2); ctx.arc(px+12,ey,2.6,0,Math.PI*2); ctx.fill(); }
ctx.strokeStyle='#4a2c12'; ctx.lineWidth=1.8; ctx.beginPath(); ctx.arc(px,ey+2,5,Math.PI*0.15,Math.PI*0.85); ctx.stroke();
// blush
ctx.fillStyle='rgba(255,120,150,0.45)';
ctx.beginPath(); ctx.arc(px-21,ey+3,3.2,0,Math.PI*2); ctx.arc(px+21,ey+3,3.2,0,Math.PI*2); ctx.fill();
ctx.restore();
}
```
Check vertical extents: bun top at py+17-16=py+1; bottom bun bottom py+38 = py+h (h=38). Good. Paddle y=H-54 → bottom at H-16. Shadow at py+h+7=H-9. ok.
drawBall:
```
function drawBall(){
const b=ball;
if(b.stuck){
const p=clamp(1-serveTimer/1.2,0,1);
ctx.strokeStyle='rgba(214,51,108,'+(0.5+0.3*Math.sin(time*6))+')';
ctx.lineWidth=3;
ctx.beginPath(); ctx.arc(b.x,b.y,b.r+6,-Math.PI/2,-Math.PI/2+p*Math.PI*2); ctx.stroke();
}
ctx.save(); ctx.translate(b.x,b.y); ctx.rotate(b.spin*0.4);
const g=ctx.createRadialGradient(-b.r*0.35,-b.r*0.4,b.r*0.2,0,0,b.r);
g.addColorStop(0,'#ff8787'); g.addColorStop(0.6,'#f03e3e'); g.addColorStop(1,'#c92a2a');
ctx.fillStyle=g; ctx.beginPath(); ctx.arc(0,0,b.r,0,Math.PI*2); ctx.fill();
ctx.fillStyle='rgba(255,255,255,0.85)'; ctx.beginPath(); ctx.arc(-b.r*0.35,-b.r*0.42,b.r*0.26,0,Math.PI*2); ctx.fill();
// stem
ctx.fillStyle='#2f9e44'; ctx.fillRect(-1.6,-b.r-4,3.2,5);
ctx.save(); ctx.translate(2.5,-b.r-3); ctx.rotate(-0.5); ctx.beginPath(); ctx.ellipse(0,0,3.4,1.6,0,0,Math.PI*2); ctx.fill(); ctx.restore();
ctx.restore();
}
```
drawTexts:
```
ctx.textAlign='center'; ctx.textBaseline='middle';
for(const t of texts){ const a=clamp(t.life/0.4,0,1); ctx.globalAlpha=a; ctx.font='800 '+t.size+'px ui-rounded,"Segoe UI",system-ui,sans-serif'; ctx.fillStyle=t.color; ctx.strokeStyle='rgba(255,255,255,0.85)'; ctx.lineWidth=3; ctx.strokeText(t.text,t.x,t.y); ctx.fillText(t.text,t.x,t.y); }
ctx.globalAlpha=1;
```
drawBanners:
```
for(const b of banners){ const a=clamp(b.life/(b.max*0.5),0,1); const sc=1+ (1-a)*0.2? do pop: const k=1-(b.life/b.max); const sc=1+Math.sin(k*Math.PI)*0.15; ctx.save(); ctx.globalAlpha=clamp(b.life/0.4,0,1); ctx.translate(W/2,H*0.38); ctx.scale(sc,sc); ctx.font='900 '+b.size+'px ui-rounded,"Segoe UI",system-ui,sans-serif'; ctx.textAlign='center'; ctx.textBaseline='middle'; ctx.lineWidth=8; ctx.strokeStyle='rgba(255,255,255,0.9)'; ctx.strokeText(b.text,0,0); ctx.fillStyle='#d6336c'; ctx.fillText(b.text,0,0); ctx.restore(); }
```
addText(x,y,text,size,color): texts.push({x,y,text,size,color,life:1.1,vy:-46});
updateHUD():
```
scoreEl.textContent=score; levelEl.textContent=level;
let lh=''; for(let i=0;i<3;i++) lh+= i<lives? '<span class="on">♥</span>':'<span class="off">♥</span>';
livesEl.innerHTML=lh;
```
popScore(): scoreEl.classList.remove('pop'); void scoreEl.offsetWidth; scoreEl.classList.add('pop');
setCard(kind):
```
function setCard(kind){
if(kind==='start'){ card.innerHTML= BURGER + '<h2>Click or press any key to start!</h2><p>The burger is playing itself in demo mode — smash some bricks and watch them burst into hearts. Click anytime to take the paddle!</p>'; }
else if(kind==='pause'){ card.innerHTML='<h2>Paused</h2><p>Press <b>P</b> or click to keep smashing</p>'; }
else if(kind==='over'){ card.innerHTML='<h2>Game over <span class="bh">♥</span></h2><p class="bigscore">Score <b>'+score+'</b> · Level '+level+'</p><p>Click or press any key to play again</p>'; }
overlay.classList.remove('hidden');
}
function hideOverlay(){ overlay.classList.add('hidden'); }
```
onTakeControl(): hideOverlay(); badge.style.display='none'; hintEl.innerHTML='You control the burger — <b>mouse / ← → / touch</b> · <b>P</b> pause · <b>R</b> restart';
BURGER svg string:
```
const BURGER='<svg viewBox="0 0 64 58" xmlns="http://www.w3.org/2000/svg" class="bsvg">'
+'<ellipse cx="32" cy="52" rx="25" ry="5" fill="#e8a558"/>'
+'<rect x="9" y="42" width="46" height="9" rx="4.5" fill="#7c4a22"/>'
+'<path d="M13 37h38v5h-5l-2.5 6-2.5-6h-9l-2.5 7-2.5-7H13z" fill="#ffd43b"/>'
+'<path d="M8 33q4-7 8 0t8 0 8 0 8 0 8 0 8 0v4H8z" fill="#6cc24a"/>'
+'<path d="M6 31a26 17 0 0 1 52 0z" fill="#f0b269"/>'
+'<ellipse cx="20" cy="22" rx="2.4" ry="1.4" fill="#fff3d0" transform="rotate(-18 20 22)"/>'
+'<ellipse cx="32" cy="18" rx="2.4" ry="1.4" fill="#fff3d0"/>'
+'<ellipse cx="44" cy="22" rx="2.4" ry="1.4" fill="#fff3d0" transform="rotate(18 44 22)"/>'
+'<ellipse cx="26" cy="27" rx="2.2" ry="1.3" fill="#fff3d0" transform="rotate(-8 26 27)"/>'
+'<ellipse cx="38" cy="27" rx="2.2" ry="1.3" fill="#fff3d0" transform="rotate(8 38 27)"/>'
+'<circle cx="26" cy="29" r="1.8" fill="#4a2c12"/><circle cx="38" cy="29" r="1.8" fill="#4a2c12"/>'
+'<path d="M28 32q4 3.4 8 0" stroke="#4a2c12" stroke-width="1.6" fill="none" stroke-linecap="round"/>'
+'</svg>';
```
Check lettuce path: M8 33 q4 -7 8 0 → wave; t8 0 repeats smooth quadratic — `t` after q reflects control → continues waves. "q4-7 8 0 t8 0 t8 0..." valid. Ends at x=8+8*6=56, then v4 H8 z. Good.
Cheese path: M13 37 h38 v5 h-5 l-2.5 6 -2.5 -6 h-9 l-2.5 7 -2.5 -7 H13 z — starts (13,37) → (51,37) → (51,42) → (46,42) → (43.5,48) → (41,42) → (32,42) → (29.5,49) → (27,42) → (13,42) → close to (13,37). Drips hang below 42 over patty (42-51). ok.
Top bun: M6 31 a26 17 0 0 1 52 0 z → arc from (6,31) to (58,31) top half, close chord. Good.
Title in header: `<h1><span class="bicon">${BURGER inline}</span> Burger Breakout <span class="heart">♥</span></h1>` — but BURGER defined in JS; header is static HTML. Either inline the SVG directly in HTML header too (duplicate markup) or set via JS: document.getElementById('titleBurger').innerHTML=BURGER. Do JS injection for both header icon and card. Header: `<h1><span id="tb"></span>Burger Breakout <span class="heart">♥</span></h1>`; JS: tb.innerHTML=BURGER.
Resize:
```
function resize(){
const cssW=stage.clientWidth;
const dpr=Math.min(2,window.devicePixelRatio||1);
canvas.width=Math.max(320,Math.round(cssW*dpr));
canvas.height=Math.round(canvas.width*H/W);
makeBG();
}
window.addEventListener('resize',resize);
```
canvas CSS width:100%; height auto? If we set canvas.height attribute and CSS width 100% with height:auto, aspect preserved by attribute ratio — since we set height = width*H/W, ratio matches, so height auto works. Add CSS `canvas{width:100%;height:auto;display:block}`. Good, no explicit style height needed.
Init:
```
buildLevel(); resetBall(); serveTimer=0.7; updateHUD(); setCard('start'); resize();
let last=performance.now();
function frame(now){ let dt=(now-last)/1000; last=now; if(dt>0.05)dt=0.05; if(!paused) update(dt); render(); requestAnimationFrame(frame); }
requestAnimationFrame(frame);
```
Order: resize before makeBG uses stage.clientWidth — stage has layout by then (script at end of body). ok.
Input handlers as planned. Also pointermove:
```
window.addEventListener('pointermove',e=>{
const r=canvas.getBoundingClientRect();
if(r.width>0) paddle.targetX=clamp((e.clientX-r.left)/r.width*W, paddle.w/2+5, W-paddle.w/2-5);
});
```
keydown:
```
window.addEventListener('keydown',e=>{
if(e.key==='ArrowLeft'||e.key==='ArrowRight'||e.key===' '){e.preventDefault();}
initAudio();
const k=e.key;
if(gameOver){ gameOver=false; resetRun(); state='play'; onTakeControl(); return; }
if(state==='demo'){ state='play'; onTakeControl(); if(k==='ArrowLeft')keyDir=-1; else if(k==='ArrowRight')keyDir=1; return; }
if(paused){ if(k==='p'||k==='P'||k==='Escape'||k===' '||k==='Enter'){ togglePause(); } return; }
if(k==='ArrowLeft')keyDir=-1; else if(k==='ArrowRight')keyDir=1;
else if(k==='p'||k==='P'||k==='Escape') togglePause();
else if(k==='r'||k==='R'){ resetRun(); }
});
window.addEventListener('keyup',e=>{ if(e.key==='ArrowLeft'&&keyDir===-1)keyDir=0; else if(e.key==='ArrowRight'&&keyDir===1)keyDir=0; });
```
Note: in demo, paused false. If gameOver in demo auto-restart timer also fires. If user took control and game over → waits for input. Good.
togglePause():
```
function togglePause(){ if(gameOver) return; paused=!paused; if(paused) setCard('pause'); else hideOverlay(); }
```
stage pointerdown:
```
stage.addEventListener('pointerdown',e=>{
initAudio();
if(gameOver){ gameOver=false; resetRun(); state='play'; onTakeControl(); return; }
if(paused){ togglePause(); return; }
if(state==='demo'){ state='play'; onTakeControl(); }
const r=canvas.getBoundingClientRect();
if(r.width>0) paddle.targetX=clamp((e.clientX-r.left)/r.width*W,paddle.w/2+5,W-paddle.w/2-5);
});
```
visibilitychange:
```
document.addEventListener('visibilitychange',()=>{ if(document.hidden&&!paused&&!gameOver&&state==='play') togglePause(); });
```
keyDir declared `let keyDir=0;`.
HUD badge: `<div class="badge" id="badge"><span class="dot"></span>DEMO · CLICK TO TAKE OVER</div>`.
Check demo AI catches ball: ball speed level1 335; AI maxSpd 595. easy. Level grows → 650 cap vs AI up to 760. ok.
Combo display uses texts.
One more: when ball stuck and state demo, AI target = ball.x (closing false since vy=0) → paddle under ball → serve straight up. fine.
Potential tunneling through paddle handled by substeps + safety.
Now hearts cap: explode splices when >640 — splice(0, hearts.length-640) removes oldest; careful: I wrote `if(hearts.length>640) hearts.splice(0, hearts.length-640+1)` inside loop — just do before push: if(hearts.length>640) hearts.shift()? shift O(n) fine at this size. Use: if(hearts.length>650) hearts.splice(0,50);
Text stroke for readability good.
Check first-paint: overlay visible with card; behind it demo running: bricks visible (blur slight), ambient hearts. After ~0.7s serve, ball flies up, hits brick ~1.5-2s → hearts explosion before 3s screenshot.
Score pop CSS: #score{display:inline-block} .pop{animation:pop .3s ease} @keyframes pop{40%{transform:scale(1.4);color:#ff2e63}}.
Badge dot pulse: @keyframes pulse{50%{opacity:.3}}.
Fonts: use ui-rounded first — Chrome on Linux may not have ui-rounded; falls to system-ui. fine.
Colors contrast ok.
Now assemble full HTML. Also meta viewport. Title "Burger Breakout ♥".
Double-check overlay hidden class transition; initial state visible (no hidden class in markup) and setCard('start') fills content.
Edge: `stage.clientWidth` when overlay inside — fine.
Canvas cursor: maybe cursor:none in play? Keep default.
Also prevent text selection on rapid clicks: user-select none on body.
Let me also make sure `rr` used before defined? Function declarations hoist — fine.
`rand` used in makeBG? no.
In drawPaddle drips: use fixed drip: lineTo(cx, py+29).
One concern: `ctx.ellipse(px, py+17, w/2, 16, 0, Math.PI, 0)` — start angle PI (left point), end 0 (right point), drawing counterclockwise? Default anticlockwise=false → from PI to 0 going through... angles increase clockwise in canvas (y down). From PI to 2PI(≡0) sweeps the top half? Angle PI is left point (px-w/2, py+17). Increasing angle goes to 3PI/2 which is (px, py+17-16) top (since y down, angle 3π/2 → (cos, sin)=(0,-1) → up). So PI→2PI passes through 3π/2 = top. Yes top half. closePath chords bottom.
Ball spin rotation of stem — rotates whole tomato including stem; spin*0.4 small. fine.
Bricks area: top=86, rows up to 7 → bottom = 86+7*30-8=288. Ball region below. ok.
Ball-paddle: paddle.y=H-54=506. Ball lost at y>H+26=586.
Hearts floor bounce H-14=546 — near bottom bun shadow area; fine.
Let me also add slight trail to ball? skip.
Write CSS fully:
```
:root{--pink:#d6336c;}
*{box-sizing:border-box;margin:0;padding:0}
html,body{height:100%}
body{
font-family:ui-rounded,"Hiragino Maru Gothic ProN","Segoe UI",system-ui,-apple-system,sans-serif;
background:linear-gradient(160deg,#fff6ec,#ffe4ef 55%,#ffd9e0);
color:#5b2b3a; min-height:100%; display:flex; justify-content:center;
padding:16px 12px 26px; overflow-x:hidden; -webkit-user-select:none; user-select:none;
}
.wrap{width:100%;max-width:900px}
header{text-align:center;margin-bottom:8px}
h1{font-size:clamp(26px,4.6vw,38px);color:var(--pink);letter-spacing:.5px;text-shadow:0 2px 0 rgba(255,255,255,.9);display:flex;align-items:center;justify-content:center;gap:10px;flex-wrap:wrap}
h1 .bsvg{width:44px;height:40px;filter:drop-shadow(0 3px 3px rgba(214,51,108,.3))}
h1 .heart{color:#ff4d6d;text-shadow:0 0 12px rgba(255,77,109,.5)}
.tag{color:#a86b7d;font-size:clamp(12px,2vw,14.5px);margin-top:2px}
.hud{display:flex;align-items:center;gap:18px;flex-wrap:wrap;background:rgba(255,255,255,.7);border:2px solid #fff;border-radius:16px;padding:8px 16px;margin:10px 0;box-shadow:0 6px 18px rgba(214,51,108,.14)}
.stat{display:flex;flex-direction:column;line-height:1.05}
.stat span{font-size:9.5px;letter-spacing:2.2px;color:#c08497;font-weight:800}
.stat b{font-size:21px;color:var(--pink);font-variant-numeric:tabular-nums}
#score{display:inline-block}
#score.pop{animation:pop .3s ease}
@keyframes pop{40%{transform:scale(1.45);color:#ff2e63}}
#lives{letter-spacing:3px;font-size:19px}
#lives .on{color:#ff4d6d;text-shadow:0 0 8px rgba(255,77,109,.45)}
#lives .off{color:#e7ccd6}
.badge{margin-left:auto;display:flex;align-items:center;gap:7px;background:var(--pink);color:#fff;font-size:10.5px;font-weight:800;letter-spacing:1.4px;padding:7px 12px;border-radius:999px;box-shadow:0 4px 10px rgba(214,51,108,.35);animation:bob 2s ease-in-out infinite}
.badge .dot{width:8px;height:8px;border-radius:50%;background:#fff;animation:pulse 1s infinite}
@keyframes pulse{50%{opacity:.25}}
@keyframes bob{50%{transform:translateY(-3px)}}
.stage{position:relative;border-radius:20px;overflow:hidden;border:3px solid #fff;background:#fff;box-shadow:0 20px 45px rgba(214,51,108,.28),0 4px 12px rgba(0,0,0,.08)}
canvas{display:block;width:100%;height:auto;touch-action:none}
.overlay{position:absolute;inset:0;z-index:5;display:flex;align-items:center;justify-content:center;background:rgba(255,238,245,.38);backdrop-filter:blur(3px);-webkit-backdrop-filter:blur(3px);transition:opacity .35s ease}
.overlay.hidden{opacity:0;pointer-events:none}
.card{background:rgba(255,255,255,.88);border:2px solid #fff;border-radius:22px;padding:22px 30px;text-align:center;max-width:min(430px,88%);box-shadow:0 14px 34px rgba(214,51,108,.32);animation:bob 2.6s ease-in-out infinite}
.card .bsvg{width:62px;height:56px}
.card h2{color:var(--pink);font-size:clamp(17px,3vw,23px);margin:6px 0 6px}
.card p{color:#9c5f74;font-size:clamp(11.5px,2vw,13.5px);line-height:1.5}
.card .bigscore b{color:var(--pink);font-size:20px}
.card .bh{color:#ff4d6d}
.hint{text-align:center;margin-top:10px;color:#b98296;font-size:clamp(10.5px,1.9vw,12.5px)}
.hint b{color:#d6336c}
@media (max-width:480px){ .hud{gap:12px;padding:7px 12px} .badge{margin-left:0} .card{padding:16px 18px} }
```
HTML body:
```
<div class="wrap">
<header>
<h1><span id="tb"></span>Burger Breakout <span class="heart">♥</span></h1>
<p class="tag">The paddle is a hamburger — and every brick explodes into a shower of hearts!</p>
</header>
<div class="hud">
<div class="stat"><span>SCORE</span><b id="score">0</b></div>
<div class="stat"><span>LEVEL</span><b id="level">1</b></div>
<div class="stat"><span>LIVES</span><b id="lives"></b></div>
<div class="badge" id="badge"><span class="dot"></span>DEMO · CLICK TO TAKE OVER</div>
</div>
<div class="stage" id="stage">
<canvas id="game"></canvas>
<div class="overlay" id="overlay"><div class="card" id="card"></div></div>
</div>
<p class="hint" id="hint">A demo is playing — <b>click or press any key</b> to grab the paddle. Move with mouse, ← → or touch.</p>
</div>
```
Now finalize JS ordering: define everything, then init. Also `let keyDir=0;` before handlers.
Potential bug: in update, when paused we skip update but render still draws — fine.
In demo when gameOver auto-restart: resetRun() sets state? state stays 'demo', badge visible still. hideOverlay called. good. But if user had taken control (state 'play') and game over, we wait for input; keydown gameOver branch sets state='play' again — fine.
clearTO guard: if resetRun during clear timeout (game over can't happen while stuck). R restart during clearTO → level increments later anyway; acceptable. Clear timeout callback checks `if(gameOver) return;`? If user pressed R, gameOver false. fine. Add guard `if(clearTO===null)` no. Keep simple but set clearTO=null inside.
Also after levelCleared, ball.stuck=true so update's serve timer decrements and relaunches even during the 1.1s wait — launch happens then setTimeout rebuilds... resetBall in timeout sets stuck again. During wait, ball launches and may hit new... bricks still old (all dead) → passes through to top, bounces, comes down, paddle AI catches... then reset. Slightly odd but harmless. Better: during clearTO, keep ball stuck: in update, `if(ball.stuck){...serveTimer...}` — to prevent launch while clear pending: `if(serveTimer<=0 && !clearTO) launch();`. Add that.
Also loseLife sets ball.stuck=true then resetBall anyway.
In stepBall after loseLife sets stuck=true, outer loop `if(ball.stuck) break;` — good.
hitBrick may trigger levelCleared which sets ball.stuck=true → break outer. good.
updateHUD pop: call popScore separately when score changes (in hitBrick). updateHUD just sets text.
Serve ring uses serveTimer/1.2 but serveTimer initial 0.9 → p negative → clamp 0. fine.
Now, one more check on heart path orientation: with scale s and rotation, heart bottom point at (0,0.32s), top dip at (0,-0.42s). Good.
drawHearts pop: (p.max-p.life)*6 — max stored as 2.4 fixed but life random 1.2-2.4; store max per particle = life initial. In explode set max: the same random life value. Let me store p.max=p.life at creation. For ambient max=8 with life up to 8; amb alpha uses life/2 clamp .5. ok.
Sparks: draw with 'lighter'.
I think code is solid. Let me also ensure no `alert` etc. No external requests. No localStorage.
Canvas font strings with quotes inside JS single quotes: '800 17px ui-rounded,"Segoe UI",system-ui,sans-serif' — fine.
Emoji: none used in text except maybe none. Good — I avoided emoji entirely (using SVG + ♥).
Write final file now. Also include `<meta name="viewport" content="width=device-width, initial-scale=1">`.
One more detail: the demo badge text "DEMO · CLICK TO TAKE OVER" — middle dot fine.
Let me count hearts "tons": 30-44 per brick plus sparks — yes.
Also add slight rotation to bricks? no.
Final check of stepBall paddle safety clause:
```
if(ball.vy>0 && ball.y+ball.r>=paddle.y && ball.y-ball.r<=paddle.y+paddle.h+12 && Math.abs(ball.x-paddle.x)<=paddle.w/2+ball.r){ ...reflect... }
else if(ball.y>paddle.y-2 && ball.y<paddle.y+paddle.h+4 && Math.abs(ball.x-paddle.x)<paddle.w/2){ ball.y=paddle.y-ball.r-1; ball.vy=-Math.abs(ball.vy||200); }
```
Also ball could get stuck inside brick after reflection nudged wrong direction; nudge handles.
dt clamp 0.05; substeps based on sp*dt — with dt 0.05 and sp 700 → 35px, r 9 → steps ceil(35/7.2)=5 ≤8 ok.
Resize: also listen to orientationchange — resize covers.
Write it.