Hamburger breakoutOne-line
GLM 5.3
by Z.aiReasoningClean
Running live in a sandboxed frame. Click inside it before using the keyboard.
How it went
- Total time
- 8 min 12 s
- Cost
- $0.301
- Output tokens
- 68,307
- Spent thinking
- 55,699 tokens
- Speed
- 139.1 tok/s
- Console errors
- 0
- First token after
- 513 ms
- First visible text after
- 7 min 32 s
- Prompt tokens
- 357
- Code size
- 33.5 KB
- Finish reason
- stop
- Served by
- Parasail
- Model id
z-ai/glm-5.3- Reasoning sent
{"effort":"max"}- Max output
- 943,718 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 · 819 lines
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Burger Breakout · Heartbreaker Diner</title>
<style>
/* ============ Heartbreaker Diner — flat sticker aesthetic ============ */
*{margin:0;padding:0;box-sizing:border-box}
html{overflow-x:hidden}
body{
min-height:100vh;display:flex;padding:12px;
font-family:'Trebuchet MS',Verdana,sans-serif;color:#4A2A18;
background-color:#F6E3C2;
background-image:radial-gradient(#EBD5AC 2.2px,transparent 2.4px);
background-size:26px 26px;
-webkit-tap-highlight-color:transparent;
user-select:none;-webkit-user-select:none;
}
.cabinet{
margin:auto;width:min(92vw,810px);
background:#FFFBF0;border:5px solid #4A2A18;border-radius:22px;
box-shadow:10px 12px 0 rgba(74,42,24,.16);
padding:14px 14px 11px;
}
.top{display:flex;align-items:center;justify-content:space-between;gap:14px}
h1{
font-family:Impact,'Arial Black','Franklin Gothic Bold',sans-serif;
font-size:clamp(29px,4.6vw,40px);line-height:.95;letter-spacing:1px;
color:#E8432F;white-space:nowrap;
text-shadow:
-2px -2px 0 #4A2A18,2px -2px 0 #4A2A18,-2px 2px 0 #4A2A18,2px 2px 0 #4A2A18,
-2px 0 0 #4A2A18,2px 0 0 #4A2A18,0 -2px 0 #4A2A18,0 2px 0 #4A2A18,
5px 6px 0 rgba(74,42,24,.2);
}
h1 svg{width:.6em;height:.6em;margin:0 .1em;transform:rotate(-10deg);vertical-align:-1%}
.tag{margin-top:6px;font-weight:700;font-size:10px;letter-spacing:.3em;color:#8A6A4B}
.badge{
flex:none;width:84px;height:84px;border-radius:50%;
background:#F5B32F;border:4px solid #4A2A18;transform:rotate(-8deg);
display:flex;flex-direction:column;align-items:center;justify-content:center;gap:2px;
box-shadow:4px 5px 0 rgba(74,42,24,.18);
}
.badge span{font-family:Impact,'Arial Black',sans-serif;font-size:11px;letter-spacing:1px}
.badge svg{width:19px;height:19px}
.hud{
margin-top:12px;display:flex;align-items:center;gap:6px 24px;flex-wrap:wrap;
background:#FFF3DA;border:4px solid #4A2A18;border-radius:14px;padding:7px 14px;
}
.stat{display:flex;align-items:baseline;gap:8px}
.stat .lbl{font-size:10px;font-weight:700;letter-spacing:.22em;color:#8A6A4B}
.stat .val{font-family:Impact,'Arial Black',sans-serif;font-size:22px;letter-spacing:1px;color:#4A2A18;font-variant-numeric:tabular-nums}
.stat .val.red{color:#E8432F}
.lives{display:flex;align-items:center;gap:5px;margin-left:auto}
.lives svg{width:20px;height:20px;filter:drop-shadow(1.5px 2px 0 rgba(74,42,24,.22))}
#muteBtn{
width:35px;height:35px;border:3px solid #4A2A18;border-radius:11px;
background:#FFFBF0;color:#4A2A18;cursor:pointer;display:grid;place-items:center;
box-shadow:2px 3px 0 rgba(74,42,24,.22);
}
#muteBtn:active{transform:translate(1px,2px);box-shadow:none}
#muteBtn svg{width:18px;height:18px}
.stage{
position:relative;margin-top:12px;
border:5px solid #4A2A18;border-radius:14px;overflow:hidden;
background:#FFF9EC;touch-action:none;
}
canvas{display:block;width:100%;height:auto}
.overlay{
position:absolute;inset:0;display:flex;align-items:center;justify-content:center;
background:rgba(255,249,236,.55);padding:18px;cursor:pointer;
}
.overlay.hidden{display:none}
.panel{
background:#FFFBF0;border:4px solid #4A2A18;border-radius:18px;
box-shadow:8px 9px 0 rgba(74,42,24,.18);
padding:22px 32px 20px;text-align:center;max-width:min(92%,440px);
animation:float 3.2s ease-in-out infinite;
}
@keyframes float{50%{transform:translateY(-5px)}}
.panel .eyebrow{font-size:10px;font-weight:700;letter-spacing:.28em;color:#8A6A4B}
.panel h2{
font-family:Impact,'Arial Black',sans-serif;font-size:clamp(25px,4vw,36px);
color:#E8432F;letter-spacing:1px;margin:7px 0 5px;
text-shadow:
-2px -2px 0 #4A2A18,2px -2px 0 #4A2A18,-2px 2px 0 #4A2A18,2px 2px 0 #4A2A18,
4px 5px 0 rgba(74,42,24,.18);
}
.panel .meta{font-size:12.5px;line-height:1.75;color:#6B4A2F;font-weight:700}
.panel .blink{margin-top:15px;font-size:12px;font-weight:800;letter-spacing:.18em;color:#4A2A18;animation:pulse 1.4s ease-in-out infinite}
@keyframes pulse{50%{opacity:.25}}
.hints{display:flex;flex-wrap:wrap;justify-content:center;gap:5px 20px;margin-top:11px;font-size:10px;font-weight:700;letter-spacing:.18em;color:#8A6A4B}
@media (max-width:600px){
.badge{display:none}
.stat .val{font-size:18px}
.hud{gap:5px 14px;padding:6px 10px}
}
</style>
</head>
<body>
<main class="cabinet">
<header class="top">
<div class="brand">
<h1>BURGER<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z" fill="#F5B32F" stroke="#4A2A18" stroke-width="1.8" stroke-linejoin="round"/></svg>BREAKOUT</h1>
<p class="tag">SMASH THE STACK · SPREAD THE LOVE</p>
</div>
<div class="badge" aria-hidden="true">
<span>MADE WITH</span>
<svg viewBox="0 0 24 24"><path d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z" fill="#E8432F" stroke="#4A2A18" stroke-width="1.8" stroke-linejoin="round"/></svg>
<span>REAL LOVE</span>
</div>
</header>
<div class="hud">
<div class="stat"><span class="lbl">SCORE</span><span class="val" id="scoreVal">000000</span></div>
<div class="stat"><span class="lbl">HEARTS</span><span class="val red" id="heartsVal">0</span></div>
<div class="stat"><span class="lbl">LEVEL</span><span class="val" id="levelVal">1</span></div>
<div class="lives" id="livesBox"></div>
<button id="muteBtn" aria-label="Toggle sound">
<svg id="icoOn" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M4 9.5v5h3.5L12 19V5L7.5 9.5H4z" fill="currentColor"/><path d="M15.5 9.2a4.2 4.2 0 0 1 0 5.6"/><path d="M18 6.8a7.6 7.6 0 0 1 0 10.4"/></svg>
<svg id="icoOff" viewBox="0 0 24 24" style="display:none" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M4 9.5v5h3.5L12 19V5L7.5 9.5H4z" fill="currentColor"/><path d="M16 9.5l5 5M21 9.5l-5 5"/></svg>
</button>
</div>
<div class="stage" id="stage">
<canvas id="game"></canvas>
<div class="overlay" id="overlay">
<div class="panel">
<p class="eyebrow" id="ovEyebrow"></p>
<h2 id="ovTitle"></h2>
<p class="meta" id="ovMeta"></p>
<p class="blink" id="ovBlink"></p>
</div>
</div>
</div>
<footer class="hints">
<span>MOVE · MOUSE OR ← →</span><span>LAUNCH · SPACE</span><span>PAUSE · P</span><span>SOUND · M</span>
</footer>
</main>
<script>
(() => {
'use strict';
/* =====================================================================
BURGER BREAKOUT — the wall is a giant hamburger. The paddle is a
hamburger. Every brick explodes into a burst of little hearts that
bounce across the checkerboard diner floor. Self-playing "auto-chef"
demo runs until the player takes over.
===================================================================== */
const $ = id => document.getElementById(id);
const clamp = (v,a,b)=> v<a?a:(v>b?b:v);
const rand = (a,b)=> a + Math.random()*(b-a);
const pick = arr => arr[(Math.random()*arr.length)|0];
const backOut = t => { const c1=1.70158, c3=c1+1; return 1 + c3*Math.pow(t-1,3) + c1*Math.pow(t-1,2); };
/* rounded-rect path (number radius or [tl,tr,br,bl]) */
function rr(g,x,y,w,h,r){
let a,b,c,d;
if(Array.isArray(r)){ [a,b,c,d]=r; } else { a=b=c=d=r; }
const m=Math.min(w/2,h/2);
a=Math.min(a,m); b=Math.min(b,m); c=Math.min(c,m); d=Math.min(d,m);
g.beginPath();
g.moveTo(x+a,y);
g.lineTo(x+w-b,y); g.arcTo(x+w,y, x+w,y+b, b);
g.lineTo(x+w,y+h-c); g.arcTo(x+w,y+h, x+w-c,y+h, c);
g.lineTo(x+d,y+h); g.arcTo(x,y+h, x,y+h-d, d);
g.lineTo(x,y+a); g.arcTo(x,y, x+a,y, a);
g.closePath();
}
/* ---------------- canvas ---------------- */
const W=900, H=600, WALL=14;
const cv=$('game'), ctx=cv.getContext('2d');
const DPR=Math.min(2, window.devicePixelRatio||1);
cv.width=W*DPR; cv.height=H*DPR;
const INK='#4A2A18';
const HEART_COLORS=['#F43F5E','#FF5A7E','#E8432F','#FF8FA3','#FB7185','#D6336C','#FFB3C1','#FFC233'];
const CRUMB_COLORS={bun:'#EDAA62',lettuce:'#7FC94E',tomato:'#E8483A',cheese:'#FFC233',patty:'#7A4A26',bun2:'#E2955A'};
/* ---------------- heart sprites (pre-rendered) ---------------- */
function heartPath(g,s){
g.beginPath();
g.moveTo(0,.5*s);
g.bezierCurveTo(-.62*s,.05*s,-.62*s,-.5*s,-.28*s,-.5*s);
g.bezierCurveTo(-.08*s,-.5*s,0,-.34*s,0,-.18*s);
g.bezierCurveTo(0,-.34*s,.08*s,-.5*s,.28*s,-.5*s);
g.bezierCurveTo(.62*s,-.5*s,.62*s,.05*s,0,.5*s);
g.closePath();
}
const heartSprites={};
function heartSprite(color,outline){
const key=color+(outline?'*':'');
if(heartSprites[key]) return heartSprites[key];
const c=document.createElement('canvas'); c.width=c.height=96;
const g=c.getContext('2d'); g.translate(48,48);
heartPath(g,62);
if(outline){ g.lineWidth=10; g.lineJoin='round'; g.strokeStyle='#FFFBF0'; g.stroke(); }
g.fillStyle=color; g.fill();
g.lineWidth=2.5; g.strokeStyle='rgba(74,42,24,.35)'; g.stroke();
g.beginPath(); g.ellipse(-13,-16,7,4.5,-0.7,0,Math.PI*2);
g.fillStyle='rgba(255,255,255,.65)'; g.fill();
heartSprites[key]=c; return c;
}
/* ---------------- brick sprites: the wall is one big burger ---------------- */
const COLS=10, SIDE=40, GAPX=8, TOP=54, BRICK_H=27, GAPY=10;
const BRICK_W=(W - SIDE*2 - GAPX*(COLS-1))/COLS;
function seedDot(g,x,y,rot){
g.save(); g.translate(x,y); g.rotate(rot);
g.beginPath(); g.ellipse(0,0,4.6,2.3,0,0,Math.PI*2); g.fill(); g.restore();
}
function makeBrickSprite(type,damaged){
const c=document.createElement('canvas');
c.width=Math.ceil(BRICK_W*DPR); c.height=Math.ceil((BRICK_H+4)*DPR);
const g=c.getContext('2d'); g.scale(DPR,DPR);
const w=BRICK_W, h=BRICK_H;
g.fillStyle='rgba(74,42,24,.16)'; rr(g,0,3,w,h,8); g.fill(); // flat drop shadow
g.lineJoin='round'; g.lineWidth=2.5; g.strokeStyle='rgba(74,42,24,.85)';
if(type==='bun'||type==='bun2'){
const base=type==='bun'?'#EDAA62':'#E2955A';
const dark=type==='bun'?'#D18A45':'#C97F44';
g.fillStyle=base; rr(g,0,0,w,h,type==='bun'?8:[8,8,11,11]); g.fill(); g.stroke();
g.save(); rr(g,0,0,w,h,8); g.clip();
g.fillStyle=dark; g.fillRect(0,h-8,w,8);
g.fillStyle='rgba(255,255,255,.3)'; g.fillRect(6,3,w*0.4,4);
g.restore();
if(type==='bun'){ g.fillStyle='#FBE7C6'; seedDot(g,w*0.2,h*0.45,-0.5); seedDot(g,w*0.5,h*0.35,0.25); seedDot(g,w*0.8,h*0.45,0.55); }
} else if(type==='lettuce'){
g.fillStyle='#7FC94E'; rr(g,0,0,w,h-4,8); g.fill(); g.stroke();
g.fillStyle='#7FC94E';
for(let i=0;i<6;i++){ g.beginPath(); g.arc(w*(i+0.5)/6,h-6,6,0,Math.PI); g.fill(); }
g.fillStyle='#A5DE79'; rr(g,6,4,w-12,4,2); g.fill();
} else if(type==='tomato'){
g.fillStyle='#C33327'; rr(g,0,0,w,h,9); g.fill(); g.stroke();
g.fillStyle='#E8483A'; rr(g,3,3,w-6,h-7,7); g.fill();
g.fillStyle='#F06A57'; rr(g,7,5,w-14,4,2); g.fill();
g.fillStyle='#F9A08F';
g.beginPath(); g.arc(w*0.3,h*0.62,2,0,7); g.fill();
g.beginPath(); g.arc(w*0.55,h*0.7,1.6,0,7); g.fill();
g.beginPath(); g.arc(w*0.75,h*0.58,1.8,0,7); g.fill();
} else if(type==='cheese'){
g.fillStyle='#FFC233'; rr(g,0,1,w,h-2,6); g.fill(); g.stroke();
rr(g,w*0.28,h-9,11,10,5); g.fill(); // melt drip
g.fillStyle='#FFD966'; rr(g,5,4,w-10,4,2); g.fill();
g.fillStyle='#E8A81C';
g.beginPath(); g.arc(w*0.62,h*0.5,3.4,0,7); g.fill();
g.beginPath(); g.arc(w*0.78,h*0.66,2.4,0,7); g.fill();
} else if(type==='patty'){
g.fillStyle='#7A4A26'; rr(g,0,0,w,h,7); g.fill(); g.stroke();
g.fillStyle='#8F5B31'; rr(g,4,3,w-8,5,3); g.fill();
g.fillStyle='#5E3618';
[[.14,.68],[.3,.55],[.48,.7],[.64,.56],[.8,.66],[.92,.5]].forEach(p=>{ g.beginPath(); g.arc(w*p[0],h*p[1],2.2,0,7); g.fill(); });
if(damaged){
g.strokeStyle='#3F2410'; g.lineWidth=2.2; g.lineCap='round';
g.beginPath(); g.moveTo(w*0.2,h*0.95); g.lineTo(w*0.34,h*0.5); g.lineTo(w*0.26,h*0.3); g.stroke();
g.beginPath(); g.moveTo(w*0.72,h); g.lineTo(w*0.66,h*0.55); g.lineTo(w*0.8,h*0.35); g.stroke();
}
}
return c;
}
const BRICK_SPRITES={};
['bun','lettuce','tomato','cheese','bun2'].forEach(t=>BRICK_SPRITES[t]=[makeBrickSprite(t,false)]);
BRICK_SPRITES.patty=[makeBrickSprite('patty',false), makeBrickSprite('patty',true)];
/* ---------------- entities & state ---------------- */
const pad ={x:W/2, y:H-84, w:116, h:36};
const ball={x:W/2, y:0, vx:0, vy:0, r:9, speed:320, attached:true};
const S={mode:'demo', score:0, level:1, lives:3, hearts:0, combo:0, bestCombo:0,
bricks:[], brickCount:0, clearing:false, clearTimer:0, squash:0, shake:0, t:0};
const parts=[], popups=[], trail=[], powerups=[];
let serveTimer=0, paused=false, pointerX=null, lastBonus=0, muted=false;
const keys={};
const baseSpeed=()=> Math.min(540, 320 + (S.level-1)*26);
const ROWS=[
{t:'bun', hp:1},{t:'lettuce',hp:1},{t:'tomato',hp:1},
{t:'cheese', hp:1},{t:'patty', hp:2},{t:'bun2', hp:1}
];
function buildLevel(){
S.bricks.length=0; S.brickCount=0;
ROWS.forEach((rd,ri)=>{
for(let ci=0;ci<COLS;ci++){
S.bricks.push({x:SIDE+ci*(BRICK_W+GAPX), y:TOP+ri*(BRICK_H+GAPY),
w:BRICK_W, h:BRICK_H, type:rd.t, hp:rd.hp, maxHp:rd.hp});
S.brickCount++;
}
});
}
/* ---------------- tiny synth (no assets, gesture-gated) ---------------- */
const sfx=(()=>{
let ac=null, mute=false;
function ensure(){
if(!ac){ try{ ac=new (window.AudioContext||window.webkitAudioContext)(); }catch(err){ ac=null; } }
if(ac && ac.state==='suspended') ac.resume().catch(()=>{});
}
function tone(f0,f1,dur,type,vol,delay){
if(mute||!ac||ac.state!=='running') return;
try{
const t0=ac.currentTime+(delay||0);
const o=ac.createOscillator(), g=ac.createGain();
o.type=type;
o.frequency.setValueAtTime(Math.max(30,f0),t0);
if(f1&&f1!==f0) o.frequency.exponentialRampToValueAtTime(Math.max(30,f1),t0+dur);
g.gain.setValueAtTime(vol,t0);
g.gain.exponentialRampToValueAtTime(0.0001,t0+dur);
o.connect(g); g.connect(ac.destination);
o.start(t0); o.stop(t0+dur+0.03);
}catch(err){}
}
return {
ensure,
set muted(v){ mute=v; },
wall(){ tone(340,300,.045,'square',.045); },
paddle(){ tone(200,120,.08,'triangle',.22); },
thud(){ tone(150,95,.07,'square',.16); },
pop(c){ const f=Math.min(1150,470+Math.min(c,14)*45); tone(f,f*1.35,.09,'square',.13); tone(f*1.5,f*2.1,.08,'triangle',.07,.03); },
power(){ tone(640,640,.09,'triangle',.16); tone(960,960,.13,'triangle',.16,.1); },
lose(){ tone(330,85,.5,'sawtooth',.17); },
clear(){ [523,659,784,1047].forEach((f,i)=>tone(f,f,.13,'square',.13,i*.09)); },
over(){ [392,330,262,196].forEach((f,i)=>tone(f,f*.97,.22,'triangle',.16,i*.17)); },
launch(){ tone(250,520,.09,'triangle',.15); }
};
})();
/* ---------------- particles: hearts everywhere ---------------- */
function brickBurst(b,combo){
const x=b.x+b.w/2, y=b.y+b.h/2;
const n=14+Math.min(12,combo*2)+(Math.random()*4|0);
for(let i=0;i<n;i++){
const a=rand(0,Math.PI*2), sp=rand(60,300);
parts.push({type:'heart',x,y,vx:Math.cos(a)*sp*1.15,vy:Math.sin(a)*sp-150,
rot:rand(-.6,.6),rv:rand(-9,9),s:rand(4.5,11),life:rand(.75,1.45),age:0,g:780,color:pick(HEART_COLORS)});
}
for(let i=0;i<4;i++) parts.push({type:'dot',x:x+rand(-16,16),y:y+rand(-8,8),
vx:rand(-100,100),vy:rand(-180,20),life:rand(.4,.7),age:0,g:520,
color:Math.random()<.5?'#FFFFFF':'#FFD966',s:rand(1.6,3)});
for(let i=0;i<3;i++) parts.push({type:'crumb',x:x+rand(-22,22),y:y+rand(-8,8),
vx:rand(-150,150),vy:rand(-240,-40),life:rand(.5,.9),age:0,g:900,
rot:rand(0,3),rv:rand(-10,10),color:CRUMB_COLORS[b.type]});
return n;
}
function grayBurst(x,y){
for(let i=0;i<10;i++) parts.push({type:'heart',x,y,vx:rand(-160,160),vy:rand(-260,-60),
rot:rand(-.5,.5),rv:rand(-6,6),s:rand(5,9),life:rand(.7,1.1),age:0,g:820,color:'#B9AA9C'});
for(let i=0;i<5;i++) parts.push({type:'dot',x:x+rand(-12,12),y:y+rand(-6,6),
vx:rand(-60,60),vy:rand(-120,0),life:rand(.4,.6),age:0,g:600,color:'#C9AE8C',s:2.4});
}
function heartRain(){
for(let i=0;i<55;i++) parts.push({type:'heart',x:rand(20,W-20),y:rand(-220,-10),
vx:rand(-40,40),vy:rand(60,170),rot:rand(-.4,.4),rv:rand(-3,3),s:rand(5,11),
life:rand(2,2.8),age:0,g:120,color:pick(HEART_COLORS),sw:rand(0,6)});
}
function sesameBurst(){
for(let i=0;i<3;i++) parts.push({type:'seed',x:pad.x+rand(-30,30),y:pad.y+3,
vx:rand(-70,70),vy:rand(-180,-60),life:rand(.5,.8),age:0,g:800,rot:rand(0,3),rv:rand(-8,8)});
}
function popup(x,y,txt,color,size){ popups.push({x,y,txt,color,size:size||18,age:0,life:.9}); if(popups.length>30) popups.shift(); }
/* ---------------- game logic ---------------- */
function launch(){
if(!ball.attached) return;
ball.attached=false;
const a=-Math.PI/2+rand(-0.3,0.3);
ball.vx=Math.cos(a)*ball.speed; ball.vy=Math.sin(a)*ball.speed;
S.squash=0.6; trail.length=0; sfx.launch();
}
function paddleBounce(){
const rel=clamp((ball.x-pad.x)/(pad.w/2),-1,1);
const ang=rel*1.02;
ball.vx=Math.sin(ang)*ball.speed;
ball.vy=-Math.cos(ang)*ball.speed;
ball.y=pad.y-ball.r-0.5;
S.squash=1; S.combo=0;
sesameBurst(); sfx.paddle();
}
function hitBrick(b){
b.hp--;
if(b.hp>0){ // patty takes two bites
sfx.thud(); S.shake=Math.max(S.shake,1.6);
for(let i=0;i<3;i++) parts.push({type:'crumb',x:ball.x,y:ball.y,vx:rand(-120,120),
vy:rand(-200,-40),life:rand(.4,.7),age:0,g:900,rot:rand(0,3),rv:rand(-9,9),color:CRUMB_COLORS[b.type]});
popup(b.x+b.w/2,b.y,'CRUNCH','#6B4A2F',13);
return;
}
breakBrick(b);
}
function breakBrick(b){
S.combo++; if(S.combo>S.bestCombo) S.bestCombo=S.combo;
const pts=25+(S.combo-1)*10+(S.level-1)*5;
S.score+=pts;
const cx=b.x+b.w/2, cy=b.y+b.h/2;
S.hearts+=brickBurst(b,S.combo);
S.shake=Math.max(S.shake,2.6);
S.brickCount--;
ball.speed=Math.min(600,ball.speed+2.2);
const m=Math.hypot(ball.vx,ball.vy); ball.vx*=ball.speed/m; ball.vy*=ball.speed/m;
popup(cx,cy,'+'+pts,INK,17);
if(S.combo>=2) popup(cx,cy+22,'COMBO ×'+S.combo,'#E8432F',13);
sfx.pop(S.combo);
if(S.lives<5 && Math.random()<0.09) powerups.push({bx:cx,x:cx,y:cy,vy:50,ph:rand(0,6),age:0,s:16});
if(S.brickCount<=0) levelCleared();
updateHUD();
}
function levelCleared(){
S.clearing=true; S.clearTimer=1.9;
ball.attached=true; ball.vx=ball.vy=0; trail.length=0;
lastBonus=200+100*(S.level-1);
S.score+=lastBonus;
popup(W/2,332,'+'+lastBonus+' BONUS','#E8432F',22);
heartRain(); sfx.clear(); updateHUD();
}
function tickClear(dt){
S.clearTimer-=dt;
if(S.clearTimer<=0){
S.clearing=false; S.level++;
buildLevel();
ball.speed=baseSpeed(); ball.attached=true; ball.vx=ball.vy=0;
if(S.mode==='demo') serveTimer=0.7;
popup(W/2,300,'LEVEL '+S.level,'#E8432F',24);
updateHUD();
}
}
function loseBall(){
sfx.lose(); S.shake=7; S.combo=0;
grayBurst(clamp(ball.x,30,W-30),H-40);
ball.speed=baseSpeed();
if(S.mode==='demo'){ ball.attached=true; serveTimer=0.9; }
else {
S.lives--; updateHUD();
if(S.lives<=0) gameOver(); else ball.attached=true;
}
}
function gameOver(){
S.mode='over'; sfx.over();
showOverlay('OUT OF BURGERS','GAME OVER',
'SCORE '+fmtScore(S.score)+' · LEVEL '+S.level+' · BEST COMBO ×'+S.bestCombo+
'<br>HEARTS SPREAD · '+S.hearts,
'CLICK OR PRESS ANY KEY TO PLAY AGAIN');
}
function startGame(){
S.score=0; S.level=1; S.lives=3; S.hearts=0; S.combo=0; S.bestCombo=0;
S.clearing=false; powerups.length=0; popups.length=0;
buildLevel();
ball.speed=baseSpeed(); ball.attached=true; ball.vx=ball.vy=0; trail.length=0;
pad.x=W/2;
S.mode='play'; paused=false; hideOverlay(); updateHUD();
popup(W/2,300,'LEVEL 1','#E8432F',24);
}
/* ---------------- physics ---------------- */
function stepBall(dt){
ball.x+=ball.vx*dt; ball.y+=ball.vy*dt;
const r=ball.r;
if(ball.x<WALL+r){ ball.x=WALL+r; ball.vx=Math.abs(ball.vx); sfx.wall(); }
else if(ball.x>W-WALL-r){ ball.x=W-WALL-r; ball.vx=-Math.abs(ball.vx); sfx.wall(); }
if(ball.y<WALL+r){ ball.y=WALL+r; ball.vy=Math.abs(ball.vy); sfx.wall(); }
if(ball.vy>0 && ball.y<pad.y+pad.h){
const nx=clamp(ball.x,pad.x-pad.w/2,pad.x+pad.w/2);
const ny=clamp(ball.y,pad.y,pad.y+pad.h);
const dx=ball.x-nx, dy=ball.y-ny;
if(dx*dx+dy*dy<=r*r) paddleBounce();
}
for(const b of S.bricks){
if(b.hp<=0) continue;
const nx=clamp(ball.x,b.x,b.x+b.w), ny=clamp(ball.y,b.y,b.y+b.h);
const dx=ball.x-nx, dy=ball.y-ny;
if(dx*dx+dy*dy<=r*r){
if(Math.abs(dx)>Math.abs(dy)){
ball.vx = dx>0? Math.abs(ball.vx) : -Math.abs(ball.vx);
ball.x = dx>0? b.x+b.w+r+0.5 : b.x-r-0.5;
}else{
ball.vy = dy>0? Math.abs(ball.vy) : -Math.abs(ball.vy);
ball.y = dy>0? b.y+b.h+r+0.5 : b.y-r-0.5;
}
hitBrick(b);
break;
}
}
if(Math.abs(ball.vy)<50){ // never lock flat
ball.vy = ball.vy===0? -50 : (ball.vy<0?-50:50);
const m=Math.hypot(ball.vx,ball.vy); ball.vx*=ball.speed/m; ball.vy*=ball.speed/m;
}
if(ball.y-r>H-24){ loseBall(); return true; }
return false;
}
function updateBall(dt){
if(ball.attached){
ball.x=pad.x+pad.w*0.18; ball.y=pad.y-ball.r-2;
trail.length=0;
if(S.mode==='demo'){ serveTimer-=dt; if(serveTimer<=0) launch(); }
return;
}
trail.push({x:ball.x,y:ball.y}); if(trail.length>10) trail.shift();
const steps=Math.max(1,Math.ceil(ball.speed*dt/6));
for(let i=0;i<steps;i++){ if(stepBall(dt/steps)) break; }
}
function predictX(){
const r=ball.r, lo=WALL+r, hi=W-WALL-r, span=hi-lo;
if(ball.vy<=0) return ball.x;
const t=(pad.y-ball.y)/ball.vy;
let m=((ball.x+ball.vx*t-lo)%(2*span)+2*span)%(2*span);
if(m>span) m=2*span-m;
return lo+m;
}
function updatePaddle(dt){
if(S.mode==='demo'){ // the auto-chef
let target;
if(ball.attached) target=W/2+Math.sin(S.t*0.55)*160;
else if(ball.vy>0) target=predictX();
else target=W/2+(ball.x-W/2)*0.35;
target+=Math.sin(S.t*2.1)*9;
pad.x+=clamp(target-pad.x,-640*dt,640*dt);
} else {
const kb=((keys['arrowright']||keys['d'])?1:0)-((keys['arrowleft']||keys['a'])?1:0);
if(kb) pad.x+=kb*640*dt;
else if(pointerX!=null) pad.x+=(pointerX-pad.x)*Math.min(1,16*dt);
}
pad.x=clamp(pad.x,WALL+pad.w/2+2,W-WALL-pad.w/2-2);
}
function updatePowerups(dt){
for(let i=powerups.length-1;i>=0;i--){
const u=powerups[i];
u.age+=dt; u.vy=Math.min(150,u.vy+40*dt); u.y+=u.vy*dt;
u.x=u.bx+Math.sin(u.age*2.4+u.ph)*16;
if(u.y>pad.y-8 && u.y<pad.y+pad.h+10 && Math.abs(u.x-pad.x)<pad.w/2+14){
powerups.splice(i,1); catchHeart(u.x,pad.y); continue;
}
if(u.y>H-26) powerups.splice(i,1);
}
}
function catchHeart(x,y){
if(S.lives<5){ S.lives++; popup(x,y-30,'+1 LIFE','#E8432F',20); }
else { S.score+=250; popup(x,y-30,'+250','#E8432F',20); }
for(let i=0;i<12;i++) parts.push({type:'heart',x,y:y+6,vx:rand(-160,160),vy:rand(-320,-120),
rot:rand(-.5,.5),rv:rand(-7,7),s:rand(4,8),life:rand(.6,1),age:0,g:700,color:pick(HEART_COLORS)});
sfx.power(); updateHUD();
}
function updateParticles(dt){
for(let i=parts.length-1;i>=0;i--){
const p=parts[i];
p.age+=dt;
if(p.age>=p.life){ parts.splice(i,1); continue; }
p.vy+=(p.g!==undefined?p.g:760)*dt;
p.x+=p.vx*dt; p.y+=p.vy*dt;
if(p.rv) p.rot+=p.rv*dt;
if(p.sw) p.x+=Math.sin(p.age*3+p.sw)*16*dt;
if(p.type==='heart' && p.y>H-28 && p.vy>0){ p.y=H-28; p.vy*=-0.42; p.vx*=0.65; if(p.rv)p.rv*=0.6; }
if((p.type==='seed'||p.type==='crumb') && p.y>H-24){ p.y=H-24; p.vy*=-0.3; }
}
if(parts.length>430) parts.splice(0,parts.length-430);
}
function updatePopups(dt){
for(let i=popups.length-1;i>=0;i--){ popups[i].age+=dt; if(popups[i].age>=popups[i].life) popups.splice(i,1); }
}
function update(dt){
S.t+=dt;
if(S.clearing) tickClear(dt);
if(S.mode!=='over'){ updatePaddle(dt); updateBall(dt); updatePowerups(dt); }
updateParticles(dt); updatePopups(dt);
S.squash=Math.max(0,S.squash-dt*4.2);
S.shake*=Math.exp(-7*dt); if(S.shake<0.05) S.shake=0;
}
/* ---------------- rendering ---------------- */
function drawFloor(){
const top=H-24;
ctx.fillStyle='#FFF3DA'; ctx.fillRect(0,top,W,24);
ctx.fillStyle='#E8483A';
for(let r=0;r<2;r++) for(let c=0;c<Math.ceil(W/20);c++)
if((r+c)%2===0) ctx.fillRect(c*20,top+r*12,20.5,12.5);
ctx.fillStyle=INK; ctx.fillRect(0,top-3,W,3);
}
function drawBricks(){
for(const b of S.bricks){
if(b.hp<=0) continue;
const set=BRICK_SPRITES[b.type];
const spr=(b.hp<b.maxHp && set.length>1)? set[1] : set[0];
ctx.drawImage(spr,b.x,b.y,BRICK_W,BRICK_H+4);
}
}
function drawPowerups(){
const spr=heartSprite('#F43F5E',true);
for(const u of powerups){
const s=u.s*2*(1+Math.sin(S.t*6+u.ph)*0.12);
ctx.save(); ctx.translate(u.x,u.y); ctx.rotate(Math.sin(S.t*2.2+u.ph)*0.16);
ctx.drawImage(spr,-s/2,-s/2,s,s); ctx.restore();
}
}
function drawBall(){
for(let i=0;i<trail.length;i++){
const k=(i+1)/trail.length;
ctx.globalAlpha=k*0.22; ctx.fillStyle='#E8483A';
ctx.beginPath(); ctx.arc(trail[i].x,trail[i].y,ball.r*k*0.85,0,7); ctx.fill();
}
ctx.globalAlpha=1;
ctx.beginPath(); ctx.arc(ball.x,ball.y,ball.r,0,7);
ctx.fillStyle='#E8432F'; ctx.fill();
ctx.lineWidth=2.5; ctx.strokeStyle='#B33326'; ctx.stroke();
ctx.beginPath(); ctx.ellipse(ball.x-2.8,ball.y-3.2,2.8,1.9,-0.6,0,7);
ctx.fillStyle='rgba(255,255,255,.8)'; ctx.fill();
}
function drawBurger(){
const g=ctx, w=pad.w, x=pad.x-w/2, y=pad.y, h=pad.h;
g.save();
g.translate(pad.x,y+h); g.scale(1+S.squash*0.09,1-S.squash*0.2); g.translate(-pad.x,-(y+h));
g.translate(0,Math.sin(S.t*3)*0.8); // idle bob
g.lineWidth=2; g.strokeStyle='rgba(74,42,24,.45)';
// bottom bun
g.fillStyle='#E2955A'; rr(g,x+7,y+28,w-14,8,[3,3,5,5]); g.fill(); g.stroke();
g.fillStyle='#EDAA62'; rr(g,x+13,y+29,w-26,3,2); g.fill();
// patty
g.fillStyle='#7A4A26'; rr(g,x+2,y+20,w-4,10,5); g.fill(); g.stroke();
g.fillStyle='#5E3618';
[[.16,.72],[.32,.58],[.5,.75],[.66,.6],[.82,.7]].forEach(s=>{ g.beginPath(); g.arc(x+w*s[0],y+25,1.8,0,7); g.fill(); });
// cheese with a drip over the patty
g.fillStyle='#FFC233'; rr(g,x-2,y+16,w+4,8,4); g.fill(); g.stroke();
rr(g,x+w*0.2,y+19,12,10,5); g.fill();
g.fillStyle='#E8A81C'; g.beginPath(); g.arc(x+w*0.68,y+20.5,2.4,0,7); g.fill();
// lettuce ruffle
g.fillStyle='#7FC94E'; rr(g,x-5,y+10,w+10,8,4); g.fill(); g.stroke();
for(let i=0;i<6;i++){ g.beginPath(); g.arc(x-5+(w+10)*(i+0.5)/6,y+18,5,0,Math.PI); g.fill(); }
g.fillStyle='#A5DE79'; rr(g,x+2,y+12,w-4,3,2); g.fill();
// tomato
g.fillStyle='#E8483A'; rr(g,x+3,y+8,w-6,7,3.5); g.fill(); g.stroke();
g.fillStyle='#F06A57'; rr(g,x+7,y+10,w-14,3,1.5); g.fill();
// sesame top bun
g.fillStyle='#EDAA62'; rr(g,x+4,y,w-8,14,[10,10,4,4]); g.fill(); g.stroke();
g.save(); rr(g,x+4,y,w-8,14,[10,10,4,4]); g.clip();
g.fillStyle='#D18A45'; g.fillRect(x+4,y+10,w-8,4);
g.fillStyle='rgba(255,255,255,.3)'; g.fillRect(x+14,y+2,w*0.34,3);
g.restore();
g.fillStyle='#FBE7C6';
seedDot(g,x+w*0.2,y+5,-0.5); seedDot(g,x+w*0.38,y+3,0.2); seedDot(g,x+w*0.56,y+6,-0.25);
seedDot(g,x+w*0.72,y+3.5,0.5); seedDot(g,x+w*0.86,y+6,-0.4);
g.restore();
}
function drawParticles(){
for(const p of parts){
const k=p.age/p.life;
if(p.type==='heart'){
let sc=p.age<0.12? backOut(p.age/0.12) : 1;
if(k>0.72) sc*=1-((k-0.72)/0.28)*0.45;
const al=k>0.6? 1-(k-0.6)/0.4 : 1;
const s=p.s*2.1*sc;
ctx.globalAlpha=clamp(al,0,1);
const spr=heartSprite(p.color);
ctx.save(); ctx.translate(p.x,p.y); ctx.rotate(p.rot);
ctx.drawImage(spr,-s/2,-s/2,s,s); ctx.restore();
} else if(p.type==='dot'){
ctx.globalAlpha=1-k; ctx.fillStyle=p.color;
ctx.beginPath(); ctx.arc(p.x,p.y,(p.s||2.4)*(1-k*0.4),0,7); ctx.fill();
} else if(p.type==='crumb'){
ctx.globalAlpha=1-k;
ctx.save(); ctx.translate(p.x,p.y); ctx.rotate(p.rot);
ctx.fillStyle=p.color; rr(ctx,-4,-3,8,6,2); ctx.fill(); ctx.restore();
} else if(p.type==='seed'){
ctx.globalAlpha=1-k;
ctx.save(); ctx.translate(p.x,p.y); ctx.rotate(p.rot);
ctx.fillStyle='#FBE7C6';
ctx.beginPath(); ctx.ellipse(0,0,4.4,2.2,0,0,7); ctx.fill(); ctx.restore();
}
}
ctx.globalAlpha=1;
}
function chunkText(txt,x,y,size,fill,ls){
ctx.font="900 "+size+"px 'Arial Black','Arial',sans-serif";
ctx.letterSpacing=ls||'1.5px';
ctx.textAlign='center'; ctx.textBaseline='middle'; ctx.lineJoin='round';
ctx.lineWidth=Math.max(3,size/5); ctx.strokeStyle=INK;
ctx.strokeText(txt,x,y);
ctx.fillStyle=fill; ctx.fillText(txt,x,y);
ctx.letterSpacing='0px';
}
function drawPopups(){
for(const p of popups){
const k=p.age/p.life;
ctx.globalAlpha=1-k*k;
chunkText(p.txt,p.x,p.y-44*k,p.size,p.color);
}
ctx.globalAlpha=1;
}
function drawServeHint(){
const y=H-128, a=0.6+0.4*Math.sin(S.t*4.5);
ctx.globalAlpha=a;
const txt='CLICK OR PRESS SPACE TO LAUNCH';
ctx.font='700 12px "Trebuchet MS",Verdana,sans-serif';
const w=ctx.measureText(txt).width+30;
rr(ctx,W/2-w/2,y-14,w,28,14);
ctx.fillStyle='#FFFBF0'; ctx.fill();
ctx.lineWidth=3; ctx.strokeStyle=INK; ctx.stroke();
ctx.fillStyle=INK; ctx.textAlign='center'; ctx.textBaseline='middle';
ctx.fillText(txt,W/2,y);
ctx.globalAlpha=1;
}
function drawClearBanner(){
const y=272;
rr(ctx,W/2-228,y-64+8,456,124,18); ctx.fillStyle='rgba(74,42,24,.2)'; ctx.fill();
rr(ctx,W/2-235,y-64,470,124,18); ctx.fillStyle='#FFFBF0'; ctx.fill();
ctx.lineWidth=5; ctx.strokeStyle=INK; ctx.stroke();
chunkText('STACK CLEARED!',W/2,y-16,33,'#E8432F','2px');
ctx.font='700 13px "Trebuchet MS",Verdana,sans-serif';
ctx.fillStyle='#6B4A2F'; ctx.textAlign='center'; ctx.textBaseline='middle';
ctx.fillText('LEVEL '+(S.level+1)+' INCOMING · +'+lastBonus+' PTS',W/2,y+28);
}
function render(){
ctx.setTransform(DPR,0,0,DPR,0,0);
ctx.fillStyle='#FFF9EC'; ctx.fillRect(0,0,W,H);
if(S.shake>0.05) ctx.translate(rand(-1,1)*S.shake,rand(-1,1)*S.shake);
drawFloor();
drawBricks();
drawPowerups();
if(S.mode!=='over') drawBall();
drawBurger();
drawParticles();
drawPopups();
if(S.clearing) drawClearBanner();
if(S.mode==='play' && ball.attached && !S.clearing) drawServeHint();
}
/* ---------------- HUD & overlay ---------------- */
const ov=$('overlay'), ovEyebrow=$('ovEyebrow'), ovTitle=$('ovTitle'), ovMeta=$('ovMeta'), ovBlink=$('ovBlink');
function showOverlay(eyebrow,title,meta,blink){
ovEyebrow.textContent=eyebrow; ovTitle.textContent=title;
ovMeta.innerHTML=meta; ovBlink.textContent=blink;
ov.classList.remove('hidden');
}
function hideOverlay(){ ov.classList.add('hidden'); }
const elScore=$('scoreVal'), elHearts=$('heartsVal'), elLevel=$('levelVal'), elLives=$('livesBox');
const fmtScore=n=>String(n).padStart(6,'0');
const HEART_D='M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z';
function renderLives(){
const slots=Math.max(3,Math.min(5,S.lives));
let h='';
for(let i=0;i<slots;i++){
const on=i<S.lives;
h+='<svg viewBox="0 0 24 24" width="20" height="20" aria-hidden="true"><path d="'+HEART_D+
'" fill="'+(on?'#E8432F':'none')+'" stroke="'+(on?'#4A2A18':'#C9AE8C')+
'" stroke-width="2" stroke-linejoin="round"/></svg>';
}
elLives.innerHTML=h;
}
function updateHUD(){
elScore.textContent=fmtScore(S.score);
elHearts.textContent=S.hearts;
elLevel.textContent=S.level;
renderLives();
}
/* ---------------- input ---------------- */
window.addEventListener('keydown',e=>{
const k=e.key.toLowerCase();
if([' ','arrowleft','arrowright','a','d','p','m','enter'].includes(k)) e.preventDefault();
keys[k]=true;
sfx.ensure();
if(k==='m'){ toggleMute(); return; }
if(k==='p'){
if(paused){ paused=false; hideOverlay(); }
else if(S.mode==='play'){ paused=true; showOverlay('KITCHEN CLOSED','PAUSED','','PRESS P TO RESUME'); }
return;
}
if(paused) return;
if(S.mode==='demo'||S.mode==='over'){ startGame(); return; }
if(S.mode==='play' && ball.attached && (k===' '||k==='enter')) launch();
});
window.addEventListener('keyup',e=>{ keys[e.key.toLowerCase()]=false; });
window.addEventListener('pointermove',e=>{
const r=cv.getBoundingClientRect();
if(r.width>0) pointerX=(e.clientX-r.left)/r.width*W;
});
$('stage').addEventListener('pointerdown',()=>{
sfx.ensure();
if(paused) return;
if(S.mode==='demo'||S.mode==='over'){ startGame(); return; }
if(S.mode==='play' && ball.attached) launch();
});
window.addEventListener('blur',()=>{
if(S.mode==='play' && !paused){ paused=true; showOverlay('KITCHEN CLOSED','PAUSED','','PRESS P TO RESUME'); }
});
function toggleMute(){
muted=!muted; sfx.muted=muted;
$('icoOn').style.display=muted?'none':'block';
$('icoOff').style.display=muted?'block':'none';
}
$('muteBtn').addEventListener('click',e=>{ e.stopPropagation(); sfx.ensure(); toggleMute(); });
/* ---------------- boot: attract-mode demo ---------------- */
buildLevel();
ball.speed=baseSpeed(); ball.attached=true; serveTimer=0.5;
updateHUD();
showOverlay('THE AUTO-CHEF IS COOKING','TAKE A BITE?',
'MOVE · MOUSE OR ← → LAUNCH · SPACE<br>CATCH THE BIG HEARTS FOR EXTRA LIVES',
'CLICK OR PRESS ANY KEY TO START');
let last=performance.now();
function frame(now){
const dt=Math.min(0.033,(now-last)/1000)||0.016;
last=now;
if(!paused) update(dt);
render();
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
})();
</script>
</body>
</html>Reasoning trace (55,699 tokens)
The user wants a Breakout game with a hamburger paddle and bricks that explode into little hearts when broken. This needs to be a self-contained HTML file with everything inline.
Let me think about the design:
**Core game: Breakout clone**
- Paddle = hamburger (drawn with canvas - bun top with sesame seeds, lettuce, patty, cheese, bun bottom)
- Ball = maybe a ketchup droplet or tomato slice? Or a heart? Let's make the ball something food-related... maybe a ketchup/mustard ball. Actually a little heart ball could be cute too — but hearts are the explosion. Let me make the ball a shiny tomato-ish red ball, or maybe a mustard-yellow ball. Or a pickle slice! A pickle ball bouncing off a hamburger — that's fun. Actually let's do a ketchup-red glossy ball with a little highlight, or a fried egg? Hmm. A pickle slice (green circle with darker rim and seed specks) is cute. Or keep it simple: a red glossy ball (tomato). I'll go with a glossy red ball — reads as a cherry tomato / ketchup blob.
**Bricks explode into hearts**
- When a brick is hit, spawn a burst of 15-25 small heart particles with physics (gravity, velocity, rotation, fade out).
- Draw hearts with canvas path (bezier curves) — small pixel-perfect hearts.
**Demo mode (required)**
- AI plays automatically behind a "Click or press a key to start" overlay. The AI paddle follows the ball with some easing/imperfection.
**Visual design considerations (I want this to look genuinely good, not generic):**
- I want to avoid gradient banners and default blue/purple. This is a game, so the aesthetic should be deliberate. Let me think of a "diner" theme: warm cream/paper background, a checkerboard strip maybe, warm red accents (ketchup red), mustard yellow. Kind of retro American diner.
- Typography: use a fun display font from Google Fonts. Something like "Lilita One" or "Titan One" or "Fredoka" or "Chewy" — a chunky playful font fits a burger game. I'll use "Lilita One" for headings and maybe "Nunito" for small text. Actually the rules say no external files except CDN explicitly allowed — Google Fonts is a CDN reference... The instruction says "No external files, images, fonts or network requests, except the CDN URLs explicitly allowed by the task." Hmm, the task doesn't explicitly allow any CDN. To be safe, I should NOT load Google Fonts and instead use system fonts creatively. Wait — "No external files, images, fonts" — so I can't use Google Fonts. I need to use system font stacks. But I dislike using generic system-ui as identity font... but rules take priority over taste here. I can style with heavy letter-spacing, weights, and maybe use a font like "Arial Black"/"Impact" fallback stack? Impact is widely available and gives a chunky arcade feel. Or I could render text on canvas with custom styling. Let me use a stack like `'Arial Rounded MT Bold', 'Helvetica Rounded', ...` hmm.
Actually, let me reconsider: the requirement says no network requests except CDN URLs explicitly allowed by the task. The task (user's message) doesn't mention any CDN. So I must go fully offline. I'll craft the typography with careful use of available system fonts — I can use `font-family: 'Impact', 'Arial Black', sans-serif` for the chunky arcade title which actually looks quite fitting for a retro game, with letter-spacing and text-shadow for a chunky sticker look. That's a deliberate aesthetic choice, not laziness.
Alternatively, I could draw the title on canvas... but Impact/Arial Black with outline effect (multiple text-shadows to fake stroke, or -webkit-text-stroke) can look genuinely great for arcade games. `-webkit-text-stroke` works in Chrome (target is latest Chrome). I'll use that.
**Color palette (diner):**
- Background: warm cream `#FFF3DC` or a soft paper `#FDF3E3`. Maybe with subtle polka dots or a checkerboard floor strip at the bottom.
- Accents: ketchup red `#E84B3C` / `#D93A2B`, mustard `#F2B01E`... let me refine:
- Cream: `#FFF6E5`
- Deep brown (for text): `#3E2A1E` or `#4A2C1A`
- Ketchup red: `#E8432F`
- Mustard: `#F5B942`
- Lettuce green: `#7CB342` / `#8BC34A`
- Cheese: `#FFC233`
- Patty brown: `#6B4226`
**Bricks:** Instead of plain rectangles, make them look like... maybe little burger ingredients? Like each brick row is an ingredient: top buns (tan), cheese (yellow), patties (brown), lettuce (green), tomatoes (red)? That's a nice thematic tie — the wall is a giant stacked burger and you're breaking it apart! That's a great signature idea: the brick field IS a giant hamburger cross-section. Rows from top: sesame bun top (2 rows of tan), cheese, patty, lettuce, tomato, bun bottom. Each brick drawn with its ingredient texture: sesame seeds on bun bricks, drips on cheese, wavy lettuce edges... Keep it simple enough in canvas drawing but distinctive.
Actually — signature moment: "the brick wall is a giant burger, and breaking it releases hearts." The whole level is "deconstructing the burger". When you clear a level, the burger is rebuilt (next level) maybe faster ball.
Let me design the brick layout:
- Grid: maybe 10 columns × 6 rows. Each brick ~64×26 px.
- Rows labeled by ingredient:
- Row 0: Top bun (light tan `#E8A25E` with sesame seeds)
- Row 1: Top bun (slightly darker or same)
- Row 2: Cheese (`#FFC233` with drip)
- Row 3: Patty (`#6B4226`)
- Row 4: Lettuce (`#8BC34A` wavy bottom)
- Row 5: Tomato (`#E8432F`) or bottom bun `#E8A25E`
Hmm, order in a real burger from top: bun, lettuce, tomato, cheese, patty, bun. Let me do: top bun (2 rows), lettuce, tomato, cheese, patty, bottom bun. That's 8 rows — maybe too many. 6 rows: bun-top, lettuce, tomato, cheese, patty, bun-bottom.
Each brick could have 1 HP mostly, maybe patty rows have 2 HP (thicker). When hit, hearts burst with the ingredient color? Hearts in a mix of reds/pinks — "tons of little hearts" — maybe hearts colored per ingredient (green hearts from lettuce? weird). Better: hearts in romantic pinks/reds regardless, since "explodes into tons of little hearts". I'll use a palette of heart colors: `#FF5A7E`, `#FF8FA3`, `#E8432F`, `#FFC0CB`... plus a few white sparkle particles. Mixed pinks/reds look like love explosion. Maybe occasionally a big heart.
**Heart particles physics:** spawn 14-22 particles at brick center, random velocity outward + upward bias, gravity pulls down, they rotate/wobble, shrink/fade over ~0.8-1.4s, bounce slightly? Maybe they fall and fade. Some could be "floaty" hearts that drift up like emojis. I'll do a mix: burst with gravity that fall like confetti, spinning. Additive glow? Keep crisp flat design.
Also when the ball hits the paddle (burger), maybe tiny crumb particles or a little squash animation on the burger (bounce/squash). The burger paddle should squash when hit — juicy feel.
**Ball:** red glossy sphere with a highlight; trail of fading circles. Speed increases slightly as bricks break.
**Paddle (burger):** Draw with canvas:
- Top bun: rounded dome shape (arc) with sesame seeds.
- Lettuce: wavy green strip.
- Tomato: red strip? keep it to: top bun, lettuce, cheese, patty, bottom bun. Height ~28-34px total, width ~110px.
- When ball hits, burger squashes (scaleY down, scaleX up briefly) and maybe seeds pop off? Cute detail: on hit, a couple of sesame seeds fly off as particles. Nice juice.
- Paddle movement: mouse/touch + arrow keys/A-D. In demo mode, AI tracks ball x with easing and slight error.
**Game states:**
- `demo` (AI playing, overlay "Click or press a key to start")
- `ready` (after start, ball on paddle, launch on click/space)? Since demo auto-plays, when user clicks to start, reset to a fresh game with ball on paddle, then click/space launches. Or immediately serve. I'll do: click/press → game starts with ball glued to paddle, "Click / Space to launch" then launch. Simpler: on start, serve ball after a brief countdown. Let me do: start → ball attached, moves with paddle, any click/space launches. Good.
- `playing`, `levelup` (brief), `gameover` (show score, press to restart), `win`... Levels: when all bricks cleared, show "LEVEL UP", rebuild wall with +speed, +1 level. Keep going.
**Lives:** 3 lives shown as little hearts in HUD (thematic!). Losing a ball: hearts scatter downward? Sad effect: gray hearts fall. On game over, show final score and restart hint. Also allow restart.
**HUD:** Score, Level, lives (hearts). Drawn in DOM above canvas or in canvas? I'll do DOM header for crispness: title "BURGER BREAKOUT" with chunky style, and HUD row. Actually, let me draw HUD inside canvas top area for cohesion... DOM is easier to style nicely and stays crisp. I'll do a DOM header bar: left = score, center title, right = hearts. Hmm — layout: title above canvas, HUD strip between title and canvas or overlaid at top of canvas. Let me overlay HUD at top inside the play area with canvas-drawn text? Canvas text at devicePixelRatio scale is crisp too. I'll do DOM for the frame (title, score, lives, hints) around the canvas — cleaner code separation.
Layout plan:
- Page: cream background with subtle pattern (CSS radial-gradient dots — wait, I avoid decorative gradients... a dot pattern via radial-gradient background is a pattern, not a "gradient banner". Subtle polka-dot pattern on cream feels like diner wallpaper — I think a repeating dot pattern is fine and tasteful, it's a pattern not a gradient wash. I'll use `background-image: radial-gradient(#f2e2c4 8%, transparent 9%)` repeating — classic polka dots. Hmm, "no decorative gradients to fake design sense" — a polka dot pattern isn't a gradient visually. It reads as flat dots. I'll keep it very subtle. Alternatively CSS checkerboard at bottom. Let me keep a very subtle dot pattern.
- Center: a "cabinet" card containing the game: header with title + HUD, canvas below.
- Footer hint line: controls.
Responsive: canvas fixed logical size e.g. 900×620, scaled down via CSS max-width: 100%. Use DPR scaling for crispness. At 375px width, canvas scales to fit. HUD wraps.
**Canvas logical size:** Let me use 960×640 internal, displayed responsively. Play field with a border (like a tray). Maybe draw a "table" surface at bottom — checkerboard floor strip at the bottom below paddle? Cute: red-white checker strip like a diner floor. And walls drawn as inner border.
**Sound:** Could add WebAudio beeps (no external files). Small blip on paddle hit, pop on brick break, arpeggio on lose. Sounds make it feel alive. Must handle audio context creation on first user gesture; in demo mode (no gesture) audio won't play — that's fine, guard it. I'll add a tiny synth: brick pop (pitch rises with combo), paddle thock, life lost sad tone. Keep code compact. Add mute toggle button. Sounds are nice-to-have; I'll include a compact implementation.
**Combo system:** consecutive brick hits without touching paddle increases combo; score multiplier; hearts burst bigger with combo. Display combo popups ("x4!") floating. Adds juice and "arcade" feel.
**Power-ups?** Maybe falling items occasionally: bigger paddle? extra life heart? Could add a falling heart that grants +1 life if caught — thematic and simple. And maybe a "big bite" power-up making ball bigger? Keep scope controlled: one power-up type (falling heart = extra life, rare, like 8% chance per brick, capped). Actually hearts falling everywhere could be confusing with explosion hearts. Falling power-up heart is bigger, with pulse and outline, falls slower. Catch with paddle. It's a nice touch. Also maybe "mustard speed" no... keep it: heart = +1 life (max 5). Fine.
Also maybe multiball? Scope creep. Skip.
**Ball-paddle physics:** reflect angle based on hit position relative to paddle center (classic). Ensure ball doesn't get stuck horizontal: clamp min vertical speed.
**Collision:** ball vs bricks — AABB with circle, determine side to flip axis. Simple approach: check overlap, compute penetration on each axis, reflect on smaller penetration axis. Ball speed increases per brick slightly and per level.
**AI demo:** paddle.x lerps toward ball.x + small predicted offset with wobble; it should be decent but occasionally imperfect (add sin wobble). Also AI launches ball automatically after short delay. Demo shows hearts explosions within seconds — good for screenshot. Also maybe pre-break a few bricks right at start so the screenshot at ~3s shows hearts? The AI plays from a fresh board; in 3 seconds it might break 1-2 bricks. To make the first paint screenshot juicy: start demo with some bricks already... no — better: on load, do an intro "burst": maybe the game field starts with a few bricks pre-broken? Hmm, that's weird. Alternative: hearts ambient floating in background? Could have a couple of decorative floating hearts behind the title...
Better idea: when the page loads, run a quick "intro": ball auto-launched quickly (0.6s after load), AI plays well (fast tracking), so within 3 seconds several bricks break. Demo AI should be strong (tracks ball with good speed) so lots of hearts in screenshot. Also I could slightly bias ball trajectory... The AI with perfect tracking will keep ball alive and breaking bricks. In 3s at moderate ball speed (~7px/frame at 60fps → 1260px in 3s) it'll break maybe 3-8 bricks. Good enough. Also add a subtle idle animation: the burger paddle gently bobbing, title slight bounce.
Also make the demo start with ball already launched (no waiting) — yes, in demo mode ball serves automatically immediately.
**Overlay:** "CLICK OR PRESS A KEY TO START" — big chunky text over a translucent panel. Clicking canvas or any key starts. Per requirement, show that message clearly. After start: overlay hides, game resets, ball on paddle with "LAUNCH: click or space".
**Pause:** maybe P to pause. Nice to have. Add visibility pause.
Let me now write the drawing functions carefully.
**Drawing the burger paddle (canvas, function drawBurger(ctx, x, y, w, h, squash)):**
Center x, y = top of burger. Layers (relative heights of h):
- top bun: dome — use path: rounded rect with big top radius, or arc. Simple: draw rounded rect where top corners radius large. Sesame seeds: small ellipses rotated randomly but fixed positions (precomputed offsets).
- lettuce: wavy — draw a rect with scalloped bottom edge (series of arcs), color green, extends slightly wider than bun.
- cheese: yellow rect with drips (few rounded drops hanging).
- patty: dark brown rounded rect.
- bottom bun: tan rounded rect with flat bottom.
Order top→bottom: topBun (h*0.34), lettuce (0.14), cheese (0.16 with drips over patty), patty (0.22), bottomBun (0.2). Cheese typically drips over patty — draw cheese after patty? In side view: top bun, then lettuce hangs, cheese below lettuce above patty... Let me stack: topBun, lettuce (wavy), tomato? skip, cheese, patty, bottomBun. Drawing order = vertical order. I'll draw each as simple shapes with slight width variation: topBun width w, lettuce w+8, cheese w+4, patty w-2, bottomBun w-4? Actually bottom bun usually narrower or same. Fine.
Squash animation: maintain `squash` value 0..1, on hit set to 1, decay; render with ctx.translate(x, y+h); ctx.scale(1+squash*0.15, 1-squash*0.3); translate back anchored at bottom. Also add slight idle bob: y += sin(t)*1.5 when idle? During play maybe not; keep bob only subtle or skip during play. I'll add tiny bob always (1px) — alive feeling.
**Drawing bricks by ingredient:** Each brick type has draw function with w,h:
- `bun`: tan `#E9A15B`... top bun color `#EDAA62` with darker bottom edge `#D18A45`, sesame seeds (2-3 small white-yellow ellipses per brick, fixed positions per brick via seed).
- `lettuce`: `#7FC94E` with wavy top edge? Draw rect with scalloped top/bottom, darker green `#5FA83A` accents.
- `tomato`: `#E8483A` with darker rim `#C33327` and a lighter inner.
- `cheese`: `#FFC233`, with a drip bump at bottom and highlight `#FFD966`.
- `patty`: `#7A4A26` dark brown with texture dots `#5E3618`, hp 2 → when hit once, show bite/crack? For hp2 bricks (patty), draw slightly darker + when damaged, add crack lines / lighter inner showing "cooked" — simpler: draw crack lines when damaged.
- `bunbottom`: `#E2955A` flat.
Rounded corners on all bricks (r=5), subtle bottom shadow line for depth (flat offset darker, not gradient).
Row config top→bottom:
```
rows = [
{type:'bun', hp:1},
{type:'lettuce', hp:1},
{type:'tomato', hp:1},
{type:'cheese', hp:1},
{type:'patty', hp:2},
{type:'bunbottom', hp:1},
]
```
6 rows × 10 cols = 60 bricks. Patty row 2hp → effectively 66 hits. Fine.
Maybe alternate: cols 10 at width 960 playfield: margin 40 each side → 880/10 = 88 wide bricks, height 30, gap 6 → brick w=80, h=26. Let me compute: field inner width 880, 10 cols with 8px gaps: total gaps 9*8=72... plus edges. brick w = (880 - 9*8)/10 = (880-72)/10 = 80.8. Let me just do grid with gap: cols=10, gap=7, side margin = 40. brickW = (960-80 - 9*7)/10 = (880-63)/10=81.7 → use 81.7, fine canvas floats ok. Rows start at y=90 (below in-canvas HUD space? I'm using DOM HUD, so canvas top can host bricks from y=60). Row gap 8, brick h=28. 6 rows → 6*36=216 → bricks occupy y 60..276. Paddle at y = 640-70=570. Ball loses below 640.
DOM HUD outside canvas keeps canvas clean. Also draw score popups inside canvas.
**Background inside canvas:** cream field slightly lighter/darker than page; thin brown border; checkerboard strip at bottom (last 26px) red/white? A diner checkered floor at bottom under the death line — cute. Draw at very bottom: 2 rows of checker squares in muted red `#E8483A` and white with low alpha... The ball falls "through" it when lost. Also side wall inner shadow flat.
Maybe subtle vertical guide? Keep clean.
**Hearts particle rendering:** function drawHeart(ctx, size) path:
```
ctx.beginPath();
ctx.moveTo(0, s*0.35);
ctx.bezierCurveTo(s, -s*0.45, s*2? ...
```
Standard heart path with two bezier curves. Let me write:
```
function heartPath(ctx, s){ // s = half-width-ish
ctx.moveTo(0, s*0.4);
ctx.bezierCurveTo(-s*1.1, -s*0.5, -s*0.5, -s*1.2, 0, -s*0.4);
...
}
```
Let me use a known-good heart: with center at origin, size s:
```
ctx.moveTo(0, s*0.3);
ctx.bezierCurveTo(-s*1.2, -s*0.6, -s*0.4, -s*1.3, 0, -s*0.5);
```
Hmm, better construct: heart = two circles + triangle. Simplest param heart:
```
function heartPath(c, s){
c.beginPath();
c.moveTo(0, s*0.35);
c.bezierCurveTo(-s*1.1, -s*0.45, -s*0.55, -s*1.25, 0, -s*0.35);
c.bezierCurveTo(s*0.55, -s*1.25, s*1.1, -s*0.45, 0, s*0.35);
c.closePath();
}
```
Wait that's symmetric-ish: start bottom point (0, 0.35s), curve left up to lobes and back to top middle dip (0,-0.35s)? Let me think: classic heart path (size = s):
- Bottom tip at (0, s).
- Left curve: bezier from (0, s) with control points (-1.2s, 0.2s) and (-1.0s, -1.0s)?
Let me just use the well-known:
```
ctx.moveTo(0, s*0.3);
ctx.bezierCurveTo(0, 0, -s*0.5, -s*0.3, -s*0.5, -s*0.6);
```
This is getting fiddly. Use a cleaner known param set (from common heart drawing code):
```
function drawHeart(ctx, x, y, size, rot, color){
ctx.save(); ctx.translate(x,y); ctx.rotate(rot); ctx.scale(size, size);
ctx.beginPath();
ctx.moveTo(0, 0.35);
ctx.bezierCurveTo(0.35, 0.0, 0.5, -0.15, 0.5, -0.4);
ctx.bezierCurveTo(0.5, -0.65, 0.28, -0.8, 0.05? ...
```
Let me define in unit coords: heart occupying roughly [-0.5,0.5]×[-0.8,0.5]:
```
ctx.moveTo(0, 0.5); // bottom tip
ctx.bezierCurveTo(-0.55, 0.1, -0.5, -0.45, -0.5?
```
Standard approach (widely used):
```
ctx.moveTo(0, 0.5);
ctx.bezierCurveTo(-0.6, 0.0, -0.6, -0.5, -0.25, -0.5);
ctx.bezierCurveTo(-0.07, -0.5, 0, -0.35, 0, -0.2);
ctx.bezierCurveTo(0, -0.35, 0.07, -0.5, 0.25, -0.5);
ctx.bezierCurveTo(0.6, -0.5, 0.6, 0.0, 0, 0.5);
```
Hmm let me sanity check: bottom tip (0, 0.5). Curve to left lobe top at (-0.25, -0.5) via controls (-0.6,0), (-0.6,-0.5) — that makes left side bulge out to x=-0.6, going up. Then small curve from (-0.25,-0.5) to center dip (0,-0.2) via controls (-0.07,-0.5) and (0,-0.35) — creates the notch. Mirror right. Then right side curve from (0.25,-0.5) via (0.6,-0.5),(0.6,0) back to (0,0.5). This is a classic construction and yields a decent heart.
I'll multiply all by a size factor when drawing. For hearts drawn many times, I'll pre-render a heart sprite to an offscreen canvas per color? Simpler: just path each time — 300 particles path drawing is fine perf-wise (hearts are like 2 beziers ×4 = 8 curves; 300×~10 path ops at 60fps is OK). To be safe, prerender heart shapes per color into small offscreen canvases and drawImage with rotation. That's much faster. Colors: ~6 heart colors → 6 sprites at high res (e.g., 64px) drawn scaled. Also add tiny white highlight dot on sprite. I'll implement makeHeartSprite(color) with the path, plus a lighter shine ellipse.
Particle counts: per brick 18 hearts + few sparkles. Multiple bricks per second → up to ~150 alive particles. Fine.
Heart particle update: pos += vel*dt; vel.y += g*dt; vel *= drag; rot += rotVel; life decreases; scale = pop-in (easeOutBack first 100ms) then shrink near end; alpha fades last 30%. Some hearts get "flutter" — sway x with sin. Draw with drawImage centered, rotated.
**Score popups:** "+50 x3" text floats up fades.
**Screen shake:** small shake on brick break (magnitude 2-4px, decays), bigger on life lost. Juicy but not nauseating.
**Ball trail:** store last N positions, draw fading circles.
**Sound synth (WebAudio):**
```
function beep(freq, dur, type='square', vol=0.15, slide=0){ ... }
```
- paddle: 180Hz square short.
- brick: pitch = 400 + combo*40, triangle.
- patty hit (damage): 200Hz.
- lose life: descending saw.
- level clear: quick arpeggio.
- catch heart: two-note up.
Mute button toggles. AudioContext created lazily on first user gesture; in demo, sounds muted automatically (no ctx). Guard all in try/catch.
**Level progression:** speed = base + level*0.4 + brokenCount*small. When bricks all cleared → LEVEL CLEAR banner 1.5s → rebuild, ball resets to paddle (auto-launch in demo, wait-for-input in play... after level clear auto relaunch after 1s both modes with countdown maybe). Keep: after clear, state 'levelup' 1.6s with banner, then reset ball attached; in play mode show "click/space to launch" — or auto-launch after 1s to keep flow. I'll auto-serve after 1s in both modes with a little "Ready..." Actually for player mode, waiting for input is standard; but auto-launch keeps momentum. I'll do: in player mode after level clear, ball attaches to paddle with hint "click / space to launch". Initial start same.
**Lives:** start 3, max 5. Lose ball → sad gray heart burst at paddle? Actually hearts fall from where ball was lost. Shake. If lives 0 → GAME OVER overlay (in-canvas or DOM). DOM overlay panel with score, "press to restart". In demo mode, no lives lost anyway (AI too good? AI might miss sometimes — make AI strong). If demo loses life, just respawn silently.
**Code structure:**
- Constants, state object.
- init canvas DPR.
- buildLevel().
- input handlers (mousemove, touch, keys, click).
- update(dt): paddle (input or AI), ball physics + collisions, particles, popups, powerups, shake decay.
- render(): bg, bricks, hearts particles, ball+trail, burger, powerups, popups, HUD-in-canvas maybe combo meter.
- loop with rAF, dt clamp.
**Overlay DOM:** one overlay div with states: start (title + "click or press a key"), gameover (score + restart), paused. Styled chunky. Pointer-events handling: overlay covers canvas; click starts.
Wait: clicking overlay to start — also should count as launching? Start → ball attached; then next click launches. Fine.
Keyboard: keydown anywhere (window) when overlay visible → start. Space/click launches when attached. Arrow/A/D move. P pause. M mute.
**Demo AI details:**
- target = predicted ball x when descending: simple: ball.x + ball.vx*distance-based lead, or just track ball.x with lerp factor 0.1 + max speed. Add imperfection: aim offset = sin(t*0.7)*30. AI strong. When ball moving away, drift to center.
- In demo, auto-serve after 0.8s.
- Demo shouldn't end: infinite lives.
**HTML/CSS structure:**
```
<body>
<main class="cabinet">
<header> title, tagline </header>
<div class="hud"> score | level | hearts | mute </div>
<div class="stage">
<canvas id="game"></canvas>
<div id="overlay"> ... </div>
</div>
<footer class="hints">controls</footer>
</main>
</body>
```
Styling: cream bg with dots, cabinet with thick brown border (6px solid #4A2C1A), rounded corners, offset flat shadow (box-shadow: 8px 8px 0 rgba(74,44,26,.15)) — flat "sticker" aesthetic. Title: Impact/Arial Black with -webkit-text-stroke, cream fill? Or red fill with brown stroke and hard drop shadow. Tagline small letter-spaced uppercase.
Hearts in HUD: draw as inline SVG? I can inline small SVG hearts — allowed since inline, no external. Yes inline SVG heart icons for lives + a heart icon in tagline. For mute button: inline SVG speaker icon. No emoji.
Colors CSS:
```
--cream:#FFF4DF; --paper:#FFFBF0; --ink:#42210B?
```
Let me pick: ink `#4A2A18`, red `#E8432F`, mustard `#F5B32F`, tan `#EBAA60`, green `#79BE4C`.
Canvas field bg: `#FFF9EC` with faint grid? Keep plain with subtle dot pattern drawn once? I'll draw subtle dots in canvas bg each frame (cheap: prerender pattern to offscreen once). Or plain — checker floor at bottom gives enough. Add faint side wall shading via flat 4px darker strips? Keep clean.
Checkerboard bottom: height 22, squares 22 alternating `rgba(232,67,47,0.85)` and `#fff`... maybe softer: `#F6D8CE`/`#FFF` with thin top border `#4A2A18`. Hmm, classic diner checker in red & white reads great: use `#E8432F` alpha 0.9 and `#FFF8EC`. Draw beneath a 3px ink line. The death line = top of checker? Ball lost when y > H. Visually ball falls onto checker floor and poofs. Nice: spawn gray poof at floor.
**Level themes:** change ingredient colors slightly per level? Or rearrange rows order? Keep same but speed up and maybe add a row. Simple: level N: ballSpeed base 6.2 + N*0.5; maybe brick hp+? Patty hp 2 always. Fine.
**Score:** brick hit +10 (damage) +? break +25 + combo*5. Combo resets on paddle hit. Show combo counter near burger "COMBO x5" pulsing when ≥2.
Now, sizes: canvas W=960, H=600? Let's compute layout: bricks y 60..~280, paddle ~570, H=600. Use W 960 H 600. Displayed at max-width ~920px within page. Page fits 1280×800 with header — total page height: title(~60) + hud(40) + canvas(570 at 920 width→ scaled 920/960=0.958→575) + footer 30 ≈ 780. OK-ish; maybe make cabinet width 900. Let me set stage width min(92vw, 940px). Canvas aspect 960/600. At 1280×800 viewport, page height must fit-ish (scrolling a bit is okay but better avoid). Title compact.
Actually let me reconsider: keep W=900, H=580 for slightly less scaling. Brick grid: 10 cols, margin 30 each side → 840; gap 8: brickW=(840-72)/10=76.8. h=26, gap 7 vertical, start y=54. 6 rows → 54+6*33-7=245. Paddle y = 580-56 = 524, burger h=34, w=112. Ball r=9. Checker floor h=20 at bottom (y 560..580). Ball lost when y-r > 580.
DPR: canvas.width = 900*dpr etc., ctx.setTransform(dpr...). CSS width 100% of stage.
Mouse control mapping: rect = canvas.getBoundingClientRect(); x = (e.clientX-rect.left)/rect.width*W.
**Demo auto-play visibility:** runs immediately on load; requestAnimationFrame loop always running.
**Serving the ball:** attached position = paddle.x + paddle.w*0.18 offset, y = paddle.y - r-2. Launch angle: slight random ±20° from up, or aim toward largest cluster/center. vx = sin(a)*speed, vy=-cos(a)*speed.
**Ball-brick collision:** iterate alive bricks; if circleRectOverlap: compute nearest point; reflect based on penetration. Standard:
```
const nx = clamp(ball.x, b.x, b.x+b.w), ny = clamp(ball.y, b.y, b.y+b.h);
dx=ball.x-nx, dy=ball.y-ny; if dx*dx+dy*dy < r*r → hit.
```
Determine axis: if |dx|>|dy| flip vx else flip vy (if both 0, i.e., center inside, flip vy). Move ball out of overlap. Only handle one brick per frame? Could handle first hit then break — but with fast ball might tunnel; add sub-stepping: move ball in 2-3 substeps if speed high. Ball speed max ~11, r=9, brick h=26 → no tunneling at 11px/frame? brick height 26 > 11 so fine, but two substeps for safety. I'll implement substeps = ceil(speed/8).
**Power-up hearts:** on brick break, 9% chance (if lives<5) spawn falling heart (size 13, pulse, outline, slower gravity 120px/s, sway). Catch when overlaps paddle rect → +1 life (cap 5), sparkle burst, sound. Missed → disappears below. Distinguish from explosion hearts: bigger, white outline ring + gentle pulse + slower fall. OK.
**Popups:** array {x,y,text,life,color}.
**Shake:** shake = magnitude; each frame offset = random in [-shake, shake]; decay shake *= 0.85. Apply ctx.translate at render start.
**Level clear banner:** state 'clear' with timer; big text "BURGER CLEARED!" hearts confetti across screen (spawn 80 hearts top). Then next level. Fun: big heart rain on clear — signature-ish.
**Game over:** overlay DOM: "GAME OVER", final score, level reached, best combo? and "click or press a key to play again". Restart resets score, lives, level 1.
Hmm — also idle attract: after 20s of gameover maybe return to demo? Not needed.
**Title:** "BURGER BREAKOUT" — maybe better: "HEARTBREAKER DINER"? The user asked breakout with burger paddle and heart explosions — name it something memorable: "HEARTBREAK — a breakout at the burger joint"? I'll go with title "HEARTBREAKER" and subtitle "a burger-flavored breakout". Or "BURGER BREAKOUT". Title: "HEARTBREAKER BURGER"? Keep "HEARTBREAKER" big with small "BURGER BREAKOUT" tag. Actually clarity for judges: "BURGER BREAKOUT" main, tagline "break the burger · collect the love". Let me do:
- Title: "BURGER BREAKOUT"
- Tagline: "SMASH THE STACK · SPREAD THE LOVE"
HUD: SCORE 000000 | LEVEL 1 | hearts | sound btn.
**Fonts:** Title: `font-family: Impact, 'Arial Black', 'Franklin Gothic Bold', sans-serif;` with -webkit-text-stroke: maybe paint-order not applicable to text-stroke... -webkit-text-stroke draws centered stroke over fill; use with larger stroke and text-shadow offset for chunky look. Alternative: use multiple text-shadows for outline. I'll use -webkit-text-stroke: 6px ink + text-shadow: 4px 4px 0 ink — need stroke behind fill: Chrome supports `paint-order: stroke fill` for text? paint-order works on SVG text and canvas... For HTML text, Chrome supports paint-order for text with webkit-text-stroke? Yes — Chrome supports `paint-order: stroke fill` on HTML text-stroke? Hmm, `paint-order` applies to SVG; for HTML canvas text it works with strokeText/fillText order controlled manually. For HTML elements, Chrome does support paint-order with -webkit-text-stroke since ~Chrome 123? I recall paint-order: stroke fill works in Chrome for -webkit-text-stroke on HTML text (it was added for web fonts outline effect). Yes, Chrome supports it. Latest Chrome target — fine. But to be safe I can also layer: use text-shadow stack for outline (8 shadows) — robust everywhere. I'll do text-shadow outline stack: simple and reliable:
```
text-shadow: 3px 0 0 ink, -3px 0 0 ink, 0 3px 0 ink, 0 -3px 0 ink, 2px 2px 0 ink, -2px 2px..., 6px 6px 0 rgba(ink,.25);
```
Good enough with bold Impact. Letter-spacing 0.04em.
Score numerals: monospace-ish bold: `font-family: 'Courier New'`? Digits with Impact too, letter-spaced. Fine — use Impact with tabular feel; or `font-variant-numeric: tabular-nums` with system bold. Use 'Trebuchet MS' bold for HUD labels? I'd rather: labels small caps in a humanist bold (Verdana/Trebuchet), numbers big Impact. OK.
**Canvas text** (popups, banners): font: `900 28px 'Arial Black', sans-serif` etc.
Now, "no gradient" rule: I'm using flat colors + hard offset shadows — the flat sticker aesthetic. For the ball I'd like a glossy highlight — that's a white ellipse, flat. Fine, no gradients needed at all. Maybe tiny radial for ball shine — I'll use flat white ellipse 40% alpha. Good, zero gradients. Actually background dot pattern uses radial-gradient function in CSS but renders as flat dots — visually not a gradient. I think it's within spirit (a pattern). I'll keep dots extremely subtle. Or use repeating checker? I'll keep polka dots via radial-gradient technique — it's the standard way to make dots in CSS. Fine.
**Let me now write all drawing code carefully.**
Burger paddle draw (w=112, h=36):
```
function drawBurger(g, pad){
const cx = pad.x, yTop = pad.y; // pad.x = center x
const w = pad.w * (1 + squash*0.12), anchored bottom at pad.y+pad.h.
```
I'll implement with ctx.save(); translate(cx, pad.y + pad.h); scale(sx, sy); translate(-cx, -(pad.y+pad.h)); then draw in normal coords with bottom fixed.
Layers (h=36 total): let me define fractions:
- topBun: from y0 to y0+12: dome shape: path: moveTo(x-w/2, y0+12) ... use roundRect with radii [14,14,4,4]? ctx.roundRect(x-w/2, y0, w, 13, [10,10,5,5]) — roundRect with array radii supported in Chrome. Color `#EDAA62`, then bottom edge line `#D18A45` (rect h=3 at bottom rounded?) and sesame seeds: fixed positions [{-0.3w, +4, rot}, ...] small ellipses `#FBE7C6`.
- lettuce: y=12..19: wavy: draw path across width with scallops: I'll draw rect + circles: simpler: fillStyle green; roundRect(x-w/2-5, 13, w+10, 6, 3) then scallop circles of r=4 along bottom edge? Wavy lettuce look: draw a series of arcs. Implementation:
```
g.beginPath();
g.moveTo(lx, ly);
for scallops along bottom: arcs bulging down
```
Simple approach: fill rounded rect base + draw 6 semicircles (r) protruding below it in same green → ruffled lettuce.
- cheese: y=17..24: yellow roundRect(x-w/2-2, 18, w+4, 6, 3) plus one drip: small roundRect at fixed x hanging to y=29. Color `#FFC233`, highlight top `#FFD966` thin.
- patty: y=22..31: roundRect(x-w/2, 23, w, 8, 4) color `#7A4A26` with speckles `#5E3618` dots fixed.
- bottomBun: y=30..36: roundRect(x-w/2+3, 31, w-6, 6, [3,3,6,6]) color `#E2955A`.
Heights: total ~36. Order overlaps slightly (ingredients overlap like real burger).
Sesame seeds positions (relative fractions of w): (-0.32,0.32),( -0.1,0.18),(0.12,0.34),(0.3,0.2),(0,-0.02)... place within dome. Each seed: ellipse rotated.
I'll precompute seed offsets array and speckle offsets.
**Brick drawing:** function per type with (b.x, b.y, b.w, b.h). Prerender each brick type to offscreen sprite once (per type) and drawImage — much faster and consistent. Sizes fixed per type. Sprites at DPR resolution for crispness: make sprite canvas at brickW*dpr × brickH*dpr, draw scaled. Since all bricks same size per type, easy. Patty damaged state → second sprite variant with crack. So sprites map: type → [spriteFull, spriteDamaged?].
drawBrickSprite(type, damaged):
- bun: base roundRect r=6 `#EDAA62`; bottom shading strip `#D18A45` (roundRect clipped bottom half height 7 at bottom, same radius); seeds: 3 ellipses `#FBE7C6` with darker stroke? plain fill; small highlight top-left `rgba(255,255,255,.25)` roundRect thin.
- lettuce: base `#7FC94E` roundRect; ruffle: bottom edge semicircles in same color extending 4px below? For tiling across bricks, ruffles per brick: draw 3 scallop arcs at bottom (arcs of circles r=7) same fill → looks ruffled; top darker line `#5FA83A`; vein arcs lighter `#A5DE79`.
- tomato: base `#E8483A` roundRect; inner roundRect `#F06A57` inset 4 (top-left highlight feel); seeds? little darker dots `#C33327` 3-4.
- cheese: `#FFC233` roundRect; drip at bottom center-left: roundRect narrow hanging 5px, same color; highlight strip `#FFD966` along top; tiny holes darker `#E8A81C`? Swiss holes cute: 2 circles `#E8A81C`.
- patty: `#7A4A26` roundRect; speckles darker; edge highlight top `#8F5B31`; damaged: add crack: jagged polyline `#4A2A18` + missing chip: small cream circle at edge? Crack lines suffice, plus slightly darker tint overlay `rgba(0,0,0,0.12)`.
- bunbottom: `#E2955A` roundRect r=[6,6,8,8]; top highlight `#EDAA62` strip; sesame? bottom bun no seeds; add slight darker bottom.
Each brick also gets a subtle drop shadow drawn under (flat offset 0,3 rgba(74,42,24,0.18)) — draw shadow first as roundRect offset. Draw via sprite including shadow? Shadow drawn separately per brick position fine (drawImage of shadow sprite). Simpler: bake shadow into sprite (sprite canvas taller by 3px). Let me bake: sprite height h+4, draw shadow roundRect at (0,3) then brick at 0,0. Then drawImage at (b.x, b.y).
**Heart sprite:** size 48px canvas: path scaled s=20 centered; fill color; plus small white ellipse highlight at (-6,-8) alpha .7; plus darker bottom? Keep flat + highlight. Also outline? Flat with subtle darker stroke (rgba(0,0,0,0.12))? Hearts pop against cream if colored `#FF5A7E`, `#F43F5E`, `#E8432F`, `#FF8FA3`, `#FFB3C1`, `#D6336C`. Add white shine dot. Prerender per color.
Sparkles: small 4-point star drawn as path, color white/gold — or reuse tiny hearts in gold `#FFD966`. I'll include 2 gold hearts in mix + tiny white circles as confetti dots.
**Falling power heart:** bigger heart sprite (32px) with white thick outline and pulsing scale; maybe draw outline by drawing sprite onto bigger... prerender outlined variant: draw heart path scaled up stroke white lineWidth 5 then fill. Also small wings? overkill.
**Trail:** ball positions history each frame push {x,y}; keep 10; draw circles radius decreasing alpha 0.25→0, color red.
**Ball:** red circle `#E8432F`, darker ring stroke `#C33327` lw2, white highlight ellipse offset (-3,-3). Also slight squash on bounce? skip.
**Now AI demo must look good in screenshot:** Ball red, burger tan, hearts pink flying, bricks colorful — yes vibrant.
**Ambient background:** floating faint hearts in bg? The wall + action enough. Add subtle: no.
**Combo indicator:** near top-right in canvas or DOM? In-canvas text popup on each break: "+25" and if combo≥3 "COMBO xN" small. Also maybe streak flame? skip.
**Lives display DOM:** inline SVG hearts generated by JS (update innerHTML when lives change). Empty hearts gray outline for lost.
SVG heart path (24x24): `M12 21s-7.5-4.9-10-9.6C.6 8.6 2.3 5 5.7 5c2 0 3.4 1.1 4.3 2.6C11 6.1 12.4 5 14.4 5 17.8 5 19.5 8.6 21 11.4 18.4 16.1 12 21 12 21z` — I'll write my own simple path: use path from common icon sets (memory): `M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z` — the classic material heart. Use that.
Speaker icons: simple polygon + waves paths, hand-write:
- on: `M4 9v6h4l5 4V5L8 9H4z` plus arcs `M16 8a5 5 0 0 1 0 8` stroke.
- muted: same polygon + X lines.
**Pause:** key P toggles; overlay "PAUSED". Also auto-pause on blur (only in play mode). Keep simple: P toggles, show DOM overlay.
**Now writing code.** Structure:
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Burger Breakout — Heartbreaker</title>
<style> ... </style>
</head>
<body>
<main class="cabinet">
<header class="top">
<h1>BURGER<span> </span>_BREAKOUT</h1>
<p class="tag">SMASH THE STACK · SPREAD THE LOVE</p>
</header>
<div class="hud">
<div class="stat"><span class="lbl">SCORE</span><span id="score" class="val">0</span></div>
<div class="stat"><span class="lbl">LEVEL</span><span id="level" class="val">1</span></div>
<div class="stat"><span class="lbl">COMBO</span><span id="combo" class="val">×0</span></div>
<div class="lives" id="lives"></div>
<button id="mute" aria-label="toggle sound">svg</button>
</div>
<div class="stage">
<canvas id="game"></canvas>
<div id="overlay" class="overlay">
<div class="panel">
<h2 id="ovTitle">HEARTBREAKER</h2>
<p id="ovText">...</p>
<p class="blink">CLICK OR PRESS ANY KEY TO START</p>
</div>
</div>
</div>
<footer class="hints">
<span>MOUSE / ← → MOVE</span><span>SPACE LAUNCH</span><span>P PAUSE</span>
</footer>
</main>
<script> ... </script>
</body>
</html>
```
CSS details:
```css
:root{
--ink:#43210F?
}
```
Let me finalize palette:
- page bg: `#F7E7C9` with dots `#EFDDBA`
- ink: `#4A2A18`
- red: `#E8432F`
- cream card: `#FFFBF0`
- mustard: `#F5B32F`
Body: `display:grid; place-items:center; min-height:100vh; padding:18px;` background dots:
```css
background-color:#F6E3C4;
background-image: radial-gradient(#EDD9B4 2.4px, transparent 2.6px);
background-size: 26px 26px;
```
Hmm radial-gradient for dots — acceptable pattern technique. Also add vignette? no.
Cabinet: `background:var(--cream); border:5px solid var(--ink); border-radius:18px; box-shadow: 10px 12px 0 rgba(74,42,24,.18); padding:14px 16px 12px; width:min(96vw, 940px);`
Title: Impact, color red with ink outline via text-shadow stack + hard offset shadow; letter-spacing: .02em; size clamp(28px, 5vw, 44px). Add small heart SVG between words? Title: "BURGER ♥ BREAKOUT" — no emoji; inline SVG heart in mustard or red. Cute: BURGER [heart] BREAKOUT with the heart slightly rotated. Yes.
Tag: `letter-spacing:.28em; font:700 11px Verdana; color: #8A6A4B`.
HUD: flex row, gap, background `#F3E2C4`? Let me make HUD strip: `border:3px solid var(--ink); border-radius:12px; background:#FFF3DA; padding:8px 14px; display:flex; align-items:center; gap:18px;` labels small, values Impact 22px ink. Combo value red when >1 (class). Lives: svg hearts 20px, gap 4. Mute button: circle border ink, transparent bg, hover translate.
Stage: `position:relative; margin-top:12px; border:5px solid var(--ink); border-radius:14px; overflow:hidden; background:#FFF9EC;` canvas display:block width:100%.
Overlay: absolute inset 0, `display:flex; align-items:center; justify-content:center; background: rgba(255,249,236,.72); backdrop-filter: blur(2px)?` maybe not needed. Panel: cream bg, ink border 4px, radius 16, padding 26px 34px, shadow offset flat. blink text: animation blink 1.2s steps.
Hints footer: flex, gap, small letterspaced, color #8A6A4B, margin-top 10px, justify center; separators via borders or "·".
Responsive: at 375px: cabinet width 96vw≈360; hud wraps (flex-wrap); title smaller via clamp; canvas scales (aspect ratio via width:100%; height auto — set canvas CSS `width:100%; height:auto;` intrinsic ratio from attributes? Canvas element with width/height attrs scales; but we set style width 100%, need aspect: since canvas has intrinsic size from attributes, `height:auto` keeps ratio. But we set canvas.width = W*dpr — attribute changes ratio? dpr scales both, ratio preserved. good.)
Overlay text sizes clamp.
**JS full implementation.** Let me write it carefully now, watching for bugs.
```js
'use strict';
const W = 900, H = 600;
const cv = document.getElementById('game');
const ctx = cv.getContext('2d');
const DPR = Math.min(2, window.devicePixelRatio||1);
cv.width = W*DPR; cv.height = H*DPR;
ctx.setTransform(DPR,0,0,DPR,0,0); // wait, need to reset each frame? set once, then ctx.save/restore preserve. I'll call setTransform at frame start.
```
In render: `ctx.setTransform(DPR,0,0,DPR,0,0);` then apply shake translate.
roundRect helper: use native ctx.roundRect (Chrome 99+). I'll add a small fallback? Chrome latest — fine, but I'll add tiny polyfill guard just in case? Latest Chrome only — skip polyfill, but a fallback is cheap:
```js
if(!CanvasRenderingContext2D.prototype.roundRect){ ... }
```
I'll include a compact polyfill for safety.
Game constants:
```js
const WALL = 14; // inner wall thickness visual
const PADDLE_Y = H - 74;
```
Let me define: checker floor occupies bottom 22px: from H-22 to H. Paddle bottom around H-40. Paddle y (top) = H-72, h=34 → bottom at H-38. Ball lost when y > H - 10 (into floor). Floor visual: checkers.
Bricks: cols=10, top margin 52, side margin 46, gap 8, rows config 6. brickW = (W - 46*2 - (10-1)*8)/10 = (900-92-72)/10 = 73.6. h=27, vgap 9 → row step 36. Rows y: 52, 88, 124, 160, 196, 232 → bottom of last row 259. Paddle top 528. Space between 269px for play. Good.
Actually let me widen slightly: side margin 40 → (900-80-72)/10 = 74.8. fine.
Row types:
```js
const ROWS = [
{t:'bun', hp:1},
{t:'lettuce', hp:1},
{t:'tomato', hp:1},
{t:'cheese', hp:1},
{t:'patty', hp:2},
{t:'bun', hp:1}, // bottom bun
];
```
Bottom bun different type key 'bun2' for color variance.
Level builder: for level>1 maybe reverse order or shuffle? Keep same layout; increase ball speed. Could also randomize row order per level for variety: shuffle rows except keep visual stack? Shuffling breaks burger metaphor. Keep same; speed+.
**State:**
```js
const S = {
mode:'demo', // demo | play | over | clear | paused(flag separate)
score:0, level:1, lives:3, combo:0, bestCombo:0,
bricks:[], balls:[], // single ball actually; but keep array for future multiball? single simpler
particles:[], popups:[], powerups:[], sparkles? merge into particles with kind
shake:0, squash:0, t:0, clearTimer:0, serveTimer:0,
attached:true, ...
};
```
I'll keep single ball object.
Paddle: {x, w, targetX(for ai), h:34}.
**Ball:**
```js
ball = {x,y,vx,vy,r:9,speed}
```
speed property used to normalize after reflections (keep constant magnitude): after computing vx,vy changes, renormalize to speed.
Serve:
```js
function serveBall(){
attached=false;
const a = (Math.random()*0.5 - 0.25) + (ball side?)
angle = -PI/2 + rand(-0.35,0.35)
ball.vx = Math.cos(angle)*sp; ball.vy=Math.sin(angle)*sp; // -PI/2 up
}
```
**Update loop:**
```js
let last = performance.now();
function frame(now){
const dt = Math.min(0.033, (now-last)/1000); last=now;
S.t += dt;
if(!paused) update(dt);
render();
requestAnimationFrame(frame);
}
```
update(dt):
- update paddle:
```js
if(S.mode==='demo'){ aiControl(dt); } else { playerControl from input state }
```
input: keys object; mouse x stored `input.px` (null until mousemove). If mouse moved recently, use mouse; arrows override. Implementation:
```js
let move=0;
if(keys.ArrowLeft||keys.a) move-=1;
if(keys.ArrowRight||keys.d) move+=1;
if(move) { pad.x += move*520*dt; input.mouseActive=false? }
else if(mouseX!=null) pad.x = lerp(pad.x, mouseX, 1-Math.pow(0.001,dt)) // fast follow
```
Simplify: if move keys pressed → keyboard velocity 560px/s; else if pointer target set → pad.x += clamp(target - pad.x, -maxstep, maxstep) with max = 1400*dt? Direct follow with pointer feels best: pad.x = lerp to pointer with strong factor. Use: pad.x += (target - pad.x)*Math.min(1, 20*dt).
Clamp pad.x to [WALL+pad.w/2, W-WALL-pad.w/2].
AI:
```js
let target;
if(attached) target = W/2 + Math.sin(S.t*0.5)*120; // drift while waiting
else {
// predict landing x when moving down
if(ball.vy>0){
// simple prediction: project ball to paddle y with wall bounces
target = predictX();
} else target = ball.x*0.4 + W/2*0.6? // hover between ball and center
target += Math.sin(S.t*2.3)*10; // human wobble
}
pad.x += clamp(target-pad.x, -aiSpeed*dt, aiSpeed*dt); // aiSpeed ~ 600
```
predictX: simulate: time to reach paddle plane: ty = (pad.y - ball.y)/vy; px = ball.x + vx*ty; fold into [WALL+r, W-WALL-r] with reflection:
```js
let span = (W-2*WALL-2*r);
let p = (px - (WALL+r)) % (2*span); if(p<0)p+=2*span; if(p>span) p=2*span-p; px = WALL+r+p;
```
Good.
aiSpeed 620 (strong but capped so occasionally misses on fast balls → still demo loses rarely; demo has infinite lives, fine).
- serve logic: if attached: if demo → S.serveTimer countdown then serve. If play → ball follows paddle top; launch on input flag (space/click sets wantLaunch).
- ball physics with substeps:
```js
const steps = Math.max(1, Math.ceil((ball.speed*dt)/7));
for(i<steps){ moveBall(dt/steps); if(breakLoop) break; }
```
moveBall: x+=vx*dt etc; wall collisions (left/right/top with WALL inset? walls at x=WALL, W-WALL, top WALL? Top wall maybe below a header zone — canvas has DOM hud so top wall at y=WALL (14). Hmm bricks start at 52; ball bounces at y=14 top. ok).
On wall hit: sound tick, tiny spark particles.
- paddle collision: if ball.vy>0 and circle overlaps burger rect (pad.x±w/2, pad.y..pad.y+h) plus some tolerance: reflect:
```js
const rel = clamp((ball.x - pad.x)/(pad.w/2), -1, 1);
const ang = rel * 1.05; // max 60deg from vertical
const sp = ball.speed;
ball.vx = Math.sin(ang)*sp; ball.vy = -Math.cos(ang)*sp;
ball.y = pad.y - ball.r - 0.1;
squash=1; combo reset? Combo resets on paddle touch — yes classic.
seed particles: spawn 3 sesame particles.
sound: thock.
```
Also give slight influence from paddle movement? skip.
- brick collisions as described. On hit: b.hp--; sound; if hp<=0: break → remove, score += 25*mult? scoring: base 25 + level*5, combo increments BEFORE calc? Let me: combo++ on break; mult = combo; score += 25 + (combo-1)*10. popup "+"+pts (and "×combo" if ≥2). heartsBurst(b). shake += 3. maybe powerup. brickCount--.
If brickCount==0 → levelClear(): mode stays 'play'/'demo' but state clear timer 1.6s, heart rain, then rebuild.
Also every 12 bricks broken → ball.speed += 0.15 gradually? ball.speed = min(max, base + broken*0.02). Fine.
- powerups update: fall, sway; catch test vs paddle rect → lives=min(5,lives+1), updateHUD, popup "+1 LIFE" pink, sparkle burst, sound chime.
- particles update.
- popups update.
- squash decay: squash = max(0, squash - dt*5); shake *= Math.pow(0.0018? use exp decay: shake *= Math.exp(-8*dt)) and clamp small→0.
- lose ball: if ball.y - r > H - 6:
- if mode demo: lives not decremented; small sad poof, respawn attached, serveTimer=1.
- else lives--; combo=0; shake=6; sound sad; if lives<=0 → gameOver() else attached=true, serveTimer? in play attached until input.
Actually in play after losing life, require click/space to serve again (attached). Good.
sad gray hearts burst at loss point + gray poof on floor.
- gameover(): mode='over'; overlay shows GAME OVER, score; click/key → restart to play mode fresh.
- startGame(): from demo: reset score/lives/level, buildLevel, attached, mode='play', overlay hidden, show "click/space to launch" hint — where? small canvas text near paddle: "CLICK OR SPACE TO LAUNCH" pulsing while attached in play mode. Draw in canvas.
**Level clear flow:** when brickCount hits 0: state clear: clearTimer=1.7; heart rain: spawn 60 hearts from top with random fall; big banner text "STACK CLEARED!" and "LEVEL N+1". During clear, ball hidden/attached. After timer: level++, buildLevel(), attached=true (demo auto-serve 0.8s), play continues. Score bonus +500*level? add popup.
Also increase ball base speed: ball.speed = baseSpeed(level) = 340 + level*30 (px/s). Wait — units: I'll use px/s velocities with dt. Speeds: base 330 px/s + level*25, cap 560. Hmm typical breakout ~ 300-400 px/s on 900px width. Let me set base 340, cap 620. Paddle keyboard 620px/s. Ball r 9. These feel reasonable.
Actually let me double check playability: distance paddle(528) to bricks(259) ≈ 270px; at 340px/s vertical-ish, ~0.8s per traverse. Fine.
**Rendering order:**
1. clear + bg: fill `#FFF9EC`; side/top walls: draw ink frame? The stage div has CSS border 5px ink — so canvas edges already framed by CSS border. Inside canvas: draw checker floor bottom; maybe subtle dotted pattern? skip. Draw faint horizontal guide? skip. Keep clean.
2. apply shake translate.
3. bricks (sprites).
4. powerup hearts.
5. particles behind or above ball? hearts above bricks, below burger? Draw order: bricks → ball trail → ball → burger → powerups → particles (hearts on top so they fly over everything) → popups.
6. banners (clear/serve hint/combo flame?).
7. reset transform.
Checker floor: squares 20px: for x step 20: alternate colors by (x/20 + row) parity; rows 1? height 20 → one row of 20px checkers. Colors: `#F1C7B8`? Let me do two-tone: `#F9DCD2` & `#FFF` with top border 3px `#4A2A18`? Actually punchier: `#E8432F`/`#FFF9EC` alternating, with slight darkening line at top: ink 3px. Red-white checkers = diner. But red floor may distract from red ball falling... it's the "out" zone; visual cue good. Keep alpha 0.9? Solid fine. Also draw it ABOVE shake or below? Below shake region—include in shake, whatever.
Draw walls: none needed beyond CSS border. But bricks start x=40 — side margins plain cream, fine.
Also add subtle "kitchen tiles" bg? Keep cream.
**Banners:** in-canvas: function bigText(txt, sub). During clear: banner. During attached (play): pulsing "CLICK OR PRESS SPACE TO LAUNCH" above paddle in small chunky font. During demo: the DOM overlay covers with start message. Good.
**Popups:** {x,y,txt,life(1),color, size}. Rise 40px, fade.
**Particles:** two kinds: 'heart' (sprite color idx, size, rot, rotV, vx, vy, flutter phase, life, gravity 700, drag) and 'dot' (tiny circles confetti) and 'seed' (sesame — small tan ellipse falling). One array with type field.
heartsBurst(x,y,n=16+combo?): count = 14 + min(10, combo*2). colors random from palette; sizes 5-11 (sprite scaled: draw sprite of 48px at size*2? drawImage w = size*2.2). velocities: angle random full circle but upward bias: vy = -(60..320), vx = ±(40..260). gravity 780. drag 0.9^? use vx *= Math.pow(0.5, dt*0.8)? mild air drag on x. rotV ±6 rad/s. life 0.9±0.4. flutter: x += sin(t*freq+phase)*amp*dt? keep simple: rotational wobble via rot.
Also 3 white sparkle dots + 1 big heart (size 14) center sometimes.
Scale animation: scale = popIn(life progress) — s = min(1, age/0.08) with overshoot: use easeOutBack-ish: `k = age<0.12 ? easeOutBack(age/0.12) : 1`; end shrink: last 25%: s *= (life remaining/0.25). alpha similar.
Catch sparkle burst: gold hearts + white dots.
**Sesame seeds on paddle hit:** 3 particles type 'seed': tan small ellipses, gravity, short life.
**Lose poof:** 10 gray hearts falling with gravity from ball position, plus 6 gray dots. gray color sprite '#B9A89B'? make gray heart sprite '#B7A99A'. Also when catching? no.
Heart rain (level clear): particles type 'heart' spawned across top with vy 60-140, vx ±40, gravity small 120, life 2.2, sizes 6-12 — gentle rain.
**Sounds:**
```js
let AC=null, muted=false;
function ac(){ if(!AC){ AC = new (window.AudioContext||window.webkitAudioContext)(); } if(AC.state==='suspended') AC.resume(); return AC; }
function tone({f=440,f2,dur=0.08,type='square',vol=0.12,delay=0}){
if(muted) return; try{ const a=ac(); ... }catch(e){}
}
```
Create osc + gain, freq ramp f→f2, gain envelope exp decay.
Sound events:
- wall: tone f=220,dur=.04,vol=.05,type='square'
- paddle: f=150,f2=90? thock: type 'triangle' f 180→120 dur .07 vol .18
- brickHit (damage): f=160 square .05
- brickBreak: f=520+combo*30 (cap 1200), type 'square', dur .09, vol .12; plus a second quick tone 1.5× freq delay .03? adds pop.
- lose: f=300→120 saw .5 vol .15
- powerup catch: two tones 660, 990 triangle.
- levelclear: arpeggio [523,659,784,1047] delays.
- gameover: [330,262,196] slow.
Demo mode: sounds only if AC exists (created on gesture). On start click, ac() resumes — then play sounds from then. During demo before any click, `muted` effectively since AC null → create() would fail? Creating AudioContext without gesture is allowed but starts suspended; resume() without gesture fails (returns rejected promise — unhandled rejection!). Guard: only create AC on user gesture events (click/keydown). So: in tone(), if !AC return (silent). And in input handlers call initAudio().
Handle promise: `AC.resume().catch(()=>{})`.
**HUD updates:** score element textContent; lives: render hearts SVGs (filled = lives, up to max 5? show current count as filled, but lost ones as outlines up to 3 baseline). Show max(3,lives) slots? Simpler: render `lives` filled hearts + if lives<3 (base) render (3-lives) outline hearts... but lives can exceed 3 via powerups (up to 5) — then show 5 filled. Slots = max(3, lives). Render: for i<slots: i<lives ? filled : outline. Good.
Combo display: "×N"; add class 'hot' when ≥3 (red + pulse animation).
**Overlay content management:** function showOverlay(title, html, blinkText). States:
- demo: title "BURGER BREAKOUT"? Panel: big "HEARTBREAKER" no... Let me design:
- h2: "READY TO MAKE SANDWICHES?" hmm. Copy: title: "HEARTBREAKER!"? Panel content:
- small label "A BREAKOUT AT THE BURGER JOINT"
- big "HEARTBREAKER" hmm but title already at top of page. Overlay panel: big "PLAY" button-ish text: "CLICK OR PRESS ANY KEY" blinking, plus small hints: "MOVE: MOUSE or ←→ · LAUNCH: SPACE". And note "demo is playing — take over anytime". Keep panel light so demo visible behind: translucent bg. Panel small, centered bottom? Center is fine with rgba backdrop 0.55.
- over: "GAME OVER", "SCORE 12345 · LEVEL 3 · BEST COMBO ×7", blink "CLICK OR PRESS ANY KEY TO PLAY AGAIN".
- paused: "PAUSED", "PRESS P TO RESUME".
Clicking overlay triggers action. Any key too (but P during pause also handled — ensure pause toggle works while overlay shows paused; the global keydown handles: if paused & key 'p' → resume else if paused ignore others? If paused and user presses other key, do nothing. fine.)
Note requirement: "Games must start in a self-playing demo mode ... behind a 'Click or press a key to start' message". ✓.
**Edge cases:**
- Prevent space scrolling page: preventDefault on keydown for handled keys.
- Touch: touchmove → pointer x; touchstart → if overlay start / if attached launch. Use pointer events (pointermove/pointerdown) unify mouse+touch. pointerdown on canvas: if mode play & attached → launch. If overlay visible → start (overlay covers canvas so its click handler fires; also pointer-events). Overlay has its own click. But keydown target: window.
- Also prevent context menu? skip.
**Serving after start:** startGame(): buildLevel(1), score=0, lives=3, combo=0, attached=true, mode='play'. Hint text drawn in canvas. Launch: on pointerdown anywhere on stage or Space.
**Demo start on load:** buildLevel(1), mode='demo', attached=true, serveTimer=0.9 → AI serves automatically.
**Respawn after miss (play):** attached=true; hint shows.
**Patty hp 2 & damaged sprite** ✓.
**Score popup font:** '900 20px "Arial Black",sans-serif' with ink stroke? Canvas text with strokeText ink 4 then fillText cream/white. For popups fill color param.
**Combo text near burger:** when combo≥2 draw at top center? Let me draw combo as popup on each break: "+pts" and if combo≥2 second line "×c". Also HUD combo updates. Enough.
**Check performance:** 60 bricks sprites drawImage, particles up to ~250 drawImage rotated — fine.
**Now — writing the actual code. I'll be meticulous with syntax.**
Let me draft key functions fully:
```js
function heartPath(g, s){
g.beginPath();
g.moveTo(0, .5*s);
g.bezierCurveTo(-.62*s, .05*s, -.62*s, -.5*s, -.28*s, -.5*s);
g.bezierCurveTo(-.08*s, -.5*s, 0, -.34*s, 0, -.18*s);
g.bezierCurveTo(0, -.34*s, .08*s, -.5*s, .28*s, -.5*s);
g.bezierCurveTo(.62*s, -.5*s, .62*s, .05*s, 0, .5*s);
g.closePath();
}
```
Check shape: bottom tip (0,.5s). Left side control (-.62, .05) and (-.62,-.5) end (-.28,-.5): curve goes out to x≈-0.55 then to lobe top. Then notch: from (-.28,-.5) to (0,-.18) with controls (-.08,-.5),(0,-.34): dips to center. Mirror. Looks like a heart.
Heart sprite:
```js
function makeHeartSprite(color, outline){
const s=48, c=document.createElement('canvas'); c.width=c.height=s*2; // 96 for hi-res
const g=c.getContext('2d'); g.translate(s,s);
heartPath(g, 62); // radius-ish 62 → spans 124 > 96? too big.
```
Let me compute: heart extends y from -0.5s to 0.5s → total height s; width ~1.15s. For canvas 96, want heart ~ 80px wide: 1.15s=80 → s≈70? then height 70 fits. x extent ±0.62*70=±43 fits in 48 half-width. ok s=66: width ±41, height 66, fits 96. Also I want a white outline variant for powerups: stroke lineWidth 8 before fill? Stroke path with lineWidth 8 strokeStyle white then fill color — stroke centered expands 4px beyond: x extent 45 ok.
Highlight: small white ellipse at (-s*0.22, -s*0.22) w s*0.16 rotated -30°, alpha 0.85? Fill 'rgba(255,255,255,.55)'.
Return {c, size:96} — when drawing at particle size p (meaning approx half-width), drawImage(c, x - p, y - p, 2p, 2p) where heart width ≈ 2p*(1.15*66/96≈0.79)... eh just treat sprite as 2p box; heart inside ~0.8 of box. Fine visually.
Heart colors:
```js
const HEART_COLORS = ['#F43F5E','#FF5A7E','#E8432F','#FF8FA3','#FB7185','#D6336C','#FFB3C1'];
const GRAY = '#B9AA9C';
sprites: map color→canvas.
```
Brick sprite maker:
```js
const BRICK_W=74.8, BRICK_H=27, GAP=8, TOP=52, SIDE=40, COLS=10;
```
Compute: (900 - 80 - 72)/10 = 74.8 ✓.
```js
function makeBrickSprite(type, damaged){
const pad=3; // shadow pad bottom
const c=document.createElement('canvas');
c.width=Math.ceil(BRICK_W*DPR); c.height=Math.ceil((BRICK_H+pad)*DPR);
const g=c.getContext('2d'); g.scale(DPR,DPR);
const w=BRICK_W, h=BRICK_H;
// shadow
g.fillStyle='rgba(74,42,24,.18)';
rr(g,0,3,w,h,7); g.fill();
... per type
return c;
}
```
rr helper wraps roundRect:
```js
function rr(g,x,y,w,h,r){ g.beginPath(); if(g.roundRect){g.roundRect(x,y,w,h,r);} else { /* manual */ } }
```
I'll write a manual roundRect always (safer, small code):
```js
function rr(g,x,y,w,h,r){
if(typeof r==='number') r={tl:r,tr:r,br:r,bl:r};
...
}
```
Simplify: support number or array [tl,tr,br,bl]:
```js
function rr(g,x,y,w,h,r){
let [a,b,c2,d] = Array.isArray(r)? r : [r,r,r,r];
const m = Math.min(w/2,h/2);
a=Math.min(a,m); b=Math.min(b,m); c2=Math.min(c2,m); d=Math.min(d,m);
g.beginPath();
g.moveTo(x+a,y);
g.lineTo(x+w-b,y); g.arcTo(x+w,y,x+w,y+b,b);
g.lineTo(x+w,y+h-c2); g.arcTo(x+w,y+h,x+w-c2,y+h,c2);
g.lineTo(x+d,y+h); g.arcTo(x,y+h,x,y+h-d,d);
g.lineTo(x,y+a); g.arcTo(x,y,x+a,y,a);
g.closePath();
}
```
Wait arcTo(x+w, y, x+w, y+b, b): corner top-right. Order: from (x+w-b, y) arcTo control (x+w,y) end (x+w,y+b) radius b. ✓. Top-left: from (x, y+a) arcTo (x,y) end (x+a,y) radius a ✓.
Brick type draws:
bun:
```js
g.fillStyle='#EDAA62'; rr(g,0,0,w,h,7); g.fill();
g.fillStyle='#D18A45'; rr(g,0,h-8,w,8,[0,0,7,7]?)
```
Hmm bottom strip inside rounded rect: clip approach: save, clip rr full, fillRect bottom strip. Easier:
```js
g.save(); rr(g,0,0,w,h,7); g.clip();
g.fillStyle='#D18A45'; g.fillRect(0,h-8,w,8);
g.fillStyle='rgba(255,255,255,.28)'; g.fillRect(0,3,w,4);
g.restore();
// seeds
g.fillStyle='#FBE7C6';
seed(g, w*0.22, h*0.42, -0.5); seed(g, w*0.5, h*0.32, 0.2); seed(g, w*0.78, h*0.45, 0.6);
function seed(g,x,y,rot){ g.save(); g.translate(x,y); g.rotate(rot); g.beginPath(); g.ellipse(0,0,5,2.6,0,0,7); g.fill(); g.restore(); }
```
seed helper inside scope or global drawing helper.
lettuce:
```js
g.fillStyle='#7FC94E';
rr(g,0,0,w,h,7); g.fill();
// ruffles: scallops along bottom
g.save(); rr(g,0,0,w,h,7); g.clip();
g.fillStyle='#7FC94E';
for(let i=0;i<4;i++){ g.beginPath(); g.arc(w*(i+0.5)/4, h-2, 5.5, 0, Math.PI); g.fill(); }
// wait semicircle below y? arc at y=h-2 radius 5.5 drawn 0..PI = lower half → bulge below brick. But clip prevents below. Remove clip for scallops or draw after restore.
```
Better: base rounded rect h-6 tall at top? Lettuce layering: draw main rr(0,0,w,h-4,7); then scallop circles centered y=h-4 r=6 → bulge to h+2 (into shadow zone — but sprite canvas height h+3, ok slight). Then darker top line + veins:
```js
g.fillStyle='#5FA83A'; g.fillRect(2,3,w-4,4)?
veins: strokeStyle '#A5DE79', lw2, arcs.
```
Let me write concrete:
```js
// base
g.fillStyle='#7FC94E'; rr(g,0,2,w,h-6,8); g.fill();
// ruffle scallops (bottom)
g.beginPath();
for(let i=0;i<5;i++){ g.moveTo? }
for(let i=0;i<5;i++){ g.beginPath(); g.arc(w*(i+0.5)/5, h-4, 6, 0, Math.PI); g.fill(); }
// top highlight
g.fillStyle='#A5DE79'; rr(g,4,5,w-8,4,2); g.fill();
```
5 scallops of r6 across 74.8: spacing 15 — scallops overlap? arcs r6 spaced 15 → gaps between bulges. More: 6 scallops r 6.5 spaced 12.5. Use count 6.
tomato:
```js
g.fillStyle='#C33327'; rr(g,0,0,w,h,8); g.fill();
g.fillStyle='#E8483A'; rr(g,2,2,w-4,h-6,6); g.fill();
g.fillStyle='#F06A57'; rr(g,5,4,w-10,5,3); g.fill();
// seeds
g.fillStyle='#F9A08F'? small dots
```
Add 3 seed dots `#FBC0B0`.
cheese:
```js
g.fillStyle='#FFC233'; rr(g,0,1,w,h-3,6); g.fill();
// drip
rr(g, w*0.3, h-8, 10, 12, 4); g.fill(); // hangs below into pad zone; sprite height h+3 → 12px from h-8 → to h+4 exceeds canvas h+3 by 1. adjust: drip rr(g,w*0.3,h-9,10,11,4) → bottom h+2 ok.
// holes
g.fillStyle='#E8A81C'; circles (w*0.62,h*0.45,3.5),(w*0.75,h*0.6,2.5);
// top highlight
g.fillStyle='#FFD966'; g.fillRect? rr(g,4,4,w-8,4,2)
```
Wait drip same fillStyle as base — set before drip. Order: base+highlight+holes+then drip with base color.
patty:
```js
g.fillStyle='#7A4A26'; rr(g,0,0,w,h,7); g.fill();
g.fillStyle='#8F5B31'; rr(g,3,3,w-6,6,3); g.fill(); // grilled top highlight
g.fillStyle='#5E3618';
speckles: dots at fixed positions (w*0.15,h*0.62),(w*0.34,h*0.5),(w*0.52,h*0.68),(w*0.7,h*0.55),(w*0.86,h*0.66) r2-3
if(damaged){ crack: strokeStyle '#3F2410', lw 2.5, zigzag path from (w*0.2,h) up... plus g.fillStyle='rgba(0,0,0,.14)' overlay rr fill }
```
bunbottom:
```js
g.fillStyle='#E2955A'; rr(g,0,0,w,h,[7,7,10,10]); g.fill();
g.fillStyle='#EDAA62'; rr(g,4,2,w-8,5,3); g.fill(); // top edge lighter
g.fillStyle='#C97F44'; rr(g,4,h-6,w-8,4,2); g.fill();
```
Also each brick: outline stroke ink 2px? A thin dark outline makes sprites crisp sticker-like. Add: `g.strokeStyle='rgba(74,42,24,.9)'; lw 2; rr(...); g.stroke();` on main shape. Yes — outline everything (bricks, burger?) for cohesive sticker style. Burger drawn per-frame: outline its shapes too? The burger is composite; outline each layer lightly? Might look busy. Give burger an overall subtle ink outline on bun tops/bottom? Let me outline bun layers only (top dome, bottom bun) and keep inner ingredients clean — hmm. Simplest cohesive: stroke main silhouette shapes of each layer with 'rgba(74,42,24,.35)' lw 2. I'll add light outline to each layer of burger for definition.
Actually for the bricks, dark outline 2px is good. For the burger, thin 2px ink at 0.5 alpha around each layer.
**drawBurger implementation:**
```js
const SEEDS=[[-.32,.30,-.6],[-.13,.12,.25],[.08,.30,-.3],[.27,.14,.5],[-.02,.42? ...]];
```
Seeds on dome (top bun occupies top ~13px of 36): relative to bun rect: positions as fractions. I'll define in draw:
```js
function drawBurger(g){
const p=pad, w=p.w, x=p.x-w/2, y=p.y, h=p.h;
const sq=S.squash;
g.save();
g.translate(p.x, p.y+h);
g.scale(1+sq*0.10, 1-sq*0.22);
g.translate(-p.x, -(p.y+h));
// slight idle bob
const bob = Math.sin(S.t*3)*0.8;
g.translate(0,bob);
// shadow under burger? on floor: ellipse at bottom... paddle floats above floor; skip shadow or small: draw ellipse rgba under before transform? skip.
const L = (fx, fw, fy, fh, r, fill) => { g.fillStyle=fill; rr(g, x+ (w-fw)/2 *? }
```
Let me hand-place layers with absolute offsets (w=112 baseline; use fractions of w so resize powerups possible — no resize powerup, keep fixed):
- topBun: x0=x+2, w0=w-4, y+0, h 13, radii [9,9,4,4], fill #EDAA62; bottom shade: clip fill #D18A45 rect (y+9..13); seeds at (x+w*(...), y+4..8).
- lettuce: base rr(x-4, y+11, w+8, 7, 3) fill #7FC94E; scallops below: arcs r4.5 at y+18 spaced w/6; top lighter strip.
- tomato? add tomato layer red for color pop: rr(x-2, y+16, w+4, 6,3) fill #E8483A with lighter #F06A57 inner? This makes burger taller. Let me budget 36px:
- topBun y+0..13 (13)
- lettuce y+11..19 (8, overlapped 2)
- cheese y+17..24 (7, drip)
- patty y+22..31 (9)
- bottomBun y+29..36 (7)
Total visual height 36 ✓ (overlaps give stacked look.)
- cheese: rr(x-1, y+17, w+2, 7, 3) #FFC233 + drip at x+w*0.25: rr(w*0.25, y+20, 11, 9, 4)? bottom y+29 under patty top y+22 — drip should overlay patty: draw cheese BEFORE patty? Then drip hidden. Real cheese drips over patty below it — draw patty first, then cheese? Vertical order in stack (top→bottom): bun, lettuce, tomato, cheese, patty, bottomBun. Visually cheese sits above patty and drips DOWN over patty → draw patty first then cheese on top? But cheese also needs to appear between lettuce and patty... Draw order in code (painter): bottom-up? If I draw patty, then cheese over it, cheese covers patty top edge — that's exactly how it looks in cartoons. Order: bottomBun, patty, cheese(drip over patty), lettuce (over cheese top), tomato? tomato above lettuce below bun: draw lettuce then tomato then topBun last. Painter order: bottomBun → patty → cheese → lettuce → tomato → topBun.
Final layer coords:
- bottomBun: rr(x+6, y+29, w-12, 8, [3,3,5,5]) #E2955A; top edge lighter strip.
- patty: rr(x+1, y+22, w-2, 10, 5) #7A4A26; speckles.
- cheese: rr(x-2, y+17, w+4, 8, 4) #FFC233; drip rr(x+w*0.22, y+20, 12, 10, 5) same color (bottom y+30 over patty ✓); hole dot #E8A81C at (x+w*0.7, y+20).
- lettuce: rr(x-5, y+11, w+10, 8, 4) #7FC94E; scallop arcs r5 centers y+18; highlight #A5DE79 strip.
- tomato: rr(x+2, y+9, w-4, 7, 3) #E8483A with inner #F06A57? draw two: base then inner rr(x+5,y+11,w-10,3). Hmm tomato between lettuce(11) and bun... overlaps lettuce top 2px fine.
- topBun: rr(x+3, y, w-6, 14, [10,10,4,4]) #EDAA62; bottom shade clip rect y+10..14 #D18A45; highlight rr(x+8,y+2,w*0.4,4,2) rgba white .3; seeds 5.
Seeds on topBun (absolute): [(x+w*0.24,y+6),(x+w*0.42,y+4),(x+w*0.6,y+7),(x+w*0.76,y+5),(x+w*0.5,y+9)] rotations vary. Seed = ellipse 4.5×2.4 #FBE7C6.
Outline: after each layer rr fill, stroke 'rgba(74,42,24,.45)' lw2. For drip etc skip stroke.
Catch highlight: when powerup caught maybe burger glows? skip.
**Ball draw:**
```js
// trail
for i: alpha, radius
g.fillStyle='#E8432F'; arc fill;
g.lineWidth=2.5; g.strokeStyle='#B33326'; stroke;
g.fillStyle='rgba(255,255,255,.75)'; ellipse(x-3,y-3.5, 2.6, 1.8, -0.6) fill;
```
**Popups draw:** font `900 ${size}px 'Arial Black','Arial',sans-serif`; textAlign center; strokeStyle ink lw4 strokeText then fill color. Rise & fade.
**Serve hint (play attached):** text above paddle: pulsing alpha: "CLICK OR PRESS SPACE TO LAUNCH" font 800 15px Verdana? Use Arial Black 14px letterspaced manually? Canvas no letter-spacing (Chrome has ctx.letterSpacing! Chrome 99+ supports ctx.letterSpacing = '2px'). Use if available, harmless string assign otherwise ignored? Setting unknown property is just ignored (it's a property; assigning non-supported just no-op). OK.
**Clear banner:** big "STACK CLEARED!" center + "LEVEL {n} — TASTY!" sub; bg rounded panel translucent cream w/ ink border drawn in canvas: rr fill rgba(255,249,236,.92), stroke ink 4, then texts.
**Game over handled by DOM overlay.**
**Demo overlay panel** shows above canvas — demo visible around it (panel centered, backdrop subtle). Also small "DEMO" badge? The overlay panel has: eyebrow "WATCHING THE CHEF" hmm. Copy:
- small: "THE AUTO-CHEF IS PLAYING"
- blink: "CLICK OR PRESS ANY KEY TO START"
- small hints row.
That's clear.
**HUD combo:** show current combo, resets often. Also maybe show "HEARTS ×N" total hearts exploded counter! Cute stat: total hearts burst count, persists. HUD: ♥ count "HEARTS 128". That's the theme metric! HUD stats: SCORE, LEVEL, HEARTS (total), lives, mute. Combo shown in-canvas. Yes — "HEARTS" counter is delightful (counts every heart particle spawned? that grows fast — fun! each brick ~18 hearts → after full level ~1100). It's a charming stat. Keep it, big number red.
HUD layout: [SCORE] [HEARTS] [LEVEL] | lives hearts | mute. On mobile wraps.
**Now write ALL the code.** I'll be careful with every bracket. Let me write the final file mentally section by section.
Also remember: "no errors in console." Careful with:
- ctx.ellipse supported ✓.
- performance.now in rAF ✓.
- All element IDs referenced exist.
- Avoid AudioContext before gesture entirely (create only in pointerdown/keydown handler).
Keydown handler:
```js
window.addEventListener('keydown', e=>{
const k=e.key.toLowerCase();
if(['arrowleft','arrowright','a','d',' ','spacebar'].includes(k)) e.preventDefault();
keys[k]=true;
userGesture();
if(k==='m'){ toggleMute(); return; }
if(k==='p'){ if(S.mode==='play'||paused){ togglePause(); } return; }
if(overlayVisible){ // start/restart
if(paused) return; // only p resumes
startFromOverlay(); return;
}
if((k===' '||k==='enter') && S.mode==='play' && ball.attached) launch();
});
```
Hmm ordering: if paused, overlay shows "PAUSED"; pressing other keys should not restart game. So: if(paused){ if(k==='p') resume; return; }. Restructure:
```js
function onKey(e){
const k = e.key.toLowerCase();
if([' ','arrowleft','arrowright','arrowup','arrowdown','a','d'].includes(k)) e.preventDefault();
keys[k]=true;
initAudio();
if(k==='m'){ toggleMute(); return; }
if(k==='p'){ if(paused){ resume(); } else if(S.mode==='play'){ pause(); } return; }
if(paused) return;
if(S.mode==='demo' || S.mode==='over'){ startGame(); return; }
if(S.mode==='play' && ball.attached && (k===' '||k==='enter')) launch();
}
```
' ' key.toLowerCase() stays ' '. e.key for space is ' '. ok.
Pointer:
```js
cv? stage element handles pointermove: update pointerX in game coords.
window 'pointermove' → compute via canvas rect.
'pointerdown' on stage (includes overlay since inside stage):
initAudio();
if(paused) return;
if(S.mode==='demo'||S.mode==='over'){ startGame(); return; }
if(S.mode==='play' && ball.attached) launch();
```
Attach pointerdown to the stage div (covers overlay too). Overlay has pointer-events none? If overlay div covers, pointerdown on stage still receives (bubbles from overlay child). Just put listener on stage. ✓.
pointermove: only track when over stage; but page-wide fine:
```js
window.addEventListener('pointermove', e=>{
const r=cv.getBoundingClientRect();
pointerX = (e.clientX - r.left)/r.width * W;
});
```
If outside canvas, still maps — clamp later. OK. Touch: pointermove fires on touch drag ✓ (need touch-action: none on stage to prevent scroll while dragging: `touch-action:none` on stage).
Keyboard control: in update:
```js
let kb=0;
if(keys['arrowleft']||keys['a']) kb-=1;
if(keys['arrowright']||keys['d']) kb+=1;
if(kb!==0){ pad.x += kb*640*dt; lastInput='kb'; }
else if(pointerX!=null){ pad.x += (pointerX - pad.x)*Math.min(1, 14*dt); }
clamp pad.
```
This means mouse takes over whenever keys released — good.
**Pause implementation:** `paused` boolean; overlay shows paused panel; update skipped when paused (in frame check). Demo mode: no pause key effect (P only in play). Blur: if playing → pause automatically? Might annoy in iframe screenshot? Screenshot takes without interaction; blur unlikely. Add visibilitychange pause for play mode — safe. Actually in sandboxed iframe the page is visible; fine. I'll skip auto-pause to reduce bug surface. Hmm, "Something meaningful must be visible on first paint" — demo running ✓.
**Level clear while attached ball? N/A.**
**Attach ball to paddle each frame when attached:**
```js
ball.x = pad.x + pad.w*0.15; ball.y = pad.y - ball.r - 1;
```
Slight offset right like resting on burger.
**Launch:**
```js
function launch(){
if(!ball.attached) return;
ball.attached=false;
const sp = ball.speed;
const a = -Math.PI/2 + (Math.random()*0.5-0.25);
ball.vx = Math.cos(a)*sp; ball.vy=Math.sin(a)*sp;
S.combo=0; updateHUD();
sfx.launch?
}
```
**ballUpdate with substeps:**
```js
function stepBall(dt){
ball.x += ball.vx*dt; ball.y += ball.vy*dt;
const r=ball.r;
if(ball.x < WALL+r){ ball.x=WALL+r; ball.vx=Math.abs(ball.vx); sfx wall; spark; }
if(ball.x > W-WALL-r){ ... }
if(ball.y < WALL+r){ ball.y=WALL+r; ball.vy=Math.abs(ball.vy); sfx; }
// paddle
if(ball.vy>0){
const px=pad.x-pad.w/2, py=pad.y;
// circle-rect
const nx=clamp(ball.x,px,px+pad.w), ny=clamp(ball.y,py,py+pad.h);
const dx=ball.x-nx, dy=ball.y-ny;
if(dx*dx+dy*dy <= r*r){
paddleBounce();
}
}
// bricks
for(const b of S.bricks){
if(b.hp<=0) continue;
const nx=clamp(ball.x,b.x,b.x+b.w), ny=clamp(ball.y,b.y,b.y+b.h);
const dx=ball.x-nx, dy=ball.y-ny;
if(dx*dx+dy*dy <= r*r){ hitBrick(b, dx, dy); break; }
}
// floor
if(ball.y - r > H-4){ loseBall(); return true; }
return false;
}
```
paddleBounce: also handle side hits of paddle? If ball hits side of burger, formula with rel still fine-ish (reflect up). Add: only if ball.y < pad.y + pad.h (coming from above) else skip. Condition `ball.vy>0` mostly ensures. If ball hits side while moving down but below top... rare; accept.
Also ensure ball not trapped above paddle: after bounce set y = pad.y - r - 0.5. ✓ in formula.
hitBrick(b,dx,dy):
```js
// reflect
if(Math.abs(dx) > Math.abs(dy)){ ball.vx = dx>0? Math.abs(ball.vx) : -Math.abs(ball.vx); ball.x = nx?...}
```
I need nx,ny — recompute or pass. I'll pass clamped values. Reflect on dominant axis; also nudge ball out: if dx dominant: ball.x = dx>0 ? b.x+b.w+r : b.x-r; else ball.y = dy>0 ? b.y+b.h+r : b.y-r. When dx=dy=0 (center inside), flip vy.
```js
b.hp--;
if(b.hp>0){ sfx.thud(); S.shake=Math.max(S.shake,1.5); tinyParticles few crumbs; }
else { breakBrick(b); }
```
breakBrick:
```js
S.combo++; S.bestCombo=max;
const pts = 25 + (S.combo-1)*10 + (S.level-1)*5;
S.score += pts;
spawnHearts(b.x+b.w/2, b.y+b.h/2);
S.hearts += count; // hearts stat
S.brickCount--;
S.shake = Math.max(S.shake, 2.5);
sfx.pop(S.combo);
popup(bx, by, '+'+pts, S.combo>=3? '#E8432F':'#4A2A18', S.combo);
maybe powerup: if(lives<5 && Math.random()<0.09) spawnPower(x,y);
if(S.brickCount<=0) levelCleared();
updateHUD();
```
levelCleared:
```js
S.mode='clear'; S.clearTimer=1.8; ball.attached=true; ball.vx=ball.vy=0;
heartRain();
S.score += 250*S.level? add: bonus = 200*level; popup center.
sfx.clear();
```
During 'clear' mode update: still update particles, paddle control allowed (pad moves), clearTimer -= dt; when <=0: S.level++; buildLevel(); S.mode = demoWasDemo?'demo':'play'... keep prior mode: store S.playMode ('demo'|'play'). Let me restructure modes: S.mode ∈ {'demo','play','over'}; plus flags: attached, clearing (clearTimer>0), paused. Simplify: mode 'demo'|'play'|'over'; substate clearing boolean.
After clear ends: buildLevel(S.level); ball.speed = baseSpeed(); attached=true; if demo serveTimer=0.8.
loseBall:
```js
sfx.lose(); S.shake=7; S.combo=0;
grayBurst(ball.x, min(ball.y,H-30));
if(S.mode==='demo'){ ball.attached=true; serveTimer=0.9; }
else { S.lives--; updateHUD(); if(S.lives<=0){ gameOver(); } else { ball.attached=true; } }
```
Also reset ball.speed to base? keep current speed (with level base). On life loss reset to level base speed for mercy: yes ball.speed = baseSpeed().
gameOver: S.mode='over'; showOverlay(...); sfx.over();
startGame (from overlay):
```js
S.score=0; S.level=1; S.lives=3; S.hearts=0; S.combo=0; S.bestCombo=0;
buildLevel(); resetBall(); S.mode='play'; hideOverlay(); updateHUD();
```
Also serve hint shows.
startFromDemo — same function.
**updateHUD:**
```js
elScore.textContent = fmt(S.score); pad zeros: String(S.score).padStart(6,'0')? For chunky look: yes 6-digit padded.
elHearts.textContent = S.hearts;
elLevel.textContent = S.level;
renderLives();
```
renderLives: slots = Math.max(3, S.lives); but cap display 5: slots=Math.max(3, Math.min(5,S.lives)); for i<slots: filled = i<S.lives. Build innerHTML with inline svg heart fill red or none (stroke ink, fill none? outline gray). Compose string.
Heart SVG inline (viewBox 0 0 24 24) path M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5... fill.
**Demo AI serveTimer:** in update: if attached && mode==='demo': serveTimer-=dt; if <=0 launch(). Set initial 0.9 and after misses. During clear in demo also auto.
**Ball speed ramp:** in breakBrick: ball.speed = Math.min(cap, ball.speed + 3) px/s per brick? Level base 330+level*22, cap 640. Ramp +2.5/brick. After ~60 bricks +150. OK.
**Hearts stat increments:** in spawnHearts return count.
**spawnHearts(x,y,opts):**
```js
function spawnHearts(x,y, n){
for(i<n){
const a = Math.random()*Math.PI*2;
const sp = 60+Math.random()*230;
parts.push({type:'heart', x,y, vx:Math.cos(a)*sp*1.1, vy:Math.sin(a)*sp - 140, rot:(Math.random()-0.5)*1.2, rv:(Math.random()-0.5)*9, s: 4.5+Math.random()*6.5, life: 0.8+Math.random()*0.6, age:0, ci: randColorIdx});
}
// dots
for(j<4) parts.push({type:'dot', ...});
return n;
}
```
Update particle:
```js
p.age+=dt;
if(p.age>=p.life) remove;
p.vy += (p.g||760)*dt;
p.vx *= Math.exp(-0.6*dt) mild;
p.x+=p.vx*dt; p.y+=p.vy*dt; p.rot+=p.rv*dt;
if(p.type==='heart' && p.y > H-26){ p.y=H-26; p.vy*=-0.35; p.vx*=0.6; } // bounce on floor! hearts bouncing on checker floor = delightful.
```
Hearts bouncing off the floor — very nice touch. Also bounce side walls? keep floor only.
Draw:
```js
const k = p.age/p.life;
let sc = p.age<0.1 ? (p.age/0.1) : 1; // pop in
with overshoot: sc = easeOutBack(min(1, age/0.12))
let al = k>0.7 ? 1-(k-0.7)/0.3 : 1;
scale also shrink: if k>0.8: sc *= (1-(k-0.8)/0.2*0.5)?
size draw = p.s*sc*2 (sprite box).
ctx.globalAlpha=al; translate rotate drawImage centered.
```
Rain hearts: type 'rain': vy 80-160 positive, g=60, life 2.4, s 5-10, sway phase: x += sin(age*3+ph)*20*dt. Draw same as heart.
Dot: fillStyle color (white `#FFFFFF` with ink alpha? or `#FFD966`), circle r2-3, no rot.
Seed (sesame): type 'seed': ellipse 4×2.2 `#F6E3B4` rotate rot, gravity, life 0.7.
Gray hearts: heart with GRAY color idx — I'll store color string per particle for hearts instead of idx (sprite map by color string). Sprites: `heartSprites[color]` lazily created.
**Popups:**
```js
{txt,x,y,age,life:0.9,color,size}
draw: y - age*46, alpha 1-age/life.
```
**Render function:**
```js
function render(){
ctx.setTransform(DPR,0,0,DPR,0,0);
// bg
ctx.fillStyle='#FFF9EC'; ctx.fillRect(0,0,W,H);
// shake
if(S.shake>0.2){ ctx.translate((Math.random()*2-1)*S.shake, (Math.random()*2-1)*S.shake); }
drawFloor();
drawBricks();
drawParticlesBelow? all above ball? order: bricks, particles, ball, burger, powerups, popups, banner.
```
Hmm hearts should fly over burger — draw particles after burger. Ball under hearts ok. Order: floor, bricks, powerups, ball(+trail), burger, particles, popups, serve-hint/banner.
drawFloor:
```js
const fh=20; y0=H-fh;
ctx.fillStyle='#E8432F';
for x squares: if((i%2)==0) fillRect(i*20, y0, 20, fh) — alternate; then thin top line: fillStyle ink rect(0,y0-3,W,3);
```
Also slightly mute red: '#E4715F'? Diner red-white: alternate `#E8432F` & `#FFEFF0`? On cream bg, red+cream checkers: use `#E8432F` and `#FFF3DA`. Top line ink 3px. Add slight darken overlay on floor? fine.
drawBricks:
```js
for(b of S.bricks) if(b.hp>0) ctx.drawImage(b.hp<b.maxHp? sprDamaged[b.type] : spr[b.type], b.x, b.y, BRICK_W, BRICK_H+3);
```
Sprite canvas is (BRICK_W)×(BRICK_H+3) at DPR; drawImage with explicit dest w/h handles scaling. ✓.
**Serve hint drawing (play, attached):**
```js
const a = 0.55+0.45*Math.sin(S.t*5);
ctx.globalAlpha=a; text "CLICK OR PRESS SPACE TO LAUNCH" at (pad.x?, H-110) center W/2, font 800 15px; stroke cream?
```
Draw with ink fill on small cream pill? Simple: text with alpha ink. Also arrows ↑? no emoji, draw nothing. Fine. Also first moments of play, add "LEVEL 1" popup center.
**Clear banner:** if clearing:
```js
panel: w 420 h 120 centered (W/2, 300): rr fill 'rgba(255,251,240,.95)', stroke ink 4, shadow offset flat.
text "STACK CLEARED!" 34px impact fill red stroke? use strokeText ink lw6 + fillText '#E8432F'.
sub: "LEVEL {S.level+1} COMING UP — +{bonus} PTS" 14px ink.
```
Also count down? nah.
**Demo mode canvas decor:** small "DEMO" ribbon? The overlay panel says it. skip.
**HUD DOM ids:** score, hearts, level, lives, mute btn. Also combo? skip DOM combo.
**Fonts stack:**
- Display: `Impact, 'Arial Black', 'Haettenschweiler', 'Franklin Gothic Bold', sans-serif`
- Body/labels: `'Trebuchet MS', Verdana, sans-serif`
**Mute button:** toggles `muted`, swaps svg icon (two svgs inside button, toggle display). aria-pressed.
**Now write CSS carefully.**
```css
*{box-sizing:border-box; margin:0; padding:0;}
html,body{height:100%;}
body{
font-family:'Trebuchet MS', Verdana, sans-serif;
background-color:#F6E4C4;
background-image:radial-gradient(#EBD6AE 2.2px, transparent 2.4px);
background-size:26px 26px;
display:flex; align-items:center; justify-content:center;
min-height:100vh; padding:16px;
color:#4A2A18;
}
.cabinet{
width:min(96vw, 950px);
background:#FFFBF0;
border:5px solid #4A2A18;
border-radius:20px;
box-shadow:10px 12px 0 rgba(74,42,24,.16);
padding:16px 18px 14px;
}
header.top{display:flex; flex-wrap:wrap; align-items:baseline?...}
```
Header: flex row space-between: left title+tag; right maybe a small "EST. TODAY · DINER No. 5" stamp? Cute decorative: right side small rotated sticker "HOT & FRESH" in mustard circle? Let me add a round badge: circle border ink, text "HEARTS GUARANTEED" curved? Curved text needs SVG. Simple rotated square badge: mustard bg, ink border, rotated -8°, text "HOT & FRESH" 2 lines small. Adds character without gradients. Yes.
```css
.top{display:flex; align-items:center; justify-content:space-between; gap:12px;}
h1{
font-family:Impact,'Arial Black',sans-serif;
font-size:clamp(30px, 4.6vw, 46px);
color:#E8432F;
letter-spacing:.5px;
text-shadow:
-2px -2px 0 #4A2A18, 2px -2px 0 #4A2A18, -2px 2px 0 #4A2A18, 2px 2px 0 #4A2A18,
-2px 0 0 #4A2A18, 2px 0 0 #4A2A18, 0 -2px 0 #4A2A18, 0 2px 0 #4A2A18,
5px 6px 0 rgba(74,42,24,.22);
line-height:.95;
}
h1 .heart-ico{ inline svg in h1: width .7em; transform rotate; }
.tag{font:700 10.5px 'Trebuchet MS'; letter-spacing:.34em; color:#8A6A4B; margin-top:6px;}
.badge{
width:86px;height:86px; border:4px solid #4A2A18; border-radius:50%;
background:#F5B32F; transform:rotate(-8deg);
display:flex;flex-direction:column;align-items:center;justify-content:center;
font:900 11px Impact...; text-align:center; line-height:1.15;
box-shadow:4px 5px 0 rgba(74,42,24,.2);
flex:none;
}
```
Badge text: "HEARTS<br>GUARANTEED" with tiny heart svg between? Keep: "MADE<br>WITH ♥"? no emoji — small svg heart inline. Badge: line1 "MADE WITH", svg heart, line2 "100% LOVE"? Too much text for 86px. Simple: "HOT &<br>FRESH" + tiny burger glyph? draw mini burger with css? overkill. Badge: eyebrow "THE" small, "HEARTBREAK" bold? Hmm. Badge text: "EST. TODAY" over line "DINER" ... I'll do: small "HOUSE SPECIAL" arc? Keep: two lines: "MADE WITH" (8px) + heart svg (16px) + "REAL LOVE" (8px). Cute. At small width hide badge (media query <560px display:none).
HUD:
```css
.hud{
margin-top:14px;
display:flex; align-items:center; gap:10px 22px; flex-wrap:wrap;
background:#FFF3DA; border:4px solid #4A2A18; border-radius:14px;
padding:8px 16px;
}
.stat{display:flex; align-items:baseline; gap:8px;}
.stat .lbl{font:700 10px 'Trebuchet MS'; letter-spacing:.22em; color:#8A6A4B;}
.stat .val{font-family:Impact...; font-size:24px; color:#4A2A18; letter-spacing:1px; min-width: ch? }
#hearts val color #E8432F.
.lives{margin-left:auto; display:flex; gap:5px; align-items:center;}
.lives svg{width:20px;height:20px; filter:drop-shadow(1.5px 2px 0 rgba(74,42,24,.25));}
#mute{margin-left:4px? place after lives}
#mute{width:34px;height:34px;border:3px solid #4A2A18;border-radius:10px;background:#FFFBF0;display:grid;place-items:center;cursor:pointer;color:#4A2A18; box-shadow:2px 3px 0 rgba(74,42,24,.2);}
#mute:active{transform:translate(1px,2px); box-shadow:none;}
#mute svg{width:18px;height:18px;}
```
Stage:
```css
.stage{position:relative; margin-top:14px; border:5px solid #4A2A18; border-radius:14px; overflow:hidden; background:#FFF9EC; touch-action:none; cursor:none? no — crosshair? default}
canvas{display:block; width:100%; height:auto;}
```
Height auto keeps ratio from attributes (900*DPR / 600*DPR ratio 1.5) ✓.
Overlay:
```css
.overlay{position:absolute; inset:0; display:flex; align-items:center; justify-content:center; background:rgba(255,249,236,.55); padding:20px;}
.overlay.hidden{display:none;}
.panel{background:#FFFBF0; border:4px solid #4A2A18; border-radius:16px; box-shadow:8px 9px 0 rgba(74,42,24,.18); padding:22px 30px; text-align:center; max-width:88%;}
.panel .eyebrow{font:700 10px; letter-spacing:.3em; color:#8A6A4B;}
.panel h2{font-family:Impact; font-size:clamp(26px,4vw,40px); color:#E8432F; same outline shadow; margin:6px 0 4px;}
.panel p.meta{font-size:13px; color:#6B4A2F; }
.blink{margin-top:14px; font:800 13px 'Trebuchet MS'; letter-spacing:.18em; color:#4A2A18; animation:blink 1.1s steps(2,start) infinite;}
@keyframes blink{50%{opacity:0;}} — steps might be harsh; use ease pulse: opacity .25/1.
```
Use `animation: pulse 1.4s ease-in-out infinite; @keyframes pulse{50%{opacity:.25}}` smoother.
Hints:
```css
.hints{display:flex; flex-wrap:wrap; gap:6px 18px; justify-content:center; margin-top:12px; font:700 10px; letter-spacing:.2em; color:#8A6A4B;}
.hints span{white-space:nowrap;}
```
Media <560px: badge none; hud val 20px; padding smaller.
Page height check 1280×800: cabinet ~ header 70 + hud 50 + stage (900px wide → canvas 900*? cabinet inner width = min(96vw,950) - padding 36 - border 10 ≈ 904 at max → canvas height 904/1.5=603) + hints 30 + paddings ≈ 60+50+603+30+50 ≈ 793 + body padding 32 → 825 > 800 slight scroll. Tighten: cabinet width min(94vw, 880px) → canvas ~834 wide, height 556; total ≈ 60+50+556+30+60+32= 788 ✓ fits 800. Set width: min(94vw, 880px). Good. Title 40px max.
**Total hearts HUD label:** "HEARTS" with small svg heart next to value? Value red bold with heart icon before it — do label "HEARTS" and val red number, fine. Maybe prepend tiny svg inside val — keep simple text.
**updateHUD hearts element id 'heartsVal'** avoid id 'hearts' confusion fine either way.
**Write sfx:**
```js
const sfx = (()=>{
let ctxA=null, muted=false;
function ensure(){ if(!ctxA){ try{ ctxA = new (window.AudioContext||window.webkitAudioContext)(); }catch(e){} } if(ctxA&&ctxA.state==='suspended'){ ctxA.resume().catch(()=>{}); } return ctxA; }
function tone(f0,f1,dur,type,vol,delay=0){
if(muted||!ctxA) return;
const t0=ctxA.currentTime+delay;
const o=ctxA.createOscillator(), g=ctxA.createGain();
o.type=type; o.frequency.setValueAtTime(f0,t0);
if(f1) o.frequency.exponentialRampToValueAtTime(Math.max(30,f1), t0+dur);
g.gain.setValueAtTime(vol,t0);
g.gain.exponentialRampToValueAtTime(0.0001, t0+dur);
o.connect(g); g.connect(ctxA.destination);
o.start(t0); o.stop(t0+dur+0.02);
}
return {
ensure,
get muted(){return muted;}, set muted(v){muted=v;},
wall(){tone(300,260,.05,'square',.05);},
paddle(){tone(190,120,.08,'triangle',.2);},
thud(){tone(140,90,.07,'square',.15);},
pop(c){const f=Math.min(1180,480+c*45); tone(f,f*1.4,.09,'square',.12); tone(f*1.5,f*2,.07,'triangle',.06,.02);},
power(){tone(620,620,.09,'triangle',.15); tone(930,930,.12,'triangle',.15,.09);},
lose(){tone(320,90,.45,'sawtooth',.16);},
clear(){[523,659,784,1047].forEach((f,i)=>tone(f,f,.12,'square',.12,i*.09));},
over(){[392,311,262,196].forEach((f,i)=>tone(f,f*.98,.2,'triangle',.16,i*.16));},
launch(){tone(240,480,.09,'triangle',.14);}
};
})();
```
initAudio = sfx.ensure called on gesture.
**Main state + loop.** Let me also handle the demo AI serve & attached follow.
Careful bug: paddle collision uses pad.y top; burger visual top at pad.y ✓.
**buildLevel:**
```js
function buildLevel(){
S.bricks.length=0; S.brickCount=0;
const rowDefs=[
{t:'bun',hp:1},{t:'lettuce',hp:1},{t:'tomato',hp:1},
{t:'cheese',hp:1},{t:'patty',hp:2},{t:'bun2',hp:1}
];
rowDefs.forEach((rd,ri)=>{
for(let ci=0;ci<COLS;ci++){
S.bricks.push({x: SIDE + ci*(BRICK_W+GAP), y: TOP + ri*(BRICK_H+3+GAP), w:BRICK_W, h:BRICK_H, type:rd.t, hp:rd.hp, maxHp:rd.hp});
S.brickCount++;
}
});
}
```
v step: BRICK_H+3 sprite extra + GAP... bricks stored h=BRICK_H; sprite height BRICK_H+3 (includes shadow). Vertical spacing: y step = BRICK_H + 10. Rows: 52, 89, 126, 163, 200, 237 → last bottom 264 ✓.
x step = BRICK_W + GAP = 82.8; 10 cols: SIDE=40 start, last x = 40+9*82.8=785.2, right edge 785.2+74.8=860 = 900-40 ✓.
**Sprites creation:** after DOM ready (script at end of body). Sprites map:
```js
const BRICK_SPRITES = {};
['bun','lettuce','tomato','cheese','bun2'].forEach(t=>BRICK_SPRITES[t]=[makeBrick(t,false)]);
BRICK_SPRITES.patty=[makeBrick('patty',false), makeBrick('patty',true)];
```
Access: b.hp<b.maxHp && sprite[1] ? [1] : [0].
**Heart sprites:**
```js
const heartSprites={};
function heartSprite(color){ if(!heartSprites[color]){ ...canvas 96... draw path fill color, highlight, optional } return heartSprites[color]; }
```
Power heart sprite separate: heartSprite with outline flag → key color+'-o'.
**Powerup:**
```js
{x,y,vy:120,phase, s:15}
update: y+=vy*dt; vy+=60*dt? keep slow: vy constant 130 + sway x = baseX + sin(age*2.2)*14.
catch: |x-pad.x|<pad.w/2+10 && y>pad.y-8 && y<pad.y+pad.h+10 → catch.
```
Draw: pulse scale 1+0.12sin(t*6); drawImage heart sprite with white outline, size 2*s box (~30px). Maybe add small "1UP" text? popup "+1 LIFE" on catch.
**Ball trail:** array push each frame in play when !attached: keep 9; draw i-indexed alpha (i/len)*0.3, radius r*(i/len)*0.9. Color '#F08080'? red-ish alpha. Use rgba(232,67,47,alpha).
**One more juicy detail:** brick break also spawns 1-2 ingredient crumb particles matching brick color (small rounded rects rotating) — makes the brick feel like it shatters into its matter AND hearts. Use 'crumb' type: rect 6×4 color by type. Yes adds richness. Colors map: bun #EDAA62, lettuce #7FC94E, tomato #E8483A, cheese #FFC233, patty #7A4A26, bun2 #E2955A.
**Demo AI targeting with attached ball:** when attached & demo: serveTimer; target = W/2? burger should slide to a position under a gap between brick columns then serve — the serve angle random anyway. Keep: wander target = W/2 + sin(t*.6)*140.
**Now consider screenshot at 3s:** load → demo: buildLevel, ball attached, serveTimer .8 → launch. Ball speed 340. From paddle (528) to bricks (264) ~0.8s + AI returns... by 3s maybe 2-4 bricks broken with hearts mid-flight. Decent. To guarantee more action in the first 3s: set demo initial serveTimer 0.4, and demo ball speed slightly higher? Same speed fine. Also could pre-break? No. It's fine — hearts visible from first break ~1.5s.
Actually improve: at t=0 launch quickly (0.35s) and give demo first ball an upward angle aimed at brick center. Fine with random.
**Pause overlay** reuses overlay div with different content and blink text "PRESS P TO RESUME".
**Overlay management:**
```js
const ov=document.getElementById('overlay');
function showOverlay(eyebrow, title, meta, blink){ fill spans; ov.classList.remove('hidden'); }
function hideOverlay(){ ov.classList.add('hidden'); }
```
Elements: #ovEyebrow, #ovTitle, #ovMeta (innerHTML, can contain <br>), #ovBlink.
Demo overlay: eyebrow "THE AUTO-CHEF IS COOKING", title "TAKE THE SPATULA?" hmm title maybe "BURGER BREAKOUT" — but page h1 already says that. Overlay title: "READY TO FLIP?" cute. meta: "MOVE — MOUSE OR ← → · LAUNCH — SPACE · PAUSE — P". blink: "CLICK OR PRESS ANY KEY TO START".
Over: eyebrow "OUT OF BURGERS", title "GAME OVER", meta "SCORE {s} · LEVEL {l} · BEST COMBO ×{c}<br>HEARTS SPREAD: {h}", blink "CLICK OR PRESS ANY KEY TO PLAY AGAIN".
Pause: eyebrow "KITCHEN CLOSED", title "PAUSED", meta "", blink "PRESS P TO RESUME".
Nice diner copy.
**In-canvas "READY?" between lives?** skip.
**combo popup:** on break: popup `+${pts}` at brick; if combo>=2 add small second popup `COMBO ×${combo}` offset y+20 color red. Two popups each break ok.
**HUD combo element?** I decided no DOM combo. ok.
**Wait — hearts counter increments by particles spawned (n per brick ~14-24 + rain + gray?). Only count burst hearts (positive ones), not rain/gray/seed.** In spawnHearts return n; in breakBrick S.hearts += n. ✓.
**Potential bug:** ball speed changes while attached (after level up) — vx/vy recomputed at launch ✓. During flight speed changes on brick break: renormalize:
```js
const m=Math.hypot(ball.vx,ball.vy); ball.vx*=ball.speed/m; ball.vy*=ball.speed/m;
```
after increment. ✓.
**Also anti-horizontal-lock:** after brick bounce if |vy| < 60 → nudge vy sign*60 & renormalize. After paddle bounce formula ensures vy=-cos(a)*sp ≥ cos(1.05)*sp≈0.5sp ✓. Brick side hits flip vx only — vy unchanged, fine. Wall top flip. Add tiny random ±0.02 rad jitter on brick bounce? skip, nudge enough.
**Anti-stuck ball above bricks horizontal:** min vy guard in stepBall each frame: if !attached && Math.abs(ball.vy)<40 → ball.vy = (ball.vy<0?-1:1)*40, renormalize.
**Clearing mode ball:** attached=true so ball glued; burger moves; fine.
**When level cleared while powerups falling:** they continue; catch still possible ✓. During 'clear' the paddle still updates.
**gameOver while particles active:** overlay shows over canvas; canvas still renders last frame + particles? update() skips in 'over'? Keep updating particles & paddle idle in over mode for liveliness: in update, if mode==='over': only particles/popups update. Demo: full. Let me structure:
```js
function update(dt){
S.t+=dt;
if(S.mode!=='over'){ updatePaddle(dt); updateBall(dt); updatePowerups(dt); }
updateParticles(dt); updatePopups(dt);
S.squash=Math.max(0,S.squash-dt*4.5);
S.shake*=Math.exp(-7*dt); if(S.shake<0.05)S.shake=0;
}
```
over: keep burger bob (S.t drives bob) ✓ render still runs.
**Clearing timer handling:** in updateBall? Put in update: if(S.clearing){ S.clearTimer-=dt; if(<=0){ S.clearing=false; S.level++; buildLevel(); resetBallToPaddle(); if(S.mode==='demo') serveTimer=0.7; sfx? } and skip ball physics while clearing (attached anyway → updateBall handles attached branch). attached ball follows paddle ✓.
levelCleared():
```js
S.clearing=true; S.clearTimer=1.9; ball.attached=true;
const bonus=150*S.level? let's 100*level+... use bonus = 200 + 100*(S.level-1);
S.score+=bonus;
popup(W/2, 320, '+'+bonus+' BONUS', '#E8432F', big);
heartRain(); sfx.clear(); updateHUD();
```
**loseBall when clearing?** attached so no.
**Pointer move mapping when canvas scaled:** using rect each move is fine.
**Cursor over canvas:** hide cursor during play? `cursor:crosshair`? Keep default; maybe `cursor:none` feels cleaner for paddle control but bad for clicking. Keep default.
**TouchAction none on stage ✓** to allow drag.
**Prevent double start from pointerdown + click:** only pointerdown listener. ✓.
**Resize:** none needed (CSS scales).
**Now, also the demo overlay should not block seeing the game:** translucent ✓.
**Edge: keydown 'm' toggles mute — also when overlay? fine, returns early.
**Init sequence:**
```js
buildLevel(); resetBallToPaddle(); S.mode='demo'; serveTimer=0.5; updateHUD(); showOverlay(demo...);
requestAnimationFrame(frame);
```
resetBallToPaddle: attached=true, vx=vy=0, speed=baseSpeed(), position set in update (attached branch) — but set immediately too for render before first update: fine, first frame runs update first? frame() calls update then render ✓.
**updateBall:**
```js
function updateBall(dt){
if(S.clearing) return;
if(ball.attached){
ball.x=pad.x+pad.w*0.18; ball.y=pad.y-ball.r-2;
if(S.mode==='demo'){ serveTimer-=dt; if(serveTimer<=0) launch(); }
return;
}
trail push...
const steps=Math.max(1, Math.ceil((ball.speed*dt)/6));
for(...){ if(stepBall(dt/steps)) break; }
}
```
**stepBall returns true when lost (stop further steps).**
Let me also add small wall-hit spark: 3 tiny cream dots. minor; include via dots.
**Render popups & banner fonts:**
```js
function chunkText(txt,x,y,size,fill,stroke=true,ls='2px'){
ctx.font=`900 ${size}px 'Arial Black','Arial',sans-serif`;
try{ctx.letterSpacing=ls;}catch(e){}
ctx.textAlign='center'; ctx.textBaseline='middle';
if(stroke){ctx.lineWidth=size/5; ctx.strokeStyle='#4A2A18'; ctx.lineJoin='round'; ctx.strokeText(txt,x,y);}
ctx.fillStyle=fill; ctx.fillText(txt,x,y);
try{ctx.letterSpacing='0px';}catch(e){}
}
```
letterSpacing assignment of invalid won't throw. Safe without try but keep.
**Banner for clearing:** draw panel + chunkText('STACK CLEARED!' red 34) + sub text 14 Trebuchet ink `LEVEL ${S.level+1} INCOMING`.
Wait during clearing S.level not yet incremented → show next = S.level+1 ✓.
**Serve hint:** chunkText small with cream stroke instead: draw pill: rr cream fill + ink stroke then ink text 12px Trebuchet bold. Position (W/2, H-120). Pulse alpha via globalAlpha.
**Demo badge in canvas?** skip.
**Let me also double-check the patty damaged: b.maxHp=2; after first hit hp=1 → sprite[1]. ✓**
**Lives max 5:** catch: if(S.lives<5){lives++} else score+=250 (popup "+250"). ✓
**Popups cap:** if popups.length>30 shift.
**Particles cap:** if parts.length>420 splice oldest.
**Gray burst on lose:**
```js
for(i<10) parts.push heart GRAY with vy upward small then fall, from ball x, y=H-60.
```
color '#B9AA9C'.
**Hearts bounce floor:** for type heart: if(p.y>H-30){p.y=H-30; p.vy*=-0.4; p.vx*=0.7; p.rv*=0.6;}
Rain hearts also bounce? rain spawns above and falls slow, bounce ok too (they're 'heart' type with flag rain? I'll give rain same type 'heart' with gravity smaller g property. Particle has g field default 760; rain g=90 and vy start positive.) sway: add p.sway phase for rain: x+=sin. Add generic: if(p.sw) p.x+=Math.sin(p.age*3+p.sw)*14*dt.
**Now let me also handle the 'clear' heart rain spawn positions:** x random W, y -20..-200, life 2.6.
Level-clear check should not trigger multiple times: breakBrick checks brickCount<=0 → levelCleared() once (since called only on break). ✓.
**AI prediction function:**
```js
function predictX(){
const r=ball.r, lo=WALL+r, hi=W-WALL-r, span=hi-lo;
if(ball.vy<=0) return ball.x;
const t=(pad.y-ball.y)/ball.vy;
let px=ball.x+ball.vx*t;
let m=((px-lo)%(2*span)+2*span)%(2*span);
if(m>span) m=2*span-m;
return lo+m;
}
```
AI update:
```js
function aiControl(dt){
let target;
if(ball.attached){ target = W/2 + Math.sin(S.t*0.55)*150; }
else if(ball.vy>0){ target=predictX(); }
else { target = W/2 + (ball.x-W/2)*0.35; }
target += Math.sin(S.t*2.1)*8;
const sp=640;
pad.x += clamp(target-pad.x, -sp*dt, sp*dt);
}
```
Note predictX called every frame — fine.
Hmm: AI with strong paddle might hit ball with edge frequently creating steep angles — fine, breaks bricks.
**updatePaddle:**
```js
if(S.mode==='demo') aiControl(dt);
else playerControl(dt);
pad.x=clamp(pad.x, WALL+pad.w/2+2, W-WALL-pad.w/2-2);
```
**Player:**
```js
let kb=(keys['arrowright']||keys['d']?1:0)-(keys['arrowleft']||keys['a']?1:0);
if(kb) pad.x+=kb*620*dt;
else if(pointerX!=null) pad.x+=(pointerX-pad.x)*Math.min(1,16*dt);
```
**Squash on paddle hit:** S.squash=1; sesame seeds spawn 3.
Also squash tiny on launch (0.5).
**Draw burger with squash anchored bottom** ✓ implement.
Also burger slight lean based on velocity? tilt = clamp(vxish)… skip.
**HUD lives render:**
```js
const HEART_D='M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z';
function renderLives(){
const slots=Math.max(3, Math.min(5, Math.max(S.lives,3))); // show 3 minimum outline
let h='';
for(let i=0;i<slots;i++){
const on=i<S.lives;
h+=`<svg viewBox="0 0 24 24" width="20" height="20"><path d="${HEART_D}" fill="${on?'#E8432F':'none'}" stroke="${on?'#4A2A18':'#C9B090'}" stroke-width="2" stroke-linejoin="round"/></svg>`;
}
elLives.innerHTML=h;
}
```
If lives>5 cap 5.
**Mute icons:**
```html
<button id="mute" aria-label="Sound on/off">
<svg id="icoOn" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linejoin="round" stroke-linecap="round"><path d="M4 9.5v5h3.5L12 19V5L7.5 9.5H4z" fill="currentColor" stroke="currentColor"/><path d="M15.5 9a4.2 4.2 0 0 1 0 6"/><path d="M18 6.8a7.6 7.6 0 0 1 0 10.4"/></svg>
<svg id="icoOff" style="display:none" viewBox="0 0 24 24" ...><path d="M4 9.5v5h3.5L12 19V5L7.5 9.5H4z" fill="currentColor"/><path d="M16 9.5l5 5M21 9.5l-5 5"/></svg>
</button>
```
toggleMute(): muted=!muted; swap displays; sfx.muted set.
**h1 heart between words:**
```html
<h1>BURGER<svg class="h1heart" viewBox="0 0 24 24"><path d="M12 21.35..." fill="#F5B32F" stroke="#4A2A18" stroke-width="1.6"/></svg>BREAKOUT</h1>
```
CSS .h1heart{width:.62em;height:.62em;margin:0 .12em;display:inline-block;transform:rotate(-8deg) translateY(-2%);vertical-align:-2%;} text-shadow won't apply to svg; give svg filter drop-shadow similar. ok.
Hmm — title color red with ink outline; heart mustard.
**Badge:**
```html
<div class="badge"><span>MADE WITH</span><svg .../><span>REAL LOVE</span></div>
```
**Hints:** `<span>MOVE · MOUSE OR ← →</span><span>LAUNCH · SPACE</span><span>PAUSE · P</span><span>SOUND · M</span>` Use separators as spans with border-left? Simple gap fine, maybe interpuncts inside.
**Assemble everything now.** Also remember meta charset etc. Let me carefully write the JS in full, then re-scan for bugs.
One more consideration — the update of `pad` object; define:
```js
const pad={x:W/2, y:H-84, w:116, h:36};
```
H-84 = 516, bottom 552, floor top at 580. Ball lost at y> H-4=596? floor top 580; ball should vanish at floor: lost when ball.y - r > H - 26 (into floor). Let me set: floorY = H-20; lost when ball.y > H-30+r? Simpler: ball.y - ball.r > H-24 → poof at (ball.x, H-24). Eh: loseBall trigger `ball.y - ball.r > H - 26`. And gray burst at (ball.x, H-30). ok.
Paddle at y=H-84: gap between paddle bottom (552) and floor (580) = 28. fine.
**Wait — burger bob translation affects collision?** bob is visual only (±0.8px) fine.
**Draw order for powerups before ball** ok.
**drawParticles:**
```js
for(p of parts){
const k=p.age/p.life;
if(p.type==='heart'){
const spr=heartSprite(p.color);
let sc = p.age<0.12 ? backOut(p.age/0.12) : 1;
if(k>0.75) sc*= 1-(k-0.75)/0.25*0.4;
const al = k>0.6? 1-(k-0.6)/0.4 : 1;
ctx.save(); ctx.globalAlpha=Math.max(0,al); ctx.translate(p.x,p.y); ctx.rotate(p.rot);
const s=p.s*sc*2.1; ctx.drawImage(spr, -s/2, -s/2, s, s); ctx.restore();
} else if(p.type==='dot'){ ctx.globalAlpha=1-k; fillStyle p.color; arc r*(1-k*0.5) }
else if(p.type==='seed'){ rotate, ellipse fill '#F6E3B4' }
else if(p.type==='crumb'){ rotate, fillRect -3..3, p.color }
}
ctx.globalAlpha=1;
```
backOut(t)=1+2.7*Math.pow(t-1,3)+1.7*... standard easeOutBack: c1=1.70158; c3=c1+1; f=1 + c3*Math.pow(t-1,3) + c1*Math.pow(t-1,2). ✓
heart sprite box: draw at s=p.s*2.1 px box. p.s 4.5-11 → box 9.5-23px. Heart inside ~0.8 → visual 7-18px. good "tons of little hearts".
Count per brick: 14 + min(12, combo*2) + rand 0-3 → typically 15-30. 60 bricks → ~1200 hearts per level. counter grows juicy. Cap alive particles 420.
**heartRain:** 55 hearts: parts.push({type:'heart', x:rand*W, y:-rand*160-10, vx:(r-.5)*60, vy:60+rand*120, g:110, rot:(r-.5)*1, rv:(r-.5)*4, s:5+r*7, life:2.2+r, age:0, color:pick, sw:r*6}).
**Popups draw:**
```js
ctx.save(); ctx.globalAlpha=1-k;
chunkText(p.txt, p.x, p.y - 42*k? p.y - p.age*46, size, p.color, true);
```
popups y clamp within canvas.
**Ensure canvas text fonts referenced with quotes correctly in template strings.**
**letterSpacing property:** ctx.letterSpacing = '2px' — supported Chrome 99+. Assign inside try not needed; assignment to unsupported property is harmless (it's defined on CanvasRenderingContext2D only if supported; assigning creates expando — harmless). ok skip try.
**Check: font '900 28px "Arial Black"' — Arial Black has weight 900 only; with weight 900 works.** ok.
**Potential issue: `keys` object and ' ' key while overlay start → startGame also triggered by any key ✓ but also sets keys[' ']=true — harmless.
**startGame also called from pointerdown on stage. Also 'p' during demo shouldn't pause ✓ handled (only play mode).
**After gameOver → overlay; pointerdown starts new game. ✓**
**Pause during attached: paused overlay; resume returns; keys state might be stale (e.g., holding arrow) — fine.
**Blur auto-pause:** add window blur → if(S.mode==='play' && !attached? ) pause? If user alt-tabs mid game, auto pause nice:
```js
window.addEventListener('blur', ()=>{ if(S.mode==='play' && !paused) pause(); });
```
Include; low risk. Hmm, in the sandboxed iframe, does the screenshot process cause blur? Unlikely to focus iframe anyway. If it blurs immediately... the iframe itself never has focus (user hasn't clicked) — blur fires only if it had focus. Safe.
**pause():** paused=true; showOverlay('KITCHEN CLOSED','PAUSED','', 'PRESS P TO RESUME'); resume(): paused=false; hideOverlay().
But wait: if paused overlay shown while mode demo? P only pauses in play ✓.
**In frame loop: if(!paused) update(dt).** During paused, render still — shows frozen game behind translucent overlay ✓.
**Edge: startGame when clicked while paused?** paused only in play; pointerdown handler: `if(paused) return;` before mode checks ✓.
**One more: after gameOver, demo? stays over until input. fine.**
**Ball trail while attached: skip push.**
Let me now also think whether `hearts` id conflicts with anything — rename element id 'heartsCount'.
**Writing final HTML now.** I'll write ~600-700 lines total. Let me carefully produce it.
Also double check brick sprite: makeBrick draws at scale DPR and returns canvas sized BRICK_W*DPR × (BRICK_H+3)*DPR. In render drawImage(spr, b.x, b.y, BRICK_W, BRICK_H+3). ✓ crisp because dest scaled from hi-res source roughly matches actual pixels (canvas CSS might scale down ~0.93 — fine).
Burger drawn vector each frame — crisp at DPR ✓.
Ball trail array: `trail` of {x,y}; unshift/push: push and shift when >10.
```js
if(!ball.attached){ trail.push({x:ball.x,y:ball.y}); if(trail.length>10) trail.shift(); } else trail.length=0;
```
**Also demo: hide "CLICK OR PRESS SPACE" hint (only play mode) ✓.**
**Draw powerups:**
```js
for(u of S.powerups){
const pu=1+Math.sin(S.t*6+u.ph)*0.12;
const s=u.s*2*pu;
ctx.save(); ctx.translate(u.x,u.y); ctx.rotate(Math.sin(S.t*2+u.ph)*0.15);
ctx.drawImage(heartSprite('#F43F5E',true), -s/2,-s/2, s, s);
ctx.restore();
}
```
heartSprite(color, outline): key = color+(outline?'#o':'').
Outline sprite: draw white stroke lw 9 around heartPath first (stroke), then fill color, then highlight.
**Catch check in updatePowerups:**
```js
u.age+=dt; u.y+=u.vy*dt; u.x=u.bx+Math.sin(u.age*2.4+u.ph)*16; u.vy=Math.min(150, u.vy+30*dt);
if(u.y > H-26){ remove; poof dots; }
if(u.y>pad.y-6 && u.y<pad.y+pad.h+8 && Math.abs(u.x-pad.x)<pad.w/2+12){ catch }
```
vy start 60.
catch: lives<5? lives++ popup '+1 LIFE' '#E8432F' else score+=250 popup '+250'. sparkles: spawn 12 hearts small upward from paddle + gold dots. sfx.power(). updateHUD.
**OK also initial `S` object:**
```js
const S={mode:'demo', score:0, level:1, lives:3, hearts:0, combo:0, bestCombo:0, bricks:[], brickCount:0, powerups:[], clearing:false, clearTimer:0, squash:0, shake:0, t:0};
let serveTimer=0, paused=false;
const parts=[], popups=[], trail=[];
```
ball separate `const ball={x:0,y:0,vx:0,vy:0,r:9,speed:0,attached:true};`
baseSpeed(): 330+ (S.level-1)*24, cap 560.
**launch() also sfx.launch() + squash 0.6.**
**hitBrick nudge:** careful when ball center inside brick (dx=dy=0): then set ball.y=b.y+b.h+r, vy positive down? Actually flip vy and push below? Rare; handle: vy=-vy, y=b.y+b.h+r.
**Now floor draw:**
```js
function drawFloor(){
const s=22, y0=H-s;
for(let i=0;i<W/s;i++){
ctx.fillStyle = i%2? '#FFF3DA' : '#E8432F';
ctx.fillRect(i*s, y0, s, s);
}
ctx.fillStyle='#4A2A18'; ctx.fillRect(0,y0-4,W,4);
}
```
W/s not integer: 900/22=40.9 → last square cut off, fine (fillRect clipped). Use s=22.5 → 40 exact: use s=W/40=22.5. ok compute s = W/40.
**drawBricks function** straightforward.
**Serve-hint pill:**
```js
if(S.mode==='play' && ball.attached && !S.clearing){
const y=H-128, a=0.6+0.4*Math.sin(S.t*4.5);
ctx.globalAlpha=a;
const txt='CLICK OR PRESS SPACE TO LAUNCH';
ctx.font='700 12px "Trebuchet MS", Verdana, sans-serif';
const w=ctx.measureText(txt).width+30;
rr(ctx, W/2-w/2, y-14, w, 28, 14); ctx.fillStyle='#FFFBF0'; ctx.fill(); ctx.lineWidth=3; ctx.strokeStyle='#4A2A18'; ctx.stroke();
ctx.fillStyle='#4A2A18'; ctx.textAlign='center'; ctx.textBaseline='middle'; ctx.fillText(txt, W/2, y);
ctx.globalAlpha=1;
}
```
letterSpacing maybe; skip.
**Clear banner draw:**
```js
if(S.clearing){
const y=270;
rr(ctx, W/2-230, y-62, 460, 124, 18);
ctx.fillStyle='rgba(255,251,240,.96)'; ctx.fill();
ctx.lineWidth=5; ctx.strokeStyle='#4A2A18'; ctx.stroke();
chunkText('STACK CLEARED!', W/2, y-16, 36, '#E8432F', true, '2px');
ctx.font='700 13px "Trebuchet MS",sans-serif'; ctx.fillStyle='#6B4A2F'; ctx.textAlign='center';
ctx.fillText(`LEVEL ${S.level+1} INCOMING · +${lastBonus} PTS`, W/2, y+26);
}
```
store lastBonus global.
**Also during clearing maybe ball invisible? attached shows on paddle — fine.
**Now — write everything. Also ensure no `console.error`.**
Also HTML lang en, title.
Double-check clamp helper, lerp, rand helpers:
```js
const clamp=(v,a,b)=>v<a?a:v>b?b:v;
const rand=(a,b)=>a+Math.random()*(b-a);
const pick=a=>a[(Math.random()*a.length)|0];
```
**Seeds helper global:**
```js
function drawSeed(g,x,y,rot){ g.save(); g.translate(x,y); g.rotate(rot); g.beginPath(); g.ellipse(0,0,4.6,2.4,0,0,Math.PI*2); g.fill(); g.restore(); }
```
**Burger draw full code:**
```js
function drawBurger(){
const g=ctx, w=pad.w, x=pad.x-w/2, y=pad.y, h=pad.h;
g.save();
g.translate(pad.x, y+h);
g.scale(1+S.squash*0.09, 1-S.squash*0.2);
g.translate(-pad.x, -(y+h));
g.translate(0, Math.sin(S.t*3)*0.8);
const ink='rgba(74,42,24,.45)';
g.lineWidth=2; g.strokeStyle=ink;
// bottom bun
g.fillStyle='#E2955A'; rr(g,x+7,y+29,w-14,8,[3,3,5,5]); g.fill(); g.stroke();
g.fillStyle='#EDAA62'; rr(g,x+12,y+30,w-24,3,2); g.fill();
// patty
g.fillStyle='#7A4A26'; rr(g,x+2,y+22,w-4,10,5); g.fill(); g.stroke();
g.fillStyle='#5E3618';
[[.16,.7],[.32,.55],[.5,.72],[.66,.58],[.82,.68]].forEach(s=>{ g.beginPath(); g.arc(x+w*s[0], y+27, 1.8, 0, 7); g.fill(); });
// cheese
g.fillStyle='#FFC233'; rr(g,x-2,y+17,w+4,8,4); g.fill(); g.stroke();
rr(g,x+w*0.2,y+20,12,10,5); g.fill(); // drip (no stroke)
g.fillStyle='#E8A81C'; g.beginPath(); g.arc(x+w*0.68,y+20.5,2.4,0,7); g.fill();
// lettuce
g.fillStyle='#7FC94E'; rr(g,x-5,y+11,w+10,8,4); g.fill(); g.stroke();
g.fillStyle='#7FC94E';
for(let i=0;i<6;i++){ g.beginPath(); g.arc(x-5+ (w+10)*(i+0.5)/6, y+19, 5, 0, Math.PI); g.fill(); }
g.fillStyle='#A5DE79'; rr(g,x+2,y+13,w-4,3,2); g.fill();
// tomato
g.fillStyle='#E8483A'; rr(g,x+3,y+9,w-6,7,3.5); g.fill(); g.stroke();
g.fillStyle='#F06A57'; rr(g,x+7,y+11,w-14,3,1.5); g.fill();
// top bun
g.fillStyle='#EDAA62'; rr(g,x+4,y,w-8,14,[10,10,4,4]); g.fill(); g.stroke();
g.save(); rr(g,x+4,y,w-8,14,[10,10,4,4]); g.clip();
g.fillStyle='#D18A45'; g.fillRect(x+4,y+10,w-8,4);
g.fillStyle='rgba(255,255,255,.3)'; g.fillRect(x+14,y+2,w*0.34,3);
g.restore();
g.fillStyle='#FBE7C6';
drawSeedAt(g,x+w*0.2,y+5,-0.5); drawSeedAt(g,x+w*0.38,y+3,0.2); drawSeedAt(g,x+w*0.56,y+6,-0.25); drawSeedAt(g,x+w*0.72,y+3.5,0.5); drawSeedAt(g,x+w*0.86,y+6,-0.4);
g.restore();
}
```
Note g.stroke() strokes last path — after rr() + fill, path still current so stroke works ✓ (rr builds path via beginPath). But after scallop arcs, path is arc; drip rr then fill — stroke would stroke drip; I skipped stroke there ✓ order: I call g.fill() only for drip. Careful sequence: after `rr(...drip); g.fill();` then set fillStyle hole and arc fill — fine.
For patty speckles after stroke: after `g.fill(); g.stroke();` then arcs with beginPath ✓.
Also note: drip drawn AFTER stroke of cheese rect — path changed by rr(drip) before fill; the earlier cheese stroke already done ✓.
Layer overlap check: cheese y+17..25; patty y+22..32; drip y+20..30 hangs over patty ✓. lettuce y+11..19 + scallops to y+24. tomato y+9..16 overlaps lettuce top ✓. topbun y..14 overlaps tomato ✓. bottom bun y+29..37 — h=36 so 1px overflow; set h=37? pad.h=36, bottom bun y+29..37 exceeds by 1. Adjust bottomBun y+28, h 8 → 28..36 ✓ and patty y+21? Let me re-tune: patty y+21..31 (h10), bottomBun y+29..37→ change to y+28..36 (h8, rr(x+7,y+28,w-14,8,[3,3,5,5])). patty y+20..30? Overlap patty/bottom: patty y+20 h10 → 20..30, bottomBun 28..36 ✓. cheese y+16..24, drip y+19..29. lettuce y+10..18, scallops base y+18 r5 → to 23. tomato y+8..15. topbun y..13, shade y+9..13.
Fine — set: topBun rr(x+4,y,w-8,14); tomato rr(x+3,y+8,w-6,7); lettuce rr(x-5,y+10,w+10,8) + scallops cy y+18; highlight rr(x+2,y+12,w-4,3); cheese rr(x-2,y+16,w+4,8); drip rr(x+w*0.2,y+19,12,10); patty rr(x+2,y+20,w-4,10); speckles y+25; bottomBun rr(x+7,y+28,w-14,8).
Heights: 36 total ✓.
drawSeedAt same as drawSeed — one function `seed(g,x,y,rot)`.
**Collision uses pad rect y..y+36 — visually burger extends lettuce slightly wider (-5) — use pad.w for physics (close enough).
**Now the AI perfect tracking with serveTimer only in demo. In play, after clear auto? level clear → attached, waiting for player input (click/space). OK.
**Also after startGame: popup "LEVEL 1" center? add popup('LEVEL 1', W/2, 300, '#E8432F', 26). Cute. Similarly each level: after clear ends popup `LEVEL ${S.level}`.
**updatePopups / parts loops backwards for splice.**
**Ball render:**
```js
function drawBall(){
// trail
for(let i=0;i<trail.length;i++){
const k=(i+1)/trail.length;
ctx.globalAlpha=k*0.22;
ctx.fillStyle='#E8483A';
ctx.beginPath(); ctx.arc(trail[i].x,trail[i].y, ball.r*k*0.85,0,7); ctx.fill();
}
ctx.globalAlpha=1;
ctx.beginPath(); ctx.arc(ball.x,ball.y,ball.r,0,7);
ctx.fillStyle='#E8432F'; ctx.fill();
ctx.lineWidth=2.5; ctx.strokeStyle='#B33326'; ctx.stroke();
ctx.beginPath(); ctx.ellipse(ball.x-2.8,ball.y-3.2,2.8,1.9,-0.6,0,7);
ctx.fillStyle='rgba(255,255,255,.8)'; ctx.fill();
}
```
**Full code assembly time. Also verify no leftover `alert` etc. None.**
Let me also add subtle canvas bg decorations: faint oversized hearts pattern behind bricks? Could clutter. Add very subtle dotted circles? Keep clean cream — good contrast with colorful bricks.
Actually one nice bg touch: faint dashed line at ball serve? no. Keep clean.
**Write the file.** Also remember to include everything and keep code robust. I'll write it now completely, and mentally trace for syntax errors.
Let me also double check `rr` usage with radii arrays everywhere defined.
Potential issue: `rr(g,x+4,y,w-8,14,[10,10,4,4])` — my rr destructures `[a,b,c2,d]` = [tl,tr,br,bl]. For top bun dome: big top radii [10,10,4,4] ✓.
In rr, arcTo radius must be ≤ available; clamped by m=min(w/2,h/2). h=14 → m=7, so 10→7 ✓ fine.
**Compute BRICK_W:**
```js
const COLS=10, SIDE=40, GAPX=8, TOP=54, BRICK_H=27, GAPY=10;
const BRICK_W=(W - SIDE*2 - GAPX*(COLS-1))/COLS; // (900-80-72)/10=74.8
```
Row y: TOP + r*(BRICK_H+GAPY) = 54 + r*37: rows 54,91,128,165,202,239 bottom 266 ✓.
**Check paddle reach:** pad.w 116, y 516..552.
Distance brick bottom 266 to paddle 516: 250 ✓.
**Level speed:** base = 320+ (level-1)*26 cap 540. Ramp: on break speed=min(cap? separate flight cap 600, speed+2.2).
Also slight speed up over time? enough.
**Test mental run of first frames:** buildLevel fills 60 bricks. ball attached at pad.x+0.18*116=+20.9. serveTimer 0.5 → launch angle -PI/2±0.125 → up. Ball travels to top wall? passes through bricks region — will hit a brick on the way ✓ (bricks span x 40..860 full width except gaps; ball from center x≈470 → hits row0 (bun) at y54..81). Substeps: speed 320*dt(0.016)=5.3px per frame < 6 → steps=1. Hit: dx,dy computed from nearest point. Ball moving up: dy negative large (ball above brick? ball comes from below: nearest point on brick bottom edge (ny=81), ball.y<ny → dy=ball.y-ny<0, dx small if near center of brick x-span. |dx|>|dy|? If ball hits bottom edge middle: dx≈0, |dy| up to r → dominant dy → flip vy ✓ down. breakBrick: hearts burst downward toward paddle ✓ nice.
Ball goes down, AI catches, etc.
**Combo popup position:** brick center.
**Now — powerup catch when attached (ball glued) also works (paddle collision independent) ✓.
**HUD update frequency:** called on events ✓.
**Let me now also guard `ctx.letterSpacing` reset after chunkText — set to '0px'.**
**Font loading: Arial Black available on Chrome/Windows/Mac ✓ (macOS has Arial Black). Impact on mac ✓. Fine fallback sans-serif.**
**overlay z-index above canvas ✓ (default order).**
**Write final code.** I'll aim for clean, commented (comments in English since user wrote English).
One more check on requirement: "Reply with exactly ONE fenced code block containing the complete file". ✓ only the html block, no prose. Hmm, but they also said explain implementation after code — the outer system prompt says reply with exactly one code block and no text before/after. The user instructions also say include explanation after code. The system prompt is stricter: "No text before or after the code block." Conflict: I'll follow the system-level formatting rule (single code block, nothing else) — the system prompt takes precedence and explicitly defines output rules for this task. Actually wait — user message says "After writing code, include a brief text explanation" but system says no text. System wins. I'll output only the code block. Hmm, but maybe a brief comment header inside the HTML can serve as explanation. I'll add concise comments in code.
Let me also double check there's no horizontal scrollbar at 375px: body padding 16, cabinet 96vw... width min(94vw,880) → 352px at 375 viewport ✓; canvas width 100% of that ✓; hud flex-wrap ✓; h1 clamp ✓; badge hidden <600px; hints wrap ✓. min-height 100vh with flex center — content taller than viewport on small phones → body scrolls vertically ok (no horizontal).
Also `overflow-x:hidden` on body for safety? add `max-width:100%`... body flex centering with fixed children — cabinet is in flow, fine. Add `overflow-x:hidden` to be safe? It can hide accidental spills; add on html,body — harmless.
**One subtle thing: pointermove before any move on touch devices — pointerX null → keyboard default. fine.
**Prevent text selection on rapid clicks: `user-select:none` on body; and `-webkit-tap-highlight-color: transparent`.
**Now write it all out.**
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Burger Breakout · Heartbreaker Diner</title>
<style>
... (as designed)
</style>
</head>
<body>
<main class="cabinet">
<header class="top">
<div class="brand">
<h1>BURGER<svg ...>...</svg>BREAKOUT</h1>
<p class="tag">SMASH THE STACK · SPREAD THE LOVE</p>
</div>
<div class="badge" aria-hidden="true">
<span>MADE WITH</span>
<svg viewBox="0 0 24 24"><path d="..."/></svg>
<span>REAL LOVE</span>
</div>
</header>
<div class="hud">
<div class="stat"><span class="lbl">SCORE</span><span class="val" id="scoreVal">000000</span></div>
<div class="stat"><span class="lbl">HEARTS</span><span class="val hearts" id="heartsVal">0</span></div>
<div class="stat"><span class="lbl">LEVEL</span><span class="val" id="levelVal">1</span></div>
<div class="lives" id="livesBox"></div>
<button id="muteBtn" aria-label="Toggle sound">...</button>
</div>
<div class="stage" id="stage">
<canvas id="game"></canvas>
<div class="overlay" id="overlay">
<div class="panel">
<p class="eyebrow" id="ovEyebrow"></p>
<h2 id="ovTitle"></h2>
<p class="meta" id="ovMeta"></p>
<p class="blink" id="ovBlink"></p>
</div>
</div>
</div>
<footer class="hints">
<span>MOVE · MOUSE OR ← →</span>
<span>LAUNCH · SPACE</span>
<span>PAUSE · P</span>
<span>SOUND · M</span>
</footer>
</main>
<script>
...
</script>
</body>
</html>
```
HEART path constant reused in HTML svgs — write path literal each time (3-4 places). The path: `M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z` — hmm let me double-check this classic material heart path from memory:
`M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z`
I believe that's right (the favorite icon). Even if slightly off it still looks like a heart. Good.
Badge svg heart small mustard w/ ink stroke.
Now the JS in full. Let me write it carefully:
```js
(() => {
'use strict';
/* ---------- helpers ---------- */
const $ = id => document.getElementById(id);
const clamp = (v,a,b)=> v<a?a:(v>b?b:v);
const rand = (a,b)=> a + Math.random()*(b-a);
const pick = arr => arr[(Math.random()*arr.length)|0];
const backOut = t => { const c1=1.70158, c3=c1+1; return 1 + c3*Math.pow(t-1,3) + c1*Math.pow(t-1,2); };
function rr(g,x,y,w,h,r){
let a,b,c,d;
if(Array.isArray(r)){ [a,b,c,d]=r; } else { a=b=c=d=r; }
const m=Math.min(w/2,h/2);
a=Math.min(a,m); b=Math.min(b,m); c=Math.min(c,m); d=Math.min(d,m);
g.beginPath();
g.moveTo(x+a,y);
g.lineTo(x+w-b,y); g.arcTo(x+w,y, x+w,y+b, b);
g.lineTo(x+w,y+h-c); g.arcTo(x+w,y+h, x+w-c,y+h, c);
g.lineTo(x+d,y+h); g.arcTo(x,y+h, x,y+h-d, d);
g.lineTo(x,y+a); g.arcTo(x,y, x+a,y, a);
g.closePath();
}
```
Wait arcTo(x, y, x+a, y, a): from current point (x, y+a) arcTo corner (x,y) end (x+a,y) radius a ✓.
```js
/* ---------- canvas ---------- */
const W=900,H=600, WALL=14;
const cv=$('game'), ctx=cv.getContext('2d');
const DPR=Math.min(2, window.devicePixelRatio||1);
cv.width=W*DPR; cv.height=H*DPR;
/* ---------- palette ---------- */
const INK='#4A2A18';
const HEART_COLORS=['#F43F5E','#FF5A7E','#E8432F','#FF8FA3','#FB7185','#D6336C','#FFB3C1','#FFC233'];
const CRUMB_COLORS={bun:'#EDAA62',lettuce:'#7FC94E',tomato:'#E8483A',cheese:'#FFC233',patty:'#7A4A26',bun2:'#E2955A'};
/* ---------- sprites: hearts ---------- */
function heartPath(g,s){
g.beginPath();
g.moveTo(0,.5*s);
g.bezierCurveTo(-.62*s,.05*s,-.62*s,-.5*s,-.28*s,-.5*s);
g.bezierCurveTo(-.08*s,-.5*s,0,-.34*s,0,-.18*s);
g.bezierCurveTo(0,-.34*s,.08*s,-.5*s,.28*s,-.5*s);
g.bezierCurveTo(.62*s,-.5*s,.62*s,.05*s,0,.5*s);
g.closePath();
}
const heartSprites={};
function heartSprite(color, outline){
const key=color+(outline?'*':'');
if(heartSprites[key]) return heartSprites[key];
const c=document.createElement('canvas'); c.width=c.height=96;
const g=c.getContext('2d'); g.translate(48,48);
heartPath(g,62);
if(outline){ g.lineWidth=10; g.lineJoin='round'; g.strokeStyle='#FFFBF0'; g.stroke(); }
g.fillStyle=color; g.fill();
g.lineWidth=2.5; g.strokeStyle='rgba(74,42,24,.35)'; g.stroke();
g.beginPath(); g.ellipse(-13,-16,7,4.5,-0.7,0,Math.PI*2);
g.fillStyle='rgba(255,255,255,.65)'; g.fill();
heartSprites[key]=c; return c;
}
```
Wait: heart s=62: y range -31..31, x ±~38 → fits 96 with outline stroke 5 outside → 43 < 48 ✓.
```js
/* ---------- sprites: bricks ---------- */
const COLS=10, SIDE=40, GAPX=8, TOP=54, BRICK_H=27, GAPY=10;
const BRICK_W=(W - SIDE*2 - GAPX*(COLS-1))/COLS;
function seedDot(g,x,y,rot){
g.save(); g.translate(x,y); g.rotate(rot);
g.beginPath(); g.ellipse(0,0,4.6,2.3,0,0,Math.PI*2); g.fill(); g.restore();
}
function makeBrickSprite(type,damaged){
const c=document.createElement('canvas');
c.width=Math.ceil(BRICK_W*DPR); c.height=Math.ceil((BRICK_H+4)*DPR);
const g=c.getContext('2d'); g.scale(DPR,DPR);
const w=BRICK_W, h=BRICK_H;
g.fillStyle='rgba(74,42,24,.16)'; rr(g,0,3,w,h,8); g.fill(); // flat drop shadow
g.lineJoin='round'; g.lineWidth=2.5; g.strokeStyle='rgba(74,42,24,.85)';
if(type==='bun' || type==='bun2'){
const base = type==='bun' ? '#EDAA62' : '#E2955A';
const dark = type==='bun' ? '#D18A45' : '#C97F44';
g.fillStyle=base; rr(g,0,0,w,h, type==='bun'?8:[8,8,11,11]); g.fill(); g.stroke();
g.save(); rr(g,0,0,w,h,8); g.clip();
g.fillStyle=dark; g.fillRect(0,h-8,w,8);
g.fillStyle='rgba(255,255,255,.3)'; g.fillRect(6,3,w*0.4,4);
g.restore();
if(type==='bun'){ g.fillStyle='#FBE7C6'; seedDot(g,w*0.2,h*0.45,-0.5); seedDot(g,w*0.5,h*0.35,0.25); seedDot(g,w*0.8,h*0.45,0.55); }
} else if(type==='lettuce'){
g.fillStyle='#7FC94E'; rr(g,0,0,w,h-4,8); g.fill(); g.stroke();
g.fillStyle='#7FC94E';
for(let i=0;i<6;i++){ g.beginPath(); g.arc(w*(i+0.5)/6, h-6, 6, 0, Math.PI); g.fill(); }
g.fillStyle='#A5DE79'; rr(g,6,4,w-12,4,2); g.fill();
} else if(type==='tomato'){
g.fillStyle='#C33327'; rr(g,0,0,w,h,9); g.fill(); g.stroke();
g.fillStyle='#E8483A'; rr(g,3,3,w-6,h-7,7); g.fill();
g.fillStyle='#F06A57'; rr(g,7,5,w-14,4,2); g.fill();
g.fillStyle='#F9A08F';
g.beginPath(); g.arc(w*0.3,h*0.62,2,0,7); g.fill();
g.beginPath(); g.arc(w*0.55,h*0.7,1.6,0,7); g.fill();
g.beginPath(); g.arc(w*0.75,h*0.58,1.8,0,7); g.fill();
} else if(type==='cheese'){
g.fillStyle='#FFC233'; rr(g,0,1,w,h-2,6); g.fill(); g.stroke();
rr(g,w*0.28,h-9,11,10,5); g.fill(); // drip
g.fillStyle='#FFD966'; rr(g,5,4,w-10,4,2); g.fill();
g.fillStyle='#E8A81C';
g.beginPath(); g.arc(w*0.62,h*0.5,3.4,0,7); g.fill();
g.beginPath(); g.arc(w*0.78,h*0.66,2.4,0,7); g.fill();
} else if(type==='patty'){
g.fillStyle='#7A4A26'; rr(g,0,0,w,h,7); g.fill(); g.stroke();
g.fillStyle='#8F5B31'; rr(g,4,3,w-8,5,3); g.fill();
g.fillStyle='#5E3618';
[[.14,.68],[.3,.55],[.48,.7],[.64,.56],[.8,.66],[.92,.5]].forEach(p=>{ g.beginPath(); g.arc(w*p[0],h*p[1],2.2,0,7); g.fill(); });
if(damaged){
g.strokeStyle='#3F2410'; g.lineWidth=2.2; g.lineCap='round';
g.beginPath(); g.moveTo(w*0.2,h*0.95); g.lineTo(w*0.34,h*0.5); g.lineTo(w*0.26,h*0.3); g.stroke();
g.beginPath(); g.moveTo(w*0.72,h); g.lineTo(w*0.66,h*0.55); g.lineTo(w*0.8,h*0.35); g.stroke();
}
}
return c;
}
const BRICK_SPRITES={};
['bun','lettuce','tomato','cheese','bun2'].forEach(t=>BRICK_SPRITES[t]=[makeBrickSprite(t,false)]);
BRICK_SPRITES.patty=[makeBrickSprite('patty',false), makeBrickSprite('patty',true)];
```
Careful: 0,7 as end angle for arcs (7 > 2π) fine.
Lettuce scallop arcs: `g.arc(cx, h-6, 6, 0, Math.PI)` — from angle 0 to PI = lower semicircle (canvas angles clockwise, 0..PI covers bottom half ✓).
**State, entities:**
```js
const pad={x:W/2, y:H-84, w:116, h:36};
const ball={x:W/2, y:0, vx:0, vy:0, r:9, speed:320, attached:true};
const S={mode:'demo', score:0, level:1, lives:3, hearts:0, combo:0, bestCombo:0,
bricks:[], brickCount:0, clearing:false, clearTimer:0, squash:0, shake:0, t:0};
const parts=[], popups=[], trail=[], powerups=[];
let serveTimer=0, paused=false, pointerX=null, lastBonus=0;
const keys={};
const baseSpeed=()=> Math.min(540, 320 + (S.level-1)*26);
```
Rows config:
```js
const ROWS=[
{t:'bun', hp:1},{t:'lettuce',hp:1},{t:'tomato',hp:1},
{t:'cheese',hp:1},{t:'patty',hp:2},{t:'bun2',hp:1}
];
function buildLevel(){
S.bricks.length=0; S.brickCount=0;
ROWS.forEach((rd,ri)=>{
for(let ci=0;ci<COLS;ci++){
S.bricks.push({x:SIDE+ci*(BRICK_W+GAPX), y:TOP+ri*(BRICK_H+GAPY), w:BRICK_W, h:BRICK_H, type:rd.t, hp:rd.hp, maxHp:rd.hp});
S.brickCount++;
}
});
}
```
Sound module (as drafted).
Particles:
```js
function spawnHearts(x,y,extra){
const n = 14 + Math.min(12, extra*2) + (Math.random()*4|0);
for(let i=0;i<n;i++){
const a=rand(0,Math.PI*2), sp=rand(50,290);
parts.push({type:'heart', x, y, vx:Math.cos(a)*sp*1.15, vy:Math.sin(a)*sp-150,
rot:rand(-.6,.6), rv:rand(-9,9), s:rand(4.5,11), life:rand(.75,1.45), age:0,
g:780, color:pick(HEART_COLORS)});
}
for(let i=0;i<4;i++){
parts.push({type:'dot', x:x+rand(-14,14), y:y+rand(-8,8), vx:rand(-90,90), vy:rand(-160,40),
life:rand(.4,.7), age:0, g:500, color: Math.random()<.5?'#FFFFFF':'#FFD966', s:rand(1.6,3)});
}
for(let i=0;i<3;i++){
parts.push({type:'crumb', x:x+rand(-20,20), y:y+rand(-8,8), vx:rand(-140,140), vy:rand(-220,-40),
life:rand(.5,.9), age:0, g:900, rot:rand(0,3), rv:rand(-10,10), color:CRUMB_COLORS[extraType]});
}
...
```
Hmm crumb color needs brick type — pass type param. Change signature spawnHearts(x,y,type,comboExtra). Let me define breakBrick calling spawnHearts(b, combo).
Rewrite:
```js
function brickBurst(b, combo){
const n = 14 + Math.min(12, combo*2) + (Math.random()*4|0);
for(let i=0;i<n;i++){ ... }
for dots 4...
for crumbs 3 with color CRUMB_COLORS[b.type]...
return n;
}
```
grayBurst(x,y): 10 gray hearts + 6 dots gray.
heartRain(): as designed.
sesameBurst(): 3 seeds from paddle top: parts.push({type:'seed', x:pad.x+rand(-30,30), y:pad.y+3, vx:rand(-70,70), vy:rand(-180,-60), life:rand(.5,.8), age:0, g:800, rot:rand(0,3), rv:rand(-8,8)}).
updateParticles:
```js
for(let i=parts.length-1;i>=0;i--){
const p=parts[i];
p.age+=dt;
if(p.age>=p.life){ parts.splice(i,1); continue; }
p.vy += (p.g!==undefined?p.g:760)*dt;
p.x += p.vx*dt; p.y += p.vy*dt;
if(p.rv) p.rot+=p.rv*dt;
if(p.sw) p.x+=Math.sin(p.age*3+p.sw)*16*dt;
if(p.type==='heart' && p.y>H-26 && p.vy>0){ p.y=H-26; p.vy*=-0.42; p.vx*=0.65; if(p.rv)p.rv*=0.6; }
if(p.type==='seed'||p.type==='crumb'){ if(p.y>H-24){p.y=H-24; p.vy*=-0.3;} }
}
if(parts.length>430) parts.splice(0, parts.length-430);
```
popups: updatePopups: age, remove.
powerups update as designed; store on S? use `powerups` array.
```js
function updatePowerups(dt){
for(let i=powerups.length-1;i>=0;i--){
const u=powerups[i];
u.age+=dt; u.vy=Math.min(150,u.vy+40*dt); u.y+=u.vy*dt;
u.x=u.bx+Math.sin(u.age*2.4+u.ph)*16;
if(u.y>pad.y-8 && u.y<pad.y+pad.h+10 && Math.abs(u.x-pad.x)<pad.w/2+14){
powerups.splice(i,1); catchHeart(u.x,pad.y); continue;
}
if(u.y>H-24){ powerups.splice(i,1); }
}
}
function catchHeart(x,y){
if(S.lives<5){ S.lives++; popup(x,y-30,'+1 LIFE','#E8432F',20); }
else { S.score+=250; popup(x,y-30,'+250','#E8432F',20); }
for(let i=0;i<12;i++){ parts.push({type:'heart',x,y:y+6,vx:rand(-160,160),vy:rand(-320,-120),rot:rand(-.5,.5),rv:rand(-7,7),s:rand(4,8),life:rand(.6,1),age:0,g:700,color:pick(HEART_COLORS)}); }
sfx.power(); updateHUD();
}
```
popup(x,y,txt,color,size): popups.push({x,y,txt,color,size:size||18,age:0,life:.9}).
**bricks & ball physics:**
```js
function stepBall(dt){
ball.x+=ball.vx*dt; ball.y+=ball.vy*dt;
const r=ball.r;
if(ball.x<WALL+r){ ball.x=WALL+r; ball.vx=Math.abs(ball.vx); sfx.wall(); }
else if(ball.x>W-WALL-r){ ball.x=W-WALL-r; ball.vx=-Math.abs(ball.vx); sfx.wall(); }
if(ball.y<WALL+r){ ball.y=WALL+r; ball.vy=Math.abs(ball.vy); sfx.wall(); }
if(ball.vy>0 && ball.y < pad.y+pad.h){
const nx=clamp(ball.x, pad.x-pad.w/2, pad.x+pad.w/2);
const ny=clamp(ball.y, pad.y, pad.y+pad.h);
const dx=ball.x-nx, dy=ball.y-ny;
if(dx*dx+dy*dy <= r*r){ paddleBounce(); }
}
for(const b of S.bricks){
if(b.hp<=0) continue;
const nx=clamp(ball.x,b.x,b.x+b.w), ny=clamp(ball.y,b.y,b.y+b.h);
const dx=ball.x-nx, dy=ball.y-ny;
if(dx*dx+dy*dy <= r*r){
if(Math.abs(dx)>Math.abs(dy)){
ball.vx = dx>0?Math.abs(ball.vx):-Math.abs(ball.vx);
ball.x = dx>0? b.x+b.w+r+0.5 : b.x-r-0.5;
} else {
ball.vy = dy>0?Math.abs(ball.vy):-Math.abs(ball.vy);
ball.y = dy>0? b.y+b.h+r+0.5 : b.y-r-0.5;
}
hitBrick(b);
break;
}
}
if(Math.abs(ball.vy)<50 && !ball.attached){ // never flat
ball.vy = ball.vy===0? -50 : (ball.vy<0?-50:50);
const m=Math.hypot(ball.vx,ball.vy); ball.vx*=ball.speed/m; ball.vy*=ball.speed/m;
}
if(ball.y-r > H-26){ loseBall(); return true; }
return false;
}
```
Hmm the anti-flat check each substep fine.
paddleBounce:
```js
function paddleBounce(){
const rel=clamp((ball.x-pad.x)/(pad.w/2),-1,1);
const ang=rel*1.02;
ball.vx=Math.sin(ang)*ball.speed;
ball.vy=-Math.cos(ang)*ball.speed;
ball.y=pad.y-ball.r-0.5;
S.squash=1; S.combo=0;
sesameBurst(); sfx.paddle();
}
```
hitBrick:
```js
function hitBrick(b){
b.hp--;
if(b.hp>0){
sfx.thud(); S.shake=Math.max(S.shake,1.6);
for(let i=0;i<3;i++) parts.push({type:'crumb',x:ball.x,y:ball.y,vx:rand(-120,120),vy:rand(-200,-40),life:rand(.4,.7),age:0,g:900,rot:rand(0,3),rv:rand(-9,9),color:CRUMB_COLORS[b.type]});
popup(b.x+b.w/2,b.y, 'CRUNCH','#6B4A2F',13);
return;
}
breakBrick(b);
}
```
'CRUNCH' popup on patty first hit — fun.
breakBrick:
```js
function breakBrick(b){
S.combo++; if(S.combo>S.bestCombo) S.bestCombo=S.combo;
const pts=25+(S.combo-1)*10+(S.level-1)*5;
S.score+=pts;
const cx=b.x+b.w/2, cy=b.y+b.h/2;
S.hearts+=brickBurst(b,S.combo);
S.shake=Math.max(S.shake,2.6);
S.brickCount--;
ball.speed=Math.min(600, ball.speed+2.2);
const m=Math.hypot(ball.vx,ball.vy); ball.vx*=ball.speed/m; ball.vy*=ball.speed/m;
popup(cx,cy,'+'+pts, INK, 17);
if(S.combo>=2) popup(cx,cy+22,'COMBO ×'+S.combo,'#E8432F',13);
sfx.pop(S.combo);
if(S.lives<5 && Math.random()<0.09){
powerups.push({bx:cx,x:cx,y:cy,vy:50,ph:rand(0,6),age:0,s:16});
}
if(S.brickCount<=0) levelCleared();
updateHUD();
}
```
levelCleared:
```js
function levelCleared(){
S.clearing=true; S.clearTimer=1.9; ball.attached=true; ball.vx=ball.vy=0;
lastBonus=200+100*(S.level-1);
S.score+=lastBonus;
popup(W/2,330,'+'+lastBonus+' BONUS','#E8432F',22);
heartRain(); sfx.clear(); updateHUD();
}
```
clear tick inside update:
```js
if(S.clearing){
S.clearTimer-=dt;
if(S.clearTimer<=0){
S.clearing=false; S.level++;
buildLevel(); ball.speed=baseSpeed(); ball.attached=true;
if(S.mode==='demo') serveTimer=0.7;
popup(W/2,300,'LEVEL '+S.level,'#E8432F',24);
updateHUD();
}
}
```
loseBall:
```js
function loseBall(){
sfx.lose(); S.shake=7; S.combo=0;
grayBurst(clamp(ball.x,30,W-30), H-40);
ball.speed=baseSpeed();
if(S.mode==='demo'){ ball.attached=true; serveTimer=0.9; }
else {
S.lives--; updateHUD();
if(S.lives<=0){ gameOver(); }
else { ball.attached=true; }
}
}
```
gameOver:
```js
function gameOver(){
S.mode='over'; sfx.over();
showOverlay('OUT OF BURGERS','GAME OVER',
`SCORE ${fmtScore(S.score)} · LEVEL ${S.level} · BEST COMBO ×${S.bestCombo}<br>HEARTS SPREAD · ${S.hearts}`,
'CLICK OR PRESS ANY KEY TO PLAY AGAIN');
}
```
startGame:
```js
function startGame(){
S.score=0; S.level=1; S.lives=3; S.hearts=0; S.combo=0; S.bestCombo=0;
S.clearing=false; powerups.length=0; popups.length=0;
buildLevel();
ball.speed=baseSpeed(); ball.attached=true; ball.vx=ball.vy=0;
pad.x=W/2;
S.mode='play'; paused=false; hideOverlay(); updateHUD();
popup(W/2,300,'LEVEL 1','#E8432F',24);
}
```
launch:
```js
function launch(){
if(!ball.attached) return;
ball.attached=false;
const a=-Math.PI/2+rand(-0.3,0.3);
ball.vx=Math.cos(a)*ball.speed; ball.vy=Math.sin(a)*ball.speed;
S.squash=0.6; sfx.launch(); trail.length=0;
}
```
update:
```js
function update(dt){
S.t+=dt;
if(S.clearing) tickClear(dt);
if(S.mode!=='over'){ updatePaddle(dt); updateBall(dt); updatePowerups(dt); }
updateParticles(dt); updatePopups(dt);
S.squash=Math.max(0,S.squash-dt*4.2);
S.shake*=Math.exp(-7*dt); if(S.shake<0.05) S.shake=0;
}
```
updateBall:
```js
function updateBall(dt){
if(ball.attached){
ball.x=pad.x+pad.w*0.18; ball.y=pad.y-ball.r-2;
trail.length=0;
if(S.mode==='demo'){ serveTimer-=dt; if(serveTimer<=0) launch(); }
return;
}
trail.push({x:ball.x,y:ball.y}); if(trail.length>10) trail.shift();
const steps=Math.max(1, Math.ceil(ball.speed*dt/6));
for(let i=0;i<steps;i++){ if(stepBall(dt/steps)) break; }
}
```
updatePaddle:
```js
function updatePaddle(dt){
if(S.mode==='demo'){
let target;
if(ball.attached) target=W/2+Math.sin(S.t*0.55)*160;
else if(ball.vy>0) target=predictX();
else target=W/2+(ball.x-W/2)*0.35;
target+=Math.sin(S.t*2.1)*9;
pad.x+=clamp(target-pad.x,-640*dt,640*dt);
} else {
const kb=((keys['arrowright']||keys['d'])?1:0)-((keys['arrowleft']||keys['a'])?1:0);
if(kb) pad.x+=kb*640*dt;
else if(pointerX!=null) pad.x+=(pointerX-pad.x)*Math.min(1,16*dt);
}
pad.x=clamp(pad.x, WALL+pad.w/2+2, W-WALL-pad.w/2-2);
}
```
predictX as designed.
**Rendering:**
```js
function render(){
ctx.setTransform(DPR,0,0,DPR,0,0);
ctx.fillStyle='#FFF9EC'; ctx.fillRect(0,0,W,H);
if(S.shake>0.05) ctx.translate(rand(-1,1)*S.shake, rand(-1,1)*S.shake);
drawFloor();
drawBricks();
drawPowerups();
drawBall();
drawBurger();
drawParticles();
drawPopups();
if(S.clearing) drawClearBanner();
if(S.mode==='play' && ball.attached && !S.clearing) drawServeHint();
}
```
drawFloor, drawBricks, drawBall, drawBurger, drawParticles, drawPopups, drawPowerups, drawClearBanner, drawServeHint, chunkText — as designed above.
drawBricks:
```js
for(const b of S.bricks){
if(b.hp<=0) continue;
const set=BRICK_SPRITES[b.type];
const spr= (b.hp<b.maxHp && set.length>1)? set[1] : set[0];
ctx.drawImage(spr, b.x, b.y, BRICK_W, BRICK_H+4);
}
```
drawParticles — with globalAlpha reset at end.
drawPopups:
```js
ctx.textAlign='center'; ctx.textBaseline='middle';
for(const p of popups){
const k=p.age/p.life;
ctx.globalAlpha=1-k*k;
chunkText(p.txt, p.x, p.y-44*k, p.size, p.color);
}
ctx.globalAlpha=1;
```
chunkText includes stroke.
chunkText:
```js
function chunkText(txt,x,y,size,fill,ls){
ctx.font=`900 ${size}px 'Arial Black','Arial',sans-serif`;
ctx.letterSpacing=(ls||'1.5px');
ctx.lineJoin='round';
ctx.lineWidth=Math.max(3,size/5);
ctx.strokeStyle=INK;
ctx.strokeText(txt,x,y);
ctx.fillStyle=fill;
ctx.fillText(txt,x,y);
ctx.letterSpacing='0px';
}
```
drawClearBanner:
```js
const y=272;
ctx.save();
ctx.shadowColor='rgba(74,42,24,.25)'; ctx.shadowBlur=0; ctx.shadowOffsetX=8; ctx.shadowOffsetY=9;
rr(ctx,W/2-235,y-64,470,128,18); ctx.fillStyle='#FFFBF0'; ctx.fill();
ctx.shadowColor='transparent';
ctx.lineWidth=5; ctx.strokeStyle=INK; ctx.stroke();
ctx.restore();
chunkText('STACK CLEARED!',W/2,y-18,34,'#E8432F','2px');
ctx.font='700 13px "Trebuchet MS",Verdana,sans-serif';
ctx.fillStyle='#6B4A2F'; ctx.textAlign='center'; ctx.textBaseline='middle';
ctx.fillText(`LEVEL ${S.level+1} INCOMING · +${lastBonus} PTS`, W/2, y+26);
```
Wait — stroke path: after fill with shadow, the path persists; stroke after resetting shadow ✓ but I restored ctx which resets path? No — path is not part of save/restore state? Actually the current path IS preserved across save/restore. Path is not saved/restored. ✓ ok. But shadow set inside save then restored... I set shadowOffset then fill, then set shadowColor transparent then stroke, then restore — fine. Simpler: skip shadow, use offset fill: draw a second rr offset fill ink first then main fill. Let me simplify:
```js
rr(ctx,W/2-235+7,y-64+8,470,128,18); ctx.fillStyle='rgba(74,42,24,.2)'; ctx.fill();
rr(ctx,W/2-235,y-64,470,128,18); ctx.fillStyle='#FFFBF0'; ctx.fill();
ctx.lineWidth=5; ctx.strokeStyle=INK; ctx.stroke();
```
✓ flat offset shadow, consistent style.
drawServeHint as designed.
**Overlay & HUD wiring:**
```js
const ov=$('overlay'), ovEyebrow=$('ovEyebrow'), ovTitle=$('ovTitle'), ovMeta=$('ovMeta'), ovBlink=$('ovBlink');
function showOverlay(eyebrow,title,meta,blink){
ovEyebrow.textContent=eyebrow; ovTitle.textContent=title;
ovMeta.innerHTML=meta; ovBlink.textContent=blink;
ov.classList.remove('hidden');
}
function hideOverlay(){ ov.classList.add('hidden'); }
const elScore=$('scoreVal'), elHearts=$('heartsVal'), elLevel=$('levelVal'), elLives=$('livesBox');
const fmtScore=n=>String(n).padStart(6,'0');
const HEART_D='M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z';
function renderLives(){
const slots=Math.max(3, Math.min(5, S.lives));
let h='';
for(let i=0;i<slots;i++){
const on=i<S.lives;
h+=`<svg viewBox="0 0 24 24" width="20" height="20" aria-hidden="true"><path d="${HEART_D}" fill="${on?'#E8432F':'none'}" stroke="${on?'#4A2A18':'#C9AE8C'}" stroke-width="2" stroke-linejoin="round"/></svg>`;
}
elLives.innerHTML=h;
}
function updateHUD(){
elScore.textContent=fmtScore(S.score);
elHearts.textContent=S.hearts;
elLevel.textContent=S.level;
renderLives();
}
```
Hmm slots when lives=4: slots=4 all filled; lives=5: 5 filled; lives=2: slots=3, 2 filled 1 outline; lives=0 → game over anyway.
**Input handlers:**
```js
window.addEventListener('keydown', e=>{
const k=e.key.toLowerCase();
if([' ','arrowleft','arrowright','a','d','p','m','enter'].includes(k)) e.preventDefault();
keys[k]=true;
sfx.ensure();
if(k==='m'){ toggleMute(); return; }
if(k==='p'){
if(paused){ paused=false; hideOverlay(); }
else if(S.mode==='play'){ paused=true; showOverlay('KITCHEN CLOSED','PAUSED','','PRESS P TO RESUME'); }
return;
}
if(paused) return;
if(S.mode==='demo'||S.mode==='over'){ startGame(); return; }
if(S.mode==='play' && ball.attached && (k===' '||k==='enter')) launch();
});
window.addEventListener('keyup', e=>{ keys[e.key.toLowerCase()]=false; });
window.addEventListener('pointermove', e=>{
const r=cv.getBoundingClientRect();
if(r.width>0) pointerX=(e.clientX-r.left)/r.width*W;
});
const stage=$('stage');
stage.addEventListener('pointerdown', e=>{
sfx.ensure();
if(paused) return;
if(S.mode==='demo'||S.mode==='over'){ startGame(); return; }
if(S.mode==='play' && ball.attached) launch();
});
window.addEventListener('blur', ()=>{ if(S.mode==='play' && !paused){ paused=true; showOverlay('KITCHEN CLOSED','PAUSED','','PRESS P TO RESUME'); } });
```
preventDefault list includes 'p' — typing p anywhere prevented default — no inputs on page, fine.
Mute:
```js
let muted=false;
function toggleMute(){
muted=!muted; sfx.muted=muted;
$('icoOn').style.display=muted?'none':'block';
$('icoOff').style.display=muted?'block':'none';
}
$('muteBtn').addEventListener('click', e=>{ e.stopPropagation(); sfx.ensure(); toggleMute(); });
```
Note: mute button is outside stage, no conflict. click also triggers... button not inside stage ✓.
**sfx module:**
```js
const sfx=(()=>{
let ac=null, muted=false;
function ensure(){
if(!ac){ try{ ac=new (window.AudioContext||window.webkitAudioContext)(); }catch(err){ ac=null; } }
if(ac && ac.state==='suspended') ac.resume().catch(()=>{});
}
function tone(f0,f1,dur,type,vol,delay){
if(muted||!ac||ac.state!=='running') return;
try{
const t0=ac.currentTime+(delay||0);
const o=ac.createOscillator(), g=ac.createGain();
o.type=type;
o.frequency.setValueAtTime(Math.max(30,f0),t0);
if(f1&&f1!==f0) o.frequency.exponentialRampToValueAtTime(Math.max(30,f1),t0+dur);
g.gain.setValueAtTime(vol,t0);
g.gain.exponentialRampToValueAtTime(0.0001,t0+dur);
o.connect(g); g.connect(ac.destination);
o.start(t0); o.stop(t0+dur+0.03);
}catch(err){}
}
return {
ensure,
set muted(v){ muted=v; },
wall(){ tone(340,300,.045,'square',.045); },
paddle(){ tone(200,120,.08,'triangle',.22); },
thud(){ tone(150,95,.07,'square',.16); },
pop(c){ const f=Math.min(1150,470+Math.min(c,14)*45); tone(f,f*1.35,.09,'square',.13); tone(f*1.5,f*2.1,.08,'triangle',.07,.03); },
power(){ tone(640,640,.09,'triangle',.16); tone(960,960,.13,'triangle',.16,.1); },
lose(){ tone(330,85,.5,'sawtooth',.17); },
clear(){ [523,659,784,1047].forEach((f,i)=>tone(f,f,.13,'square',.13,i*.09)); },
over(){ [392,330,262,196].forEach((f,i)=>tone(f,f*.97,.22,'triangle',.16,i*.17)); },
launch(){ tone(250,520,.09,'triangle',.15); }
};
})();
```
`set muted(v)` — property setter in object literal ✓.
Note `ac.state!=='running'` guard prevents scheduling while suspended ✓.
**Loop:**
```js
let last=performance.now();
function frame(now){
const dt=Math.min(0.033,(now-last)/1000)||0.016;
last=now;
if(!paused) update(dt);
render();
requestAnimationFrame(frame);
}
```
First frame now-last small ✓. dt guard: if 0? (now-last) could be 0 → dt 0 fine (min .033, ||0.016 only if NaN/0 → 0||0.016=0.016; 0 is falsy → 0.016. hmm dt=0 unlikely; fine).
**Init:**
```js
buildLevel();
ball.speed=baseSpeed(); ball.attached=true; serveTimer=0.5;
updateHUD();
showOverlay('THE AUTO-CHEF IS COOKING','TAKE A BITE?',
'MOVE · MOUSE OR ← → LAUNCH · SPACE<br>CATCH THE BIG HEARTS FOR EXTRA LIVES',
'CLICK OR PRESS ANY KEY TO START');
requestAnimationFrame(frame);
})();
```
Overlay title 'TAKE A BITE?' — fun.
**Wait: overlay hidden class initial?** Overlay starts visible with demo message ✓ (HTML has content empty; JS fills on init).
**Also add popups update fn:**
```js
function updatePopups(dt){
for(let i=popups.length-1;i>=0;i--){ popups[i].age+=dt; if(popups[i].age>=popups[i].life) popups.splice(i,1); }
}
```
**grayBurst:**
```js
function grayBurst(x,y){
for(let i=0;i<10;i++) parts.push({type:'heart',x,y,vx:rand(-160,160),vy:rand(-260,-60),rot:rand(-.5,.5),rv:rand(-6,6),s:rand(5,9),life:rand(.7,1.1),age:0,g:820,color:'#B9AA9C'});
for(let i=0;i<5;i++) parts.push({type:'dot',x:x+rand(-12,12),y:y+rand(-6,6),vx:rand(-60,60),vy:rand(-120,0),life:rand(.4,.6),age:0,g:600,color:'#C9AE8C',s:2.4});
}
```
**heartRain:**
```js
function heartRain(){
for(let i=0;i<55;i++) parts.push({type:'heart',x:rand(20,W-20),y:rand(-220,-10),vx:rand(-40,40),vy:rand(60,170),rot:rand(-.4,.4),rv:rand(-3,3),s:rand(5,11),life:rand(2,2.8),age:0,g:120,color:pick(HEART_COLORS),sw:rand(0,6)});
}
```
**brickBurst final:**
```js
function brickBurst(b,combo){
const x=b.x+b.w/2, y=b.y+b.h/2;
const n=14+Math.min(12,combo*2)+(Math.random()*4|0);
for(let i=0;i<n;i++){
const a=rand(0,Math.PI*2), sp=rand(60,300);
parts.push({type:'heart',x,y,vx:Math.cos(a)*sp*1.15,vy:Math.sin(a)*sp-150,rot:rand(-.6,.6),rv:rand(-9,9),s:rand(4.5,11),life:rand(.75,1.45),age:0,g:780,color:pick(HEART_COLORS)});
}
for(let i=0;i<4;i++) parts.push({type:'dot',x:x+rand(-16,16),y:y+rand(-8,8),vx:rand(-100,100),vy:rand(-180,20),life:rand(.4,.7),age:0,g:520,color:Math.random()<.5?'#FFFFFF':'#FFD966',s:rand(1.6,3)});
for(let i=0;i<3;i++) parts.push({type:'crumb',x:x+rand(-22,22),y:y+rand(-8,8),vx:rand(-150,150),vy:rand(-240,-40),life:rand(.5,.9),age:0,g:900,rot:rand(0,3),rv:rand(-10,10),color:CRUMB_COLORS[b.type]});
return n;
}
```
**drawParticles:**
```js
function drawParticles(){
for(const p of parts){
const k=p.age/p.life;
if(p.type==='heart'){
let sc=p.age<0.12? backOut(p.age/0.12) : 1;
if(k>0.72) sc*=1-((k-0.72)/0.28)*0.45;
const al=k>0.6? 1-(k-0.6)/0.4 : 1;
const spr=heartSprite(p.color);
const s=p.s*2.1*sc;
ctx.globalAlpha=Math.max(0,Math.min(1,al));
ctx.save(); ctx.translate(p.x,p.y); ctx.rotate(p.rot);
ctx.drawImage(spr,-s/2,-s/2,s,s);
ctx.restore();
} else if(p.type==='dot'){
ctx.globalAlpha=1-k;
ctx.fillStyle=p.color;
ctx.beginPath(); ctx.arc(p.x,p.y,(p.s||2.4)*(1-k*0.4),0,7); ctx.fill();
} else if(p.type==='crumb'){
ctx.globalAlpha=1-k;
ctx.save(); ctx.translate(p.x,p.y); ctx.rotate(p.rot);
ctx.fillStyle=p.color; rr(ctx,-4,-3,8,6,2); ctx.fill();
ctx.restore();
} else if(p.type==='seed'){
ctx.globalAlpha=1-k;
ctx.save(); ctx.translate(p.x,p.y); ctx.rotate(p.rot);
ctx.fillStyle='#FBE7C6';
ctx.beginPath(); ctx.ellipse(0,0,4.4,2.2,0,0,7); ctx.fill();
ctx.restore();
}
}
ctx.globalAlpha=1;
}
```
**drawPowerups:**
```js
function drawPowerups(){
const spr=heartSprite('#F43F5E',true);
for(const u of powerups){
const pu=1+Math.sin(S.t*6+u.ph)*0.12;
const s=u.s*2*pu;
ctx.save(); ctx.translate(u.x,u.y); ctx.rotate(Math.sin(S.t*2.2+u.ph)*0.16);
ctx.drawImage(spr,-s/2,-s/2,s,s);
ctx.restore();
}
}
```
**drawServeHint & drawClearBanner & chunkText & drawFloor** as above.
**Check ball hidden while clearing? attached shows at paddle; fine.**
**HUD hearts val maybe long (4 digits) fine.**
**Meta viewport ✓.**
**Everything drawn — let me double check draw order issue: powerups drawn before ball/burger but hearts particles on top ✓.**
**One risk: `ctx.letterSpacing` — assigning string; in older Chrome it's undefined property → expando, no error ✓.
**Also `ctx.ellipse` in seed ✓ Chrome.
**Check ID collisions with variable names: none.
**A subtle bug: in keydown, `if(S.mode==='demo'||S.mode==='over')` — pressing 'm' or 'p' handled earlier with return ✓. But pressing e.g. 'arrowleft' during demo starts the game — intended ("press a key to start"). Also the demo overlay says click or press any key ✓.
**During 'over' → startGame resets ✓.
**Bug check: `S.clearing` while mode 'over'? loseBall→gameOver only when lives 0; clearing can't coincide.
**Popups during clear drawn ✓.
**updateHUD called in breakBrick each brick — fine.
**renderLives called via updateHUD ✓.
**hearts HUD count formatting: raw number ok.
**Now check start: `ball.y` initial 0 before first update — update runs before render in first frame ✓ attached branch sets position.
**Demo AI when ball attached: target wanders — ball follows pad; serveTimer 0.5s ✓.
**Edge: stepBall bounce-off floor check `ball.y - r > H-26` — H-26=574; floor top at H-20=580 with border line at 576. Ball center 574+ → mostly below floor line ✓.
**Paddle near floor: pad.y=516..552, ball can still go under burger sides and fall — fine.
**Possible visual: burger overlaps floor? bottom 552 < 576 ✓.
**powerups catch check uses pad rect incl. y range pad.y-8..pad.y+pad.h+10 → 508..562 ✓.
**AI predictX: pad.y=516. ✓.
Now the CSS. Write full:
```css
*{margin:0;padding:0;box-sizing:border-box}
html,body{height:100%}
html{overflow-x:hidden}
body{
min-height:100vh;
display:flex;align-items:center;justify-content:center;
padding:16px;
font-family:'Trebuchet MS',Verdana,sans-serif;
color:#4A2A18;
background-color:#F6E3C2;
background-image:radial-gradient(#EBD5AC 2.2px,transparent 2.4px);
background-size:26px 26px;
-webkit-tap-highlight-color:transparent;
user-select:none;-webkit-user-select:none;
}
.cabinet{
width:min(94vw,880px);
background:#FFFBF0;
border:5px solid #4A2A18;
border-radius:22px;
box-shadow:10px 12px 0 rgba(74,42,24,.16);
padding:16px 18px 13px;
}
.top{display:flex;align-items:center;justify-content:space-between;gap:14px}
h1{
font-family:Impact,'Arial Black','Franklin Gothic Bold',sans-serif;
font-size:clamp(30px,4.6vw,42px);
line-height:.95;
letter-spacing:1px;
color:#E8432F;
text-shadow:
-2px -2px 0 #4A2A18,2px -2px 0 #4A2A18,-2px 2px 0 #4A2A18,2px 2px 0 #4A2A18,
-2px 0 0 #4A2A18,2px 0 0 #4A2A18,0 -2px 0 #4A2A18,0 2px 0 #4A2A18,
5px 6px 0 rgba(74,42,24,.2);
white-space:nowrap;
}
h1 svg{width:.6em;height:.6em;margin:0 .1em;transform:rotate(-10deg);vertical-align:-1%;}
.tag{margin-top:7px;font-weight:700;font-size:10px;letter-spacing:.32em;color:#8A6A4B}
.badge{
flex:none;width:88px;height:88px;border-radius:50%;
background:#F5B32F;border:4px solid #4A2A18;
transform:rotate(-8deg);
display:flex;flex-direction:column;align-items:center;justify-content:center;gap:2px;
box-shadow:4px 5px 0 rgba(74,42,24,.18);
}
.badge span{font-family:Impact,'Arial Black',sans-serif;font-size:11px;letter-spacing:1px}
.badge svg{width:20px;height:20px}
.hud{
margin-top:14px;display:flex;align-items:center;gap:8px 26px;flex-wrap:wrap;
background:#FFF3DA;border:4px solid #4A2A18;border-radius:14px;
padding:8px 16px;
}
.stat{display:flex;align-items:baseline;gap:9px}
.stat .lbl{font-size:10px;font-weight:700;letter-spacing:.24em;color:#8A6A4B}
.stat .val{
font-family:Impact,'Arial Black',sans-serif;font-size:23px;letter-spacing:1px;color:#4A2A18;
font-variant-numeric:tabular-nums;
}
.stat .val.red{color:#E8432F}
.lives{display:flex;align-items:center;gap:5px;margin-left:auto}
.lives svg{width:20px;height:20px;filter:drop-shadow(1.5px 2px 0 rgba(74,42,24,.22))}
#muteBtn{
width:36px;height:36px;border:3px solid #4A2A18;border-radius:11px;
background:#FFFBF0;color:#4A2A18;cursor:pointer;
display:grid;place-items:center;
box-shadow:2px 3px 0 rgba(74,42,24,.22);
}
#muteBtn:active{transform:translate(1px,2px);box-shadow:none}
#muteBtn svg{width:18px;height:18px}
.stage{
position:relative;margin-top:14px;
border:5px solid #4A2A18;border-radius:14px;
overflow:hidden;background:#FFF9EC;touch-action:none;
}
canvas{display:block;width:100%;height:auto}
.overlay{
position:absolute;inset:0;display:flex;align-items:center;justify-content:center;
background:rgba(255,249,236,.55);padding:18px;cursor:pointer;
}
.overlay.hidden{display:none}
.panel{
background:#FFFBF0;border:4px solid #4A2A18;border-radius:18px;
box-shadow:8px 9px 0 rgba(74,42,24,.18);
padding:24px 34px 22px;text-align:center;max-width:min(92%,430px);
}
.panel .eyebrow{font-size:10px;font-weight:700;letter-spacing:.3em;color:#8A6A4B}
.panel h2{
font-family:Impact,'Arial Black',sans-serif;
font-size:clamp(26px,4vw,38px);color:#E8432F;letter-spacing:1px;margin:8px 0 6px;
text-shadow:
-2px -2px 0 #4A2A18,2px -2px 0 #4A2A18,-2px 2px 0 #4A2A18,2px 2px 0 #4A2A18,
4px 5px 0 rgba(74,42,24,.18);
}
.panel .meta{font-size:12.5px;line-height:1.7;color:#6B4A2F;font-weight:700}
.panel .blink{
margin-top:16px;font-size:12px;font-weight:800;letter-spacing:.18em;color:#4A2A18;
animation:pulse 1.4s ease-in-out infinite;
}
@keyframes pulse{50%{opacity:.25}}
.hints{
display:flex;flex-wrap:wrap;justify-content:center;gap:6px 22px;
margin-top:12px;font-size:10px;font-weight:700;letter-spacing:.2em;color:#8A6A4B;
}
@media (max-width:600px){
.badge{display:none}
.stat .val{font-size:19px}
.hud{gap:6px 16px;padding:7px 12px}
.cabinet{padding:12px 12px 10px}
}
```
h1 white-space nowrap with clamp 30px min — at 375px viewport: 94vw=352 cabinet minus padding ~326px; "BURGER♥BREAKOUT" at 30px Impact ≈ 15 chars × ~16px = 240 + heart ≈ 270 fits ✓.
Now — the demo must look good on FIRST PAINT (screenshot ~3s). One concern: overlay covers center with panel — game visible around it (translucent bg 0.55). Panel max-width 430 on ~830 canvas — fine, action visible above/below panel. Bricks at top fully visible ✓, hearts mid-screen visible partially. Good.
Also make overlay's `cursor:pointer` ✓.
**Double-check: no external anything, no gradients (except dot pattern via radial-gradient — pattern dots; acceptable? The rule targets decorative gradient washes; polka dots pattern is flat. I'm comfortable.)**
**One more consideration on `body` centering when content taller than viewport: flex align center + min-height → content clipped top? With `align-items:center` and overflow, top gets cut when content exceeds — use `margin:auto` trick: flex + `align-items:safe center`? Safer: body flex with `align-items:center` but add `margin:auto 0`? Standard fix: use `body{display:flex}` and `.cabinet{margin:auto}` — this prevents overflow clipping (auto margins collapse to 0 when overflowing, keeping top accessible). I'll use margin:auto on cabinet instead of justify/align center. ✓
```css
body{display:flex;min-height:100vh}
.cabinet{margin:auto}
```
**Total height at 1280×800: cabinet width 880 → inner 880-10(border)-36(padding)=834 → canvas height 834/1.5=556. Header ~86 (badge 88 + margins) hmm badge 88px tall + tag → header ≈ 92. HUD ≈ 56. stage = 556+10 border=566. hints ≈ 26. paddings 16+13+14*3 margins ≈ 70. Total ≈ 92+56+566+26+70+32(body padding) ≈ 842 > 800 → slight vertical scroll (~42px). Hmm. "must look right in 1280×800" — a small scroll isn't ideal. Reduce: make cabinet width min(92vw, 820px) → canvas 774/1.5=516 → total ≈ 802. Still borderline. Let me trim: badge 78px, header margin, hud padding 6, stage border 4? Let me compute more carefully with width 820:
- canvas inner width: 820 - 2*5(border) - 2*14(padding) = 782 → height 521.
- header: h1 42px line .95 ≈ 40 + tag 10+7 ≈ 57; badge 88 > 57 → header height 88.
- hud: 23px val + padding 16 + border 8 ≈ 50.
- stage margin 14 + canvas 521 + border 10 = 545.
- hints 26 + margin 12.
- cabinet padding top 16 bottom 13 = 29.
- body padding 32.
Total ≈ 88+50+545+38+29+32 = 782 ✓ < 800.
Set width: min(92vw, 820px). Also reduce badge to 84. Fine — keep numbers roughly; scrollbars only appear if fonts bigger etc. Let me be a bit safer: cabinet 800px → canvas 762→ height 508 → total ~770. Use `min(92vw, 810px)`. At 1280 wide the game is 810 — plenty.
Hmm but bigger is prettier for the game. 782 estimate already under 800. But my estimates could be off by ~20px (h1 line-height, margins). I'll set 820 and trim body padding to 12 and cabinet padding 14. Should be ≈790. Also `height:100%` on html with body min-height — fine.
Actually also the screenshot is "about 3 seconds after load" — full page? Probably viewport 1280×800. If slight scroll, screenshot still fine (top portion). But requirement "look right" — keep within. I'll go 810px and compact paddings. Final: cabinet `width:min(92vw,810px)`.
Canvas display width ≈ 810-10-28=772 → height 515.
**Let me also double check hearts HUD updates frequently (score/hearts change per brick) — DOM text updates cheap ✓.
**Potential problem: `updateHUD` not called on `hearts` during rain — only brickBurst returns n; S.hearts+=n in breakBrick ✓ (I wrote S.hearts+=brickBurst(b,S.combo)).
**One more: popup 'CRUNCH' spam on patty — fine.
**Bug: in hitBrick I push crumb with CRUMB_COLORS[b.type] — defined for all types ✓.
**In stepBall paddle check `ball.y < pad.y+pad.h` — ball below paddle but moving down won't collide from underneath ✓.
Ball hitting burger from side while descending at pad.y+20: circle-rect nearest point on side edge; rel ~±1 → steep angle ~58° — fine.
**Now the top wall: bricks at y=54, wall bounce y=14+9=23. Gap 23..54 = 31px ball travels — ok.
Ball trapped between top wall and brick row? Row0 at y=54, ball diameter 18 < gap 31 ✓ can bounce there.
**Gaps between bricks (8px) narrower than ball 18 → ball can't pass between columns; will hit edges properly ✓.
**Level design check: 10 cols × 74.8 wide. Fun.
Alright — also the requirement "If the demo uses the keyboard, display a clear 'Click or press a key to start' message" ✓ overlay blink text.
**Write the final HTML now, complete.** Also add `<noscript>`? unnecessary.
Let me write out, double-checking every referenced id: game, overlay, ovEyebrow, ovTitle, ovMeta, ovBlink, scoreVal, heartsVal, levelVal, livesBox, muteBtn, icoOn, icoOff, stage.
h1 svg heart path fill mustard stroke ink.
Badge svg heart fill #E8432F stroke ink.
Let me also verify the `S` object property `clearing` used in tickClear and update ✓.
`tickClear(dt)` implement:
```js
function tickClear(dt){
S.clearTimer-=dt;
if(S.clearTimer<=0){
S.clearing=false; S.level++;
buildLevel();
ball.speed=baseSpeed(); ball.attached=true; ball.vx=ball.vy=0; trail.length=0;
if(S.mode==='demo') serveTimer=0.7;
popup(W/2,300,'LEVEL '+S.level,'#E8432F',24);
updateHUD();
}
}
```
**One catch: in update() I call tickClear then updateBall — attached ball ok.
Also during clearing, powerups still update & catchable ✓ nice.
**drawBall during clearing: attached at paddle ✓ trail cleared.
**Also when game over, ball frozen last position visible — maybe hide ball on over: in render, skip ball if mode over? Ball lost below floor when game over (loseBall then gameOver). Ball sits at floor y>threshold. Render would draw it below floor over the checker. Draw ball clipped? Simply skip drawing ball when mode==='over' ✓.
**Check: loseBall → gameOver sets mode 'over'; update() skips updateBall for 'over' ✓ ball stays.
**Gray burst at H-40 ✓ visible above floor.
**Also: score formatting fmtScore(0)='000000' ✓.
**demo AI serve after level clear: serveTimer=0.7 ✓.
**Potential runtime error: `ctx.letterSpacing='1.5px'` then `'0px'` — ok.
**drawServeHint measureText with font set AFTER measure? Set font BEFORE measureText ✓ in my code order: set font, measure, draw pill, set fillStyle, fillText — but fillText needs font — still set ✓. textBaseline set ✓.
**chunkText textAlign/baseline set inside? I set them in drawPopups before loop and in banner calls after other ctx ops might have changed align? chunkText itself should set align/baseline to be safe:
add `ctx.textAlign='center'; ctx.textBaseline='middle';` inside chunkText ✓.
**In drawClearBanner I call chunkText then ctx.font set for sub and fillText — need textAlign center ✓ set inside chunkText persists (canvas state persists) ✓.
Alright, also I should double check the `render()` shake translate after setTransform — subsequent drawing offset ✓, and next frame setTransform resets ✓.
**Fonts in canvas: 'Arial Black' quoted properly inside template: `ctx.font = \`900 ${size}px 'Arial Black','Arial',sans-serif\`;` ✓.
**Meta string in overlay uses innerHTML with <br> ✓.
**Now double-check heartPath correctness by evaluating a few points mentally. s=62 in sprite. Points:
- Start (0, 31) bottom tip.
- CP1 (-38.4, 3.1), CP2 (-38.4, -31), end (-17.4, -31): curve leaves tip heading left-up, bulges to x≈-35 midway, arrives at left-lobe top-left corner (-17,-31). Hmm the left lobe "top" is flat from (-17,-31)... then next curve: CP (-5, -31), (0, -21), end (0, -11.2): from (-17,-31) to center dip (0,-11.2) — control points pull toward center creating rounded lobe inner edge. Hmm, is the top of the left lobe at y=-31 correct shape? A heart's lobes are rounded domes peaking around y≈-0.35s, with the notch dipping to y≈-0.15s. My construction: lobes at y=-0.5s flat-ish via control points; the dome roundness comes from CP arrangement. Let me instead use the widely-used canvas heart:
```
moveTo(0, 0.35s)?
```
Alternative robust heart (two arcs + V): Use classic:
```
ctx.moveTo(0, s*0.3);
ctx.bezierCurveTo(0, 0, -s*0.5, -s*0.3, -s*0.5, -s*0.55)?
```
Honestly my construction should render an acceptable heart; symmetric, tip at bottom, two lobes with a notch. The lobes will be somewhat squarish tops but bezier smoothing rounds them: from tip (0,.5) CP(-.62,.05) CP(-.62,-.5) to (-.28,-.5): the curve's end tangent direction at (-.28,-.5) is from CP2 (-.62,-.5) → (-.28,-.5): pointing +x, horizontal — so lobe top is flat there, and the next segment continues with CP1 (-.08,-.5) horizontal then dips to (0,-.18): creates rounded dome from (-.28,-.5) rising... stays at -0.5 then curves down to notch. The lobe dome peak flat-ish at y=-0.5 between x=-0.28..? Then right lobe similarly. Overall: heart with slightly flat-topped lobes — acceptable, looks like emoji-style heart. Good enough; highlight dot adds cuteness.
Actually let me improve lobes: change first curve to end at (-0.5s? Standard heart path (from memory of common snippets):
```
function heart(g,s){
g.beginPath();
g.moveTo(0, 0.5*s);
g.bezierCurveTo(0.5*s, 0.15*s, 1.1*s, -0.35*s, 0.55*s, -0.75*s);
...
}
```
Hmm that's asymmetric units. Let me just take the classic two-curve heart:
```
M 0,0.35
C 0.5,-0.3 1.0,0.1 1.0,0.45?
```
I'll trust my symmetric construction — it genuinely produces a heart shape. Let me verify by rough sampling the left-side curve: P0=(0,0.5), P1=(-0.62,0.05), P2=(-0.62,-0.5), P3=(-0.28,-0.5). At t=0.5: B = (P0+3P1+3P2+P3)/8 = ((0-1.86-1.86-0.28)/8, (0.5+0.15-1.5-0.5)/8) = (-0.5, -0.16875). So midpoint of left edge at (-0.5,-0.17) — bulged left ✓. Curve from bottom tip (0,.5) sweeping left and up to (-0.28,-0.5): nice rounded left side. Notch curve P0=(-0.28,-0.5),P1=(-0.08,-0.5),P2=(0,-0.34),P3=(0,-0.18): midpoint = ((-0.28-0.24+0+0)/8, (-0.5-1.5-1.02-0.18)/8)=(-0.065,-0.4) hmm y: (-0.5 + 3*(-0.5) + 3*(-0.34) + (-0.18))/8 = (-0.5-1.5-1.02-0.18)/8 = -3.2/8 = -0.4. So from lobe top (-0.28,-0.5) curving to notch (0,-0.18) passing (-0.065,-0.4) — a rounded inner lobe ✓. Symmetric right. Result: recognizable heart ✓.
**Also confirm heartSprite drawn at translate(48,48) — center. Sprite box 96; drawn scaled to s px: drawImage(-s/2..) ✓.
**Great. Also `heartSprite('#B9AA9C')` gray on demand ✓.
Let me also think: brick sprite height BRICK_H+4 = 31; drawImage dest h = BRICK_H+4 ✓. Stored b.h=BRICK_H for collisions; shadow overdraw 4px below — visual only ✓.
Row spacing GAPY=10 with 4px shadow → visual gap 6px. ok.
**Finally, write the complete file. Also add a `<link rel="preconnect">`? No external.
Title of document: "Burger Breakout — Heartbreaker Diner".
Also include small comment at top of script describing.
Let me now also scan for the 'hearts stat' HUD label id: `heartsVal` with class val red.
One more UI polish: canvas cursor: none during play? If cursor hidden, user might feel lost. Keep default.
Double-check `pointermove` uses cv rect — if canvas display none? never.
Also mobile: touch drag anywhere moves paddle since pointermove on window maps clientX relative to canvas rect — vertical position irrelevant ✓. Dragging finger = pointermove with pointerdown — works.
Also on mobile, pointerdown on stage triggers launch ✓.
**Write it.** I'll now produce the final file carefully, keeping syntax valid. During writing, I'll double check every function I reference exists.
Also careful: variable name collision `parts` and function param `p`. fine.
In drawFloor: `const s=W/40;` loop i<40 fillRect(i*s, H-20, s+0.5, 20) — add 0.5 to avoid hairline gaps. Colors: i%2 ? '#FFF3DA' : '#E8432F'. Ink line: fillRect(0,H-23,W,3)? floor 20 tall from H-20; line above at H-23..H-20. ok.
Let me also add slight second row? one row fine at 20px tall; maybe 2 rows of 12px checkers = 24: rows y H-24, squares 15×12 alternating both directions: nested loop 2 rows × 60 cols — fine cheap. Two-row checker looks more "floor". I'll do rows=2, sq=W/45=20, height 12 each: for r in 0..1, c in 0..44: if((r+c)%2==0) red. Height 24 total, line at H-27..H-24. Lose threshold H-26 ~ floor top. adjust loseBall check to ball.y - r > H-24. And heart floor bounce y > H-28. Fine tune: floorTop = H-24. line at floorTop-3.
Let me set: FLOOR_H=24, SQ=20, rows 2 (12px each).
drawFloor:
```js
const top=H-24;
ctx.fillStyle='#FFF3DA'; ctx.fillRect(0,top,W,24);
ctx.fillStyle='#E8483A';
for(let r=0;r<2;r++) for(let c=0;c<Math.ceil(W/20);c++) if((r+c)%2===0) ctx.fillRect(c*20, top+r*12, 20.5, 12.5);
ctx.fillStyle='#4A2A18'; ctx.fillRect(0,top-3,W,3);
```
loseBall condition: ball.y - ball.r > H-24 → center below floor top + r. gray burst at H-40 ✓.
heart bounce: if(p.y>H-28) → sits on floor line.
**powerup miss: u.y>H-26 remove.**
**OK writing final answer now.** Length ~700 lines. Also remember `aria-hidden` on decorative svgs. Keep HTML tidy.
One more detail — `e.preventDefault()` on keydown for 'a','d','p','m' would block typing in... no inputs exist ✓. But careful: preventing default on ' ' stops page scroll ✓ good.
Also add `e.stopPropagation()` on mute click so stage pointerdown isn't triggered — mute is outside stage anyway ✓. But keydown 'm' path returns before start ✓.
Also I realize: in keydown handler I call `sfx.ensure()` — creates AudioContext on first keydown ✓.
Edge: iOS Safari? target Chrome ✓.
Final check of update ordering per frame in demo at start: mode 'demo', attached → updatePaddle (aiControl target wander), updateBall sets ball pos & serveTimer decrement. Good.
After startGame during a clearing? Not possible.
**Write out!**
Also — I want the bricks wall to visually read as a burger from afar. Rows: bun / lettuce / tomato / cheese / patty / bun. ✓
Small improvement: brick colors — bun top row #EDAA62 with seeds reads great.
Let me write the code block now. I'll do a final mental lint pass while writing.
Additional careful check — `makeBrickSprite` uses `seedDot` defined before it ✓. `rr` defined before ✓. `heartPath` before `heartSprite` ✓.
In makeBrickSprite for 'lettuce': after base rr + fill + stroke, then scallop arcs fill, then highlight — note stroke before scallops leaves outline not covering scallops (scallop edges unstroked) — acceptable stylistically (ruffles).
For cheese: rr base fill stroke, then drip rr path + fill (path changed — fill only). Then highlight & holes.
Also for 'bun': clip needs path: rr() then clip() then fillRects — but I called fill+stroke before clip with same path; after clip, restore. Order: fill, stroke, save, rr(path again), clip, fillRects, restore ✓.
In damaged patty: strokes with lineCap round set — later burger drawing sets its own; lineCap persists — harmless.
Also note: `g.stroke()` after `g.fill()` uses current path — but for tomato I do base rr fill stroke, then inner rr fill (no stroke) ✓.
**Let me also confirm `drawBurger` clipping: `g.save(); rr(...); g.clip(); ... g.restore();` ✓.
In drawBurger I reference `S.squash` ✓.
Alright. Also add subtle idle animation for overlay panel? CSS `animation: float 3s ease-in-out infinite` translateY ±4px on panel — subtle life. Add.
Also demo overlay: maybe show a small animated hint arrow? no need.
Final answer: one HTML code block. Writing now.
Wait, one more possible bug: `stage.addEventListener('pointerdown', ...)` — the overlay is child of stage; clicks bubble ✓. But also `muteBtn` inside `.hud` outside stage ✓.
And for pointermove — while pointer down outside canvas? fine.
Also `keys` for space is `' '` — my includes check uses `' '` ✓ and keydown preventDefault includes ' ' ✓.
**Let me also handle rare NaN in predictX when ball.vy===0** — guarded by `ball.vy>0` branch before call ✓ (strictly >0).
In aiControl: `else if(ball.vy>0) target=predictX();` ✓.
**Possible infinite ball loop with zero vx & vy? launch always sets vy≈-speed ✓; reflections preserve magnitude ✓.
**One more: paddle bounce when ball.speed increased mid-flight — bounce uses ball.speed current ✓.
Now, writing the response. Given output rules: exactly one fenced code block, nothing else. Though the user asked for explanation after code, the system output rules explicitly forbid text outside the code block — I'll include a brief comment header inside the HTML instead.
Let me carefully produce the final file.