Hamburger breakoutOne-line
MiniMax M3.1 Flash
by MiniMaxReasoningClean
Running live in a sandboxed frame. Click inside it before using the keyboard.
How it went
- Total time
- 1 min 55 s
- Cost
- $0.017
- Output tokens
- 27,476
- Spent thinking
- 16,481 tokens
- Speed
- 239 tok/s
- Console errors
- 0
- First token after
- 269 ms
- First visible text after
- 1 min 21 s
- Prompt tokens
- 541
- Code size
- 30.4 KB
- Finish reason
- stop
- Served by
- Nebius
- Model id
minimax/minimax-m3.1-flash- Reasoning sent
{"effort":"max"}- Max output
- 64,000 tokens
- Generated
- Nov 11, 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 · 849 lines
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>Hamburger Breakout — Hearts Edition</title>
<style>
*{box-sizing:border-box;}
html,body{height:100%;margin:0;padding:0;}
body{
font-family: system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
background:
radial-gradient(1200px 700px at 50% -20%, #3a1a5e 0%, #1a0c30 45%, #090512 100%);
color:#fff; overflow:hidden; display:flex; flex-direction:column;
-webkit-user-select:none; user-select:none; touch-action:none;
}
header{
flex:0 0 auto; display:flex; align-items:center; justify-content:space-between;
gap:10px; padding:8px 14px;
border-bottom:1px solid rgba(255,255,255,.08);
background:linear-gradient(180deg, rgba(40,20,70,.85), rgba(15,8,28,.55));
box-shadow:0 6px 24px rgba(0,0,0,.45);
}
.brand{display:flex;align-items:center;gap:10px;min-width:0;}
.brand .bun{
width:34px;height:34px;border-radius:10px;flex:0 0 auto;
background:linear-gradient(180deg,#ffd79b 0 34%,#67c23f 34% 44%,#ffcf3d 44% 53%,#7a4527 53% 70%,#e0a35c 70% 100%);
box-shadow:0 0 0 1px rgba(255,255,255,.18) inset, 0 4px 12px rgba(0,0,0,.45);
}
.brand h1{
margin:0; font-size:clamp(12px,2vw,19px); font-weight:800; letter-spacing:.14em; white-space:nowrap;
background:linear-gradient(90deg,#ffd76e,#ff7a59);
-webkit-background-clip:text; background-clip:text; color:transparent;
}
.brand p{margin:1px 0 0; font-size:10.5px; color:#b6a4d6; letter-spacing:.06em; white-space:nowrap;
overflow:hidden; text-overflow:ellipsis;}
.stats{display:flex;align-items:center;gap:7px;flex-wrap:wrap;justify-content:flex-end;}
.chip{
background:rgba(255,255,255,.055); border:1px solid rgba(255,255,255,.12);
border-radius:11px; padding:3px 10px; display:flex; flex-direction:column; min-width:56px;
}
.chip .label{font-size:8.5px; letter-spacing:.18em; color:#a490cd; font-weight:700;}
.chip .value{font-size:15px; font-weight:800; font-variant-numeric:tabular-nums; line-height:1.15;}
#lives{display:flex;gap:3px;align-items:center;height:17px;}
#autoBtn{
font:inherit; font-size:11px; font-weight:800; letter-spacing:.1em; cursor:pointer;
color:#0d0718; background:linear-gradient(180deg,#ffe08a,#ffb347);
border:none; border-radius:11px; padding:8px 12px;
box-shadow:0 3px 0 #a35f12, 0 6px 16px rgba(255,170,60,.28);
transition:transform .08s ease, box-shadow .08s ease;
}
#autoBtn:active{transform:translateY(3px); box-shadow:0 0 0 #a35f12;}
#autoBtn.off{background:linear-gradient(180deg,#5c4a7d,#3b2f55); color:#d9cff0; box-shadow:0 3px 0 #241a38;}
main{position:relative; flex:1 1 auto; min-height:0;}
canvas{display:block; width:100%; height:100%;}
.overlay{
position:absolute; inset:0; display:flex; align-items:center; justify-content:center;
padding:16px; background:radial-gradient(circle at 50% 50%, rgba(10,5,22,.30), rgba(6,3,14,.72));
z-index:5; cursor:pointer;
}
.overlay.hidden{display:none;}
.panel{
text-align:center; max-width:520px; padding:22px 26px; border-radius:18px;
background:linear-gradient(180deg, rgba(38,18,66,.92), rgba(16,8,30,.94));
border:1px solid rgba(255,255,255,.14);
box-shadow:0 20px 60px rgba(0,0,0,.6), 0 0 0 1px rgba(255,190,90,.08) inset;
animation:pop .45s cubic-bezier(.2,1.3,.4,1) both;
}
@keyframes pop{from{opacity:0; transform:translateY(14px) scale(.94)}to{opacity:1;transform:none}}
.badge{
display:inline-block; font-size:clamp(10px,2.4vw,13px); font-weight:900; letter-spacing:.18em;
color:#16091f; background:linear-gradient(90deg,#ffe08a,#ff8fb1);
padding:7px 16px; border-radius:999px; margin-bottom:12px;
animation:blink 1.5s ease-in-out infinite;
}
@keyframes blink{0%,100%{opacity:1; transform:scale(1)}50%{opacity:.72; transform:scale(.97)}}
.panel h2{
margin:0 0 8px; font-size:clamp(21px,5vw,36px); font-weight:900; letter-spacing:.06em;
background:linear-gradient(90deg,#ffd76e,#ff6f91,#b98cff);
-webkit-background-clip:text; background-clip:text; color:transparent;
}
.panel p{margin:0; font-size:clamp(12px,3vw,14.5px); color:#cfc0ea; line-height:1.55;}
.keys{margin-top:14px; font-size:11px; color:#9f8dc4; letter-spacing:.08em;}
.keys b{color:#ffd76e; font-weight:800;}
@media (max-width:720px){
header{padding:6px 9px; gap:6px;}
.brand p{display:none;}
.brand .bun{width:26px;height:26px;}
.chip{padding:2px 7px; min-width:44px; border-radius:9px;}
.chip .value{font-size:12.5px;}
.chip .label{font-size:7.5px; letter-spacing:.12em;}
#autoBtn{padding:7px 8px; font-size:9.5px;}
}
@media (max-width:420px){
.brand h1{font-size:11px; letter-spacing:.08em;}
.chip:nth-child(3){display:none;}
}
</style>
</head>
<body>
<header>
<div class="brand">
<div class="bun"></div>
<div>
<h1>HAMBURGER BREAKOUT</h1>
<p>smash bricks · explode hearts | move: mouse / ← → · serve: space · auto: D</p>
</div>
</div>
<div class="stats">
<div class="chip"><span class="label">SCORE</span><span class="value" id="score">0</span></div>
<div class="chip"><span class="label">LEVEL</span><span class="value" id="level">1</span></div>
<div class="chip"><span class="label">HEARTS</span><span class="value" id="hearts">0</span></div>
<div class="chip"><span class="label">BURGERS</span><span id="lives"></span></div>
<button id="autoBtn" type="button">AUTO: ON</button>
</div>
</header>
<main id="stage">
<canvas id="game"></canvas>
<div class="overlay" id="overlay">
<div class="panel">
<div class="badge" id="ovBadge">CLICK OR PRESS ANY KEY TO START</div>
<h2 id="ovTitle">HEARTS EDITION</h2>
<p id="ovSub">The AI is already playing a demo. Click to grab the bun —<br>every brick you smash erupts into a shower of little hearts.</p>
<div class="keys"><b>MOUSE</b> or <b>← →</b> to move · <b>SPACE</b> to serve · <b>D</b> toggles auto-play</div>
</div>
</div>
</main>
<script>
(function(){
"use strict";
/* ---------------- DOM ---------------- */
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
const stage = document.getElementById('stage');
const overlay = document.getElementById('overlay');
const ovBadge = document.getElementById('ovBadge');
const ovTitle = document.getElementById('ovTitle');
const ovSub = document.getElementById('ovSub');
const elScore = document.getElementById('score');
const elLevel = document.getElementById('level');
const elHearts = document.getElementById('hearts');
const elLives = document.getElementById('lives');
const autoBtn = document.getElementById('autoBtn');
/* ---------------- helpers ---------------- */
const clamp = (v,a,b)=> v<a?a:(v>b?b:v);
const rnd = (a,b)=> a + Math.random()*(b-a);
const pick = arr => arr[(Math.random()*arr.length)|0];
function rr(c,x,y,w,h,r){
let tl,tr,br,bl;
if(Array.isArray(r)){ tl=r[0]; tr=r[1]; br=r[2]; bl=r[3]; }
else { tl=tr=br=bl=r; }
const m = Math.min(Math.abs(w),Math.abs(h))/2;
tl=Math.min(tl,m); tr=Math.min(tr,m); br=Math.min(br,m); bl=Math.min(bl,m);
c.beginPath();
c.moveTo(x+tl,y);
c.lineTo(x+w-tr,y);
c.arcTo(x+w,y,x+w,y+tr,tr);
c.lineTo(x+w,y+h-br);
c.arcTo(x+w,y+h,x+w-br,y+h,br);
c.lineTo(x+bl,y+h);
c.arcTo(x,y+h,x,y+h-bl,bl);
c.lineTo(x,y+tl);
c.arcTo(x,y,x+tl,y,tl);
c.closePath();
}
function heartPath(c,s){
c.beginPath();
c.moveTo(0, s*0.34);
c.bezierCurveTo(-s*0.62,-s*0.05, -s*0.47,-s*0.74, 0,-s*0.34);
c.bezierCurveTo(s*0.47,-s*0.74, s*0.62,-s*0.05, 0,s*0.34);
c.closePath();
}
/* ---------------- state ---------------- */
let W=0, H=0, dpr=1;
let time=0, shake=0;
const keys={left:false,right:false};
const pointer={x:0, active:false};
const game = {
state:'serve', // serve | play | dead | levelclear | gameover
demo:true, // AI plays
started:false, // user took over
paused:false,
score:0, heartsGot:0, lives:3, level:1,
combo:0, comboTimer:0,
serveTimer:0.8, deadTimer:0, clearTimer:0, overTimer:0
};
const paddle = {x:0,y:0,w:160,h:34,vx:0};
const ball = {x:0,y:0,vx:0,vy:0,r:10,speed:340};
let bricks=[], hearts=[], parts=[], pops=[], trail=[];
const INGREDIENTS=[
{t:'#ff9b86', b:'#e0352f'}, // tomato
{t:'#ffe08a', b:'#f0a90c'}, // cheese
{t:'#a9ea72', b:'#48a42c'}, // lettuce
{t:'#d7a9f7', b:'#8447cf'}, // onion
{t:'#d8ec8d', b:'#6d9c1c'}, // pickle
{t:'#ffa98c', b:'#cf462c'}, // bacon
{t:'#f6d3a1', b:'#c89152'} // bun
];
const HEART_COLORS=['#ff4d7d','#ff77a8','#ff2e63','#ffb3c9','#e8368f','#ff5d8f'];
/* ---------------- layout ---------------- */
function resize(){
const rect = canvas.getBoundingClientRect();
dpr = Math.min(window.devicePixelRatio||1, 2);
W = Math.max(1, rect.width);
H = Math.max(1, rect.height);
canvas.width = Math.round(W*dpr);
canvas.height = Math.round(H*dpr);
ctx.setTransform(dpr,0,0,dpr,0,0);
layoutLevel(game.level);
paddle.w = clamp(W*0.15, 74, 160);
paddle.h = clamp(paddle.w*0.25, 20, 34);
paddle.y = H - paddle.h - clamp(H*0.055, 16, 52);
paddle.x = clamp(paddle.x, 0, Math.max(0, W-paddle.w));
ball.r = clamp(W*0.0105, 6, 12);
if(game.state==='serve'){ ball.x = paddle.x + paddle.w/2; }
}
function layoutLevel(level){
const margin = clamp(W*0.06, 14, 70);
const top = clamp(H*0.05, 18, 54);
const rows = clamp(3 + Math.floor((level-1)/2), 3, 7);
const gap = clamp(W*0.011, 5, 11);
const areaW = W - margin*2;
const desire = clamp(W*0.11, 58, 120);
const cols = Math.max(3, Math.round((areaW+gap)/(desire+gap)));
const bw = (areaW - (cols-1)*gap)/cols;
const bh = clamp((H*0.45 - (rows-1)*gap)/rows, 14, 40);
bricks = [];
for(let r=0;r<rows;r++){
for(let c=0;c<cols;c++){
if(level>1 && ((r*3 + c*5 + level) % 13 === 0)) continue; // decorative gaps
const ing = INGREDIENTS[(r + (level % INGREDIENTS.length)) % INGREDIENTS.length];
bricks.push({
x: margin + c*(bw+gap), y: top + r*(bh+gap), w:bw, h:bh,
t:ing.t, b:ing.b, alive:true, flash:0
});
}
}
}
/* ---------------- level / flow ---------------- */
function sizeFactor(){ return clamp(W/1150, 0.62, 1.18); }
function resetBall(){
game.state='serve';
game.serveTimer = game.demo ? 0.75 : 1.5;
ball.speed = (330 + (game.level-1)*22) * sizeFactor();
ball.vx=0; ball.vy=0;
ball.x = paddle.x + paddle.w/2;
ball.y = paddle.y - ball.r - 2;
trail.length=0;
}
function resetGame(){
game.score=0; game.lives=3; game.level=1; game.heartsGot=0;
game.combo=0; game.comboTimer=0; game.overTimer=0;
hearts.length=0; parts.length=0; pops.length=0;
layoutLevel(1);
resetBall();
syncHUD();
}
function launch(){
if(game.state!=='serve') return;
const a = rnd(-0.32,0.32);
ball.vx = Math.sin(a)*ball.speed;
ball.vy = -Math.cos(a)*ball.speed;
game.state='play';
blip(320, 0.06, 'square', 0.05);
}
function gameOver(){
game.state='gameover'; game.overTimer=0;
ovBadge.textContent='GAME OVER';
ovTitle.textContent='SCORE '+game.score;
ovSub.innerHTML='You smashed <b>'+game.heartsGot+'</b> hearts.<br>Click or press any key to play again.';
overlay.classList.remove('hidden');
blip(120,0.35,'sawtooth',0.06);
}
function loseLife(){
game.lives--;
game.state='dead'; game.deadTimer=1.15;
game.combo=0;
for(let i=0;i<14;i++){
parts.push({x:ball.x,y:ball.y,vx:rnd(-160,160),vy:rnd(-220,-40),life:1,decay:rnd(1.2,2),size:rnd(2,5),c:'#c98b4b'});
}
blip(160,0.25,'triangle',0.06);
syncHUD();
}
function levelClear(){
game.state='levelclear'; game.clearTimer=1.7;
blip(660,0.18,'square',0.06);
}
/* ---------------- particles / popups ---------------- */
function burst(x,y,color,n,speed){
for(let i=0;i<n;i++){
const a = Math.random()*Math.PI*2, s = rnd(speed*0.35, speed);
parts.push({x:x,y:y,vx:Math.cos(a)*s,vy:Math.sin(a)*s,life:1,decay:rnd(1.6,3.2),size:rnd(1.5,4),c:color});
}
}
function popup(x,y,text,color){
pops.push({x:x,y:y,t:text,life:1,decay:1.1,c:color||'#fff'});
}
function explodeHearts(x,y,brickColor){
const n = 18 + ((Math.random()*10)|0);
for(let i=0;i<n;i++){
const a = Math.random()*Math.PI*2, s = rnd(50, 300);
hearts.push({
x:x+rnd(-6,6), y:y+rnd(-6,6),
vx:Math.cos(a)*s, vy:Math.sin(a)*s - rnd(40,130),
rot:Math.random()*Math.PI*2, vr:rnd(-7,7),
s:rnd(5,13), life:1, decay:rnd(0.42,0.9),
c: (Math.random()<0.18 ? brickColor : pick(HEART_COLORS))
});
}
if(hearts.length>460) hearts.splice(0, hearts.length-460);
}
/* ---------------- audio (safe, optional) ---------------- */
let actx=null;
function blip(freq,dur,type,vol){
try{
if(!actx){ const AC = window.AudioContext||window.webkitAudioContext; if(!AC) return; actx=new AC(); }
if(actx.state==='suspended') actx.resume();
const o=actx.createOscillator(), g=actx.createGain();
o.type=type||'square'; o.frequency.value=freq;
g.gain.setValueAtTime(vol||0.05, actx.currentTime);
g.gain.exponentialRampToValueAtTime(0.0001, actx.currentTime+dur);
o.connect(g); g.connect(actx.destination);
o.start(); o.stop(actx.currentTime+dur+0.02);
}catch(e){}
}
/* ---------------- input ---------------- */
function startPlayer(){
if(game.started) return;
game.started = true;
game.demo = false;
autoBtn.textContent='AUTO: OFF';
autoBtn.classList.add('off');
overlay.classList.add('hidden');
blip(520,0.08,'square',0.05);
}
function toggleAuto(){
if(!game.started){
game.started = true; overlay.classList.add('hidden');
}
game.demo = !game.demo;
autoBtn.textContent = game.demo ? 'AUTO: ON' : 'AUTO: OFF';
autoBtn.classList.toggle('off', !game.demo);
}
autoBtn.addEventListener('click', function(e){
e.stopPropagation();
toggleAuto();
});
function globalPress(e){
if(e && e.target && e.target.tagName === 'BUTTON') return;
if(game.state==='gameover'){
resetGame(); overlay.classList.add('hidden');
return;
}
if(!game.started){ startPlayer(); return; }
if(game.state==='serve') launch();
}
window.addEventListener('pointerdown', globalPress);
window.addEventListener('keydown', function(e){
const k = e.key;
if(k==='ArrowLeft'||k==='a'||k==='A'){ keys.left=true; }
if(k==='ArrowRight'||k==='d'&&false||k==='D'){}
if(k==='ArrowRight'){ keys.right=true; }
if(k===' '||k==='Enter'){ e.preventDefault(); }
if(k==='d'||k==='D'){ if(!e.repeat) toggleAuto(); }
if(k==='p'||k==='P'){ game.paused=!game.paused; }
globalPress(e);
});
window.addEventListener('keyup', function(e){
if(e.key==='ArrowLeft'||e.key==='a'||e.key==='A') keys.left=false;
if(e.key==='ArrowRight'||e.key==='d'||e.key==='D') keys.right=false;
});
window.addEventListener('blur', function(){ keys.left=false; keys.right=false; });
function pointerMove(e){
const r = canvas.getBoundingClientRect();
pointer.x = clamp(e.clientX - r.left, 0, W);
pointer.active = true;
}
window.addEventListener('pointermove', pointerMove);
window.addEventListener('touchmove', function(e){ if(e.touches[0]) pointerMove(e.touches[0]); }, {passive:true});
/* ---------------- HUD ---------------- */
function heartSVG(color){
return '<svg width="15" height="15" viewBox="0 0 24 24" aria-hidden="true"><path d="M12 21.2s-7.9-4.9-10-9.4C.4 8.1 3 4.3 6.7 4.3c2 0 3.6 1.1 4.6 2.5 1-1.4 2.6-2.5 4.6-2.5 3.7 0 6.3 3.8 4.7 7.5C19.2 16.3 12 21.2 12 21.2z" fill="'+color+'"/></svg>';
}
let lastLives=-1;
function syncHUD(){
elScore.textContent = game.score;
elLevel.textContent = game.level;
elHearts.textContent= game.heartsGot;
if(game.lives!==lastLives){
lastLives=game.lives;
let h='';
for(let i=0;i<Math.max(0,game.lives);i++) h+=heartSVG('#ff4d7d');
if(game.lives<=0) h=heartSVG('#5b4a75');
elLives.innerHTML=h;
}
}
/* ---------------- update ---------------- */
function predictBallX(){
if(ball.vy<=0) return ball.x;
let x=ball.x,y=ball.y,vx=ball.vx,vy=ball.vy;
const targetY=paddle.y;
for(let i=0;i<70 && y<targetY;i++){
x+=vx*0.02; y+=vy*0.02;
if(x<ball.r){x=ball.r;vx=-vx;}
else if(x>W-ball.r){x=W-ball.r;vx=-vx;}
}
return x;
}
function updatePaddle(dt){
const prev = paddle.x;
if(game.demo){
let target = (game.state==='play') ? predictBallX() : ball.x;
target += Math.sin(time*1.25)*5;
const dx = target - (paddle.x + paddle.w/2);
const ai = clamp(W*0.62, 380, 900);
paddle.x += clamp(dx, -ai*dt, ai*dt);
}else{
let dx = 0;
if(keys.left) dx -= 1;
if(keys.right) dx += 1;
if(dx!==0){ paddle.x += dx*900*dt; }
else if(pointer.active){
const d = pointer.x - (paddle.x + paddle.w/2);
paddle.x += clamp(d, -1100*dt, 1100*dt);
}
}
paddle.x = clamp(paddle.x, 0, Math.max(0, W-paddle.w));
paddle.vx = (paddle.x - prev)/Math.max(dt,0.0001);
}
function updateBall(dt){
ball.x += ball.vx*dt;
ball.y += ball.vy*dt;
if(ball.x < ball.r){ ball.x=ball.r; ball.vx=Math.abs(ball.vx); blip(520,0.04,'square',0.03); }
if(ball.x > W-ball.r){ ball.x=W-ball.r; ball.vx=-Math.abs(ball.vx); blip(520,0.04,'square',0.03); }
if(ball.y < ball.r){ ball.y=ball.r; ball.vy=Math.abs(ball.vy); blip(520,0.04,'square',0.03); }
// paddle
if(ball.vy>0 && ball.y+ball.r>=paddle.y && ball.y+ball.r<=paddle.y+paddle.h+ball.r*1.6 &&
ball.x>=paddle.x-3 && ball.x<=paddle.x+paddle.w+3){
const rel = clamp((ball.x-(paddle.x+paddle.w/2))/(paddle.w/2), -1, 1);
const ang = rel*0.92;
ball.vx = Math.sin(ang)*ball.speed;
ball.vy = -Math.abs(Math.cos(ang))*ball.speed;
ball.y = paddle.y - ball.r - 0.5;
game.combo = 0;
burst(ball.x, paddle.y, '#ffd76e', 6, 130);
blip(240+rel*80, 0.05, 'square', 0.045);
shake = Math.min(shake+2.2, 5);
}
// bricks
for(let i=bricks.length-1;i>=0;i--){
const b = bricks[i];
if(!b.alive) continue;
const ex=b.x-ball.r, ey=b.y-ball.r, ew=b.w+ball.r*2, eh=b.h+ball.r*2;
if(ball.x>ex && ball.x<ex+ew && ball.y>ey && ball.y<ey+eh){
const ol=ball.x-ex, or=(ex+ew)-ball.x, ot=ball.y-ey, ob=(ey+eh)-ball.y;
const mn=Math.min(ol,or,ot,ob);
if(mn===ol){ ball.x=ex; ball.vx=-Math.abs(ball.vx); }
else if(mn===or){ ball.x=ex+ew; ball.vx=Math.abs(ball.vx); }
else if(mn===ot){ ball.y=ey; ball.vy=-Math.abs(ball.vy); }
else { ball.y=ey+eh; ball.vy=Math.abs(ball.vy); }
// smash!
b.alive=false;
game.combo = Math.min(game.combo+1, 9);
game.comboTimer = 1.5;
const pts = 10*game.combo;
game.score += pts;
explodeHearts(b.x+b.w/2, b.y+b.h/2, b.t);
burst(b.x+b.w/2, b.y+b.h/2, '#ffffff', 8, 200);
popup(b.x+b.w/2, b.y+b.h/2, '+'+pts, '#ffd76e');
shake = Math.min(shake+4, 10);
blip(430+game.combo*40, 0.09, 'triangle', 0.05);
syncHUD();
if(bricks.every(k=>!k.alive)){ levelClear(); }
break;
}
}
if(ball.y - ball.r > H){ loseLife(); }
}
function updateHearts(dt){
for(let i=hearts.length-1;i>=0;i--){
const h = hearts[i];
h.vy += 620*dt;
h.vx *= (1 - 0.9*dt);
h.x += h.vx*dt;
h.y += h.vy*dt;
h.rot += h.vr*dt;
h.life -= h.decay*dt;
if(h.life<=0 || h.y>H+40){ hearts.splice(i,1); continue; }
// collected by the burger?
if(h.y>paddle.y-6 && h.y<paddle.y+paddle.h+10 &&
h.x>paddle.x-4 && h.x<paddle.x+paddle.w+4){
hearts.splice(i,1);
game.heartsGot++;
game.score += 15;
popup(h.x, h.y, '+15', '#ff8fb1');
burst(h.x,h.y,'#ff6f9b',5,110);
blip(760,0.06,'sine',0.04);
syncHUD();
}
}
for(let i=parts.length-1;i>=0;i--){
const p=parts[i];
p.vy += 420*dt; p.x+=p.vx*dt; p.y+=p.vy*dt; p.life-=p.decay*dt;
if(p.life<=0) parts.splice(i,1);
}
for(let i=pops.length-1;i>=0;i--){
const p=pops[i];
p.y -= 42*dt; p.life -= p.decay*dt;
if(p.life<=0) pops.splice(i,1);
}
}
function update(dt){
time += dt;
if(game.comboTimer>0){ game.comboTimer-=dt; if(game.comboTimer<=0) game.combo=0; }
if(game.paused){ return; }
updatePaddle(dt);
updateHearts(dt);
if(game.state==='serve'){
ball.x = paddle.x + paddle.w/2;
ball.y = paddle.y - ball.r - 2;
game.serveTimer -= dt;
if(game.demo && game.serveTimer<=0) launch();
}else if(game.state==='play'){
updateBall(dt);
trail.push({x:ball.x,y:ball.y});
if(trail.length>11) trail.shift();
}else if(game.state==='dead'){
game.deadTimer -= dt;
if(game.deadTimer<=0){
if(game.lives<=0) gameOver(); else resetBall();
}
}else if(game.state==='levelclear'){
game.clearTimer -= dt;
if(game.clearTimer<=0){
game.level++;
layoutLevel(game.level);
resetBall();
syncHUD();
}
}else if(game.state==='gameover'){
game.overTimer += dt;
if(game.demo && game.overTimer>3.0){ resetGame(); }
}
}
/* ---------------- render ---------------- */
function drawBackground(){
const g = ctx.createRadialGradient(W*0.5, H*0.18, 30, W*0.5, H*0.55, Math.max(W,H)*0.85);
g.addColorStop(0,'#2b1150'); g.addColorStop(0.55,'#150a28'); g.addColorStop(1,'#080410');
ctx.fillStyle=g;
ctx.fillRect(-12,-12,W+24,H+24);
ctx.strokeStyle='rgba(255,255,255,.035)';
ctx.lineWidth=1;
ctx.beginPath();
const step = 52;
for(let x=0;x<W;x+=step){ ctx.moveTo(x+.5,-10); ctx.lineTo(x+.5,H+10); }
for(let y=0;y<H;y+=step){ ctx.moveTo(-10,y+.5); ctx.lineTo(W+10,y+.5); }
ctx.stroke();
// soft glow above the paddle
const gg = ctx.createRadialGradient(paddle.x+paddle.w/2, paddle.y, 4, paddle.x+paddle.w/2, paddle.y, paddle.w*1.5);
gg.addColorStop(0,'rgba(255,190,90,.10)'); gg.addColorStop(1,'rgba(255,190,90,0)');
ctx.fillStyle=gg;
ctx.fillRect(0,paddle.y-paddle.w*1.6,W,paddle.w*1.8);
}
function drawBricks(){
for(let i=0;i<bricks.length;i++){
const b=bricks[i]; if(!b.alive) continue;
ctx.save();
ctx.fillStyle='rgba(0,0,0,.4)';
rr(ctx,b.x,b.y+4,b.w,b.h,5); ctx.fill();
const g=ctx.createLinearGradient(0,b.y,0,b.y+b.h);
g.addColorStop(0,b.t); g.addColorStop(1,b.b);
ctx.fillStyle=g;
rr(ctx,b.x,b.y,b.w,b.h,5); ctx.fill();
ctx.fillStyle='rgba(255,255,255,.3)';
rr(ctx,b.x+3,b.y+2.5,b.w-6,Math.max(2,b.h*0.2),3); ctx.fill();
ctx.fillStyle='rgba(0,0,0,.16)';
rr(ctx,b.x+3,b.y+b.h-Math.max(2,b.h*0.18),b.w-6,Math.max(1.5,b.h*0.16),3); ctx.fill();
ctx.strokeStyle='rgba(0,0,0,.3)'; ctx.lineWidth=1;
rr(ctx,b.x+.5,b.y+.5,b.w-1,b.h-1,5); ctx.stroke();
ctx.restore();
}
}
function drawHearts(){
for(let i=0;i<hearts.length;i++){
const h=hearts[i];
const a=clamp(h.life*1.7,0,1);
ctx.save();
ctx.globalAlpha=a;
ctx.translate(h.x,h.y);
ctx.rotate(h.rot);
heartPath(ctx,h.s);
ctx.fillStyle=h.c;
ctx.fill();
ctx.globalAlpha=a*0.45;
ctx.scale(0.46,0.46); ctx.translate(-h.s*0.26,-h.s*0.3);
heartPath(ctx,h.s);
ctx.fillStyle='#ffe3ee';
ctx.fill();
ctx.restore();
}
}
function drawParticles(){
for(let i=0;i<parts.length;i++){
const p=parts[i];
ctx.globalAlpha=clamp(p.life,0,1);
ctx.fillStyle=p.c;
ctx.beginPath(); ctx.arc(p.x,p.y,p.size,0,Math.PI*2); ctx.fill();
}
ctx.globalAlpha=1;
}
function drawPopups(){
for(let i=0;i<pops.length;i++){
const p=pops[i];
ctx.save();
ctx.globalAlpha=clamp(p.life,0,1);
ctx.font='800 15px system-ui, sans-serif';
ctx.textAlign='center';
ctx.lineWidth=3; ctx.strokeStyle='rgba(0,0,0,.55)';
ctx.strokeText(p.t,p.x,p.y);
ctx.fillStyle=p.c;
ctx.fillText(p.t,p.x,p.y);
ctx.restore();
}
}
function drawBurger(px,py,pw,ph){
const x=px,y=py;
ctx.save();
ctx.fillStyle='rgba(0,0,0,.42)';
ctx.beginPath(); ctx.ellipse(x+pw/2, y+ph+5, pw*0.44, ph*0.12, 0,0,Math.PI*2); ctx.fill();
const bunBotH=ph*0.26, pattyH=ph*0.19, cheeseH=ph*0.11, lettH=ph*0.14, bunTopH=ph*0.30;
/* bottom bun */
let yy=y+ph-bunBotH;
let g=ctx.createLinearGradient(0,yy,0,yy+bunBotH);
g.addColorStop(0,'#e9ae66'); g.addColorStop(1,'#b9743a');
ctx.fillStyle=g; rr(ctx,x,yy,pw,bunBotH,[6,6,pw*0.22,pw*0.22]); ctx.fill();
ctx.strokeStyle='rgba(90,45,15,.45)'; ctx.lineWidth=1; ctx.stroke();
/* patty */
yy=y+ph-bunBotH-pattyH+2;
g=ctx.createLinearGradient(0,yy,0,yy+pattyH);
g.addColorStop(0,'#8d5636'); g.addColorStop(1,'#472614');
ctx.fillStyle=g; rr(ctx,x+3,yy,pw-6,pattyH+2,6); ctx.fill();
ctx.fillStyle='rgba(255,225,190,.14)';
for(let i=0;i<5;i++){
const dx=x+9+(pw-18)*(i/4);
ctx.beginPath(); ctx.arc(dx, yy+pattyH*0.45, Math.max(1.2,ph*0.045), 0, Math.PI*2); ctx.fill();
}
/* cheese */
yy=y+ph-bunBotH-pattyH-cheeseH+3;
ctx.fillStyle='#ffcf3d';
ctx.beginPath();
ctx.moveTo(x+4,yy); ctx.lineTo(x+pw-4,yy); ctx.lineTo(x+pw-4,yy+cheeseH);
const seg=(pw-8)/3;
for(let i=0;i<3;i++){
const x1=x+pw-4-(i+1)*seg, x2=x+pw-4-(i+2)*seg;
ctx.quadraticCurveTo((x1+x2)/2, yy+cheeseH+ph*0.13, x2, yy+cheeseH);
}
ctx.lineTo(x+4,yy+cheeseH); ctx.closePath(); ctx.fill();
ctx.strokeStyle='rgba(150,100,0,.35)'; ctx.lineWidth=1; ctx.stroke();
/* lettuce */
yy=y+ph-bunBotH-pattyH-cheeseH-lettH+4;
ctx.fillStyle='#68c23f';
ctx.beginPath();
ctx.moveTo(x+2,yy); ctx.lineTo(x+pw-2,yy); ctx.lineTo(x+pw-2,yy+lettH*0.3);
const sc=6, span=(pw-4)/sc;
for(let i=0;i<sc;i++){
const x1=x+pw-2-(i+1)*span, x2=x+pw-2-(i+2)*span;
ctx.quadraticCurveTo((x1+x2)/2, yy+lettH*1.6, x2, yy+lettH*0.3);
}
ctx.lineTo(x+2,yy); ctx.closePath(); ctx.fill();
ctx.strokeStyle='rgba(40,110,20,.45)'; ctx.lineWidth=1; ctx.stroke();
/* top bun */
yy=y;
g=ctx.createLinearGradient(0,yy,0,yy+bunTopH+3);
g.addColorStop(0,'#ffd79b'); g.addColorStop(0.55,'#f0ba71'); g.addColorStop(1,'#d59a4f');
ctx.fillStyle=g; rr(ctx,x,yy,pw,bunTopH+3,[pw*0.26,pw*0.26,8,8]); ctx.fill();
ctx.strokeStyle='rgba(120,70,20,.38)'; ctx.lineWidth=1; ctx.stroke();
/* sesame seeds */
ctx.fillStyle='rgba(255,251,232,.92)';
const seeds=[[0.20,0.46],[0.40,0.28],[0.62,0.48],[0.80,0.31],[0.48,0.64],[0.30,0.72]];
for(let i=0;i<seeds.length;i++){
const sx=seeds[i][0], sy=seeds[i][1];
ctx.save();
ctx.translate(x+pw*sx, yy+bunTopH*sy);
ctx.rotate(sx*4);
ctx.beginPath();
ctx.ellipse(0,0,Math.max(1.6,pw*0.033),Math.max(1,bunTopH*0.11),0,0,Math.PI*2);
ctx.fill();
ctx.restore();
}
ctx.restore();
}
function drawSteam(px,py,pw){
ctx.save();
ctx.strokeStyle='rgba(255,255,255,.16)';
ctx.lineWidth=2; ctx.lineCap='round';
for(let i=0;i<3;i++){
const bx=px+pw*(0.26+0.24*i);
const off=Math.sin(time*2.3+i*1.7)*4.5;
ctx.beginPath();
ctx.moveTo(bx+off, py-3);
ctx.quadraticCurveTo(bx-off, py-11, bx+off*0.6, py-20);
ctx.stroke();
}
ctx.restore();
}
function drawPaddle(){
const tilt=clamp(paddle.vx*0.00007,-0.11,0.11);
ctx.save();
ctx.translate(paddle.x+paddle.w/2, paddle.y+paddle.h/2);
ctx.rotate(tilt);
ctx.translate(-paddle.w/2,-paddle.h/2);
drawSteam(0,-1,paddle.w);
drawBurger(0,0,paddle.w,paddle.h);
ctx.restore();
}
function drawBall(){
for(let i=0;i<trail.length;i++){
const t=trail[i], f=(i+1)/trail.length;
ctx.globalAlpha=f*0.30;
ctx.fillStyle='#ffd76e';
ctx.beginPath(); ctx.arc(t.x,t.y,ball.r*f*0.95,0,Math.PI*2); ctx.fill();
}
ctx.globalAlpha=1;
const g=ctx.createRadialGradient(ball.x-ball.r*0.32,ball.y-ball.r*0.36,ball.r*0.12,ball.x,ball.y,ball.r);
g.addColorStop(0,'#fffdf0'); g.addColorStop(0.5,'#ffd76e'); g.addColorStop(1,'#ff9a3c');
ctx.fillStyle=g;
ctx.beginPath(); ctx.arc(ball.x,ball.y,ball.r,0,Math.PI*2); ctx.fill();
ctx.strokeStyle='rgba(255,255,255,.7)'; ctx.lineWidth=1.5; ctx.stroke();
}
function drawBanner(){
let text='', sub='', col='#ffd76e';
if(game.state==='levelclear'){ text='LEVEL '+game.level+' CLEAR!'; sub='next burger incoming…'; col='#8ef0a6'; }
else if(game.state==='dead'){ text='BURGER LOST'; sub=(game.lives>0? (game.lives+' left') : ''); col='#ff7a8a'; }
else if(game.state==='gameover'){ text='GAME OVER'; sub='score '+game.score; col='#ff7a8a'; }
if(!text) return;
ctx.save();
ctx.textAlign='center';
ctx.font='900 '+clamp(W*0.055,22,42)+'px system-ui, sans-serif';
ctx.lineWidth=6; ctx.strokeStyle='rgba(0,0,0,.6)';
ctx.strokeText(text, W/2, H*0.45);
ctx.fillStyle=col; ctx.fillText(text, W/2, H*0.45);
if(sub){
ctx.font='700 '+clamp(W*0.02,12,17)+'px system-ui, sans-serif';
ctx.strokeText(sub, W/2, H*0.45+26);
ctx.fillStyle='#e8dcff'; ctx.fillText(sub, W/2, H*0.45+26);
}
ctx.restore();
}
function drawCombo(){
if(game.combo<2) return;
ctx.save();
ctx.textAlign='center';
const s=1+Math.sin(time*9)*0.06;
ctx.font='900 '+Math.round(clamp(W*0.032,16,28)*s)+'px system-ui, sans-serif';
ctx.lineWidth=5; ctx.strokeStyle='rgba(0,0,0,.55)';
ctx.strokeText('COMBO x'+game.combo, W/2, 46);
ctx.fillStyle='#ff77a8'; ctx.fillText('COMBO x'+game.combo, W/2, 46);
ctx.restore();
}
function render(){
ctx.setTransform(dpr,0,0,dpr,0,0);
drawBackground();
ctx.save();
if(shake>0.05){
ctx.translate(rnd(-shake,shake), rnd(-shake,shake));
}
drawBricks();
drawParticles();
drawHearts();
drawPaddle();
drawBall();
drawPopups();
ctx.restore();
drawCombo();
drawBanner();
}
/* ---------------- loop ---------------- */
let last = performance.now();
function loop(now){
let dt=(now-last)/1000; last=now;
if(dt>0.05) dt=0.05;
update(dt);
render();
requestAnimationFrame(loop);
}
window.addEventListener('resize', resize);
if(window.ResizeObserver){ new ResizeObserver(resize).observe(stage); }
resize();
resetGame();
syncHUD();
requestAnimationFrame(loop);
})();
</script>
</body>
</html>Reasoning trace (16,481 tokens)
Let me build a breakout game where the paddle is a hamburger and bricks explode into tons of little hearts.
Key features:
- Canvas-based breakout
- Paddle rendered as a hamburger (bun top, lettuce, cheese, patty, bun bottom)
- Bricks break into many little hearts that explode outward with physics, falling down
- Auto-play demo mode: simple AI moves the paddle to track the ball, behind a "Click or press a key to start" message
- Responsive 1280x800 down to 375px
- No console errors
Let me design it nicely:
- Dark background with subtle gradient / neon arcade vibe
- Ball is a glowing dot with trail
- Bricks in rows of colors; on break, spawn ~18-24 hearts with rotation, gravity, fade out
- Particles: heart emoji shapes? Better to draw hearts with canvas paths for crispness, in pink/red colors. Could also use emoji glyphs but fonts may vary; drawing path is safer and looks nice.
Hearts drawn via bezier path, scaled, rotating.
Let me write the heart path function:
```js
function drawHeart(ctx, x, y, size, rot, color, alpha) {
ctx.save();
ctx.translate(x,y);
ctx.rotate(rot);
ctx.globalAlpha = alpha;
ctx.scale(size/100, size/100);
ctx.beginPath();
ctx.moveTo(0, 30);
ctx.bezierCurveTo(-50, -10, -30, -60, 0, -30);
...
}
```
Standard heart path:
```
ctx.moveTo(0, 20);
ctx.bezierCurveTo(-40, -10, -30, -50, 0, -25);
ctx.bezierCurveTo(30, -50, 40, -10, 0, 20);
```
Hmm, let me use a well-known heart shape:
```js
function heartPath(ctx, s) {
ctx.beginPath();
ctx.moveTo(0, s * 0.3);
ctx.bezierCurveTo(-s*0.5, -s*0.1, -s*0.35, -s*0.6, 0, -s*0.3);
ctx.bezierCurveTo(s*0.35, -s*0.6, s*0.5, -s*0.1, 0, s*0.3);
ctx.closePath();
}
```
That gives a decent heart. Let's scale appropriately: define unit heart of about 1 wide, 0.9 tall.
Actually simpler: use the classic:
```
ctx.moveTo(0, 0.3);
ctx.bezierCurveTo(-0.5, -0.05, -0.4, -0.5, 0, -0.25);
ctx.bezierCurveTo(0.4, -0.5, 0.5, -0.05, 0, 0.3);
```
with scale = size.
Bricks: rounded rects with glossy gradient, different colors per row (burger-themed? tomato, cheese, lettuce, onion, pickle, bacon). Fun.
Paddle: hamburger. Draw:
- bottom bun: rounded rect with gradient brown, plus sesame seeds on the top bun
- lettuce: wavy green
- cheese: yellow triangle-ish layer with drips
- patty: dark brown
- top bun: rounded with sesame seeds
I'll write a function drawHamburger(ctx, x, y, w, h) that draws stacked layers.
Ball: maybe a pickle? Or just a glowing sphere. Let's make it a bright white/glowing ball with trail. Could be a mini burger? Keep it a glowing golden sphere.
HUD: score, level, hearts collected counter? Let's add "Hearts" counter that accumulates as hearts fly off? Maybe count hearts that fall off screen = "hearts collected". Let's do: hearts spawn, drift down, and score bonus when they fall past the bottom? Keep simple: each heart that exits bottom adds 1 to a "Hearts" counter. That gives a nice number.
Also some hearts could be collected by paddle for bonus. Let's do: hearts fall, if they touch paddle, they get collected → +5 points and a little pop. Otherwise they drift off screen.
Game loop: classic breakout with multiple levels? Keep 3-4 levels of brick rows, then a new level with harder arrangement. Win condition: clear all bricks → next level, increase ball speed slightly.
Lives: 3. Losing a ball loses a life; reset ball onto paddle. Game over when lives = 0 → restart.
Demo AI: paddle tracks ball x with a small delay and slight error, and occasionally nudges. Also serves ball automatically.
Interactivity: Click/press key to start → moves to "player" mode (player controls with mouse/touch/arrow keys). But the demo mode should be playing immediately behind the "Click or press a key to start" overlay.
Implementation plan:
- state = 'demo' initially. Overlay shows "CLICK OR PRESS ANY KEY TO START" plus a hint that the AI is playing. After click/keypress, overlay hides and state = 'play' with player controls (mouse move, touch, arrow keys). Maybe also a key to toggle back to demo (press D). And add a small "Demo: ON" toggle button.
Let me make the "start" click also just dismiss and player takes over. Provide a toggle button top-right "Auto-play: ON/OFF".
Responsive: canvas fills container, sized via CSS with devicePixelRatio scaling; game coordinates computed from canvas CSS size. Use a fixed logical aspect? Simplest: canvas fills window, game layout computed from actual canvas width/height. Bricks laid out based on width with margins. Paddle width scales with width.
Let's set canvas element with CSS width:100%; height:100%; and resize handler to set canvas.width = clientWidth*dpr.
Layout:
- Top HUD bar (HTML) with title, score, lives, level, hearts. Overlay message on top of canvas area.
- Actually simpler to draw HUD inside canvas for cohesion, but HTML HUD is easier to make responsive and crisp. Let's do HTML header with flex, and canvas below.
Colors: background deep purple/charcoal with vignette; subtle grid.
Let me write code.
Key details for hearts explosion:
```js
function explodeHearts(x, y, color) {
const n = 16 + Math.floor(Math.random()*8);
for (let i=0;i<n;i++){
hearts.push({
x, y,
vx: (Math.random()-0.5)*8,
vy: -(Math.random()*6+1),
rot: Math.random()*Math.PI*2,
vr: (Math.random()-0.5)*0.3,
size: 6 + Math.random()*9,
life: 1,
decay: 0.006 + Math.random()*0.006,
hue: ...
});
}
}
```
Hearts drift upward initially then fall with gravity. Then fade. Cap the number of hearts (e.g., max 600) for performance.
Also add small spark particles (non-heart) for the brick break flash? The hearts themselves are the explosion. Add a quick white flash ring at break position.
Brick break: remove brick, add score, hearts explode.
Also make brick colors: rows of burger ingredients:
Row colors:
1. #f4b183 lettuce green #7ec850
Let's define palette array of 6: tomato red #ff5f57, cheese #ffc93c, lettuce #7bd35b, onion purple #b07de0, pickle #8bc34a, bacon #ff8a5c? and top bun #d9a066.
Brick drawing: rounded rect with gradient, lighter top edge, plus small pattern? Keep simple: gradient fill + inner highlight line + shadow.
Ball physics: standard breakout with paddle collision including angle based on hit position. Walls: left, right, top. Bottom: lose life.
Ball trail: store last N positions, draw fading circles.
Demo AI: target x = ball.x + ball.vx * prediction. Use simple: target = ball.x; paddle moves toward target with max speed and a slight offset error. To look natural, target = ball.x + Math.sin(time)*10 and move at limited speed (e.g., 8 px/frame scaled). Ensure AI can actually keep up — make AI speed slightly higher than needed but with occasional lag.
Serve: when ball is on paddle (state 'serve'), it follows paddle until launch. In demo mode, auto-launch after 0.8s. In player mode, launch on click/space or automatically after 2s.
Let me also add a "combo" system: breaking multiple bricks quickly multiplies score.
Score popups: floating "+50" text at brick position.
Lives: draw as small hearts in HUD (nice thematic touch).
Level progression: level N has rows = min(3 + N, 7), brick speed increases, ball speed increases slightly. When all bricks cleared, show "LEVEL CLEAR" banner for 1.5s then next level.
Now the HTML/CSS:
Body: margin 0, height 100vh, flex column, overflow hidden, background gradient.
Header: title "HAMBURGER BREAKOUT" with subtitle, stat chips.
Main: flex:1, position relative, canvas absolute inset 0.
Overlay: absolute center, semi-transparent panel with big text.
Mobile: header wraps, font sizes smaller.
Let me write the code carefully.
Canvas sizing:
```js
function resize(){
const rect = canvas.getBoundingClientRect();
const dpr = Math.min(window.devicePixelRatio||1, 2);
canvas.width = Math.max(1, Math.round(rect.width*dpr));
canvas.height = Math.max(1, Math.round(rect.height*dpr));
ctx.setTransform(dpr,0,0,dpr,0,0);
W = rect.width; H = rect.height;
layout();
}
```
Use CSS width/height 100% of container. Then W,H are CSS pixels; game logic uses W,H.
layout(): compute brick area: margin = clamp(W*0.06, 16, 60); top of brick area = 70 (below HUD? HUD is separate HTML, so top of canvas is the top of play area) → topInset = 28 maybe; rows per level, cols based on width: cols = clamp(Math.floor(W/90), 4, 10). brickH = clamp((H*0.45)/rows, 16, 34)? Let's compute brickArea height ~ 55% of H. brickH = min(34, areaH/rows - gap).
Paddle: width = clamp(W*0.16, 70, 150); height = width*0.42? Hamburger height maybe 26-34px. Let's do paddleW = clamp(W*0.15, 72, 160); paddleH = clamp(paddleW*0.28, 20, 34).
Ball radius = clamp(paddleW*0.09, 7, 12)? Let's do ball r = clamp(W*0.011, 6, 11).
Let me now write the whole thing.
Draw hamburger function:
```js
function drawHamburger(ctx, cx, cy, w, h){
const topH = h*0.28, pattyH = h*0.2, cheeseH=h*0.1, lettuceH=h*0.14, botH = h*0.28;
// bottom bun
...
}
```
Simplify: layers stacked from top:
1. Top bun: rounded rect radius large at top, color #e8b36b gradient with sesame seeds.
2. Lettuce: green wavy band (zigzag bottom).
3. Cheese: yellow rect with drips.
4. Patty: dark brown rounded rect.
5. Bottom bun: #d99a4e rounded rect with rounded bottom.
Total height h.
Implementation with rounded rect helper:
```js
function roundRect(ctx,x,y,w,h,r){
ctx.beginPath();
ctx.moveTo(x+r,y);
ctx.arcTo(x+w,y,x+w,y+h,r);
ctx.arcTo(x+w,y+h,x,y+h,r);
ctx.arcTo(x,y+h,x,y,r);
ctx.arcTo(x,y,x+w,y,r);
ctx.closePath();
}
```
Top bun: roundRect(x, y, w, topH, [w/2, w/2, 4, 4]) — use ctx.roundRect if available (Chrome supports ctx.roundRect since Chrome 99). Safe to use modern Chrome; latest Chrome fine. But to be safe implement manual helper with array radii. I'll just implement manual with per-corner radii.
Let's implement helper roundRectPath(ctx,x,y,w,h,radii) where radii = [tl,tr,br,bl].
Then:
- top bun fill gradient from #f0bd78 to #d99a4e.
- sesame seeds: small ellipses in lighter color at random-ish fixed positions (deterministic pattern based on index).
Lettuce: draw path: top line straight across, bottom wavy via arcs. Use fill green #6dc24a with darker edge.
Cheese: yellow #ffd54f, with two drips below (triangles/curves).
Patty: #6b3f2a with texture dots.
Bottom bun: gradient #dda25b to #c07f3e.
Add a shadow ellipse under the hamburger.
Now hearts drawing:
```js
function heartPath(ctx, s){
ctx.beginPath();
ctx.moveTo(0, s*0.35);
ctx.bezierCurveTo(-s*0.62, -s*0.05, -s*0.45, -s*0.72, 0, -s*0.35);
ctx.bezierCurveTo(s*0.45, -s*0.72, s*0.62, -s*0.05, 0, s*0.35);
ctx.closePath();
}
```
Width of heart ≈ 1.24*s, height ≈ 1.07*s. So size param s gives a heart roughly s tall.
Colors for hearts: pink/red/magenta variations: ['#ff5d8f','#ff3b6b','#ff8fb1','#ff2d55','#ffb3c6'] with some glow.
Add glow via shadowBlur for a few? shadowBlur is expensive with many hearts. Instead draw hearts with simple fills and occasional highlight. Maybe use lighter composite for a subtle inner shine: draw smaller heart in lighter color at top-left with alpha.
Performance: hearts up to maybe 500 at once; each is a few path ops — fine.
Now let's write the game states.
```js
const game = {
state: 'title', // title | serve | play | dead | levelclear | gameover
demo: true,
score:0, lives:3, level:1, hearts:0, combo:0, comboTimer:0,
};
```
Main update(dt):
- update paddle (demo AI or player input)
- update ball
- update hearts
- collisions
Let's write with requestAnimationFrame and dt clamp.
Ball speed: base = 380 + (level-1)*18, scaled by size factor relative to 1280 width? Use factor = clamp(W/1280, 0.6, 1.4). ballSpeed = (360 + (level-1)*22) * factor.
Hmm, at small widths the paddle is proportionally similar. Let's just use base 340 px/s and scale by min(1, W/900+0.4)? Keep: speed = 330 + (level-1)*25, times factor where factor = clamp(W/1100, 0.62, 1.25).
Paddle movement max speed: player follows mouse instantly (or smoothed). Keyboard: velocity ±600 px/s.
AI:
```js
if(game.demo){
// predict
const target = ball.x + ball.vx*0.12;
const dx = target - paddle.x;
const aiSpeed = paddleW*2.4; // px/s
paddle.x += clamp(dx, -aiSpeed*dt, aiSpeed*dt);
}
```
This should track well. Sometimes add error: target += Math.sin(t*1.7)*6.
Also AI should aim to hit ball with center-ish: it's fine.
Ball on paddle (serve): in demo auto launch after 0.9s; in play mode, launch on space/click/tap.
Collision with paddle: if ball.vy>0 and ball.y+r >= paddle.y and ball.y-r < paddle.y+paddleH and x within paddle bounds → bounce, angle based on offset = (ball.x - paddleCenter)/(paddleW/2), clamp to ±0.85, set vx = sin(a)*speed, vy = -cos(a)*speed.
Brick collision: standard AABB circle-ish. Use rectangle overlap test with the ball center vs brick rect (approx). On hit: determine which side by comparing penetration. Simpler approach: check overlap of ball circle with brick rect; compute overlap depths on each axis; choose smaller → reflect.
Implementation:
```js
function collideBrick(b){
const nx = clamp(ball.x, b.x, b.x+b.w);
const ny = clamp(ball.y, b.y, b.y+b.h);
const dx = ball.x-nx, dy = ball.y-ny;
const d2 = dx*dx+dy*dy;
if(d2 > ball.r*ball.r) return false;
// determine reflect axis
if(Math.abs(dx) > Math.abs(dy)) ball.vx = -ball.vx; else ball.vy = -ball.vy;
...
}
```
Actually with corner hits it's ambiguous but fine.
Better: use overlap with expanded rect approach: treat ball as point and check if point inside rect expanded by r.
Let's do:
```js
const ex = b.x - ball.r, ey = b.y - ball.r, ew = b.w + 2*ball.r, eh = b.h + 2*ball.r;
if(ball.x>ex && ball.x<ex+ew && ball.y>ey && ball.y<ey+eh){
// compute overlap to find side
const overlapLeft = ball.x - ex;
const overlapRight = (ex+ew) - ball.x;
const overlapTop = ball.y - ey;
const overlapBottom = (ey+eh) - ball.y;
const min = Math.min(overlapLeft, overlapRight, overlapTop, overlapBottom);
if(min===overlapLeft) ball.x = ex; else if(min===overlapRight) ball.x = ex+ew;
else if(min===overlapTop) ball.y = ey; else ball.y = ey+eh;
reflect accordingly
}
```
This is decent.
Brick break: remove from array, explodeHearts at brick center, score += base * combo multiplier, flash effect.
Heart collection: hearts fall with gravity; if a heart's y > paddle.y and within paddle x range → collect: heartsCollected++, score += 10, spawn small sparkle, remove heart. Otherwise when heart.y > H+30 → remove.
Actually hearts falling from the top of the brick area will land near the bottom — they'd often be collected by the paddle in demo mode, that's fun and adds a bonus score.
Limit hearts array to e.g. 400; when exceeding, remove oldest.
Level clear: when bricks.length === 0 → state 'levelclear', timer 1.6s → next level: level++, build bricks, ball resets to paddle, lives +1 maybe every 3 levels.
Life lost: state 'dead', timer 1.2s → reset ball (lives--). If lives <=0 → gameover, show overlay with score and restart button; in demo mode auto restart after 2.5s.
Title state: overlay "CLICK OR PRESS ANY KEY TO START" and the game runs in demo mode behind. So the game is already playing behind the overlay — good. So actually set state = 'serve' from the beginning with demo true, and the overlay visible. The overlay's click starts player mode.
Let's structure: `started` boolean. When !started: overlay visible; demo AI plays. Click/key → started=true, overlay hidden, demo=false (player control). Provide a button "Auto-play" toggle to re-enable demo. Also pressing "d" toggles.
Hmm — but instructions say game must start in self-playing demo mode behind a "Click or press a key to start" message. Good.
Also "The iframe only receives keyboard events after the user clicks it" — so the click that starts gives keyboard focus. Good.
Overlay pointer-events: we need the click to be received. Put the overlay as a div with pointer-events:auto covering the canvas, and a click handler on document/window for start. After start, hide overlay, and add mouse move listeners on canvas.
Mouse control: mousemove on canvas sets targetX. Also touch support: touchmove on canvas.
Let's also handle keyboard: ArrowLeft/Right or A/D to move paddle; Space to launch.
Now HUD design:
- Left: Title "🍔 HAMBURGER BREAKOUT" (emoji is fine — it's a font glyph, might be a color emoji; acceptable). Maybe avoid emoji in HUD to be safe? Emoji render fine in Chrome on most platforms; in a Linux sandbox they might render as tofu. Risky — better to draw the hamburger in canvas. In HTML, use text only. I'll avoid emoji in HTML and draw everything with canvas. Hearts in HUD: draw as small inline SVG? SVG inline is fine and safe (path defined in HTML). Let's use inline SVG for heart icons in the HUD lives counter — small, safe.
Actually simpler: draw lives as small hearts via SVG paths generated by JS. I'll generate SVG markup in JS for lives. Fine.
Let me make HUD:
```
<header>
<div class="brand"><span class="logo"></span><div><h1>HAMBURGER BREAKOUT</h1><p>break bricks · explode hearts</p></div></div>
<div class="stats">
<div class="chip"><span class="label">SCORE</span><span class="value" id="score">0</span></div>
<div class="chip"><span class="label">LEVEL</span><span class="value" id="level">1</span></div>
<div class="chip"><span class="label">HEARTS</span><span class="value" id="hearts">0</span></div>
<div class="chip lives"><span class="label">BURGERS</span><span id="lives"></span></div>
<button id="autoBtn">AUTO: ON</button>
</div>
</header>
```
Hmm "BURGERS" for lives is thematic. Nice.
Header should be responsive: on narrow screens, hide subtitle and shrink.
Footer? Maybe a small hint bar at bottom: "Move with mouse / ← → · Space to launch · P pause". Could be part of header or a thin footer. Let's add a footer bar with hints, hidden on mobile.
Colors: bg #0b0713 → #1a0f2e gradient; accent #ffd54f (cheese), #ff4d6d (hearts), lettuce green.
Canvas background: I'll draw background inside canvas: radial gradient + subtle horizontal "grill lines"? Let's do a dark purple radial gradient plus faint grid lines and floating burger-ish? Keep tasteful: subtle vignette + faint grid + a slow-moving glow.
Let me write the code now in full.
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>Hamburger Breakout</title>
<style>
*{box-sizing:border-box;}
html,body{height:100%;margin:0;}
body{
font-family: system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
background: radial-gradient(1200px 800px at 50% -10%, #2a1447 0%, #120a22 55%, #08050f 100%);
color:#fff; overflow:hidden; display:flex; flex-direction:column;
-webkit-user-select:none; user-select:none; touch-action:none;
}
header{ ... }
...
</style>
</head>
<body>
...
<script>...</script>
</body>
</html>
```
Let me write CSS carefully for responsiveness.
Header height ~ 64px desktop, with brand and stats. Use flex-wrap.
```css
header{
display:flex; align-items:center; justify-content:space-between; gap:12px;
padding:10px 16px;
border-bottom:1px solid rgba(255,255,255,.08);
background:linear-gradient(180deg, rgba(30,16,54,.9), rgba(16,9,30,.6));
backdrop-filter: blur(6px);
flex:0 0 auto;
}
.brand{display:flex;align-items:center;gap:10px;min-width:0}
.brand h1{font-size:clamp(13px,2.2vw,20px);margin:0;letter-spacing:.14em;font-weight:800;
background:linear-gradient(90deg,#ffd76e,#ff7a59); -webkit-background-clip:text;background-clip:text;color:transparent;white-space:nowrap}
.brand p{margin:2px 0 0;font-size:11px;color:#b9a7d6;letter-spacing:.08em}
.stats{display:flex;align-items:center;gap:8px;flex-wrap:wrap;justify-content:flex-end}
.chip{background:rgba(255,255,255,.06);border:1px solid rgba(255,255,255,.1);border-radius:10px;padding:4px 10px;display:flex;flex-direction:column;min-width:58px}
.chip .label{font-size:9px;letter-spacing:.16em;color:#a793c9}
.chip .value{font-size:15px;font-weight:700;font-variant-numeric:tabular-nums}
#lives{display:flex;gap:3px;align-items:center;height:18px}
button{...}
```
Canvas area:
```css
main{position:relative;flex:1 1 auto;min-height:0}
canvas{display:block;width:100%;height:100%}
.overlay{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;...}
```
Overlay with panel: big pulsing text "CLICK OR PRESS ANY KEY TO START", subtitle "the AI is demoing — you take over on click", and controls hint.
Add a CSS pulse animation.
Footer hint bar inside header? Put as a small line under header maybe. I'll add a `.tips` row inside header area? Let's include tips as a separate small bar at bottom of main, absolutely positioned bottom center with low opacity. Actually it may overlap the paddle area (bottom). Better put tips in the header (hidden on small screens) — e.g., under the brand subtitle: "mouse / arrows to move · space to serve". Good, use the subtitle for that.
Now the canvas drawing code.
Background draw:
```js
function drawBackground(){
const g = ctx.createRadialGradient(W*0.5, H*0.25, 40, W*0.5, H*0.5, Math.max(W,H)*0.8);
g.addColorStop(0,'#241041'); g.addColorStop(0.55,'#150a26'); g.addColorStop(1,'#0a0514');
ctx.fillStyle=g; ctx.fillRect(0,0,W,H);
// faint grid
ctx.strokeStyle='rgba(255,255,255,0.035)'; ctx.lineWidth=1;
const step = 48;
ctx.beginPath();
for(let x=0;x<W;x+=step){ctx.moveTo(x+.5,0);ctx.lineTo(x+.5,H);}
for(let y=0;y<H;y+=step){ctx.moveTo(0,y+.5);ctx.lineTo(W,y+.5);}
ctx.stroke();
}
```
Grid every frame might be a bit costly but fine (48px step → ~30+20 lines). OK.
Maybe animate a subtle glow behind the paddle (cheese glow). Add.
Now bricks:
```js
function buildLevel(level){
bricks=[];
const rows = Math.min(3 + Math.floor((level-1)/2), 7); // 3..7
...
}
```
Let's design: rows = clamp(3 + Math.floor(level/2), 3, 7). Level 1 → 3+0=3? floor(1/2)=0 → 3 rows. Level 2 → 4. Level 3 → 4. Level 4 → 5... Level 7+ → 7? floor(7/2)=3 → 6; level 8 → 7. Fine.
Cols: based on width: cols = clamp(Math.floor((W - 2*margin + gap) / (desiredBrickW + gap)), 4, 10) with desiredBrickW = clamp(W*0.11, 60, 120).
Brick height: bh = clamp(rows... ) compute: brickAreaH = H*0.55; bh = clamp((brickAreaH - (rows-1)*gap)/rows, 14, 36).
Also add some indestructible? No, keep all breakable but maybe "hard" bricks requiring 2 hits (gray/silver)? Could add extra depth but keep hearts focus. I'll add "tough" bricks (dark) that need 2 hits — adds interest. Actually simpler: skip. Hmm, a bit of variety is nice: rows alternate ingredient colors. Let's keep all 1-hit but with varied colors.
Brick look: rounded rect gradient with a slight top highlight and a "ingredient" tint. Add tiny dots for texture.
Let me write brick colors list:
```js
const INGREDIENTS = [
{name:'tomato', top:'#ff8a7a', bot:'#e23b3b'},
{name:'cheese', top:'#ffe08a', bot:'#f5b301'},
{name:'lettuce', top:'#a6e86a', bot:'#4ea832'},
{name:'onion', top:'#d5a6f5', bot:'#8b4fd1'},
{name:'pickle', top:'#cfe57a', bot:'#6b9a1e'},
{name:'bacon', top:'#ffa07a', bot:'#d1482f'},
{name:'bun', top:'#f3cf9a', bot:'#cf9a58'},
];
```
Now hearts:
```js
function spawnHearts(x,y,color){
const n = 14 + (Math.random()*8|0);
for(let i=0;i<n;i++){
const a = Math.random()*Math.PI*2;
const sp = 40 + Math.random()*230;
hearts.push({
x, y,
vx: Math.cos(a)*sp,
vy: Math.sin(a)*sp - 60,
rot: Math.random()*Math.PI*2,
vr: (Math.random()-0.5)*8,
s: 5 + Math.random()*9,
life: 1,
decay: 0.45 + Math.random()*0.45, // per second life
color: pick from heart palette,
v: 0
});
}
}
```
Update hearts: vy += gravity*dt (gravity ~ 700 px/s²), vx *= friction, x += vx*dt, rot += vr*dt, life -= decay*dt. Draw alpha = clamp(life*1.6, 0, 1).
Life decay 0.45-0.9 per second → hearts live ~1.1-2.2s. Good.
Cap hearts: if hearts.length > 420, splice front.
Collect: if heart overlaps paddle (x within paddle, y between paddle.y-8 and paddle.y+paddleH+8) → collect.
When collected: heartsCollected++, score += 15, spawn tiny sparkle particles.
Draw hearts: fill color, then lighter inner highlight path scaled 0.5 offset up-left with alpha .35.
Also draw a soft glow for big hearts: maybe skip.
Now the ball trail: array of {x,y} last 12 positions, drawn as circles with decreasing alpha/radius.
Ball draw: radial gradient gold/white with glow ring.
Paddle: hamburger with shadow and slight tilt based on movement? Keep upright. Add slight squash when hitting.
Score popups: {x,y,text,life,color}.
Now let's handle the "level clear" and "game over" banners drawn on canvas.
Restart logic:
- state 'gameover': show overlay panel (HTML) with score and "Click to play again". In demo mode, auto restart after 3 seconds (so demo keeps going). Let's implement: when game over in demo mode → after 2.5s auto restart (reset score? keep score maybe reset). Let's reset game and continue demo.
Simplify: implement `resetGame()` that sets score/lives/level and rebuilds. Called at start and on restart.
Game over handling:
```js
if(game.state==='gameover'){
game.overTimer += dt;
if(game.demo && game.overTimer > 2.6){ resetGame(); }
}
```
And overlay element content updated to show GAME OVER + final score, with click to restart. The overlay shows automatically (display flex) when game over.
But we have two overlay states: title (start) and gameover. Use one overlay element with dynamic content, and `overlay.classList.toggle('hidden', ...)`.
Careful: overlay in title state shouldn't block? It should block so that the first click starts. And in gameover, click restarts.
Implement:
```js
function showOverlay(title, sub, hint){ ... }
function hideOverlay(){ overlay.classList.add('hidden'); }
```
Click handler:
```js
function onFirstInput(e){
if(game.state==='title'){
started = true; game.demo=false; hideOverlay(); ...
} else if(game.state==='gameover'){
resetGame(); hideOverlay();
}
}
window.addEventListener('pointerdown', onFirstInput) // but pointerdown on overlay also
window.addEventListener('keydown', ...)
```
Note: When the user clicks to take over, that same click could also be used to serve the ball. Fine.
For player control: pointermove over canvas sets paddle target. pointerdown on canvas also serves.
Auto button toggles demo mode: when turned on, the AI takes over. Button text updates.
Also key 'd' toggles demo, 'p' pauses.
Let's make sure no console errors: careful with audio? I'll add simple WebAudio blips with try/catch and only after user gesture. Might be risky in sandbox (AudioContext suspended) but it's fine; wrap in try/catch. Actually let's add a small sound: on brick break a short "pop", on collect a "ding". Use a lazily-created AudioContext created on first user gesture. Guard everything in try/catch. I'll include it — adds polish. Actually the sandbox may block audio; but errors are caught. Let's include but be careful.
Let me now consider sizes on a 1280x800 viewport: header ~ 62px, footer none → canvas area ~738px tall, 1280 wide. Good.
Brick layout: margin = clamp(W*0.06, 16, 70) = 70 (at 1280: 76.8 → clamp 70). top = 40. brickArea: width = W - 2*margin = 1140. cols: desiredBrickW = clamp(W*0.11, 60, 120) = 120 (140.8→120). gap = 8. total = cols*(120+8)-8 = 1140 → cols = 1140/128 = 8.9 → 8 cols → bricks will be slightly narrower: brickW = (1140 - 7*8)/8 = 135.5. Hmm bigger than desired. Let's compute cols = round((areaW+gap)/(desiredW+gap)) = round(1148/128) = 9. Then brickW = (1140 - 8*8)/9 = 119.5. Good.
brickH: rows=3, brickAreaH = min(H*0.55, ...)? H=738 → 405; bh = (405 - 2*gap)/3 = 129 → clamp to 36. Good, so bricks occupy 3*36+2*8 = 124px. Fine, but then the brick field is small at the top. That's normal breakout.
Maybe increase desired brick height for fewer rows: bh clamp [16, 40]. Let's set rows from level, brickH = clamp((H*0.5 - (rows-1)*gap)/rows, 16, 40).
Level 1: 3 rows of 40 + gaps = 136. Then the rest of the field is empty for the ball to travel. Fine.
Paddle: paddleW = clamp(W*0.15, 72, 160) → 160 at 1280 (192→160). paddleH = clamp(paddleW*0.26, 20, 34) → 34. Ball r = clamp(W*0.0105, 6, 12) → 12? W*0.0105=13.4→12. Good.
Ball speed: 330 px/s? With a 1280 wide field, that's a decent pace. Let's do base 340 + (level-1)*22, times sizeFactor = clamp(W/1100, .65, 1.2) → 1.16 at 1280 → 395 px/s. Reasonable.
Now write the code.
Detailed update function:
```js
function update(dt){
time += dt;
if(game.state==='title' || game.state==='play' || game.state==='serve'){ ... }
}
```
Let me define states: 'serve' (ball on paddle), 'play' (ball in flight), 'dead' (life lost animation), 'levelclear', 'gameover'.
Paddle update:
```js
function updatePaddle(dt){
const maxSpeed = 900;
if(game.demo){
let target;
if(game.state==='serve'){ target = ball.x; }
else {
// predict where ball will be when it reaches paddle y
target = predictBallX();
}
target += Math.sin(time*1.3)*4;
const dx = target - paddle.x;
const aiSpeed = 780;
paddle.x += clamp(dx, -aiSpeed*dt, aiSpeed*dt);
} else {
if(keys.left) paddle.x -= maxSpeed*dt;
if(keys.right) paddle.x += maxSpeed*dt;
if(pointerActive){
const dx = pointerX - paddle.x;
paddle.x += clamp(dx, -maxSpeed*dt*1.6, maxSpeed*dt*1.6);
}
}
paddle.x = clamp(paddle.x, 0, W - paddle.w);
}
```
predictBallX: simple approach: if ball moving down, estimate time to reach paddle.y: t = (paddle.y - ball.y)/ball.vy; then x = ball.x + ball.vx*t; but walls reflect. Do a simple raycast with reflections up to a few steps. Let's implement a quick simulation:
```js
function predictBallX(){
let x=ball.x, y=ball.y, vx=ball.vx, vy=ball.vy, t=0;
const targetY = paddle.y;
if(vy<=0) return ball.x;
let steps=0;
while(y<targetY && steps<60){
const dtStep = 0.02;
x+=vx*dtStep; y+=vy*dtStep; t+=dtStep;
if(x<ball.r){x=ball.r; vx=-vx;}
else if(x>W-ball.r){x=W-ball.r; vx=-vx;}
if(y<ball.r){y=ball.r; vy=-vy;}
steps++;
}
return x;
}
```
Good enough.
Ball update:
```js
function updateBall(dt){
if(game.state==='serve'){
ball.x = paddle.x + paddle.w/2;
ball.y = paddle.y - ball.r - 2;
ball.vx = 0; ball.vy = 0;
serveTimer -= dt;
if(game.demo && serveTimer<=0) launch();
return;
}
ball.x += ball.vx*dt; ball.y += ball.vy*dt;
// walls
if(ball.x < ball.r){ball.x=ball.r; ball.vx=Math.abs(ball.vx); wallFx();}
if(ball.x > W-ball.r){ball.x=W-ball.r; ball.vx=-Math.abs(ball.vx); wallFx();}
if(ball.y < ball.r){ball.y=ball.r; ball.vy=Math.abs(ball.vy); wallFx();}
// paddle
if(ball.vy>0 && ball.y+ball.r >= paddle.y && ball.y+ball.r <= paddle.y + paddle.h + ball.r*1.5 && ball.x>paddle.x-2 && ball.x<paddle.x+paddle.w+2){
const rel = clamp((ball.x - (paddle.x+paddle.w/2))/(paddle.w/2), -1, 1);
const angle = rel*0.9; // radians from vertical
ball.vx = Math.sin(angle)*ball.speed;
ball.vy = -Math.cos(angle)*ball.speed;
ball.y = paddle.y - ball.r - 0.5;
combo = 0? (combo continues while ball stays up) -> comboTimer reset
spark at paddle
}
// bricks
for(let i=bricks.length-1;i>=0;i--){...}
// bottom
if(ball.y - ball.r > H){ loseLife(); }
}
```
Combo: combo increments per brick broken; resets if 2.5s pass without a break or when the ball hits the paddle. Let's implement: comboTimer counts down from 1.6s; each break increments combo and resets timer; score per brick = 10 * combo multiplier (cap 8).
Score popups show "+{points}".
loseLife(): lives--, state='dead', deadTimer=1.1; hearts? Also spawn a burst of hearts at the paddle? Let's just do a little burst of crumbs.
If lives<=0 → state='gameover', show overlay with score.
levelclear: when bricks.length===0 → state='levelclear', timer 1.6 → next level.
Now the drawing of the hamburger paddle in detail.
```js
function drawBurger(ctx, x, y, w, h){
// x,y = top-left
const bunTop = h*0.30, lettuceH=h*0.13, cheeseH=h*0.10, pattyH=h*0.18, bunBot=h*0.29;
// shadow
ctx.save();
ctx.fillStyle='rgba(0,0,0,.35)';
ctx.beginPath();
ctx.ellipse(x+w/2, y+h+4, w*0.45, h*0.10, 0,0,Math.PI*2);
ctx.fill();
ctx.restore();
// bottom bun
let yy = y + h - bunBot;
roundRect(ctx, x, yy, w, bunBot, [6,6,w*0.2,w*0.2]);
grad('#e2a55c','#bd7f3c'); fill;
// patty
yy = y + h - bunBot - pattyH + 1;
roundRect(ctx, x+2, yy, w-4, pattyH+2, 6);
grad('#7a4a2e','#4a2a18'); fill;
// texture dots
...
// cheese
yy = y + h - bunBot - pattyH - cheeseH + 2;
cheese with drips
// lettuce
yy = y + h - bunBot - pattyH - cheeseH - lettuceH + 1;
wavy lettuce
// top bun
yy = y;
roundRect(ctx, x, yy, w, bunTop+2, [w*0.28,w*0.28,6,6]);
grad('#f7d095','#dba35a'); fill;
// sesame seeds
}
```
Order of drawing: bottom bun first (lowest), then patty, cheese, lettuce, top bun last (on top).
Cheese drips: draw a rect plus 3 drip triangles pointing down. I'll draw the cheese as a path: rectangle from (x+3, yy) to (x+w-3, yy+cheeseH) plus drips as quadratic curves downward.
Lettuce: path from (x+1,yy) across to (x+w-1, yy) then a wavy bottom edge via a series of arcs (scallops). Use quadraticCurveTo.
Top bun: gradient light top. Sesame seeds: 3-4 small ellipses rotated.
Add a subtle outline stroke (dark) around layers for definition.
Let me write helper `roundRect(ctx,x,y,w,h,r)` where r can be number or array [tl,tr,br,bl].
```js
function rr(ctx,x,y,w,h,r){
let tl,tr,br,bl;
if(Array.isArray(r)){[tl,tr,br,bl]=r;} else {tl=tr=br=bl=r;}
const m = Math.min(w,h)/2;
tl=Math.min(tl,m);tr=Math.min(tr,m);br=Math.min(br,m);bl=Math.min(bl,m);
ctx.beginPath();
ctx.moveTo(x+tl,y);
ctx.lineTo(x+w-tr,y);
ctx.arcTo(x+w,y,x+w,y+tr,tr);
ctx.lineTo(x+w,y+h-br);
ctx.arcTo(x+w,y+h,x+w-br,y+h,br);
ctx.lineTo(x+bl,y+h);
ctx.arcTo(x,y+h,x,y+h-bl,bl);
ctx.lineTo(x,y+tl);
ctx.arcTo(x,y,x+tl,y,tl);
ctx.closePath();
}
```
Good.
Now let's handle canvas text rendering for popups and banners: ctx.font with system fonts.
Let me write the full script now.
Also HUD update: update DOM values each frame (only when changed to avoid layout thrash). I'll update on change.
Lives hearts in HUD: innerHTML with inline SVG hearts:
```js
function heartSVG(color){
return `<svg width="16" height="16" viewBox="0 0 24 24"><path d="M12 21s-7.5-4.7-9.6-9.1C.6 8.2 3 4.5 6.6 4.5c2 0 3.4 1.1 4.4 2.4 1-1.3 2.4-2.4 4.4-2.4 3.6 0 6 3.7 4.2 7.4C19.5 16.3 12 21 12 21z" fill="${color}"/></svg>`;
}
```
Good.
Now the overlay HTML:
```html
<div class="overlay" id="overlay">
<div class="panel">
<div class="panel-badge">CLICK OR PRESS ANY KEY TO START</div>
<h2>HAMBURGER BREAKOUT</h2>
<p>Every brick you smash erupts into a shower of little hearts.<br>The AI is demoing now — click to take over the bun.</p>
<div class="keys">MOVE: mouse / ← → • SERVE: space • AUTO: D</div>
</div>
</div>
```
Actually title already in header. Panel title: "READY?" Let's structure: big pulsing line "CLICK OR PRESS ANY KEY TO START", sub "AI demo is playing — you take over when you interact".
The overlay background should be semi transparent so the game is visible behind: rgba(10,5,20,0.55) with backdrop blur. But backdrop-filter over the canvas... fine, but it may blur the game making it less visible. Use light tint rgba(8,4,16,0.35) and a panel with solid bg.
Also the overlay must not obscure too much of the play area. Keep the panel compact and centered.
Hmm — but "Something meaningful must be visible on first paint". The game running behind a semi-transparent overlay is good.
Also for the demo: the game must be actively playing (ball moving, bricks breaking) within 3 seconds. At load, the game starts in 'serve' with a 0.8s delay then launches. Then the AI plays. Bricks break within a few seconds. Good.
Ensure the AI is competent: paddle speed 780 px/s, ball ~395 px/s, so the AI can track it.
One issue: at the start, the ball is served from the paddle moving upward; bricks are at the top. AI tracks the ball. Good.
Let me now also add: when the ball hits the paddle, spawn a few "crumb" particles and a cheese-glow flash.
Particles (non-heart): simple {x,y,vx,vy,life,color,size} circles. Used for paddle hits and brick break flashes.
Now write everything.
One more consideration: `requestAnimationFrame` with dt computed via performance.now.
Let me write the final code.
```js
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
let W=0,H=0,dpr=1;
function resize(){
const rect = canvas.getBoundingClientRect();
dpr = Math.min(window.devicePixelRatio||1, 2);
canvas.width = Math.max(1,Math.round(rect.width*dpr));
canvas.height = Math.max(1,Math.round(rect.height*dpr));
ctx.setTransform(dpr,0,0,dpr,0,0);
W = rect.width; H = rect.height;
layoutBricks();
}
```
Note: layoutBricks on resize should rebuild the level but keep progress? Simplest: rebuild level with current level setting. Fine.
Careful: on resize during play, paddle positions scale. Let's just re-layout bricks and clamp paddle.
window.addEventListener('resize', resize); also use ResizeObserver on main element for robustness.
Let me write layoutBricks() to rebuild the brick array for the current level, and buildLevel(level) used at level start.
```js
function layoutLevel(level){
const margin = clamp(W*0.06, 14, 70);
const top = clamp(H*0.06, 22, 60);
const rows = clamp(3 + Math.floor((level-1)/2), 3, 7);
const gap = Math.max(5, Math.min(10, W*0.012));
const areaW = W - margin*2;
const desiredW = clamp(W*0.11, 58, 120);
let cols = Math.max(3, Math.round((areaW+gap)/(desiredW+gap)));
let brickW = (areaW - (cols-1)*gap)/cols;
const brickH = clamp((H*0.45 - (rows-1)*gap)/rows, 14, 40);
bricks = [];
for(let r=0;r<rows;r++){
for(let c=0;c<cols;c++){
const ing = INGREDIENTS[(r + (level%3)) % INGREDIENTS.length];
bricks.push({x: margin + c*(brickW+gap), y: top + r*(brickH+gap), w: brickW, h: brickH, color: ing, alive:true, hit:0});
}
}
}
```
Also maybe leave gaps in the pattern for style: e.g., for level >= 2, skip some bricks in a pattern to make a shape. Let's add: if level>=2, skip bricks where (r*cols+c)%7===0? Random holes look nicer. I'll add a deterministic hole pattern for level>=2: skip if ((c+r*2)%5===0 && level>1). Keep it simple: for level>1, skip cells where (r*3+c*5+level)%11===0.
Good.
Now the paddle rect: paddle = {x,y,w,h}.
paddle.y = H - paddle.h - clamp(H*0.06, 18, 60).
Let me write the final code, then review for bugs.
Also: when the ball is lost, in demo mode the AI continues (reset ball to paddle and auto-serve).
Time to write the code fully.
Additional detail: the ball speed resets each level and increases slightly.
ball.speed = (330 + (level-1)*20) * sizeFactor, where sizeFactor = clamp(W/1150, 0.6, 1.15).
Let's write launch(): angle random between -0.35 and 0.35 rad from vertical; vx=sin(a)*speed, vy=-cos(a)*speed.
On paddle bounce, ensure minimum vertical speed: vy = -Math.abs(vy) at least 0.35*speed? Standard: use angle clamp ±0.95 rad and recompute both.
Edge case: ball moving almost horizontally could stall. Clamp angle to max 0.95 rad (~54°).
Now write the code.
I also want a "combo" indicator displayed on canvas when combo>1: draw text "COMBO x{n}" near the top center with a pulse.
Let me write the render function:
```js
function render(){
drawBackground();
drawBricks();
drawHearts();
drawParticles();
drawPaddle();
drawBall();
drawPopups();
drawBanners();
}
```
Order: hearts behind bricks or in front? In front looks better (explosion over the screen). Let's draw hearts after bricks (in front) but before the paddle? Hearts should be able to be collected by the paddle so draw hearts before the paddle? Doesn't matter visually much. Draw: background, bricks, particles, hearts, paddle, ball, popups, banner.
Bricks drawing with a slight shadow:
```js
function drawBricks(){
for(const b of bricks){
if(!b.alive) continue;
ctx.save();
// shadow
ctx.fillStyle='rgba(0,0,0,.35)';
rr(ctx, b.x, b.y+4, b.w, b.h, 5); ctx.fill();
const g = ctx.createLinearGradient(0,b.y,0,b.y+b.h);
g.addColorStop(0, b.color.top); g.addColorStop(1, b.color.bot);
ctx.fillStyle=g;
rr(ctx,b.x,b.y,b.w,b.h,5); ctx.fill();
// highlight
ctx.fillStyle='rgba(255,255,255,.28)';
rr(ctx, b.x+3, b.y+2, b.w-6, Math.max(2,b.h*0.22), 3); ctx.fill();
// bottom inner shade
ctx.strokeStyle='rgba(0,0,0,.25)'; ctx.lineWidth=1;
rr(ctx,b.x+.5,b.y+.5,b.w-1,b.h-1,5); ctx.stroke();
ctx.restore();
}
}
```
Maybe add small "sesame"-like dots on bricks? Not needed.
Brick hit flash: when a brick is hit but not broken (not applicable). Skip.
Now hearts draw:
```js
function drawHearts(){
for(const h of hearts){
const a = clamp(h.life*1.8, 0, 1);
ctx.save();
ctx.globalAlpha = a;
ctx.translate(h.x,h.h);
ctx.rotate(h.rot);
const s = h.s;
heartPath(ctx, s);
ctx.fillStyle = h.color;
ctx.fill();
// highlight
ctx.globalAlpha = a*0.4;
ctx.scale(0.5,0.5); ctx.translate(-s*0.22,-s*0.22);
heartPath(ctx, s);
ctx.fillStyle='#ffd9e6';
ctx.fill();
ctx.restore();
}
}
```
Careful: after ctx.scale and translate, the path is drawn in the scaled space; the heartPath uses absolute coordinates relative to origin — works since we translate before drawing path. Yes: heartPath draws around origin, so transform applies.
Wait: in the highlight I do ctx.scale(0.5,0.5) then translate(-s*0.22,-s*0.22) then heartPath(ctx, s) → that draws a heart of size s in a space scaled by 0.5 → effective 0.5s, centered at (-0.22s*0.5). Good.
But heartPath is defined to draw a heart with a certain aspect. Let me define:
```js
function heartPath(ctx, s){
ctx.beginPath();
ctx.moveTo(0, s*0.34);
ctx.bezierCurveTo(-s*0.62, -s*0.06, -s*0.46, -s*0.72, 0, -s*0.34);
ctx.bezierCurveTo(s*0.46, -s*0.72, s*0.62, -s*0.06, 0, s*0.34);
ctx.closePath();
}
```
Check: top points at y=-0.34s control -0.72 → the actual curve top ~ -0.5s. Bottom at 0.34s. So height ~0.85s, width ~1.1s. Good.
Now the paddle hamburger. Let me also add a slight bobbing/rotation based on velocity for charm. Keep the burger upright, maybe rotate by clamp(vx*0.00008, -0.12, 0.12).
Now the ball: golden glowing circle.
```js
function drawBall(){
// trail
for(let i=0;i<trail.length;i++){
const t = trail[i];
const f = (i+1)/trail.length;
ctx.globalAlpha = f*0.35;
ctx.fillStyle = '#ffd76e';
ctx.beginPath(); ctx.arc(t.x,t.y, ball.r*f*0.9, 0, Math.PI*2); ctx.fill();
}
ctx.globalAlpha=1;
const g = ctx.createRadialGradient(ball.x-ball.r*0.3, ball.y-ball.r*0.35, ball.r*0.1, ball.x, ball.y, ball.r);
g.addColorStop(0,'#fffbe8'); g.addColorStop(0.5,'#ffd76e'); g.addColorStop(1,'#ff9f43');
ctx.fillStyle=g;
ctx.beginPath(); ctx.arc(ball.x,ball.y,ball.r,0,Math.PI*2); ctx.fill();
ctx.strokeStyle='rgba(255,255,255,.6)'; ctx.lineWidth=1.5; ctx.stroke();
}
```
Trail update: push ball pos each frame (maybe each frame) keep last 10.
Alright, writing the final file now. I'll be thorough but concise in code.
Also add a pause on 'p' key and when the window loses focus? Keep simple: pause toggles.
Let me also handle the case where the canvas area has zero height on load (unlikely). resize handles.
One more: the "auto" button in the header must not trigger the "start" overlay weirdly. Clicking the auto button also triggers the global pointerdown handler → would start the game (switch to player). That's fine but might be confusing: clicking the button when in demo mode toggles auto → if the user clicks it while in title state, the global handler starts the game too. Let's handle: in the global pointerdown handler, ignore if the event target is a button. Then the auto button click handler manages demo toggle, and the title overlay remains. Hmm, but if the user clicks the auto button while title is showing, the title overlay stays and the game remains in demo. That's acceptable. Actually to be safe: the auto button toggles demo mode; and if the title is showing, clicking any button also starts the game. Let me make the auto button handler: if (!started) { startPlayer(); } else { toggle demo }. Simpler: the auto button toggles demo on/off always, and separately, if the title overlay is up, clicking anywhere (including the button) starts the game. I'll just exclude buttons from the global handler and have the button's own handler call startGame() if the title is showing, then toggle.
Let me define:
- `game.demo` = true initially (AI plays).
- `started` = false initially (title overlay visible).
- startGame(): started=true; overlay hide; game.demo=false (player takes over).
- Auto button: if(!started){ startGame(); game.demo=true; } else { game.demo=!game.demo; } → update label.
Hmm, if !started and the user clicks Auto, we want the AI to keep playing (it's already playing). So: if(!started){ just toggle demo (which is true → keep true) } — I'll do: clicking Auto when title is up → startGame() then set demo=true so the AI keeps playing. Effectively the same as the title. OK.
Label: "AUTO: ON"/"AUTO: OFF".
Now the "press a key" part: keydown listener → if(!started) startGame(); else handle movement keys.
Also ensure that keyboard events work: the iframe gets keyboard events after a click — we have a click handler, fine.
Let me write the code.
Final check on the hearts explosion count: "every brick you break explodes into tons of little hearts" — spawn ~18-26 hearts per brick. With 27 bricks that's a lot; cap at 450 total. Good.
Let me write out the HTML file.
Also consider adding a slight screen shake on brick break — subtle (2-3px) for juice. Implement shake magnitude decay; apply ctx.translate in render. Careful with background edges: draw the background slightly larger to cover shake. I'll translate the whole scene but draw the background without translate (draw bg first, then translate for the rest). Minor edge gaps acceptable; or draw the background with a translate too but expanded by 4px. I'll draw bg covering (-10,-10,W+20,H+20).
Write the code now.
For popups, drawing text with stroke.
Let me write the banner drawing for level clear and game over (game over also shows HTML overlay). I'll draw a canvas banner for "LEVEL CLEAR" and "LIFE LOST".
Let me write it.
const INGREDIENTS = [...]
Let me write final code text.
function drawBurger(x,y,w,h): I'll write it using ctx directly (global ctx).
Detailed burger:
```js
function drawBurger(px, py, pw, ph){
const x = px, y = py;
ctx.save();
// ground shadow
ctx.fillStyle = 'rgba(0,0,0,.40)';
ctx.beginPath();
ctx.ellipse(x+pw/2, y+ph+5, pw*0.44, ph*0.12, 0, 0, Math.PI*2);
ctx.fill();
const bunBotH = ph*0.26, pattyH = ph*0.19, cheeseH = ph*0.11, lettH = ph*0.14, bunTopH = ph*0.30;
// --- bottom bun ---
let yy = y + ph - bunBotH;
let g = ctx.createLinearGradient(0,yy,0,yy+bunBotH);
g.addColorStop(0,'#e6ab63'); g.addColorStop(1,'#b9743a');
ctx.fillStyle=g; rr(ctx,x,yy,pw,bunBotH,[7,7,pw*0.22,pw*0.22]); ctx.fill();
ctx.strokeStyle='rgba(80,40,15,.45)'; ctx.lineWidth=1; ctx.stroke();
// --- patty ---
yy = y + ph - bunBotH - pattyH + 2;
g = ctx.createLinearGradient(0,yy,0,yy+pattyH);
g.addColorStop(0,'#8a5334'); g.addColorStop(1,'#4b2916');
ctx.fillStyle=g; rr(ctx,x+3,yy,pw-6,pattyH+2,[6,6,6,6]); ctx.fill();
ctx.fillStyle='rgba(255,255,255,.10)';
for(let i=0;i<5;i++){ const dx=x+8+ (pw-16)*(i/4); ctx.beginPath(); ctx.arc(dx, yy+pattyH*0.5, 2, 0, Math.PI*2); ctx.fill(); }
// --- cheese ---
yy = y + ph - bunBotH - pattyH - cheeseH + 3;
ctx.fillStyle='#ffcf3d';
ctx.beginPath();
ctx.moveTo(x+4, yy);
ctx.lineTo(x+pw-4, yy);
ctx.lineTo(x+pw-4, yy+cheeseH);
// drips
const dripW = pw/4;
for(let i=0;i<3;i++){
const dx = x+pw-4-(i+1)*dripW;
ctx.quadraticCurveTo(dx+dripW*0.5, yy+cheeseH+ph*0.10, dx, yy+cheeseH);
}
ctx.lineTo(x+4, yy+cheeseH);
ctx.closePath(); ctx.fill();
ctx.strokeStyle='rgba(150,100,0,.35)'; ctx.lineWidth=1; ctx.stroke();
// --- lettuce ---
yy = y + ph - bunBotH - pattyH - cheeseH - lettH + 4;
ctx.fillStyle='#67c23f';
ctx.beginPath();
ctx.moveTo(x+2, yy);
ctx.lineTo(x+pw-2, yy);
ctx.lineTo(x+pw-2, yy+lettH*0.35);
const scallops = 6;
for(let i=0;i<scallops;i++){
const x1 = x+pw-2 - (i+1)*(pw-4)/scallops;
const x2 = x+pw-2 - (i+2)*(pw-4)/scallops;
ctx.quadraticCurveTo((x1+x2)/2, yy+lettH*1.5, x2, yy+lettH*0.35);
}
ctx.lineTo(x+2, yy);
ctx.closePath(); ctx.fill();
...
}
```
Hmm the scallop loop: after lineTo(x+pw-2, yy+lettH*0.35), we do quadratic curves moving leftwards. i=0: x1 = right edge - 1 seg, x2 = right - 2 segs; curve control at midpoint, dipping to yy+lettH*1.5. Then the next iteration continues from x2. OK that works.
Then lettuce stroke with darker green.
// --- top bun ---
```js
yy = y;
g = ctx.createLinearGradient(0,yy,0,yy+bunTopH+2);
g.addColorStop(0,'#ffd79b'); g.addColorStop(0.6,'#efb96f'); g.addColorStop(1,'#d59a4f');
ctx.fillStyle=g; rr(ctx,x,yy,pw,bunTopH+3,[pw*0.26,pw*0.26,8,8]); ctx.fill();
ctx.strokeStyle='rgba(120,70,20,.35)'; ctx.stroke();
// sesame seeds
ctx.fillStyle='rgba(255,250,230,.9)';
const seeds=[[0.22,0.42],[0.42,0.28],[0.62,0.46],[0.8,0.3],[0.5,0.6]];
for(const [sx,sy] of seeds){
ctx.save();
ctx.translate(x+pw*sx, yy+bunTopH*sy);
ctx.rotate(sx*3);
ctx.beginPath(); ctx.ellipse(0,0,pw*0.035,bunTopH*0.12,0,0,Math.PI*2); ctx.fill();
ctx.restore();
}
```
Good.
Note the bunTopH and total ph: 0.30+0.14+0.11+0.19+0.26 = 1.00.
Now I think the code is complete. Let me write the final answer.
One more: making sure the paddle burger is visible: paddleH ~34 at desktop; the burger will be ~34px tall and 160 wide. Visible. On mobile: W=375 → paddleW = clamp(56,72,160)=72, paddleH=clamp(18.7,20,34)=20. Small burger but visible.
Let's write the file.
Also add a subtle "steam" rising from the burger paddle for charm? Maybe a few animated wavy lines. Could be cute but adds code; skip or add simple: 3 arcs rising with sine offsets, low alpha. I'll add it — it's a nice detail. Keep it cheap.
Let me write it:
```js
function drawSteam(px,py,pw,ph){
ctx.save();
ctx.strokeStyle='rgba(255,255,255,.18)';
ctx.lineWidth=2; ctx.lineCap='round';
for(let i=0;i<3;i++){
const bx = px+pw*(0.25+0.25*i);
const off = Math.sin(time*2.2+i)*4;
ctx.beginPath();
ctx.moveTo(bx+off, py-2);
ctx.quadraticCurveTo(bx-off, py-10, bx+off, py-18);
ctx.stroke();
}
ctx.restore();
}
```
OK. Final code writing.
For the HUD hearts count, the "HEARTS" stat is the collected hearts count.
Let me write everything now, carefully checking for syntax errors.
Also: `game.combo` display.
Final structure of the JS:
```js
(function(){
// constants, dom refs
// state
// functions
// event listeners
// init: resize(); resetGame(); requestAnimationFrame(loop);
})();
```
Let me write the loop:
```js
let last = performance.now();
function loop(now){
let dt = (now-last)/1000; last=now;
dt = Math.min(dt, 0.033);
update(dt);
render();
requestAnimationFrame(loop);
}
```
update(dt) handles states.
For the title state, the game should still play in demo. So states: 'serve'|'play'|'dead'|'levelclear'|'gameover'. The title is just an overlay flag (started=false) and demo=true.
Let me code update:
```js
function update(dt){
time += dt;
updateHUD();
if(game.paused){ return; }
updatePaddle(dt);
updateParticles(dt);
updatePopups(dt);
if(game.state==='serve'){ serveTimer-=dt; ball.x=paddle.x+paddle.w/2; ball.y=paddle.y-ball.r-2; if(game.demo && serveTimer<=0) launch(); }
else if(game.state==='play'){ updateBall(dt); }
else if(game.state==='dead'){ deadTimer-=dt; if(deadTimer<=0){ if(game.lives<=0){ gameOver(); } else { resetBall(); } } }
else if(game.state==='levelclear'){ clearTimer-=dt; if(clearTimer<=0){ game.level++; layoutLevel(game.level); resetBall(); } }
else if(game.state==='gameover'){ overTimer+=dt; if(game.demo && overTimer>2.8){ resetGame(); } }
updateHearts(dt);
shake = Math.max(0, shake - dt*18);
}
```
Wait, in 'dead' state we should also update hearts (yes, at the end).
resetBall(): state='serve', serveTimer = game.demo?0.7:1.6, ball.vx=ball.vy=0, ball.speed recomputed for the level.
launch(): set velocities.
resetGame(): score=0, lives=3, level=1, heartsCollected=0, combo=0, layoutLevel(1), resetBall(), clear hearts? Keep some hearts flying. Let's clear hearts on level change? Keep them; they fade.
Game over: state='gameover', overTimer=0, show overlay with "GAME OVER" and score; in demo auto-restart after 2.8s.
When the player restarts via click: resetGame() + hide overlay.
Let me now also handle: when the game is over and the overlay is shown, clicking restarts. But the global pointerdown handler also fires on the overlay — it's fine (same action).
Careful: the overlay in the title state has pointer-events auto; clicking it triggers the global handler → startGame. Good.
Also the overlay in the gameover state → restart.
Now write the overlay content update function:
```js
function setOverlay(title, sub, badge){
ovTitle.textContent=title; ovSub.innerHTML=sub; ovBadge.textContent=badge;
overlay.classList.remove('hidden');
}
```
Title state overlay:
badge: "CLICK OR PRESS ANY KEY TO START"
title: "HAMBURGER BREAKOUT"
sub: "The AI is demoing the game. Click to grab the bun — every brick you smash bursts into hearts."
Game over overlay:
badge: "GAME OVER"
title: "SCORE " + score
sub: "Click or press any key to play again."
Now the code. Writing it out.
Regarding pointer control: pointermove on window sets pointerX; but when the overlay is up (title), we don't want the paddle to move to the mouse — actually it doesn't matter much, but in demo mode the AI controls it anyway. In player mode, pointerX is used.
Set `pointer.active = true` on pointermove only if started and not demo. Actually if demo is on, the AI controls; when the user moves the mouse, maybe switch to manual? That could be jarring. Keep the mode explicit: demo toggled by the button/key.
Player mode paddle follows pointerX smoothly.
Also keyboard in player mode: arrows.
Let me finalize. Writing the file now.
For the canvas background, also add some floating "floating hearts" in the background for atmosphere? Might be nice but adds cost. Skip; the hearts from explosions are the star.
I should double check: "every brick you break explodes into tons of little hearts" — yes, 18-26 hearts each.
Write the answer.
One more: heart colors — derive from the brick's ingredient color for variety? Mixed pink palette is more "heart"-like. Use pink palette mostly, with a few matching the brick color.
Let me finalize the code text now. I'll write it fully and check syntax mentally.
Also note: `ctx.roundRect` avoided. Good.
Let me produce the final file.