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
- 13 min 45 s
- Cost
- $0.209
- Output tokens
- 58,052
- Spent thinking
- 46,846 tokens
- Speed
- 70.4 tok/s
- Console errors
- 0
- First token after
- 448 ms
- First visible text after
- 11 min 2 s
- Prompt tokens
- 357
- Code size
- 30.1 KB
- Finish reason
- stop
- Served by
- InferenceNet
- Model id
z-ai/glm-5.3- Reasoning sent
model default- 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 · 770 lines
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
<title>Love at First Bite — a burger breakout</title>
<style>
*{margin:0;padding:0;box-sizing:border-box}
html,body{height:100%}
body{
background:#1d0d14;overflow:hidden;
font-family:"Trebuchet MS",Verdana,sans-serif;
-webkit-user-select:none;user-select:none;
-webkit-tap-highlight-color:transparent;touch-action:none;
}
#stage{position:fixed;inset:0;display:flex;align-items:center;justify-content:center}
canvas#game{display:block;touch-action:none}
#overlay{
position:absolute;inset:0;display:flex;align-items:center;justify-content:center;
background:rgba(16,6,11,.5);transition:opacity .45s ease;
}
#overlay.hidden{opacity:0;pointer-events:none}
.card{position:relative;text-align:center;padding:24px;max-width:min(94vw,760px)}
#logo{width:212px;max-width:56vw;height:auto;display:block;margin:0 auto 6px;filter:drop-shadow(0 6px 0 rgba(0,0,0,.35))}
.eyebrow{font-size:12px;letter-spacing:4px;color:#ff8fa8;font-weight:700;margin-bottom:8px}
h1{
font-family:Impact,"Franklin Gothic Bold","Arial Black",sans-serif;font-weight:400;
font-size:clamp(38px,8.2vw,74px);line-height:.98;letter-spacing:.02em;
color:#fff3e0;text-transform:uppercase;
text-shadow:0 4px 0 #c2184f,0 9px 0 rgba(0,0,0,.4);
}
h1 em{font-style:normal;color:#ff7b9c}
.tag{margin-top:12px;color:rgba(255,214,224,.85);font-size:13px;letter-spacing:1px}
.cta{
display:inline-block;margin-top:22px;background:#e8375f;color:#fff3e0;
font-weight:700;font-size:15px;letter-spacing:2.5px;padding:14px 30px;border-radius:999px;
box-shadow:0 5px 0 #7d1033;cursor:pointer;animation:pulse 1.5s ease-in-out infinite;
}
.hint{margin-top:18px;font-size:11px;letter-spacing:1.6px;color:rgba(255,220,228,.55)}
@keyframes pulse{0%,100%{transform:translateY(0)}50%{transform:translateY(-4px)}}
</style>
</head>
<body>
<div id="stage">
<canvas id="game" width="960" height="640"></canvas>
<div id="overlay">
<div class="card">
<canvas id="logo" width="420" height="248"></canvas>
<div class="eyebrow">DEMO MODE · THE CHEF IS PLAYING</div>
<h1>LOVE AT FIRST <em>BITE</em></h1>
<p class="tag">A hamburger paddle. A wide-eyed meatball. Bricks that burst into hearts.</p>
<div class="cta">CLICK OR PRESS A KEY TO START</div>
<p class="hint">MOUSE / TOUCH OR ←→ TO MOVE · SPACE TO LAUNCH · P TO PAUSE</p>
</div>
</div>
</div>
<script>
(() => {
'use strict';
/* ---------- basics ---------- */
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
const overlay = document.getElementById('overlay');
const logo = document.getElementById('logo');
const W = 960, H = 640, HUD_H = 56, TOP = HUD_H;
const PADDLE_Y = 566, PADDLE_W = 156, BALL_R = 9;
const clamp = (v,a,b)=>Math.max(a,Math.min(b,v));
let dpr = 1, bg = null;
function resize(){
dpr = Math.min(window.devicePixelRatio || 1, 2);
canvas.width = W*dpr; canvas.height = H*dpr;
const s = Math.min(window.innerWidth/W, window.innerHeight/H);
canvas.style.width = (W*s)+'px';
canvas.style.height = (H*s)+'px';
buildBG();
}
window.addEventListener('resize', resize);
function rr(g,x,y,w,h,r){
r = Math.min(r, w/2, h/2);
g.beginPath();
g.moveTo(x+r,y);
g.arcTo(x+w,y,x+w,y+h,r);
g.arcTo(x+w,y+h,x,y+h,r);
g.arcTo(x,y+h,x,y,r);
g.arcTo(x,y,x+w,y,r);
g.closePath();
}
function heartPath(g,s){
g.beginPath();
g.moveTo(0, 0.32*s);
g.bezierCurveTo(0.5*s,-0.42*s, 1.05*s,0.25*s, 0, s);
g.bezierCurveTo(-1.05*s,0.25*s, -0.5*s,-0.42*s, 0, 0.32*s);
g.closePath();
}
function heartFill(g,x,y,s,rot,h,sat,lig,a){
g.save(); g.translate(x,y); g.rotate(rot); g.globalAlpha = a;
g.fillStyle = `hsl(${h},${sat}%,${lig}%)`;
heartPath(g,s); g.fill();
if(s > 3){
g.globalAlpha = a*0.65; g.fillStyle = 'rgba(255,255,255,.65)';
g.beginPath(); g.arc(-s*0.34,-s*0.02,Math.max(.7,s*.13),0,Math.PI*2); g.fill();
}
g.restore();
}
function easeOutBack(x){ const c1=1.70158,c3=c1+1; return 1 + c3*Math.pow(x-1,3) + c1*Math.pow(x-1,2); }
function ls(v){ try{ ctx.letterSpacing = v; }catch(e){} }
/* ---------- cached backdrop ---------- */
function buildBG(){
bg = document.createElement('canvas');
bg.width = W*dpr; bg.height = H*dpr;
const g = bg.getContext('2d'); g.scale(dpr,dpr);
g.fillStyle = '#1d0d14'; g.fillRect(0,0,W,H);
g.fillStyle = 'rgba(255,190,205,.05)'; // faint dotted wall
for(let x=20;x<W;x+=40) for(let y=TOP+22;y<600;y+=40) g.fillRect(x,y,2,2);
for(let r=0;r<2;r++) for(let x=0;x<W;x+=24){ // checkered diner counter
g.fillStyle = (((x/24)|0)+r)%2 ? '#2a1420' : '#160a10';
g.fillRect(x, 616+r*12, 24, 12);
}
g.fillStyle = 'rgba(255,92,132,.35)'; g.fillRect(0,614,W,2);
const v = g.createRadialGradient(W/2,H*0.45,H*0.25, W/2,H*0.55,H*0.9);
v.addColorStop(0,'rgba(0,0,0,0)'); v.addColorStop(1,'rgba(0,0,0,.42)');
g.fillStyle = v; g.fillRect(0,0,W,H);
}
/* ---------- audio (tiny synth, created on first gesture) ---------- */
let AC = null;
function initAudio(){
if(!AC){ try{ AC = new (window.AudioContext||window.webkitAudioContext)(); }catch(e){ AC = null; } }
if(AC && AC.state === 'suspended') AC.resume().catch(()=>{});
}
function beep(f,dur,type,vol,slide){
if(!AC) return;
try{
const t = AC.currentTime;
const o = AC.createOscillator(), g = AC.createGain();
o.type = type; o.frequency.setValueAtTime(f,t);
if(slide) o.frequency.exponentialRampToValueAtTime(Math.max(40,f+slide), t+dur);
g.gain.setValueAtTime(vol,t);
g.gain.exponentialRampToValueAtTime(0.0001, t+dur);
o.connect(g).connect(AC.destination);
o.start(t); o.stop(t+dur+0.05);
}catch(e){}
}
const snd = {
wall(){ beep(340,.05,'triangle',.05); },
paddle(){ beep(140,.1,'square',.09,-30); beep(240,.06,'triangle',.05); },
brick(){ beep(420+S.combo*45,.09,'square',.06,140); beep(1150,.14,'sine',.03,250); },
crack(){ beep(230,.05,'square',.06,-40); },
lost(){ beep(260,.5,'sawtooth',.08,-190); },
launch(){ beep(480,.08,'triangle',.06,220); },
clear(){ [523,659,784,1046].forEach((f,i)=>setTimeout(()=>beep(f,.18,'triangle',.07), i*90)); }
};
function play(n){ if(S.mode==='user' && snd[n]) snd[n](); }
/* ---------- level layouts ( '#' normal · '@' sweetheart brick, 2 hits ) ---------- */
const LEVELS = [
{ name:'HEARTY', speed:6.2,
pat:[
"..@@@...@@@..",
".@###@.@###@.",
"#############",
"#############",
".###########.",
"..#########..",
"...#######...",
"....#####....",
".....@@@.....",
"......@......"],
color:(r)=>[335+r*2, 86, 72-r*3] },
{ name:'DOUBLE CHEESE', speed:6.6,
pat:[
".....#####.....",
"...#########...",
".#############.",
"###############",
"...............",
"###############",
"###############",
"..#...#....#...",
"@@@@@@@@@@@@@@@",
".#############."],
color:(r)=>({0:[28,78,64],1:[28,78,64],2:[28,78,64],3:[100,55,50],
5:[4,75,55],6:[43,100,55],7:[43,100,55],8:[20,40,30],9:[28,74,58]}[r]||[0,0,0]) },
{ name:'SWEET TALK', speed:7.0,
pat:[
"......#......",
".....@@@.....",
"....#####....",
"...@@@@@@@...",
"..#########..",
"...@@@@@@@...",
"....#####....",
".....###.....",
"......#......"],
color:(r,rows)=>{ const ring=Math.min(r,rows-1-r); return [[340,85,60],[18,85,60],[46,85,60],[100,80,58],[196,80,58]][ring]; } }
];
/* ---------- state ---------- */
const S = { mode:'demo', phase:'serve', level:0, score:0, lives:3, combo:0,
speed:6.2, levelTime:0, phaseT:0, overT:0, serveT:0, shake:0, t:0, paused:false };
let paddleX = W/2, prevPX = W/2, squash = 0, tilt = 0, mouseX = null;
const keys = { left:false, right:false };
const ball = { x:W/2, y:PADDLE_Y-BALL_R-1, dx:0, dy:-1, stuck:true, blinkT:3 };
let bricks = [], parts = [], pops = [], rings = [], trail = [];
const amb = [];
for(let i=0;i<12;i++) amb.push({ x:Math.random()*W, y:Math.random()*H, s:7+Math.random()*16,
v:0.12+Math.random()*0.3, ph:Math.random()*6.3, hue:334+Math.random()*22, al:0.05+Math.random()*0.05 });
/* ---------- level / ball ---------- */
function buildLevel(){
const def = LEVELS[S.level % LEVELS.length];
const pat = def.pat, cols = pat[0].length;
const gap = 6, mx = 26, bh = 26, by = 86;
const bw = (W - mx*2 - gap*(cols-1)) / cols;
bricks = [];
pat.forEach((line,r)=>{
for(let c=0;c<cols;c++){
const ch = line[c]; if(ch==='.') continue;
bricks.push({ x:mx+c*(bw+gap), y:by+r*(bh+gap), w:bw, h:bh,
hp: ch==='@'?2:1, max: ch==='@'?2:1, hsl:def.color(r,pat.length,c),
delay: r*0.05 + c*0.01, sp:0, dead:false });
}
});
S.speed = Math.min(def.speed + Math.floor(S.level/LEVELS.length)*0.45, 9.6);
S.levelTime = 0;
}
function resetBall(){
ball.dx = 0; ball.dy = -1; ball.stuck = true;
ball.x = paddleX; ball.y = PADDLE_Y-BALL_R-1;
trail.length = 0; S.phase = 'serve'; S.serveT = 0;
}
function launchBall(ang){
const a = (ang !== undefined) ? ang : (Math.random()*0.7-0.35);
ball.stuck = false; ball.dx = Math.sin(a); ball.dy = -Math.cos(a);
ball.x = paddleX; ball.y = PADDLE_Y-BALL_R-1;
S.phase = 'play'; play('launch');
}
/* ---------- particles ---------- */
function burstHearts(x,y,n,grey){
for(let i=0;i<n;i++){
const a = Math.random()*Math.PI*2, sp = 1.2+Math.random()*4.6;
parts.push({ t:'h', x, y, vx:Math.cos(a)*sp, vy:Math.sin(a)*sp-3.1,
g:0.11+Math.random()*0.09, rot:(Math.random()-0.5)*1.6, vr:(Math.random()-0.5)*0.28,
s:3.6+Math.random()*6.4, life:1, dec:0.008+Math.random()*0.009,
hue: grey?0:334+Math.random()*22, sat: grey?6:78+Math.random()*18, lig: grey?60:50+Math.random()*22,
wob:Math.random()*6.3, wv:0.05+Math.random()*0.06, wa:0.25+Math.random()*0.5 });
}
}
function burstDots(x,y,n,hsl){
for(let i=0;i<n;i++){
const a = Math.random()*Math.PI*2, sp = 1+Math.random()*4.5;
parts.push({ t:'d', x, y, vx:Math.cos(a)*sp, vy:Math.sin(a)*sp-2, g:0.22,
rot:0, vr:0, s:1.6+Math.random()*2.2, life:1, dec:0.02+Math.random()*0.02,
hue:hsl[0], sat:hsl[1], lig:hsl[2], wob:0, wv:0, wa:0 });
}
}
function sparks(x,y,n){
for(let i=0;i<n;i++){
const a = Math.random()*Math.PI*2, sp = 1+Math.random()*3;
parts.push({ t:'s', x, y, vx:Math.cos(a)*sp, vy:Math.sin(a)*sp-1, g:0.12,
rot:Math.random()*3, vr:0.2, s:1.5+Math.random()*1.5, life:1, dec:0.04,
hue:0, sat:0, lig:0, wob:0, wv:0, wa:0 });
}
}
function shower(){
for(let i=0;i<70;i++){
parts.push({ t:'h', x:Math.random()*W, y:-30-Math.random()*260,
vx:(Math.random()-0.5)*0.6, vy:0.4+Math.random()*1.2, g:0.008,
rot:Math.random()*6, vr:(Math.random()-0.5)*0.2, s:4+Math.random()*8,
life:1, dec:0.0035, hue:334+Math.random()*24, sat:85, lig:55+Math.random()*18,
wob:Math.random()*6.3, wv:0.03, wa:0.6 });
}
}
function updateParts(dt){
for(let i=parts.length-1;i>=0;i--){
const p = parts[i];
p.vy += p.g*dt;
p.vx *= Math.pow(0.99,dt);
if(p.t==='h') p.vy *= Math.pow(0.975,dt);
p.wob += p.wv*dt;
p.x += p.vx*dt + Math.sin(p.wob)*p.wa*dt;
p.y += p.vy*dt; p.rot += p.vr*dt; p.life -= p.dec*dt;
if(p.life<=0 || p.y>H+60) parts.splice(i,1);
}
if(parts.length>950) parts.splice(0, parts.length-950);
}
/* ---------- gameplay ---------- */
function stepBall(dt){
const steps = Math.max(1, Math.ceil((S.speed*dt)/6));
for(let i=0;i<steps;i++){
ball.x += ball.dx*S.speed*dt/steps;
ball.y += ball.dy*S.speed*dt/steps;
if(ball.x < BALL_R){ ball.x = BALL_R; if(ball.dx<0){ ball.dx = -ball.dx; play('wall'); } }
else if(ball.x > W-BALL_R){ ball.x = W-BALL_R; if(ball.dx>0){ ball.dx = -ball.dx; play('wall'); } }
if(ball.y < TOP+BALL_R){ ball.y = TOP+BALL_R; if(ball.dy<0){ ball.dy = -ball.dy; play('wall'); } }
if(ball.dy>0 && ball.y+BALL_R>=PADDLE_Y && ball.y-BALL_R<PADDLE_Y+40 &&
Math.abs(ball.x-paddleX) <= PADDLE_W/2+BALL_R) bouncePaddle();
brickCollide();
if(ball.y-BALL_R > H+6){ ballLost(); return; }
}
trail.unshift({x:ball.x, y:ball.y});
if(trail.length>9) trail.pop();
}
function bouncePaddle(){
const rel = clamp((ball.x-paddleX)/(PADDLE_W/2), -1, 1);
const ang = rel*1.02;
ball.dx = Math.sin(ang); ball.dy = -Math.cos(ang);
ball.y = PADDLE_Y-BALL_R-0.5;
S.combo = 0; squash = 1; play('paddle');
for(let k=0;k<3;k++){ // little "love tap" hearts
parts.push({ t:'h', x:ball.x+(Math.random()-0.5)*24, y:PADDLE_Y-4,
vx:(Math.random()-0.5)*1.4, vy:-(0.8+Math.random()*1.4), g:0.06,
rot:(Math.random()-0.5), vr:0.1, s:2.6+Math.random()*2.4, life:1, dec:0.03,
hue:340+Math.random()*16, sat:88, lig:66, wob:Math.random()*6, wv:0.08, wa:0.3 });
}
}
function brickCollide(){
for(const b of bricks){
if(b.dead || b.sp<1) continue;
const cx = b.x+b.w/2, cy = b.y+b.h/2;
const ox = (b.w/2+BALL_R) - Math.abs(ball.x-cx);
const oy = (b.h/2+BALL_R) - Math.abs(ball.y-cy);
if(ox>0 && oy>0){
if(ox < oy){ ball.dx = ball.x<cx ? -Math.abs(ball.dx) : Math.abs(ball.dx); ball.x += ball.x<cx ? -ox : ox; }
else { ball.dy = ball.y<cy ? -Math.abs(ball.dy) : Math.abs(ball.dy); ball.y += ball.y<cy ? -oy : oy; }
hitBrick(b);
return;
}
}
}
function hitBrick(b){
b.hp--;
const cx = b.x+b.w/2, cy = b.y+b.h/2;
if(b.hp<=0){
b.dead = true;
S.combo = Math.min(S.combo+1, 9);
const pts = (b.max>1?120:50)*S.combo;
S.score += pts;
burstHearts(cx, cy, b.max>1?32:24, false);
burstDots(cx, cy, 7, b.hsl);
rings.push({x:cx, y:cy, r:4, life:1});
pops.push({x:cx, y:cy-6, txt:'+'+pts, life:1});
S.shake = Math.min(S.shake+2.4, 7);
S.speed = Math.min(S.speed+0.03, 9.6);
play('brick');
if(!bricks.some(bb=>!bb.dead)){ S.phase='clear'; S.phaseT=0; shower(); play('clear'); }
} else {
S.score += 20;
pops.push({x:cx, y:cy-6, txt:'+20', life:1});
sparks(cx, cy, 6);
S.shake = Math.min(S.shake+1, 7);
play('crack');
}
}
function ballLost(){
S.shake = 9;
burstHearts(ball.x, Math.min(ball.y, H-20), 18, true); // hearts turn grey when love is lost
play('lost');
if(S.mode==='user'){
S.lives--;
if(S.lives<=0){ S.phase='over'; S.overT=0; }
else resetBall();
} else resetBall();
}
function nextLevel(){ S.level++; S.combo=0; buildLevel(); resetBall(); }
/* ---------- demo AI & user paddle ---------- */
function predictX(){
const sp = S.speed, dy = ball.dy;
if(dy<=0.02) return ball.x;
const t = (PADDLE_Y-BALL_R-ball.y)/(dy*sp);
let px = ball.x + ball.dx*sp*t;
const span = W-2*BALL_R;
px = (px-BALL_R)%(2*span); if(px<0) px += 2*span;
if(px>span) px = 2*span-px;
return px+BALL_R;
}
function demoPaddle(dt){
let target;
if(S.phase==='serve') target = W/2;
else if(ball.dy>0.05) target = predictX() + Math.sin(S.t*1.7)*7;
else target = ball.x*0.3 + (W/2)*0.7 + Math.sin(S.t*0.8)*26;
paddleX += clamp(target-paddleX, -11*dt, 11*dt);
}
function userPaddle(dt){
if(keys.left){ paddleX -= 8.6*dt; mouseX = null; }
if(keys.right){ paddleX += 8.6*dt; mouseX = null; }
if(mouseX!=null) paddleX = mouseX;
}
/* ---------- flow ---------- */
function startGame(){
S.mode='user'; S.level=0; S.score=0; S.lives=3; S.combo=0; S.paused=false;
buildLevel(); resetBall();
overlay.classList.add('hidden');
}
function toDemo(){
S.mode='demo'; S.level=0; S.score=0; S.lives=3; S.combo=0; S.paused=false;
buildLevel(); resetBall();
overlay.classList.remove('hidden');
}
function anyStart(){
if(S.mode==='demo'){ startGame(); return; }
if(S.paused){ S.paused=false; return; }
if(S.phase==='over'){ if(S.overT>0.5) startGame(); return; }
if(S.phase==='serve'){
const a = mouseX!=null ? clamp((mouseX-paddleX)/380, -0.5, 0.5) : (Math.random()*0.7-0.35);
launchBall(a);
}
}
/* ---------- update ---------- */
function updateAmbient(dt){
for(const a of amb){
a.y -= a.v*dt; a.ph += 0.012*dt; a.x += Math.sin(a.ph)*0.18*dt;
if(a.y < -30){ a.y = H+30; a.x = Math.random()*W; }
}
}
function updatePops(dt){
for(let i=pops.length-1;i>=0;i--){
pops[i].y += -0.7*dt; pops[i].life -= 0.022*dt;
if(pops[i].life<=0) pops.splice(i,1);
}
}
function updateRings(dt){
for(let i=rings.length-1;i>=0;i--){
rings[i].r += 3.6*dt; rings[i].life -= 0.05*dt;
if(rings[i].life<=0) rings.splice(i,1);
}
}
function update(dt){
S.t += dt/60;
if(S.shake>0.05) S.shake *= Math.pow(0.86,dt); else S.shake = 0;
squash = Math.max(0, squash-0.05*dt);
updateAmbient(dt); updateParts(dt); updatePops(dt); updateRings(dt);
if(S.phase==='clear'){ S.phaseT += dt/60; if(S.phaseT>1.9) nextLevel(); return; }
if(S.phase==='over'){ S.overT += dt/60; if(S.overT>9) toDemo(); return; }
S.levelTime += dt/60;
for(const b of bricks) if(!b.dead && b.sp<1 && S.levelTime>b.delay) b.sp = Math.min(1, b.sp+dt*0.08);
if(S.mode==='demo') demoPaddle(dt); else userPaddle(dt);
paddleX = clamp(paddleX, PADDLE_W/2+8, W-PADDLE_W/2-8);
const mv = paddleX-prevPX; prevPX = paddleX;
tilt += (clamp(mv*0.03, -0.1, 0.1) - tilt) * Math.min(1, 0.22*dt);
if(ball.blinkT<=0) ball.blinkT = 2.2+Math.random()*2.8; else ball.blinkT -= dt/60;
if(S.phase==='serve'){
ball.x = paddleX; ball.y = PADDLE_Y-BALL_R-1;
if(S.mode==='demo'){ S.serveT += dt/60; if(S.serveT>1.0) launchBall(Math.random()*0.9-0.45); }
} else if(S.phase==='play') stepBall(dt);
}
/* ---------- drawing ---------- */
function drawBurger(g,cx,y,w){
const l = cx-w/2;
g.lineJoin = 'round';
// top bun
g.beginPath();
g.moveTo(l+5, y+19);
g.bezierCurveTo(l+5, y+5, l+w*0.17, y, l+w*0.5, y);
g.bezierCurveTo(l+w*0.83, y, l+w-5, y+5, l+w-5, y+19);
g.closePath();
g.fillStyle = '#f2a95e'; g.fill();
g.strokeStyle = '#a4622b'; g.lineWidth = 2.5; g.stroke();
// sesame seeds
g.fillStyle = '#ffe9c2';
const seeds = [[.18,12],[.30,7],[.42,4],[.58,4],[.70,7],[.82,12],[.26,15],[.74,15],[.5,9]];
for(const sd of seeds){
g.save(); g.translate(l+w*sd[0], y+sd[1]); g.rotate((sd[0]-0.5)*0.9);
g.beginPath(); g.ellipse(0,0,2.8,1.7,0,0,Math.PI*2); g.fill(); g.restore();
}
// lettuce frill
const y2 = y+19, x0 = l-7, x1 = l+w+7, span = x1-x0, n = Math.round(span/11), st = span/n;
g.beginPath();
g.moveTo(x0, y2+2); g.lineTo(x1, y2+2);
for(let i=0;i<n;i++){ const xs = x1-i*st; g.quadraticCurveTo(xs-st/2, y2+11, xs-st, y2+2); }
g.closePath();
g.fillStyle = '#7ec850'; g.fill();
g.strokeStyle = '#4e8a28'; g.lineWidth = 2; g.stroke();
// tomato
const y3 = y2+9;
rr(g, l+11, y3, w-22, 8, 4);
g.fillStyle = '#e5453c'; g.fill();
g.strokeStyle = '#a92c25'; g.lineWidth = 2; g.stroke();
// cheese + drips
const y4 = y3+8;
rr(g, l+5, y4, w-10, 7, 3);
g.fillStyle = '#ffc22e'; g.fill();
g.strokeStyle = '#d9930b'; g.lineWidth = 1.8; g.stroke();
for(const f of [0.22, 0.52, 0.8]){
rr(g, l+w*f-6, y4+2, 12, 9, 4.5);
g.fillStyle = '#ffc22e'; g.fill();
g.strokeStyle = '#d9930b'; g.stroke();
}
// patty
const y5 = y4+7;
rr(g, l+3, y5, w-6, 12, 6);
g.fillStyle = '#5b3a29'; g.fill();
g.strokeStyle = '#37221a'; g.lineWidth = 2; g.stroke();
g.fillStyle = 'rgba(0,0,0,.25)';
for(const f of [0.2,0.42,0.63,0.82]){ g.beginPath(); g.arc(l+w*f, y5+6, 1.7, 0, Math.PI*2); g.fill(); }
// bottom bun
const y6 = y5+12;
rr(g, l+9, y6, w-18, 13, 6.5);
g.fillStyle = '#f2a95e'; g.fill();
g.strokeStyle = '#a4622b'; g.lineWidth = 2.5; g.stroke();
g.fillStyle = 'rgba(255,255,255,.25)';
rr(g, l+20, y6+3, w*0.28, 3, 1.5); g.fill();
}
function miniBurger(g,x,y){
g.save(); g.translate(x,y); g.lineJoin='round';
g.strokeStyle = '#a4622b'; g.lineWidth = 1.5;
rr(g,-11,-9,22,7,3.5); g.fillStyle='#f2a95e'; g.fill(); g.stroke();
rr(g,-12,-3,24,4,2); g.fillStyle='#7ec850'; g.fill();
rr(g,-11,1,22,5,2.5); g.fillStyle='#5b3a29'; g.fill();
rr(g,-10,6,20,6,3); g.fillStyle='#f2a95e'; g.fill(); g.stroke();
g.restore();
}
function drawBricks(){
for(const b of bricks){
if(b.dead) continue;
const e = easeOutBack(b.sp); if(e<=0.02) continue;
const cx = b.x+b.w/2, cy = b.y+b.h/2;
ctx.save(); ctx.translate(cx,cy); ctx.scale(e,e); ctx.translate(-cx,-cy);
const [h,s,l] = b.hsl;
rr(ctx, b.x, b.y, b.w, b.h, 6);
ctx.fillStyle = `hsl(${h},${s}%,${l}%)`; ctx.fill();
ctx.strokeStyle = `hsl(${h},${Math.max(s-15,30)}%,${Math.max(l-20,10)}%)`; ctx.lineWidth = 2; ctx.stroke();
rr(ctx, b.x+3, b.y+3, b.w-6, Math.max(3,b.h*0.28), 3);
ctx.fillStyle = 'rgba(255,255,255,.22)'; ctx.fill();
if(b.max>1){
if(b.hp===b.max){ heartFill(ctx, cx, cy-1, 7, 0, h, Math.min(s+10,95), Math.min(l+28,88), .9); }
else {
ctx.strokeStyle = 'rgba(35,8,18,.6)'; ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(cx-8,cy-7); ctx.lineTo(cx-2,cy-1); ctx.lineTo(cx-6,cy+7);
ctx.moveTo(cx+7,cy-8); ctx.lineTo(cx+2,cy-2); ctx.lineTo(cx+7,cy+5);
ctx.stroke();
}
}
ctx.restore();
}
}
function drawPaddle(){
ctx.fillStyle = 'rgba(0,0,0,.35)';
ctx.beginPath(); ctx.ellipse(paddleX, PADDLE_Y+70, PADDLE_W*0.42, 6, 0, 0, Math.PI*2); ctx.fill();
ctx.save();
ctx.translate(paddleX, PADDLE_Y+70);
ctx.rotate(tilt);
ctx.scale(1+squash*0.14, 1-squash*0.2);
ctx.translate(-paddleX, -(PADDLE_Y+70));
drawBurger(ctx, paddleX, PADDLE_Y, PADDLE_W);
ctx.restore();
}
function drawBall(){
for(let i=trail.length-1;i>=0;i--){
const k = 1 - i/(trail.length+1);
ctx.globalAlpha = k*0.16; ctx.fillStyle = '#ff9db8';
ctx.beginPath(); ctx.arc(trail[i].x, trail[i].y, BALL_R*k*0.9, 0, Math.PI*2); ctx.fill();
}
ctx.globalAlpha = 1;
ctx.fillStyle = '#7a4a2f'; ctx.strokeStyle = '#3f2519'; ctx.lineWidth = 2;
ctx.beginPath(); ctx.arc(ball.x, ball.y, BALL_R, 0, Math.PI*2); ctx.fill(); ctx.stroke();
ctx.fillStyle = 'rgba(255,228,196,.4)';
ctx.beginPath(); ctx.arc(ball.x-3, ball.y-3.5, 3, 0, Math.PI*2); ctx.fill();
let lx = 0, ly = -1;
if(!ball.stuck){ const m = Math.hypot(ball.dx,ball.dy)||1; lx = ball.dx/m; ly = ball.dy/m; }
const closed = ball.blinkT < 0.12;
for(const ex of [ball.x-3.6, ball.x+3.6]){
if(closed){
ctx.strokeStyle = '#3f2519'; ctx.lineWidth = 1.6;
ctx.beginPath(); ctx.moveTo(ex-2, ball.y-3); ctx.lineTo(ex+2, ball.y-3); ctx.stroke();
} else {
ctx.fillStyle = '#fff6e8';
ctx.beginPath(); ctx.arc(ex, ball.y-3, 2.3, 0, Math.PI*2); ctx.fill();
ctx.fillStyle = '#2b160e';
ctx.beginPath(); ctx.arc(ex+lx*1.1, ball.y-3+ly*1.1, 1.15, 0, Math.PI*2); ctx.fill();
}
}
ctx.strokeStyle = '#3f2519'; ctx.lineWidth = 1.4;
ctx.beginPath(); ctx.arc(ball.x, ball.y+2.2, 2.2, Math.PI*0.15, Math.PI*0.85); ctx.stroke();
}
function drawParts(){
for(const p of parts){
const a = clamp(p.life*2.2, 0, 1);
if(p.t==='h') heartFill(ctx, p.x, p.y, p.s, p.rot, p.hue, p.sat, p.lig, a);
else if(p.t==='d'){
ctx.globalAlpha = a; ctx.fillStyle = `hsl(${p.hue},${p.sat}%,${p.lig}%)`;
ctx.beginPath(); ctx.arc(p.x, p.y, p.s, 0, Math.PI*2); ctx.fill(); ctx.globalAlpha = 1;
} else {
ctx.globalAlpha = a; ctx.strokeStyle = '#ffd9e4'; ctx.lineWidth = 2;
ctx.save(); ctx.translate(p.x,p.y); ctx.rotate(p.rot);
ctx.beginPath(); ctx.moveTo(-p.s*2,0); ctx.lineTo(p.s*2,0); ctx.moveTo(0,-p.s*2); ctx.lineTo(0,p.s*2); ctx.stroke();
ctx.restore(); ctx.globalAlpha = 1;
}
}
for(const r of rings){
ctx.globalAlpha = Math.max(0, r.life)*0.55; ctx.strokeStyle = '#ff7ea0'; ctx.lineWidth = 2.5;
ctx.beginPath(); ctx.arc(r.x, r.y, r.r, 0, Math.PI*2); ctx.stroke();
}
ctx.globalAlpha = 1;
ctx.font = '900 15px "Arial Black","Trebuchet MS",sans-serif'; ctx.textAlign = 'center';
for(const p of pops){
ctx.globalAlpha = clamp(p.life*1.6, 0, 1); ctx.fillStyle = '#fff3e0'; ctx.fillText(p.txt, p.x, p.y);
}
ctx.globalAlpha = 1;
}
function drawHUD(){
ctx.fillStyle = 'rgba(12,5,9,.72)'; ctx.fillRect(0,0,W,HUD_H);
ctx.fillStyle = 'rgba(255,92,132,.35)'; ctx.fillRect(0,HUD_H-2,W,2);
ctx.textBaseline = 'alphabetic';
ctx.textAlign = 'left';
ls('3px'); ctx.font = '700 10px "Trebuchet MS",Verdana,sans-serif'; ctx.fillStyle = '#ff8fa8';
ctx.fillText('SCORE', 24, 22); ls('0px');
heartFill(ctx, 33, 40, 8, 0, 342, 88, 60, 1);
ctx.font = '900 21px "Arial Black","Trebuchet MS",sans-serif'; ctx.fillStyle = '#fff3e0';
ctx.fillText(String(S.score).padStart(6,'0'), 48, 47);
ctx.textAlign = 'center';
ls('3px'); ctx.font = '700 10px "Trebuchet MS",Verdana,sans-serif'; ctx.fillStyle = '#ff8fa8';
ctx.fillText('LEVEL', W/2, 20); ls('1px');
ctx.font = '700 16px "Trebuchet MS",Verdana,sans-serif'; ctx.fillStyle = '#fff3e0';
ctx.fillText((S.level+1)+' · '+LEVELS[S.level%LEVELS.length].name, W/2, 42); ls('0px');
ctx.textAlign = 'right';
ls('3px'); ctx.font = '700 10px "Trebuchet MS",Verdana,sans-serif'; ctx.fillStyle = '#ff8fa8';
ctx.fillText(S.mode==='user' ? 'LOVES' : 'DEMO', W-24, 22); ls('0px');
if(S.mode==='user'){ for(let i=0;i<S.lives;i++) miniBurger(ctx, W-36-i*34, 36); }
else {
ctx.font = '700 13px "Trebuchet MS",Verdana,sans-serif'; ctx.fillStyle = 'rgba(255,243,224,.7)';
ctx.fillText('AI AT THE GRILL', W-24, 44);
}
if(S.combo>=2){
const sc = 1 + Math.min(S.combo,6)*0.06;
ctx.save(); ctx.translate(W/2, 84); ctx.scale(sc,sc); ctx.textAlign='center';
ctx.font = '900 17px "Arial Black","Trebuchet MS",sans-serif';
ctx.fillStyle = '#ff5d8f'; ctx.fillText('COMBO ×'+S.combo, 0, 0);
ctx.restore();
}
}
function drawBanners(){
const t = S.t, name = LEVELS[S.level%LEVELS.length].name;
if((S.phase==='serve'||S.phase==='play') && S.levelTime<1.6){
const k = S.levelTime;
const a = k<0.2 ? k/0.2 : (k>1.2 ? Math.max(0,(1.6-k)/0.4) : 1);
const sc = 0.92 + Math.min(1, k*4)*0.08;
ctx.save(); ctx.globalAlpha = a; ctx.translate(W/2, 285); ctx.scale(sc,sc); ctx.textAlign='center';
ls('5px'); ctx.font = '700 13px "Trebuchet MS",Verdana,sans-serif';
ctx.fillStyle = '#ff8fa8'; ctx.fillText('LEVEL '+(S.level+1), 0, -30);
ls('2px'); ctx.font = '400 46px Impact,"Arial Black",sans-serif';
ctx.fillStyle = 'rgba(0,0,0,.5)'; ctx.fillText(name, 3, 19);
ctx.fillStyle = '#fff3e0'; ctx.fillText(name, 0, 16); ls('0px');
ctx.restore();
}
if(S.mode==='user' && S.phase==='serve' && !S.paused){
const a = 0.55 + 0.4*Math.sin(t*5);
ctx.textAlign='center'; ls('2px');
ctx.font = '700 13px "Trebuchet MS",Verdana,sans-serif';
ctx.fillStyle = `rgba(255,243,224,${a})`;
ctx.fillText('CLICK OR PRESS SPACE TO LAUNCH', W/2, 532); ls('0px');
}
if(S.phase==='clear'){
ctx.textAlign='center';
ctx.font = '400 52px Impact,"Arial Black",sans-serif';
ctx.fillStyle = 'rgba(0,0,0,.5)'; ctx.fillText('LEVEL CLEAR!', W/2+3, 293);
ctx.fillStyle = '#fff3e0'; ctx.fillText('LEVEL CLEAR!', W/2, 290);
ls('3px'); ctx.font = '700 14px "Trebuchet MS",Verdana,sans-serif';
ctx.fillStyle = '#ff8fa8'; ctx.fillText('THE KITCHEN SWOONS', W/2, 318); ls('0px');
}
if(S.phase==='over'){
ctx.fillStyle = 'rgba(10,3,8,.78)'; ctx.fillRect(0,0,W,H);
ctx.textAlign='center';
ctx.font = '400 58px Impact,"Arial Black",sans-serif';
ctx.fillStyle = 'rgba(0,0,0,.6)'; ctx.fillText('GAME OVER', W/2+4, 296);
ctx.fillStyle = '#ff5d8f'; ctx.fillText('GAME OVER', W/2, 292);
ctx.font = '900 22px "Arial Black","Trebuchet MS",sans-serif';
ctx.fillStyle = '#fff3e0'; ctx.fillText('FINAL SCORE '+S.score, W/2, 340);
const a = 0.5 + 0.4*Math.sin(t*5);
ls('2px'); ctx.font = '700 14px "Trebuchet MS",Verdana,sans-serif';
ctx.fillStyle = `rgba(255,243,224,${a})`; ctx.fillText('CLICK OR PRESS A KEY TO PLAY AGAIN', W/2, 386);
ctx.fillStyle = 'rgba(255,143,168,.85)'; ctx.fillText('THE BURGER FORGIVES YOU', W/2, 412); ls('0px');
}
if(S.paused){
ctx.fillStyle = 'rgba(10,3,8,.66)'; ctx.fillRect(0,0,W,H);
ctx.textAlign='center';
ctx.font = '400 48px Impact,"Arial Black",sans-serif';
ctx.fillStyle = '#fff3e0'; ctx.fillText('PAUSED', W/2, 300);
ctx.font = '700 13px "Trebuchet MS",Verdana,sans-serif';
ctx.fillStyle = 'rgba(255,243,224,.7)'; ctx.fillText('CLICK OR PRESS P TO RESUME', W/2, 330);
}
}
function render(){
ctx.setTransform(dpr,0,0,dpr,0,0);
ctx.drawImage(bg, 0, 0, W, H);
ctx.save();
ctx.translate((Math.random()-0.5)*S.shake, (Math.random()-0.5)*S.shake);
for(const a of amb) heartFill(ctx, a.x, a.y, a.s, Math.sin(a.ph*0.5)*0.4, a.hue, 60, 45, a.al);
drawBricks();
drawPaddle();
drawBall();
drawParts();
ctx.restore();
drawHUD();
drawBanners();
}
/* ---------- logo on the title card ---------- */
function drawLogo(){
const d = 2, w = 210, h = 124;
logo.width = w*d; logo.height = h*d;
const g = logo.getContext('2d'); g.scale(d,d);
heartFill(g, 24, 34, 11, -0.22, 342, 88, 60, .95);
heartFill(g, 188, 26, 8, 0.25, 352, 90, 66, .95);
heartFill(g, 178, 70, 5.5, -0.3, 336, 85, 54, .9);
heartFill(g, 26, 84, 6, 0.3, 350, 82, 70, .85);
g.save(); g.translate(105, 116); g.rotate(-0.05); g.scale(0.62, 0.62);
drawBurger(g, 0, -68, 156);
g.restore();
}
/* ---------- input ---------- */
window.addEventListener('pointerdown', () => { initAudio(); anyStart(); });
window.addEventListener('pointermove', e => {
const r = canvas.getBoundingClientRect();
if(r.width<=0) return;
const x = (e.clientX - r.left)/r.width*W;
mouseX = clamp(x, PADDLE_W/2+8, W-PADDLE_W/2-8);
});
window.addEventListener('keydown', e => {
initAudio();
const k = e.key;
if(k==='ArrowLeft' || k==='a' || k==='A'){ keys.left = true; mouseX = null; e.preventDefault(); }
if(k==='ArrowRight' || k==='d' || k==='D'){ keys.right = true; mouseX = null; e.preventDefault(); }
if(k===' ' || k==='Enter'){ e.preventDefault(); anyStart(); }
if(k==='p' || k==='P'){ if(S.mode==='user' && S.phase!=='over' && S.phase!=='clear') S.paused = !S.paused; }
});
window.addEventListener('keyup', e => {
const k = e.key;
if(k==='ArrowLeft' || k==='a' || k==='A') keys.left = false;
if(k==='ArrowRight' || k==='d' || k==='D') keys.right = false;
});
window.addEventListener('blur', () => {
if(S.mode==='user' && !S.paused && (S.phase==='play' || S.phase==='serve')) S.paused = true;
});
/* ---------- boot ---------- */
resize();
toDemo();
drawLogo();
let last = performance.now();
function frame(ts){
const dt = Math.min(2.5, (ts-last)/16.667); last = ts;
if(!S.paused) update(dt); else S.t += dt/60;
render();
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
})();
</script>
</body>
</html>Reasoning trace (46,846 tokens)
Let me think about what makes a great Breakout game with a hamburger paddle and heart particle explosions.
Requirements:
- Self-contained HTML file
- Game must start in self-playing demo mode (AI plays) behind a "Click or press a key to start" message
- Something meaningful visible on first paint (screenshot ~3s after load)
- No localStorage, alerts, etc.
- 1280×800 viewport, adapt down to 375px
- Zero console errors
Design plan:
- Canvas-based Breakout game
- Paddle = hamburger drawn with canvas (bun top with sesame seeds, lettuce, tomato, cheese, patty, bun bottom)
- Ball = maybe a tomato/ketchup ball or a shiny red ball
- Bricks = colorful, each explodes into heart particles (drawn heart shapes, not just squares — the task says "tons of little hearts")
- Heart particles: draw heart path on canvas, physics with gravity, rotation, fade
- Demo mode: AI moves paddle toward ball's predicted x
- Start overlay: "Click or press a key to start"
- When user starts, they control paddle with mouse/touch/keyboard arrows
- Score, lives, level
- When ball falls, lose a life; when all bricks cleared, next level with more bricks / faster ball
- Juice: screen shake on brick break? Maybe subtle. Particle bursts, floating score popups.
Visual style: I want it to feel authored. Let me pick a palette — diner aesthetic: cream/warm background? Maybe a deep warm charcoal backdrop with diner-neon accents? Let's think.
A "diner at night" vibe: dark warm background (#1a1216 or deep aubergine), bricks in candy colors (strawberry pink, mustard, mint, ketchup red, cheese orange). Hearts in pinks/reds. Hamburger paddle with realistic-ish shading. A checkered floor strip? Maybe a subtle checkerboard at the bottom (classic diner). Title in a bold display font — but I can't load external fonts... Actually I can use Google Fonts CDN? The rules say "No external files, images, fonts or network requests, except the CDN URLs explicitly allowed by the task." The task doesn't explicitly allow CDNs. So no Google Fonts. I'll use system font stacks — but avoid making Arial/Inter the identity font. I can use "Georgia" or better, use a font stack like `'Hanken...'` no. Options: use `font-family: 'Trebuchet MS'`? Or draw text on canvas with custom styling. A chunky retro feel could be achieved with `'Courier New'`? Hmm.
Better: use CSS to style text with system fonts but heavy weight + letter-spacing to give identity, or draw the title with canvas custom letterforms? Simpler: use a stack like `"Futura", "Trebuchet MS", "Avenir Next", sans-serif` — geometric sans for retro diner feel. Or go more retro-arcade with a pixel-ish look? Drawing custom pixel font on canvas is a lot of work but could be cool. Let me instead craft the HUD with a condensed bold look: `font-family: "Arial Black", "Avenir Heavy", sans-serif` with uppercase, letter-spacing. That's fine as HUD, not an identity crisis. For the big title I could hand-draw a chunky arcaded text using canvas with multiple strokes... Or use CSS text with layered text-shadow for a retro chrome/diner sign effect. I'll do a nice CSS title with heavy weight, tight tracking, and a warm neon sign vibe (but no glow per taste... "No glowing titles" is in my quality bar). Hmm — I should avoid glow. Instead: solid chunky title with offset shadow (retro sticker style). That works.
Layout: full-viewport dark stage, canvas centered, HUD at top (score, lives shown as small hamburgers, level). Actually lives as hamburgers is cute.
Bricks: rows of bricks with different colors and point values. Some bricks take 2 hits (show a cracked state or a bite taken out?). Maybe "harder" bricks have a pickle icon? Keep it simpler: 3 hit-types with color depth.
Heart particles: On brick break, spawn ~18-30 hearts with random velocities, gravity, spin, and fade. Draw heart via bezier path. Tons = make sure plenty. Also add a burst ring. Maybe hearts also fall from the top occasionally? Keep to brick explosion + paddle hit small ketchup splat? Let's add small heart "sparks" when ball hits the hamburger (mustard drips? maybe small seed particles). Keep it focused: hearts on brick break.
Ball: a glossy red tomato? Cute: the ball is a tomato! When it hits the paddle, tomato squishes. Or a meatball! Meatball ball fits the hamburger theme. Meatball: brown sphere with highlights. Cute. Let's make the ball a meatball with a little sauce shine.
Paddle: hamburger, wide, drawn nicely:
- Top bun: rounded arch, sesame seeds
- Lettuce: wavy green fringe
- Tomato: red slice
- Cheese: orange with drips
- Patty: dark brown rounded rect
- Bottom bun: rounded rect
Draw with canvas paths. Add slight squash animation when ball hits.
Controls: mouse move, touch move, arrow keys. Demo AI: paddle moves toward ball x with slight easing and a bit of error; it should play decently so bricks break and hearts fly during the screenshot. Make the AI good enough to keep ball alive and clear bricks.
Overlay: "CLICK OR PRESS A KEY TO START" message over the demo, semi-transparent so demo visible. First paint: demo already playing with hearts — great for screenshot at 3s.
Levels: when bricks cleared, regenerate with a pattern (maybe heart-shaped brick arrangement! That's a signature: bricks arranged in a big heart shape). Yes! Level 1: heart-shaped brick layout. That's memorable and thematically on point. Level 2: maybe a hamburger-shaped layout. Level 3: checker/diamond. Nice signature: brick patterns shaped like hearts and burgers.
Screen shake on brick break (subtle) and bigger on losing life.
Score popups floating.
Combo: consecutive brick hits without paddle touch multiply? Keep simple: small combo counter maybe. Let's add a combo: each brick broken without touching paddle adds combo; display "x2 COMBO". Eh, optional. I'll include a simple combo multiplier for juice.
Sound: Use WebAudio for tiny blips? Allowed (no network). Autoplay policies: audio context must be created after user gesture; in demo mode just don't play sound, or create on first interaction. Add simple synthesized sounds on hit after user starts. That adds polish. Must ensure no errors: create AudioContext lazily on first user gesture.
Responsive: canvas scales to fit viewport with letterboxing. Use fixed internal resolution (e.g., 900x600 logical? or dynamic). For 1280x800 full: maybe canvas fills most of the window with aspect handling. I'll use a fixed logical size like 960x640 and scale via CSS to fit while preserving aspect; game coordinates stay simple. On 375px wide phone, it scales down; touch controls move paddle.
Also `image-rendering` not needed.
Prevent horizontal scrollbar: body overflow hidden, canvas max-width 100%.
HUD: draw in-canvas or DOM? DOM HUD above canvas scales weirdly; drawing in canvas keeps everything consistent and scaled. I'll draw HUD inside canvas: top bar with score, hearts... wait lives as hamburgers — draw mini burger icons. Level indicator. And combo.
But also DOM overlay for start message and "level clear" transitions? Canvas-drawn messages are fine and simpler, with fade animations. Overlay "Click or press a key to start" as DOM element centered over canvas? DOM overlay positioned absolutely over the game container. I'll do DOM for start overlay (crisper text, easy), canvas for the rest.
Let me define the game structure:
```js
const W = 960, H = 640;
canvas.width = W * dpr...
```
Handle devicePixelRatio for crispness. Canvas CSS size fits container.
Game states: 'demo' (AI plays), 'ready' (after user starts, ball on paddle waiting?), 'playing', 'dying', 'levelclear', 'gameover'. When user clicks/presses key from demo, switch to user control: reset game (score 0, level 1) and start playing; ball launches after brief countdown. Maybe simpler: on start, reset to fresh game and immediately serve after 1s "GET READY".
Game over: show "GAME OVER" with score and "click to restart" — clicking restarts player mode. After some inactivity in game over, go back to demo? Not necessary.
Brick patterns: Define grid, e.g., 13 columns. Heart pattern via array of strings:
```
"..XXX....XXX..",
".XXXXX..XXXXX.",
"XXXXXXXXXXXXXX", ...
```
Let me design a heart in a 14-wide grid, 10 rows of bricks area. Heart shape:
Row patterns for heart (cols 14):
```
"..OO....OO..", // hmm
```
Let me carefully design 13 columns heart:
```
..###...###..
.#####.#####.
#############
#############
#############
.###########.
..#########..
...#######...
....#####....
.....###.....
......#......
```
That's 11 rows — maybe too many rows; each brick ~ 46px wide... 13 cols * 60px = 780 fits within 960 with side padding. Rows: brick height ~26 + gap. 11 rows = ~330px tall, plus HUD 70 and paddle area ~120. Total okay for 640 height? 70 + 330 + 150 = 550. Fine.
Actually maybe slightly fewer rows. Let me do a 9-row heart:
```
..###...###.. (13 cols? count: 2+3+3+3+2 = 13)
.#####.#####.
#############
#############
.###########.
..#########..
...#######...
....#####....
.....###.....
```
Hmm last row width 3, then maybe a final single. Let's do:
Row1: "..###...###.." -> that's 13 chars: 2 dots,3 #,3 dots,3 #,2 dots = 13. ✓
Row2: ".#####.#####." -> 1+5+1+5+1 = 13 ✓
Row3: "#############" 13 ✓
Row4: "#############" ✓
Row5: ".###########." ✓
Row6: "..#########.." ✓
Row7: "...#######..." ✓
Row8: "....#####...." ✓
Row9: ".....###....." ✓
Row10: "......#......" ✓
10 rows. Nice heart. Brick colors: gradient by row from pink to red — thematic! Rows colored: deep red at top? Classic heart: top lobes pinkish, bottom deep red? Or do candy stripes. Let's color by row: row 0 light pink → row 9 dark crimson, interpolated. Some bricks could be "special": golden (2 hits, extra points) placed randomly? Maybe make the outline bricks... keep it simple but with 2-hit bricks: bricks marked in pattern with different char, e.g., inner rows tougher. Let's mark pattern chars: '#' = normal, '@' = tough (2 hits). Maybe the heart's outline '@'? Cute: outline tougher.
Level 2: hamburger-shaped brick arrangement:
Bun top arch, lettuce wave, tomato, cheese, patty, bun bottom. With colors matching each ingredient! That's delightful: lettuce row = green wavy, tomato = red, cheese = orange with drip pattern, patty = brown, buns = tan. Use per-row color overrides for the burger level. Nice signature.
Burger pattern (13 wide):
```
...#########... bun top? but 13 cols only. Let's use 15 columns for burger? Grid can vary per level. I'll make grid width per pattern.
```
Let's allow variable columns per level pattern; compute brick width from cols.
Burger pattern with 15 cols:
```
....#######.... (bun top: arch) -> dots:4 +7+4 = 15
..###########.. (2+11+2)
.#############. (1+13+1)
```
Hmm bun arch should look like an arch: narrower at top.
```
Row0: ".....#####....." (5+5+5=15)
Row1: "...#########..." (3+9+3)
Row2: ".#############." (1+13+1)
Row3: "~~~~~~~~~~~~~~~" lettuce (wavy — full width? maybe 15)
Row4: "###############" tomato red
Row5: "..............." gap
Row6: "###..###..###.." cheese drips? tricky
```
Cheese with drips: pattern like "######### .. ######" Let me design:
Row6: "#############.." hmm.
Simpler approach: cheese row mostly full with a couple of "drip" bricks below at random columns:
Row6: "###############" cheese orange
Row7: "..O...O....O..." drips (single bricks at some columns)
Row8: "###############" patty brown (2 hits)
Row9: ".#############." bottom bun
That reads as a burger.
Level 3: maybe word "LOVE"? or diamonds. Level 3+: infinite random patterns? Let's do level 3: checkerboard diamond, then loop patterns with increased speed. Or letters "BBQ"? Let's do level 3: a diamond/rhombus of bricks in rainbow candy colors. Then level 4+ cycles patterns with speed increase.
Ball physics: standard breakout. Ball angle influenced by where it hits the paddle (offset → angle). Speed increases slightly per brick or per level. Cap vertical angle to avoid near-horizontal loops: enforce min |vy|.
Paddle: width ~ 150 (burger), shrinks? no. Move with mouse.
Collision: ball vs bricks using AABB with side detection (determine overlap to reflect axis properly). With small ball, simple approach: compute overlap on x and y, reflect on min overlap axis.
Multiple bricks per frame possible — handle by iterating and breaking after first? Break at most 1 brick per frame? Ball speed can be ~8px/frame; iterate all bricks, resolve first collision then continue could double-reflect. I'll break after one brick per frame (fine at 60fps).
Substepping: if ball speed high, move in substeps to avoid tunneling. Ball speed up to ~ 10; brick height 26; fine, but do 2 substeps for safety.
Hearts particles: array of particles with x,y,vx,vy,rotation,vr,size,life,color,hue variance. Draw heart path:
```js
function heartPath(ctx, s) {
ctx.beginPath();
ctx.moveTo(0, s*0.35);
ctx.bezierCurveTo(0, 0, -s*0.5, 0, -s*0.5, s*0.35)?
}
```
Standard heart:
```
moveTo(0, 0.3s)
bezierCurveTo(-0.5s, -0.3s, -1s?...)
```
Let me use a known heart curve (in unit space, then scale):
```js
ctx.moveTo(0, 0.35);
ctx.bezierCurveTo(0.35, 0, 0.5, -0.4, 0, ... )
```
Simplest reliable heart: two arcs + triangle:
```js
function drawHeart(ctx, size){
const s = size;
ctx.beginPath();
ctx.moveTo(0, s*0.3);
ctx.bezierCurveTo(s*0.5, -s*0.45, s*1.1, s*0.25, 0, s*1.0);
ctx.bezierCurveTo(-s*1.1, s*0.25, -s*0.5, -s*0.45, 0, s*0.3);
ctx.closePath();
}
```
Check: start at top-center dip (0, 0.3s). Curve to right up then down to bottom point (0, s). Control points: (0.5s, -0.45s) pulls up-right, (1.1s, 0.25s) pulls right, end at (0, s) bottom tip. Mirror for left. This produces a decent heart. Let me sanity-check shape: from dip at (0,0.3s), first control (0.5s,-0.45s): curve rises to the right lobe. Second control (1.1s, 0.25s): extends right side. Ends (0, s): bottom tip. Should look heart-ish. Good enough; I'll tweak visually in mind.
Colors: hearts in varying pinks/reds: hsl(340±15, 90%, 55-70%).
Also add tiny white sparkles in burst.
Score popups: "+50" floating text in canvas.
Background: deep warm charcoal with subtle vignette, plus a checkered diner floor strip at bottom (very subtle, low contrast but on dark bg careful — "No large low-contrast gray areas on dark backgrounds" — the checker strip should have enough contrast to be visible but it's a decoration, keep it small/subtle). Maybe faint dotted grid. Also faint big hearts floating in background? Could add slow drifting faint hearts in the backdrop for ambiance (very subtle, low alpha pinkish). Nice.
Screen shake: translate canvas by random offset decaying.
HUD (drawn in canvas top area): left: SCORE with value; center: level name ("LEVEL 1 — HEARTY"); right: lives as mini burgers. Also combo indicator when active.
DOM overlay: Title screen? Actually start overlay includes game title "BURGER BREAKOUT" or name... Let me name it: "HEARTBREAKER DINER"? or "BURGER BREAK". I'll title "HEART ATTACK — the burger breakout"? Keep it: **"LOVE AT FIRST BITE"** — cute! A breakout where burger breaks bricks into hearts. Title: "LOVE AT FIRST BITE". Subtitle: "a burger-ball breakout of the heart". The overlay: big title, "Click or press a key to start", plus control hints (mouse/←→). Overlay semi-transparent dark so demo visible behind with hearts flying. The screenshot at 3s: demo playing, hearts exploding, overlay with title visible.
Overlay style: chunky retro-diner sticker type: white heavy uppercase with offset shadow in ketchup red; heart accent. Avoid glow.
Fonts: use a stack with character: `"Trebuchet MS", "Verdana", sans-serif`? For chunky: `font-family: "Arial Black", "Helvetica"`... hmm "no Arial/Inter/Roboto/system-ui as primary identity font". "Arial Black" is a distinct display weight; but let's pick something with more character available broadly: `"Futura", "Century Gothic", "AppleGothic", "Trebuchet MS", sans-serif`? On Windows, Century Gothic may be absent; fallback Trebuchet. Alternatively use "Impact"?? Impact is very available and extremely diner-poster-ish when used right (uppercase condensed). Title in Impact with letter-spacing could look intentionally retro. Hmm, Impact is a bit meme-y, but for a chunky arcade title it works. I could also render the title in canvas with custom drawn blocky letters... too much.
Decision: Primary display stack: `"Impact", "Haettenschweiler", "Arial Narrow Bold", sans-serif` for title with tight tracking? Impact at large size with skew/italic + two-tone + offset shadow = retro diner sign. Body/HUD: `"Trebuchet MS", "Verdana", sans-serif`. HUD drawn in canvas uses same stacks. OK.
Actually another idea: draw HUD text in canvas with `font: '900 20px "Arial Black", sans-serif'`. Fine.
Now the AI demo: paddle target = ball.x predicted; simple: target = ball.x + ball.vx * some lookahead; clamp; move with max speed. Add slight wobble/error so it looks alive but still mostly successful. To guarantee demo looks good early (screenshot at 3s), start demo with ball already launched and aimed at bricks, maybe even pre-break a couple of bricks... no, demo starts at t=0 with ball served; at 3s the ball should have hit bricks already (ball speed decent, bricks reachable within ~1-2s). Serve the ball upward immediately in demo. Also spawn a couple of ambient hearts at load for instant visual? Could do a quick initial "confetti" of hearts falling at load — cute but might be confusing. The demo will produce hearts quickly; the ball travels from paddle (y~590) to bricks (y~150): distance 440px at speed ~7px/frame*60fps=420px/s → ~1s. So by 3s several bricks broken with hearts flying.
Demo AI accuracy: make it near-perfect: target = ball landing x when moving down, else center drift. Predict landing: if vy>0, time to paddle = (paddleY - ball.y)/vy; x_pred = ball.x + vx*t, reflect off walls. Implement wall reflection in prediction loop. Then paddle moves toward x_pred with max speed high enough (paddle speed 12 in demo). Add tiny error ±10px. Should survive long.
When demo loses all lives? Give demo infinite lives (just relaunch ball). And when demo clears level, next level. Good.
User mode: 3 lives. Start with ball stuck to paddle; click/space releases. Show "GET READY". If lives out → GAME OVER overlay (canvas-drawn) with final score, "click to try again". Click resets to level 1 fresh.
Keyboard: arrows/A-D move paddle; Space launch; mouse/touch moves paddle directly. Also pause? 'P' pause maybe — skip or include simple pause on 'P'. Include pause with overlay; cheap. Eh, keep scope controlled; include pause because it's easy: state 'paused' draws overlay "PAUSED". Only in user mode.
Audio: WebAudio bleeps: paddle hit (low blip), brick break (pop + pitch rises with combo), wall (tick), life lost (descending), level clear (arpeggio). Create context on first user gesture. Keep volumes low. Simple oscillator + gain envelope. Also a subtle "pop" noise via buffer? Keep to oscillators: square/triangle short envelopes. And maybe a little heart "sparkle" chime. Fine.
Now code structure:
HTML:
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
<title>Love at First Bite — Burger Breakout</title>
<style>...</style>
</head>
<body>
<div id="stage">
<canvas id="game"></canvas>
<div id="overlay">
<div class="card">
<h1>LOVE AT FIRST <span>BITE</span></h1>
<p class="sub">burger paddle · heart explosions</p>
<p class="cta">CLICK OR PRESS A KEY TO START</p>
<p class="hint">MOUSE / ←→ TO MOVE · SPACE TO LAUNCH</p>
</div>
</div>
</div>
<script>...</script>
</body>
</html>
```
CSS: body margin 0, background deep aubergine `#17090f` or `#1b0d12` with subtle radial. Stage fills viewport; canvas scaled to fit (contain). Overlay absolutely positioned, pointer-events none? It needs to capture click... clicks anywhere on page should start. Put click listener on window. Overlay pointer-events: none so paddle mousemove works? Once started we hide overlay anyway. During demo, overlay visible; clicks start game. Set overlay pointer-events none and listen on window — fine.
Scaling:
```js
function resize(){
const vw = innerWidth, vh = innerHeight;
const scale = Math.min(vw / W, vh / H);
canvas.style.width = W*scale + 'px';
canvas.style.height = H*scale + 'px';
}
```
Set canvas.width = W*dpr, ctx.setTransform(dpr...). Also center with flex.
On very small width (375), scale ~ 375/960 = 0.39; height 640*0.39=250, leaving big empty vertical space; maybe better fill height more on portrait by letterboxing with visible page background; fine. Alternatively make the canvas fill width and allow vertical letterbox. It's fine.
HUD text size within canvas will scale with canvas. Good.
Background inside canvas: I'll paint a vertical gradient? Careful: "No decorative gradients... as primary visual device". A subtle radial vignette on a game background is fine/normal for games; it's atmosphere, not decoration bolted on. I'll keep background mostly flat deep plum `#201018` with a subtle vignette and a faint dotted pattern + drifting hearts. Also bottom "floor" checkered strip in dark maroon.
Bricks rendering: rounded rect with slight top highlight and bottom shade — flat-ish cartoon look with crisp 2px darker outline; candy colors. 2-hit bricks show crack after first hit (draw crack lines) or lighter/desaturated state.
Heart colors for rows in level 1: from pink `hsl(340,95%,72%)` to deep red `hsl(350,85%,45%)`.
Level definitions:
```js
const LEVELS = [
{ name:'HEARTY', rows:[...heart pattern...], speed: 6.5 },
{ name:'CHEEZUS', rows:[...burger pattern...], speed: 7 },
{ name:'DIAMOND DAZE', rows:[...diamond...], speed: 7.5 },
];
```
After level 3, cycle with speed += 0.5.
Pattern chars: '.' empty, '#' normal (1 hit), '@' tough (2 hits, +points), maybe letters for forced color rows: For the burger level, define per-row color: rows array of objects? Simpler: each level has `rowColors: (rowIndex, colIndex) => color`. I'll write functions per level.
Let me define patterns as arrays of strings; each level defines a `colorFor(r,c,totalRows)` function and `tough` set (chars).
Heart pattern (13 cols, 10 rows) — mark outline '@'? Which cells are outline: cells with fewer neighbors... easier: hand-mark. Let me hand-design with '@' on the outline:
```
"..@@@...@@@..",
".@###@.@###@.",
"@##@#######@##" no keep 13
```
Row0: "..@@@...@@@.." (13)
Row1: ".@###@.@###@." → chars: . @ # # # @ . @ # # # @ . = 13 ✓
Row2: "@@@#######@@@" hmm the top corners are outline. Let me not over-engineer: outline = first and last '#' of each row plus top row all '@'?
Actually simpler and still readable: make the two top lobes '@' (tough) and rest normal:
Row0: "..@@@...@@@.." all tough (top lobes)
Rest normal '#'. The lobes take 2 hits — fine, gives visual variety as lobes crack then break.
Alternatively sprinkle tough bricks randomly (seeded). I'll do: in heart level, lobes tough; burger level: patty row tough; diamond: alternating.
Burger pattern (15 cols, 10 rows):
```
r0: ".....#####....." (5+5+5) bun top (tan)
r1: "...#########..." (3+9+3) bun (tan)
r2: ".#############." (1+13+1) bun (tan)
r3: "~~~~~~~~~~~~~~~" wait use '#'
```
Use '#' everywhere and assign color per row:
r3: "###############" lettuce green (wavy drawn? just green bricks, maybe scalloped top via drawing? keep rect)
r4: "..............." gap (thin gap)
Actually a gap row with nothing makes burger look segmented — good.
r5: "###############" tomato red
r6: "###############" cheese orange
r7: "..#......#....." cheese drips (a few single bricks at random columns, color orange)
r8: "@@@@@@@@@@@@@@@" patty (tough, brown)
r9: ".#############." bottom bun (tan, 1+13+1)
Hmm r7 drips: choose columns 2, 6, 11: "..#...#....#..." → 2 dots,#(idx2),3 dots,#(idx6),4 dots,#(idx11),3 dots: 2+1+3+1+4+1+3 = 15 ✓
Rows count: r0..r9 = 10 rows. Brick sizes: with 15 cols, brick w = ? Playfield width for bricks: W - 2*margin. margin 30 → 900 / 15 = 60 incl gap; brick w 54, gap 6, h 24, gapY 6 → total height 10*30=300. Start y = 90 (below HUD). Ends 390. Paddle at y ~ 590. OK.
For 13-col heart: brick w = 900/13 ≈ 69 → w 62, gap 7. Fine.
Diamond level (13 cols, 9 rows):
```
"......#......",
".....###.....",
"....#####....",
"...#######...",
"..#########..",
"...#######...",
"....#####....",
".....###.....",
"......#......",
```
Colors: rainbow by row (candy). Alternating rows tough? Make '@' every other ring: rows 0-1 '@'? Just make a couple '@' sprinkled: center row tough. Eh: colorFor returns rainbow hue by row; tough if (r+c)%... let's mark pattern with '@' chars for every other ring:
r0: "......@......"
r1: ".....#@#....." hmm mixed. Simpler: I'll generate diamond programmatically? Just write pattern with '@' alternating rings:
```
"......#......",
".....@@@.....",
"....#####....",
"...@@@@@@@...",
"..#########..",
"...@@@@@@@...",
"....#####....",
".....###.....",
"......#......",
```
Wait alternating: rings from outside: r0 ring0 '#', r1 ring1 '@', r2 ring2 '#', r3 ring3 '@', r4 center '#', then mirror. That gives '@' rows 1,3,5,7. Looks like concentric tough rings — fine and symmetric. But diamond rainbow with tough rings looks decent.
Colors per level:
- Heart: interpolate hue 335→350, light 72→46 by row. Or per-row palette: ['hsl(338,90%,74%)', ...]. I'll compute: `hsl(${330 + r*3}, ${85}%, ${75 - r*3}%)`.
- Burger rows: [bunTan, bunTan, bunTan, lettuce, —, tomato, cheese, cheese, pattyBrown, bunTan].
- bun: '#e8a355' / highlight '#f4b56d'; lettuce '#7bc043'→ use '#8bc34a' with darker '#689f38'; tomato '#e53935'; cheese '#ffb300'; patty '#6d4c35'/'#5d4033'; bottom bun '#e8a355'.
- Diamond: hue = 330 + r*20? For rainbow: hue = (r*40) % 360 with sat 85 light 62. Keep it candy: maybe hues [340, 25, 50, 140, 190, 260, 300, 20, 45]. Rainbow-ish but skewed to warm+pinks? Eh, full rainbow is fine for a candy level.
Brick drawing: base color; top inner highlight (rgba white 0.25) as a thin rounded rect; bottom shadow (rgba black 0.25); tough bricks have small dots pattern or darker outline + drawn "seed"? For cracked state (tough after one hit): draw crack (two zigzag lines) and desaturate slightly.
Tough bricks show a little star/pickle? Draw a small "×"? I'll draw small darker "diamond" dot in center for tough; after hit, cracks.
Now the hamburger paddle drawing. Paddle width P_W = 150, height ~ 54. Draw layers (relative to paddle center x, top y):
- top bun: rounded shape: from (x, y+? ) Draw path: dome using quadratic curves; height 20. Sesame seeds: small ellipses scattered (fixed offsets, rotated).
- lettuce: wavy ribbon under bun: green, zigzag bottom edge, height 8, wider than bun by 4 (frills sticking out).
- tomato: red rounded rect height 7, width slightly less.
- cheese: orange rect height 6 with drips (small rounded rect hanging at 3 offsets).
- patty: dark brown rounded rect height 12.
- bottom bun: tan rounded rect height 14, width = P_W.
Total ~ 20+8+7+6+12+14 = 67px tall — too tall? Paddle collision box should be its visual (or just top part). Ball collides with top of paddle anyway. Use full rect for collisions; fine. Maybe compress: bun 16, lettuce 7, tomato 6, cheese 6, patty 10, bottom 12 = 57. OK.
Squash animation: on ball hit, scaleY 0.85 for a few frames with easing; also bun sesame "jump"? Keep squash.
Paddle motion: mouse sets target x; keyboard velocity; smooth lerp for mouse? Direct is more responsive: paddle.x = mouse x (clamped). Add slight tilt based on velocity (rotate a few degrees) — juicy. Draw with rotation = clamp(vx*0.02).
Ball: meatball: radius 9, brown '#7a4a2b' radial highlight, darker outline, a tiny sauce glaze arc? Add spinning "texture" dot to show rotation. Cute detail: tiny face? A little kawaii face on meatball (two eyes) would be memorable! Eyes blink occasionally. When hitting bricks, eyes wide? Keep simple: two small eyes with pupils that look toward movement direction, blink timer. It's charming — "signature". And hearts exploding + cute meatball = memorable. I'll add it subtly.
Trail: ball leaves a short fading trail (store last N positions, draw fading circles) in warm color.
Brick break: hearts burst: count = 22 + tough bonus. Also floating score text. Also ring shockwave (expanding circle stroke fading).
Life lost: ball falls → "splat" — burger "eats"? On death: hearts fall sad? Show big heart crack animation? Keep: screen shake + paddle does a sad squash + respawn.
Level clear: text "LEVEL CLEAR!" + big heart shower across screen, then next level builds bricks with a cascade animation (bricks pop in one by one with delay). Bricks spawn with scale-in — nice.
Game over: overlay drawn on canvas: "GAME OVER", score, "CLICK TO PLAY AGAIN". Also button? Just click/key restarts.
Extra: when only a few bricks remain, ball speeds up 5%.
Serving: in user mode after start and after life loss: ball sticks to paddle; launch on click/space; show "CLICK / SPACE TO LAUNCH". In demo mode, auto-launch after 0.8s.
Timer-based update with dt normalization: use requestAnimationFrame with dt clamp; speeds defined per 60fps unit: `dt` in frames (dt = ms/16.667, clamp 0..2). Multiply velocities.
Collision details:
Walls: left/right/top reflect. Bottom: lose life (user) / respawn (demo).
Paddle collision: if ball moving down and overlaps paddle rect: reflect with angle: `rel = (ball.x - paddle.x)/ (P_W/2)` clamp -1..1; angle = rel * maxBounce (60°); speed maintained; vy = -cos*speed etc. Add paddle "english": vx influenced. Ensure min upward component.
Brick collision: loop; compute overlap; reflect axis with smaller penetration; reduce brick hp; if 0 → remove, spawn hearts, score += val * combo; combo increments per brick until paddle touch resets combo. Combo shown when ≥2: "COMBO ×n".
Score values: normal 50, tough 100 (first hit 25?). Points * combo.
Lives: 3. Display mini burgers top-right.
HUD layout: top strip y 0..64: draw at y 20: left "SCORE 000000", middle "LEVEL 1 · HEARTY", right burgers. Use canvas fonts: `'900 22px "Arial Black", "Trebuchet MS", sans-serif'`. Add tiny heart icon before score? Draw small heart.
Also draw bottom hint bar? The overlay handles hints. During play, small footer text: "P — PAUSE" tiny at bottom-left corner? Put in HUD as faint text bottom: skip clutter; put pause hint in start overlay: "MOVE: MOUSE / ← → · LAUNCH: SPACE · PAUSE: P".
Now DOM overlay design:
```
#overlay: absolute inset 0, display flex center, background: radial? Use rgba(20,8,12,0.55) with backdrop-filter blur(2px)? backdrop-filter might be heavy but fine. Keep slight dark tint.
.card: text-align center, padding, color cream #fff4e6.
h1: font Impact, size clamp(44px, 9vw, 88px), letter-spacing 2px, uppercase; two-tone: "LOVE AT FIRST" cream + "BITE" in ketchup red? Style: main cream with hard offset shadow (#ff5470?) Actually do stacked shadow: text-shadow: 3px 3px 0 #b3123a? Let's do cream text with red offset shadow 4px, plus second deeper shadow 8px dark. Retro sticker.
sub: small uppercase tracking-wide, letter-spacing 4px, color #ff9db0.
cta: pulsing (scale keyframes) — animation subtle opacity pulse; background? Make CTA a "button-ish" pill with ketchup red bg #e23a5a, cream text, chunky border-radius, hard shadow. Pulse animation.
hint: tiny, dim.
```
The overlay must not block view of demo too much: use background rgba(15,6,10,.45) only behind card? Full overlay tint 35% is okay.
Also small "DEMO" tag: show "AI CHEF AT PLAY" label in corner of overlay — communicates auto demo. Nice touch: top of card small eyebrow text: "THE AI CHEF IS SHOWING OFF —" above title? Put eyebrow: "★ DEMO ★" tiny. Cute.
Hide overlay on start: fade out via class.
Game over / level clear drawn on canvas — good.
Edge: pointer lock not needed. Touch: touchmove sets paddle x (map clientX→ canvas coords via rect). Prevent scroll: `touch-action: none` on canvas/stage.
Prevent context menu? not needed.
Audio implementation:
```js
let AC = null;
function initAudio(){ if(!AC){ try{ AC = new (window.AudioContext||window.webkitAudioContext)(); }catch(e){} } if(AC && AC.state==='suspended') AC.resume(); }
function beep(freq, dur, type='square', vol=0.08, slide=0){
if(!AC) return;
const t = AC.currentTime;
const o = AC.createOscillator(), g = AC.createGain();
o.type = type; o.frequency.setValueAtTime(freq, t);
if(slide) o.frequency.exponentialRampToValueAtTime(Math.max(30,freq+slide), t+dur);
g.gain.setValueAtTime(vol, t);
g.gain.exponentialRampToValueAtTime(0.0001, t+dur);
o.connect(g); g.connect(AC.destination);
o.start(t); o.stop(t+dur+0.02);
}
```
Sounds: wall: beep(220,0.05,'triangle'); paddle: beep(160,0.08,'square') plus 330; brick: beep(520 + combo*40, 0.09, 'square', 0.07, +200) + heart sparkle beep(880,0.12,'sine',0.04); life: beep(200,0.4,'sawtooth',0.08,-150); levelclear: arpeggio via setTimeout beeps (392,494,587,784).
Init audio on first pointerdown/keydown (user gesture) — fine in sandbox.
Make sure no console errors: guard AudioContext.
Demo mode should NOT play audio (no gesture anyway; AC null until user interacts — after user interacts we start the game anyway, so demo never has audio... unless user clicks during... click starts game. Fine).
Also after user starts, if they pause maybe demo? no.
Now, spawn hearts function:
```js
function burstHearts(x, y, n, baseHue){
for(let i=0;i<n;i++){
const a = Math.random()*Math.PI*2;
const sp = 1.5 + Math.random()*5;
particles.push({
type:'heart',
x, y,
vx: Math.cos(a)*sp, vy: Math.sin(a)*sp - 2.5,
g: 0.12 + Math.random()*0.1,
rot: Math.random()*Math.PI*2, vr: (Math.random()-0.5)*0.25,
s: 4 + Math.random()*7,
life: 1, decay: 0.008 + Math.random()*0.01,
hue: baseHue + (Math.random()*30-15),
sat: 70+Math.random()*30, li: 50+Math.random()*25,
wob: Math.random()*Math.PI*2
});
}
}
```
Update: vy += g; x += vx (+wobble via sin for float); rotation += vr; life -= decay; alpha = life; scale grows slightly then... draw with alpha = min(1, life*1.5)? Fade last 40%: alpha = min(1, life*2). Also horizontal wobble: vx *= 0.985; x += Math.sin(wob += 0.05)*0.3 for flutter. Also draw tiny white shine on hearts (small circle). Hearts also slightly scale-pulse. Keep count capped (max ~600 particles) for perf: if particles.length > 700, remove oldest. Draw hearts: precompute? Drawing bezier per particle for 300 particles fine.
Sparkles: small star/plus shapes white/pink, short life, additive-ish (globalAlpha) — use type 'spark' drawn as 4-point star (two lines).
Floating score texts: {x,y,txt,life,vy} draw with font.
Background drifting hearts: maintain ~12 ambient hearts with slow fall, alpha 0.06-0.12, varying sizes, wrap around. Subtle warm pink on dark plum. Nice depth.
Vignette: radial gradient dark edges — cache to offscreen canvas once (perf) and draw each frame, or just draw gradient each frame (createRadialGradient each frame is a bit costly but okay; better cache). I'll cache background (flat color + checker floor + vignette + dotted?) to offscreen canvas at init and drawImage each frame. Ambient hearts + floor scroll? Static floor fine.
Floor: bottom strip below paddle? Paddle at y 590; floor from y 610 to 640: checkerboard 2 rows of squares alternating #2a1420 / #351a28? subtle. Plus a thin "counter" line. Actually let's do the classic pink-white checker but darkened: colors '#3a1f2b' and '#17090f'. And a neon-ish top edge line #ff5470 at 30% alpha? Careful with taste — a thin accent line is fine.
HUD strip: top area 0..64: bg slightly darker with thin bottom border line rgba(255,132,155,0.25). HUD drawn on top.
Field margins: walls at x=0 and x=W — draw side "counter" edges? Keep the play area full-bleed with 1px inner stroke? Not needed.
Screen shake: shake = amount; each frame offset = (rand-0.5)*shake; shake *= 0.88. Apply ctx.translate before drawing world, restore after. HUD drawn after restore (or also shaken? shake HUD too—fun, but keep HUD stable: draw HUD after restore).
Now sizes:
W=960, H=640. HUD 0..56. Bricks start y=76. Paddle y (top) = 578, height 56 → bottom 634. Ball lose if y > H+20.
Paddle movement clamp: half width margin: 10..W-10.
Ball speed: base 6.2 (per frame at 60fps). Sub-stepping: split movement into ceil(speed/6) steps.
For brick collisions during substeps: check each substep.
Tough brick hit (still alive): small spark burst + score +25, crack visual.
Brick hp: normal 1, tough 2. Store `hp`, `maxHp`.
Bricks array with alive flag. Remaining count for level clear.
Level build animation: each brick gets `spawnDelay = (r+c)*20ms` and `spawnT` progress 0→1 with elastic overshoot scale. During build (first ~0.8s) ball is held on paddle anyway in user mode; in demo, auto-serve waits 1s. Good.
Level clear sequence: state 'clear': show banner "LEVEL CLEAR" + heart rain 1.6s, then buildLevel(next), state back (demo: auto; user: ready-to-launch).
Game over: state 'over': dark canvas overlay text + final score + "CLICK OR TAP TO PLAY AGAIN". Any click/key (except during first 0.5s to avoid accidental) restarts.
Restart from game over → user mode level 1, score 0, lives 3.
Also maybe a "menu" return after game over 10s idle → back to demo with overlay? Nice-to-have: after 8 seconds in game over, revert to attract mode. Implement: overTimer; if > 9s → enter demo mode with overlay shown again. That satisfies "self-playing demo behind message" requirement originally at load; not needed later but it's a nice attract loop. I'll implement it (cheap).
Key handling: keydown: if overlay active (demo state) → startGame(). In playing: arrows set held flags; Space launch (if stuck) else nothing; 'p' toggle pause. Prevent default for space/arrows to avoid scroll (sandbox iframe, but page may scroll—overflow hidden anyway; still preventDefault).
Also clicking canvas during play when ball stuck → launch. Clicking during play (not stuck) — nothing (or nudge?) nothing.
Mouse move during play: paddle target = canvas x. Use `canvas.getBoundingClientRect()` mapping.
In demo, ignore mouse (AI controls) — or if user moves mouse significantly during demo... any click/key starts game, movement alone shouldn't (spec says click or key). Mousemove shouldn't start. Good.
Structure states: `mode` = 'demo' | 'user'; `phase` = 'serve' | 'play' | 'clear' | 'over' | 'paused'. Demo uses same phases (serve auto-launch, play). Overlay visible iff mode==='demo'.
startGame(): mode='user', score=0,lives=3,level=0,buildLevel, phase='serve', hide overlay, initAudio, launch hint.
loseLife(): lives--; if 0 → phase 'over' (record overTimer) else phase 'serve' with ball stuck; shake big; sound.
Demo lose "life": just phase='serve' and auto-launch; maybe show tiny "oops" text? Fun: demo AI occasionally misses deliberately? It adds heart explosions at bottom? Not needed.
Actually for screenshot appeal at ~3s: also on initial load, spawn a gentle celebratory heart drift? The demo first serve: ball launched, reaches bricks ~1.2s. At 3s, 2-4 bricks broken → 50-120 hearts floating.
Let me make sure demo AI aims to hit bricks: it just keeps ball alive; ball will hit bricks naturally. To speed it up, initial serve direction aimed at a lobe.
Now paddle drawing function `drawBurger(cx, topY, w, squash)`:
Let w=150. Layers widths relative: bun top w-8? Let's param:
- squash: scale Y around bottom.
- rotation tilt applied outside.
Coordinates (x from cx - w/2):
```
topBun: path: leftX+6 → dome peak. Use quadratic: moveTo(l+10, ty+16); quadraticCurveTo(l+10, ty, l+w*0.18, ty)?
```
Better construct: dome as arc: begin at (l, ty+14) ; bezier to top: control points to make dome:
```js
function roundTop(ctx, l, r, ty, h){
ctx.moveTo(l, ty+h);
ctx.bezierCurveTo(l, ty, l, ty, l+ w*0.5? ...)
```
Standard dome:
```js
ctx.moveTo(l, ty+h);
ctx.bezierCurveTo(l, ty+h*0.2, l+w*0.08, ty, l+w*0.5, ty);
ctx.bezierCurveTo(l+w*0.92, ty, l+w, ty+h*0.2, l+w, ty+h);
ctx.closePath();
```
That gives a dome height h. Fill bun color, stroke darker outline 2px. Sesame seeds: at fixed relative offsets [[0.2,0.5],[0.35,0.3],[0.5,0.55],[0.65,0.3],[0.8,0.5],[0.3,0.7],[0.7,0.7]] positions scaled to (l..l+w, ty..ty+h*0.8), tiny ellipses rotated, color '#ffe9c4', slight outline? tiny.
lettuce: y = ty + bunH. Width w+10 (l-5). Shape: rect with wavy bottom: moveTo(l-5, y); lineTo(l+w+5, y); then zigzag back along bottom with small quadratic scallops: for x from right to left step ~12: quadraticCurveTo(x-6, y+8, x-12, y+3)? Make scallops pointing down: use quadratic arcs. Fill '#7ec850', stroke '#4f8f2a'? Slightly translucent overlap.
tomato: rounded rect (l+8, y2, w-16, 7, r=3) fill '#e5453c', stroke darker.
cheese: y3: rect (l+4, y3, w-8, 6) fill '#ffc02e' stroke '#d9930b'; drips: at offsets [0.22, 0.5, 0.78]: rounded rect (x-5, y3+4, 10, 8, r=4) same fill — draw drips before? after with stroke join look ok; draw drips first then cheese bar over top so drips hang beneath.
patty: rounded rect (l+2, y4, w-4, 11, r=5) fill '#5b3a29', stroke '#3f281c'; texture: few darker dots.
bottom bun: rounded rect (l+6, y5, w-12, 13, r=6) fill '#e8a355', stroke '#b06f33'. Maybe tiny shine line.
Also drop shadow under paddle: ellipse dark alpha at y+h+4.
Heights: bun h=16, lettuce ~10 (incl wave), tomato 7, cheese 6(+drips 8), patty 11, bottom 13 → total ≈ 62. Paddle collision top = 578, so occupies 578..640 hmm bottom touches floor 640. Let paddle top y = 570, height 60 → 570..630, floor strip 630..640? Floor 636..640 thin... Let me set: H=640, floor strip y=624..640 (16px, 2 rows of 8px checks?). Paddle top 562, bottom 622, shadow at 626. Ball radius 9. Lose threshold y > 660.
Mini burger lives icons: draw simplified tiny burger (three stacked rounded rects) function drawMiniBurger(x,y).
drawHeart(x,y,size,rot,hue...) fills.
Ball draw with trail: trail array of last 8 positions; draw circles decreasing radius alpha.
Meatball: radial gradient (#8a5a3a center-top highlight to #5b3a29)? Radial gradient inside a ball is shading, acceptable (not decorative UI gradient). Or flat + highlight arc: fill '#7a4a2f', stroke '#4a2a18', highlight small white-ish arc at top-left, plus a rotation marker (lighter speck). Eyes: two white circles r=2.6 at offsets, pupils r=1.2 looking toward vx/vy direction; blink: every ~3s eyes become lines for 0.12s. In demo and user both. Cute!
Ball also small mouth 'o' when falling fast? Skip.
Combo display: when combo ≥ 2 show at top center under HUD: "COMBO ×3 ♥" pink text w/ pop scale.
Score popup font: bold small canvas text.
Now let me also handle the "GET READY / aim" state visuals: when phase 'serve' in user mode, draw dashed aim line from ball upward? Simple arrow hint + text "SPACE / CLICK TO LAUNCH" pulsing at bottom.
Edge cases: window blur → pause if user playing (nice): on blur, if mode user && phase play → paused. Implement.
Resize listener updates canvas CSS size + dpr backing store.
DPR: canvas.width = W*dpr etc; ctx.setTransform(dpr,0,0,dpr,0,0). Recreate on resize (dpr changes). Redraw background cache too (bg cache at logical W,H, drawn via drawImage with same transform — fine since drawn in logical coords; cache canvas at W*dpr for crispness? Simpler: bg cache canvas at W×H (logical), drawImage(bg,0,0) under dpr transform → slightly soft on hi-dpi. Acceptable for subtle bg, but checker lines would blur. Make bg cache at dpr scale: bgCanvas.width=W*dpr, draw with scale dpr, then drawImage(bgCanvas, 0,0, W, H) — under transform dpr that becomes dpr*W backing → draws full-res. Good: drawImage(bg, 0,0, W, H) with current transform dpr maps 1:1 pixels.
Now particles cap and perf: fine.
Also ensure no `localStorage` usage. None.
Let me also add subtle CSS page bg beyond canvas letterbox: body background #120a0e with faint pattern? Keep flat dark.
Now, the overlay markup/style must look good on 375px: use clamp() for font sizes. Card max-width 90vw.
One more requirement: "In the game, every interactive element must be wired" — the CTA says click anywhere; fine. No fake buttons.
Title final: "LOVE AT FIRST BITE". Subtitle: "the burger-breakout of hearts" hmm: "A BREAKOUT WHERE THE PADDLE IS A BURGER AND THE BRICKS BURST INTO HEARTS". CTA: "CLICK OR PRESS A KEY TO START". Eyebrow: "♥ DEMO MODE — THE CHEF IS PLAYING ♥".
Fonts in overlay: title Impact-ish stack: `"Impact", "Haettenschweiler", "Franklin Gothic Bold", "Arial Black", sans-serif` — Impact widely available. letter-spacing 1px. Cream color #fff3e0, text-shadow: `0.06em 0.06em 0 #d81b53` (ketchup), maybe double: `0.06em 0.06em 0 #d81b53, 0.12em 0.12em 0 #40101f`? Hmm double offset shadows give sticker depth. Try: `4px 4px 0 #e0447a?` I'll go `0.055em 0.055em 0 #e8375f, 0.11em 0.11em 0 rgba(0,0,0,.55)`.
CTA pill: background #e8375f, color #fff3e0, padding 14px 26px, border-radius 999px, font bold letter-spacing 2px, box-shadow 0 6px 0 #8f1c3c? Hard shadow chunky (pressable). Animation: pulse translateY.
Hint: color rgba(255,243,224,0.55), letter-spacing 2px, small.
Card background? Card could be borderless text over dim overlay. To ensure readability over demo: overlay bg rgba(18,6,12,0.5) + card no bg. Add backdrop-filter: blur(3px) saturate(1.1)? backdrop-filter on overlay allowed; may cost perf but small. Use blur(2px) only. Actually blur makes demo less visible — judge wants to see game in action. Slight tint only, no blur. Use rgba(20,8,14,0.45).
But text readability: add text shadows.
Heart icon in title? Put a drawn heart via inline SVG in the overlay between words? Inline SVG allowed (self-contained). Small heart SVG next to title line "LOVE AT FIRST BITE" with a little burger SVG? Could hand-draw a tiny burger+heart inline SVG above the title as a logo. That'd look great: a mini burger icon with a heart. Let me craft simple SVG: bun dome, lettuce, tomato, cheese, patty, bottom bun + floating hearts. ~80px tall. Doable with basic shapes. I'll include modest version.
Let me write the SVG:
```html
<svg class="logo" viewBox="0 0 120 90" width="120" height="90" aria-hidden="true">
<!-- hearts -->
<path d="M14 26c... " fill="#ff5d8f"/>
...
<!-- burger -->
<path d="M20 40 q0 -16 40 -16 q40 0 40 16 z" fill="#f0a860"/> top bun: dome from x20..100
seeds: small ellipses fill #ffe9c4
lettuce: path scallops fill #7ec850
tomato: rect fill #e5453c
cheese: rect + drips fill #ffc02e
patty: rect fill #5b3a29
bottom bun: rect rx fill #e8a860
</svg>
```
Let me spec coordinates:
- top bun: M20,42 → dome height 22: path `M22 44 C22 26 38 20 60 20 C82 20 98 26 98 44 Z` fill #efa75c stroke #b06f33 stroke-width 3.
- seeds: ellipses at (42,30),(55,25)? place along dome: (40,32),(52,27),(66,27),(78,32) rotate some. small rx3 ry2 fill #ffe3b3.
- lettuce: y44..50 wavy: `M18 46 L102 46 L102 48 Q97 56 92 48 Q87 56 82 48 ...` Simpler: use a path with repeating quadratic scallops along bottom edge: start M18 44 H102 V46 then scallops leftwards: `Q99 54 96 46 Q93 54 90 46`... I'll generate scallops with step 8: from x=102 to 18: control at x-4 down +8, end x-8 at y46. Write path manually with loop in JS? It's static SVG; I can compute mentally but simpler: use 11 scallops of width ~8. Alternatively use a simpler zigzag lettuce using `l` segments: `M18 44 H102 l-6 7 -6 -7` repeated... zigzag with straight lines reads as spiky lettuce — fine cartoonish: from right: `L96 51 L90 44` repeated. I'll write: `M18 44 H102 L102 44` then for i: pairs. Let me just write full path with 21 points... tedious but fine; or use `stroke` dashed trick? no.
Alternative: make lettuce a simple rounded rect with scalloped bottom approximated by circles row: draw 10 small circles overlapping below a rect — as SVG: rect y44 h6 fill green + circles cx every 8.4 r4 at y50 same fill → scalloped look. That's easy: `<rect x="18" y="44" width="84" height="7" fill="#7ec850"/><circle cx=22 cy=51 r=4.5 .../>` circles from x=22 to 98 step ~8.5 (10 circles). Slight outline? Skip outline on lettuce circles or add stroke to rect only... Outline mismatch. Use group with fill green and no strokes for lettuce; acceptable.
Actually simpler and cleaner: skip lettuce in logo; layers: bun, tomato, cheese, patty, bottom bun. Fewer elements, still reads as burger. Add small heart floating above right. OK.
- tomato: `<rect x="26" y="52" width="68" height="8" rx="4" fill="#e5453c"/>`
- cheese: `<rect x="22" y="60" width="76" height="6" fill="#ffc02e"/>` + drips `<rect x="34" y="63" width="10" height="9" rx="4"/>` `<rect x="58" ...>` `<rect x="80" ...>`
- patty: `<rect x="22" y="69" width="76" height="12" rx="6" fill="#5b3a29"/>`
- bottom bun: `<rect x="26" y="81" width="68" height="12" rx="6" fill="#efa75c"/>` with viewBox height 100? adjust viewBox 0 0 120 100.
- hearts beside burger: two small hearts upper corners: heart path centered: `M..` Use path: heart at (14,30) size 10: `M14 34 C14 30 20 28 20 33 C20 28 26 30 26 34 C26 39 20 42 14? ` hmm. Standard small heart path: `M0 3 C0 0 -4 0 -4 3 C-4 6 0 8 0 8 C0 8 4 6 4 3 C4 0 0 0 0 3` — wait that's lobe width. Let me use known: `M10 30 A5 5 0 0 1 20 30 Q?`... Use two circles + rotated square trick in SVG: heart = `<path d="M12 32 C12 26 4 26 4 33 C4 39 12 43 12 43 C12 43 20 39 20 33 C20 26 12 26 12 32 Z"/>` — plausible heart: left lobe from (12,32) curving up-left to (4,33) then down to (12,43) bottom tip; mirrored. Good enough visually.
Honestly the canvas logo could also be drawn by the same drawBurger function on a small canvas... simpler to reuse! Instead of SVG, draw the overlay logo with the game's own drawBurger on a tiny canvas in overlay. That guarantees style consistency. But overlay is DOM; I can position a small canvas (120×90) inside card and call drawBurger on it once at init. Reuse = elegant. But drawBurger draws at paddle scale w=150; I can scale ctx. Let's do that: `<canvas id="logoCanvas" width="240" height="150"></canvas>` style width 120px (dpr 2 built in). Draw burger centered + a couple of hearts around using drawHeart.
I'll do the logo canvas approach — cohesive and less duplicate design.
Overlay hidden: class .hidden { opacity 0; pointer-events none; transition .4s }.
Also mobile: on touchstart anywhere → startGame (counts as gesture). touchmove controls paddle.
Right — also "The iframe only receives keyboard events after the user clicks it" — our overlay says click or press a key; after click, keydown listeners work. Fine.
Now, let me carefully plan collision code.
Constants:
```js
const W=960,H=640;
const HUD_H=56;
const PADDLE_Y=566; // top of paddle
const PADDLE_W=150, PADDLE_H=58;
const BALL_R=9;
```
Paddle rect: {x: paddleX (center), top: PADDLE_Y, w, h:52}. Collision uses top surface mostly.
Bricks: build per level:
```js
function buildLevel(idx){
const def = LEVELS[idx % LEVELS.length];
const rows = def.pat;
const cols = rows[0].length;
const gap = 6;
const marginX = 26;
const bw = (W - marginX*2 - gap*(cols-1)) / cols;
const bh = 26;
const startY = 84;
bricks = [];
rows.forEach((line,r)=>{ [...line].forEach((ch,c)=>{ if(ch==='.') return;
bricks.push({ x: marginX + c*(bw+gap), y: startY + r*(bh+gap), w: bw, h: bh, hp: ch==='@'?2:1, maxHp: ch==='@'?2:1, color: def.color(r, rows.length, c, cols), delay:(r*cols? ), spawn:0 }) })});
}
```
bw for 13 cols: (960-52-72)/13 = 836/13 = 64.3. h 26. Heart total height: 10 rows*(32) = 320 → 84..404. Paddle 566. Space 162px for ball travel — fine.
For 15 cols: (960-52-84)/15 = 824/15=54.9 w. ok.
Spawn animation: each brick spawn progress from 0→1 with delay = (r*cols + c)*12ms; drawn scale = easeOutBack(spawn). Track levelTime.
Remaining bricks: count.
Ball-brick collision (per substep):
```js
for (const b of bricks){
if(b.dead) continue;
// spawn not complete? still collide (or not? during serve ball is held anyway)
const cx = clamp(ball.x, b.x, b.x+b.w), cy = clamp(ball.y, b.y, b.y+b.h);
const dx = ball.x-cx, dy = ball.y-cy;
if(dx*dx+dy*dy <= BALL_R*BALL_R){
// resolve axis
const ox = (ball.x < b.x+b.w/2) ? (b.x - (ball.x+BALL_R)) : ((b.x+b.w) - (ball.x-BALL_R)); hmm
```
Simpler axis decision: compute overlaps:
```js
const overlapX = BALL_R + b.w/2 - Math.abs(ball.x - (b.x+b.w/2));
const overlapY = BALL_R + b.h/2 - Math.abs(ball.y - (b.y+b.h/2));
if(overlapX>0 && overlapY>0){ hit }
```
Reflect: if overlapX < overlapY → reflect vx (ball from side) and push out; else reflect vy. This standard approach works decently.
Then damage brick; break; (stop processing further bricks this substep).
Speed handling: `ball.speed` scalar; direction unit (vx,vy normalized*speed). Keep vx,vy as velocity and speed = hypot. On paddle hit recompute from angle.
Wall collision: x-BALL_R<0 → x=BALL_R, vx=abs; x+R>W → mirror; y-R < HUD_H? Top playfield boundary: y=HUD_H+? Let top wall at y=HUD_H (56). Ball top boundary y-R<56 → reflect. Actually give a couple px visual line under HUD.
Bottom: y - R > H → life lost (once). Use flag.
Paddle collision: if vy>0 and ball.y+R >= PADDLE_Y and ball.y+R <= PADDLE_Y+PADDLE_H and |ball.x - paddleX| <= PADDLE_W/2 + R:
```js
const rel = clamp((ball.x - paddleX)/(PADDLE_W/2), -1, 1);
const angle = rel * 1.05; // max ~60deg from vertical
const sp = ballSpeed;
vx = Math.sin(angle)*sp; vy = -Math.cos(angle)*sp;
ball.y = PADDLE_Y - R - 0.5;
combo = 0? reset combo; squash paddle; sound; tiny hearts from paddle? spawn 3 tiny hearts "love tap"? Cute: small heart puff on paddle hit (2-3 tiny hearts). Yes! reinforces theme. Small count to not clutter.
```
Combo resets on paddle/wall? Combo counts brick breaks between paddle touches — reset on paddle hit.
Also prevent multiple paddle triggers: require vy>0 (yes) and after bounce vy<0 so no retrigger.
Ball speed increase: on each brick break speed += 0.04 up to max 9.5. On new level base speed = def.speed + level*0.25.
Tunneling: substeps = Math.ceil(speed/6).
Demo AI:
```js
if(mode==='demo' && (phase==='play'||phase==='serve')){
let target;
if(phase==='serve'){ target = W/2; launch after timer }
else if(ball.vy>0){ target = predictX(); } else { target = W/2 + Math.sin(t*0.5)*80*0.3; }
const dx = target - paddleX;
const maxV = 10.5;
paddleX += clamp(dx, -maxV, maxV);
}
```
predictX: simulate with walls:
```js
function predictX(){
let x = ball.x, y = ball.y, vx = ball.vx, vy = ball.vy;
if(vy<=0) return x;
const targetY = PADDLE_Y - BALL_R;
let t = (targetY - y)/vy;
let px = x + vx*t;
const span = W - 2*BALL_R;
px = ((px - BALL_R) % (2*span) + 2*span) % (2*span);
if(px > span) px = 2*span - px;
return px + BALL_R;
}
```
That's the reflection folding formula. Good. Add error: demo aim offset = sin(time)*8.
Demo serve: auto-launch after 700ms, aim random-ish ±0.4 toward bricks.
User paddle:
- keyboard: held left/right → paddleX += 9*dt each frame.
- mouse: on mousemove over canvas → set target; then paddleX lerp to target at 0.35? Direct set feels best but combine with keyboard: keep `mouseX` target if recent (last 0.5s), else keyboard velocity. Simple: on mousemove set paddleX directly (clamped). Keyboard works via flags. Both fine simultaneously.
Tilt: track paddleVX = paddleX - prevX; tilt = clamp(paddleVX*0.03, ±0.12).
Squash: paddleSquash decays to 0: on hit set 1; scaleY = 1 - squash*0.25, scaleX = 1 + squash*0.18 around bottom center.
Draw order: bg cache → ambient hearts → bricks → particles behind ball? hearts in front mostly. Order: bg, ambient, bricks, scorepopups?, ball trail, ball, paddle, particles (hearts) on top, shockwave rings, HUD, banners.
Shockwave ring: expanding circle stroke, alpha fade, at brick break.
Level name banner at level start: "LEVEL 2 · CHEEZUS" sliding in briefly? Draw for first 1.5s of level: big text center, alpha by time. Nice.
Serve hint: "CLICK OR SPACE TO LAUNCH" pulsing under HUD center? Place above paddle center: at y = PADDLE_Y - 46.
Pause: phase 'paused' (only user mode): dim + "PAUSED — P TO RESUME". Click also resumes.
Game over screen: dim rect rgba(10,4,8,0.72); big "GAME OVER" red-cream; "SCORE 12345"; "BEST" not stored (no storage) — skip best; "CLICK OR PRESS A KEY TO PLAY AGAIN". After 9s auto → demo (overlay back).
Also WIN all levels? Endless cycle, speed up: speedBonus = floor(level/LEVELS.length)*0.4.
Now hearts count: on normal brick 26 hearts, tough 34, plus sparks 8. "tons" satisfied. Cap fine.
Let me also add: brick break also emits 2-3 "chunks" of brick color squares? No plain squares per taste... small rounded bits of brick color are fine (they're debris, not the main particle) — but to be safe, make debris also mini-hearts/round dots. I'll add round dots (circles) of brick color — circles ok.
Font stacks in canvas: `900 20px "Arial Black","Trebuchet MS",sans-serif` for numbers; For the canvas banners: `"Impact","Arial Black",sans-serif`.
Now HUD drawing:
```
score label small: 'SCORE' letter-spaced tiny pink; value big cream.
level center: 'LEVEL 1 — HEARTY'
lives right: mini burgers.
```
letter-spacing in canvas: ctx.letterSpacing supported in Chrome 99+. Use it with fallback (property just ignored elsewhere). Since target is latest Chrome, fine. Use `ctx.letterSpacing = '2px'` guarded in try.
Heart counter next to score? The score could show a small heart icon. Draw small heart before score value.
Colors:
- bg deep: #1d0d14 (plum-charcoal)
- floor: #2b1622 / #150a10 checks
- HUD bg: rgba(14,6,10,0.85) with bottom border rgba(255,90,130,0.28)
- accent pink: #ff4d7d / ketchup #e8375f
- cream: #ffeeda
- brick outline: darken color: implement shade(hex, amt)? Using hsl colors makes outline easy: store {h,s,l} and derive.
I'll store brick colors as hsl components: colorFor returns [h,s,l]. Fill `hsl(h,s%,l%)`, highlight `hsl(h,s%,l+9%)` top strip, outline `hsl(h,s-10?,l-16%)`. For burger fixed colors like cheese orange: express in HSL: cheese h=42,s=100,l=52; tomato h=4,s=72,l=56; lettuce h=95,s=55,l=52; patty h=22,s=36,l=28; bun h=30,s=74,l=62. Easy.
Level colors:
- Heart: hue 336→350? row-based: h = 335 + r*2, s=86, l = 72 - r*3.2.
- Burger rows colors map: [30/74/62, 30/74/62, 30/74/62, 95/55/52, null(gap row), 4/72/56, 42/100/55, 42/100/55, 22/36/28, 30/74/55].
- Diamond: rows 9: hues [330, 15, 45, 160? no candy: 330,20,45,95,190,260,300,340,15]? For diamond rings maybe hue by ring index: rings 0..4: hue 340, 25, 50, 160, 200? Mixed too random? Rainbow diamond is classic arcade. hues: [340, 28, 52, 130, 190, 130, 52, 28, 340] mirrored. That's candy. l=60, s=85.
Tough marks '@': heart lobes row0 all '@'? Wait row0 pattern "..@@@...@@@.." → tough lobes; also make row2 ends? fine just lobes... Hmm tough bricks should be sprinkled for variety. Also add toughs in heart bottom tip: row9 "......@......". And row5 center? Let me finalize heart pattern:
```
"..@@@...@@@..",
".@###@.@###@.",
"#############",
"#############",
".###########.",
"..#########..",
"...#######...",
"....#####....",
".....@@@.....",
"......@......",
```
Row1: ".@###@.@###@." — hmm outline '@' adjacent lobes: visually tough bricks darker? They read as outline of heart: nice!
Check row1: chars: [. @ # # # @ . @ # # # @ .] = 13 ✓ Row8: ".....@@@....." = 5+3+5=13 ✓ Row9: 6+1+6=13 ✓ Row5: 2+9+2=13 ✓ Row6: 3+7+3=13 ✓ Row7: 4+5+4=13 ✓ Row4: 1+11+1=13 ✓
Rows=10.
Burger pattern rows (15):
```
".....#####.....",
"...#########...",
".#############.",
"@@@@@@@@@@@@@@@", // lettuce? tough? make lettuce normal; hmm
"...............",
"###############", // tomato
"###############", // cheese
"..#...#....#...", // cheese drips
"@@@@@@@@@@@@@@@", // patty tough
".#############.", // bottom bun
```
Wait row indices: 0 bun,1 bun,2 bun,3 lettuce,4 gap,5 tomato,6 cheese,7 drips,8 patty,9 bottom bun. Row3 as '@' (tough lettuce? weird). Instead tough = patty row8 '@'. Lettuce normal. Let me set row3 "###############" lettuce. Patty '@'.
Drips row7: "..#...#....#..." = 2+1+3+1+4+1+3=15 ✓ at cols 2,6,11.
Colors row map (15-col burger): r0..2 bun(30,80,64), r3 lettuce(100,55,50), r5 tomato(4,75,55), r6 cheese(42,100,55), r7 cheese(42,100,55), r8 patty(20,40,30) tough, r9 bun(30,74,58).
Level names: 1 "HEARTY", 2 "DOUBLE CHEESE", 3 "SWEET TALK".
Diamond pattern rows(13 wide, 9 rows) with '@' alternating rings:
```
"......#......",
".....@@@.....",
"....#####....",
"...@@@@@@@...",
"..#########..",
"...@@@@@@@...",
"....#####....",
".....###.....",
"......#......",
```
Hmm alternating produces rings of tough: visually distinct rings (darker + marked) — like a target/diamond with ridges. OK. Row lengths: r0: 6+1+6=13 ✓ r1: 5+3+5 ✓ r2: 4+5+4 ✓ r3: 3+7+3 ✓ r4: 2+9+2 ✓ mirrors ✓.
Wait diamond ring coloring: hue by ring: ring = min(r, 8-r) → rings 0..4: hues [340, 20, 48, 95, 190]? mirror. Color fn: `ring=Math.min(r,rows-1-r)` hue=[340,18,45,100,195][ring]? includes green+teal — candy assortment. Fine, "SWEET TALK" candy level.
Level clear speed base: def base speeds [6.2, 6.6, 7.0] + cycle*0.5.
Score: normal 50, tough 120. Hit non-break tough: +20. Combo multiplier: mult = 1+ (combo-1)*0.5? score += base * comboMult where combo counts broken bricks this flight: pts = base * combo. Cap combo 8? Show "×N" when ≥2.
Popup text: "+"+pts.
Now let's think about the "first paint meaningful" and overall beauty once more. Background ambient hearts drawn faint. The bricks colorful. Burger paddle charming. Meatball cute. Should be lovely.
Implementation size is manageable (~700-900 lines). Let me write it carefully, then double-check for errors.
Detailed code plan:
```js
(() => {
'use strict';
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
const overlay = document.getElementById('overlay');
const logoCv = document.getElementById('logo');
const W=960, H=640, HUD_H=56, TOP=HUD_H;
const PADDLE_Y=566, PADDLE_W=156, PADDLE_H=56, BALL_R=9;
let dpr=1;
function resize(){
dpr = Math.min(window.devicePixelRatio||1, 2);
canvas.width = W*dpr; canvas.height = H*dpr;
const s = Math.min(innerWidth/W, innerHeight/H);
canvas.style.width = (W*s)+'px'; canvas.style.height=(H*s)+'px';
ctx.setTransform(dpr,0,0,dpr,0,0);
buildBG();
}
window.addEventListener('resize', resize);
```
Wait: canvas width fixed logical, scaled by CSS — with s possibly >1 (large screens) CSS scales up; combined with dpr backing gives crisp enough. Alternatively let canvas fill more. It's fine.
Hmm, but if innerWidth/innerHeight ratio differs, letterbox shows body bg. Body bg = same deep color + subtle pattern via CSS (repeating-linear-gradient? gradients as pattern — checker via conic-gradient? That's a background technique... taste rule targets decorative gradient banners/buttons; a flat dark body is safest). Body: solid #140a10, canvas draws its own bg. Edge seam visible slightly — acceptable since canvas bg similar tone. Make body exactly canvas bg base color #1d0d14? Then seam invisible. Body #1d0d14. Good.
buildBG: offscreen canvas W*dpr×H*dpr, scale(dpr): fill base; draw checker floor; vignette radial; faint dot grid? draw subtle diagonal stripes on side walls? Keep: base, floor checks, thin top-of-floor accent line, vignette.
Ambient hearts: array 14 items {x,y,s,spd,rot,alpha, hue} updated each frame, drawn faint (globalAlpha .05-.1). They float upward slowly (like bubbles of love) — upward drift feels dreamy. wrap.
State object:
```js
const S = {
mode:'demo', phase:'serve', level:0, score:0, lives:3, combo:0,
speed:6.2, levelTime:0, phaseT:0, overT:0, serveT:0, shake:0, t:0,
bannerT:0, paused:false
};
let paddleX = W/2, paddleVX=0, prevPaddleX=W/2, squash=0, tilt=0;
let mouseX = null;
const keys = {left:false,right:false};
let ball = {x:0,y:0,vx:0,vy:0,stuck:true, blink:0, blinkT: 2+Math.random()*3};
let bricks=[], hearts=[], sparks=[], pops=[], rings=[], trail=[];
```
Wait sparks separate from hearts? unify particles array with type. I'll keep one `parts` array with type 'heart'|'dot'|'spark'.
Level defs:
```js
const LEVELS=[
{name:'HEARTY', speed:6.2, pat:[
"..@@@...@@@..",
".@###@.@###@.",
"#############",
"#############",
".###########.",
"..#########..",
"...#######...",
"....#####....",
".....@@@.....",
"......@......"],
color:(r,rows)=>[335+r*2, 86, 72-r*3]},
{name:'DOUBLE CHEESE', speed:6.6, pat:[
".....#####.....",
"...#########...",
".#############.",
"###############",
"...............",
"###############",
"###############",
"..#...#....#...",
"@@@@@@@@@@@@@@@",
".#############."],
color:(r,rows)=>{const m={0:[28,78,64],1:[28,78,64],2:[28,78,64],3:[100,55,50],5:[4,75,55],6:[43,100,55],7:[43,100,55],8:[20,40,30],9:[28,74,58]};return m[r]||[0,0,0];}},
{name:'SWEET TALK', speed:7.0, pat:[
"......#......",
".....@@@.....",
"....#####....",
"...@@@@@@@...",
"..#########..",
"...@@@@@@@...",
"....#####....",
".....###.....",
"......#......"],
color:(r,rows)=>{const ring=Math.min(r,rows-1-r);const hues=[340,18,45,100,195];return [hues[ring],85,60];}}
];
```
Burger row3 lettuce full width w/ color (100,55,50). Row4 empty.
Wait burger rows: I listed row3 as "###############" lettuce and row4 gap "...............". Order: bun,bun,bun,lettuce,GAP,tomato,cheese,drips,patty,bottom. 10 rows. ok.
buildLevel:
```js
function buildLevel(){
const def = LEVELS[S.level % LEVELS.length];
const pat = def.pat;
const cols = pat[0].length;
const gap=6, mx=26, bh=26, by=86;
const bw=(W - mx*2 - gap*(cols-1))/cols;
bricks=[];
pat.forEach((line,r)=>{ for(let c=0;c<cols;c++){ const ch=line[c]; if(ch==='.')continue;
const [h,s,l]=def.color(r,pat.length,c);
bricks.push({x:mx+c*(bw+gap), y:by+r*(bh+gap), w:bw, h:bh, hp: ch==='@'?2:1, max: ch==='@'?2:1, hsl:[h,s,l], delay:(r*0.06+c*0.012), sp:0, dead:false});
}});
S.speed = def.speed + Math.floor(S.level/LEVELS.length)*0.45;
S.levelTime=0;
}
```
Spawn: brick.sp progress from 0; when levelTime > delay start sp→1 at rate 4/sec with easeOutBack.
Ball reset:
```js
function resetBall(){ ball.stuck=true; ball.x=paddleX; ball.y=PADDLE_Y-BALL_R-1; ball.vx=0; ball.vy=0; trail.length=0; S.serveT=0; }
function launchBall(ang){
const a = ang!==undefined?ang:(Math.random()*0.8-0.4);
ball.stuck=false; ball.vx=Math.sin(a)*S.speed; ball.vy=-Math.cos(a)*S.speed;
}
```
Demo serve: S.serveT accumulates; after 0.7s launch(random small angle). During demo serve, AI centers paddle.
update(dt) (dt in ~frames):
```js
S.t += dt/60;
S.levelTime += dt/60;
if(S.shake>0.01) S.shake*=Math.pow(0.9,dt);
squash decay; tilt calc.
bricks sp: if(sp<1 && levelTime>delay) sp=min(1, sp+dt*0.06);
ambient hearts update.
parts update.
pops update. rings update.
trail update if !stuck.
phase logic:
if(S.mode==='demo'){ demoPaddle(dt); if(S.phase==='serve'){ S.serveT+=dt/60; if(S.serveT>0.7) launchBall((Math.random()*1.0-0.5)); S.phase='play'; } }
else { user paddle: keyboard velocity; if mouseX!=null paddleX=mouseX; clamp. }
if(ball.stuck){ ball.x=paddleX; ball.y=PADDLE_Y-BALL_R-1; } else stepBall(dt);
blink timer.
if(S.phase==='clear'){ S.phaseT+=dt/60; if(S.phaseT>1.8){ S.level++; buildLevel(); resetBall(); S.phase = 'serve'; S.serveT = mode demo? ... } }
if(S.phase==='over'){ S.overT+=dt/60; if(S.overT>9){ toDemo(); } }
```
Careful: after level clear in user mode → phase 'serve' (ball stuck waiting for launch) and show "LAUNCH" hint. In demo → auto launch after 0.7.
stepBall with substeps:
```js
function stepBall(dt){
const sp=Math.hypot(ball.vx,ball.vy);
const steps=Math.max(1, Math.ceil(sp*dt/6));
for(let i=0;i<steps;i++){
ball.x += ball.vx*dt/steps; ball.y += ball.vy*dt/steps;
// walls
if(ball.x<BALL_R){ball.x=BALL_R; ball.vx=Math.abs(ball.vx); sfx wall;}
else if(ball.x>W-BALL_R){...}
if(ball.y<TOP+BALL_R){ball.y=TOP+BALL_R; ball.vy=Math.abs(ball.vy); sfx;}
// paddle
if(ball.vy>0 && ball.y+BALL_R>=PADDLE_Y && ball.y-BALL_R<PADDLE_Y+PADDLE_H && Math.abs(ball.x-paddleX)<=PADDLE_W/2+BALL_R*0.8){ bouncePaddle(); }
// bricks
brickCollide();
// bottom
if(ball.y-BALL_R>H){ ballLost(); return; }
}
}
```
bouncePaddle:
```js
const rel = clamp((ball.x-paddleX)/(PADDLE_W/2), -1, 1);
const ang = rel*1.02;
const sp = Math.min(Math.hypot(vx,vy), 9.6);
ball.vx=Math.sin(ang)*sp; ball.vy=-Math.cos(ang)*sp;
ball.y=PADDLE_Y-BALL_R-0.5;
S.combo=0; squash=1; sfxPaddle();
puff 3 tiny hearts at contact;
```
Note paddle corners: using rectangle test with |dx|<=half+R*0.8, edge hits still reflect via angle formula — fine (angle max 61°, could send slightly sideways-down if rel=±1? sin(1.02)=0.85, vy=-cos=-0.52 upward. Always upward. Good.)
brickCollide:
```js
for(const b of bricks){
if(b.dead||b.sp<1) continue;
const cx=b.x+b.w/2, cy=b.y+b.h/2;
const ox=(b.w/2+BALL_R)-Math.abs(ball.x-cx);
const oy=(b.h/2+BALL_R)-Math.abs(ball.y-cy);
if(ox>0&&oy>0){
if(ox<oy){ ball.vx = ball.x<cx? -Math.abs(ball.vx):Math.abs(ball.vx); ball.x += ball.x<cx? -ox:ox; }
else { ball.vy = ball.y<cy? -Math.abs(ball.vy):Math.abs(ball.vy); ball.y += ball.y<cy? -oy:oy; }
hitBrick(b);
return;
}
}
```
hitBrick:
```js
b.hp--;
if(b.hp<=0){
b.dead=true; S.combo=Math.min(S.combo+1,9);
const base=b.max>1?120:50; const pts=base*S.combo;
S.score+=pts;
const cx=b.x+b.w/2, cy=b.y+b.h/2;
burstHearts(cx,cy, b.max>1?30:24, b.hsl);
burstDots(cx,cy,8,b.hsl);
rings.push({x:cx,y:cy,r:6,life:1});
pops.push({x:cx,y:cy,txt:'+'+pts, life:1, vy:-0.8});
S.shake=Math.min(S.shake+2.2,7);
S.speed=Math.min(S.speed+0.035, 9.6); apply new speed? ball speed increases: scale current velocity:
const cur=Math.hypot(ball.vx,ball.vy); const f=S.speed/cur?
```
Hmm two speeds: S.speed is target scalar; ball velocity should match S.speed. Simplest: keep S.speed as authoritative; after any bounce set magnitude to S.speed? Paddle bounce sets sp=min(current,9.6) — I should always normalize ball speed to S.speed on bounces. Let me store direction only and compute velocity each step as dir*S.speed. Cleaner: keep ball.vx/vy as unit direction? Refactors: use ball.dx,dy normalized, velocity = dir*speed. Collisions: flip dir components. Paddle: compute dir from angle. Steps move x += dx*speed*dt/steps. And speed=S.speed.
I'll refactor to dir-based. top/wall reflect flips dx or dy sign. Paddle sets dir. Good — speed control central.
remaining bricks: if none alive → levelClear(): phase='clear', phaseT=0, big heart shower: spawn 60 hearts from top across screen + banner. sfx arpeggio.
else (brick survived, tough): pts=20; S.score+=20; small spark burst; sfx tick; b.cracked=true (hp<max).
ballLost():
```js
S.shake=8; sfxSad();
if(S.mode==='user'){ S.lives--; if(S.lives<=0){ S.phase='over'; S.overT=0; } else { resetBall(); S.phase='serve'; } }
else { resetBall(); S.phase='serve'; }
```
Also grey hearts falling on loss? spawn few grey hearts drifting down from ball position. Cute: hearts turn grey when you lose a life. Yes! burst hearts with sat=0 grey at ball pos.
Demo AI paddle:
```js
function demoPaddle(dt){
let target;
if(ball.stuck){ target=W/2; S.serveT+=dt/60; if(S.serveT>0.65){ launchBall((Math.random()*0.9-0.45)); } }
else if(ball.dy>0){ target=predictX() + Math.sin(S.t*1.3)*6; }
else { // drift toward ball x mildly
target = ball.x*0.35 + W/2*0.65 + Math.sin(S.t*0.7)*30;
}
const dx=target-paddleX; const v=clamp(dx, -11*dt, 11*dt);
paddleX=clamp(paddleX+v, PADDLE_W/2+6, W-PADDLE_W/2-6);
}
```
launchBall sets phase='play'.
Wait in demo the serve logic lives here; ensure phase transitions consistent: I'll unify: phase 'serve' means ball stuck. In demo update: if phase serve, serveT+=; >0.65 → launch (phase='play'). In user: waits for input.
predictX with dir-based:
```js
function predictX(){
const speed=S.speed; let dy=ball.dy; if(dy<=0.05) return ball.x;
const t=(PADDLE_Y-BALL_R-ball.y)/(dy*speed);
let px=ball.x+ball.dx*speed*t;
const span=W-2*BALL_R;
px=(px-BALL_R)%(2*span); if(px<0)px+=2*span; if(px>span)px=2*span-px;
return px+BALL_R;
}
```
User input:
```js
function userPaddle(dt){
const v=8.5*dt;
if(keys.left) paddleX-=v; if(keys.right) paddleX+=v;
if(mouseX!=null) paddleX=mouseX; // hmm conflicts: mouse sets absolute; keyboard adjusts
```
Conflict: if mouse stationary value set once, keyboard presses then mousemove overrides. Standard approach: on mousemove set mouseX; if keys pressed recently, ignore mouseX? Use: if(keys.left||keys.right){ mouseX=null; } and if mouseX!=null paddleX=mouseX (direct). That's okay: mousemove resumes mouse control.
Hmm but direct paddleX=mouseX causes paddle jumps — fine/expected.
Clamp both.
Input events:
```js
function anyStart(){
if(S.mode==='demo'){ startGame(); return; }
if(S.phase==='over' && S.overT>0.6){ startGame(); return; }
if(S.phase==='serve'){ launchBall(aimAngle()); }
if(S.paused){ togglePause(); }
}
window.addEventListener('pointerdown', e=>{ initAudio(); anyStart(); });
window.addEventListener('keydown', e=>{
initAudio();
const k=e.key;
if(k==='ArrowLeft'||k==='a'||k==='A'){keys.left=true; mouseX=null;}
if(k==='ArrowRight'||k==='d'||k==='D'){keys.right=true; mouseX=null;}
if(k===' '||k==='Enter'){ e.preventDefault(); anyStart(); }
if(k==='p'||k==='P'){ if(S.mode==='user'&&(S.phase==='play'||S.phase==='serve')) togglePause(); }
if(k==='ArrowLeft'||k==='ArrowRight') e.preventDefault();
});
keyup clears.
window.addEventListener('pointermove', e=>{
const r=canvas.getBoundingClientRect();
const x=(e.clientX-r.left)/r.width*W;
if(x>=-40&&x<=W+40){ mouseX=clamp(x, ...); }
});
```
In demo mode pointermove shouldn't matter (AI overrides). anyStart on pointerdown: also in demo clicking starts game — yes desired. But careful: pointerdown in demo also triggers when clicking overlay — overlay pointer-events none, so window gets it. Good.
Also on touch devices pointermove works.
startGame():
```js
function startGame(){
S.mode='user'; S.level=0; S.score=0; S.lives=3; S.combo=0;
buildLevel(); resetBall(); S.phase='serve'; S.paused=false;
overlay.classList.add('hidden');
}
function toDemo(){
S.mode='demo'; S.level=0; S.score=0; S.lives=3;
buildLevel(); resetBall(); S.phase='serve';
overlay.classList.remove('hidden');
}
```
Initial: toDemo().
Pause: S.paused flag; when paused, update skips gameplay but still renders + overlay text "PAUSED". Actually integrate into phase? Use separate flag; update early-return if paused (still update ambient? Freeze all but draw overlay). Simpler: if paused → skip whole update (ambient frozen, acceptable) and draw pause banner.
togglePause flips.
Blur → if user mode and play/serve and !paused → paused=true.
Render:
```js
function render(){
ctx.setTransform(dpr,0,0,dpr,0,0);
ctx.drawImage(bg,0,0,W,H);
// shake
const sx=(Math.random()-0.5)*S.shake, sy=(Math.random()-0.5)*S.shake;
ctx.save(); ctx.translate(sx,sy);
drawAmbient();
drawBricks();
drawTrail();
drawPaddle();
drawBall();
drawParts();
drawRings();
drawPops();
ctx.restore();
drawHUD();
drawBanners(); // level name, clear banner, over screen, serve hint, combo
}
```
drawBricks: for each alive: sp scale ease; translate center, scale, draw rounded rect fill/stroke, highlight top, tough marker (if hp==max==2 draw small darker heart icon? draw a mini heart in center — thematic! tough bricks have heart emblem; cracked state shows cracks) — yes mini heart emblem on tough bricks, lovely.
Rounded rect helper `rr(ctx,x,y,w,h,r)`.
Highlight: rr inset top 4 px height h*0.35 with lighter color alpha .35 — or simple white alpha .25 top strip.
Crack: when hp<max: draw jagged polyline strokes dark alpha.
drawPaddle: tilt rotate around bottom-center; squash scale; shadow ellipse; then layers; sesame seeds; if squash recently — also small mustard drip? enough.
drawBall: trail circles fading (color warm pinkish? meatball trail = motion streak light cream alpha). Meatball: circle fill '#7a4a2f' stroke '#452618' lw 2; inner highlight arc top-left rgba(255,230,200,.35); speck rotation marker: small ellipse '#9c6b45' at angle rotating with travel: rotation angle = atan2 accumulate distance. Eyes: two circles white, pupils dark offset toward dir; blink: if blinkT<0.12 draw lines. Eyes only when? Always (cute in demo too). Position eyes upper half.
Draw order ball above paddle (ball passes over? ball is above paddle in y anyway).
drawParts: hearts via heartPath fill hsla; dot: circle; spark: plus lines.
drawPops: text rising, alpha.
drawHUD: bar bg rgba(10,5,8,.65)? Actually draw translucent dark strip + hairline. Score left with mini heart icon; level center; lives right as mini burgers; combo under center when active.
drawBanners:
- level intro: if S.levelTime<1.6: big text LEVEL N — name, alpha fade in/out, slight scale.
- serve hint (user mode & phase serve & !paused): pulsing text above paddle: "CLICK OR PRESS SPACE TO LAUNCH".
- phase clear: big "LEVEL CLEAR!" + subtitle "the kitchen swoons". hearts shower handled by parts spawned at clear.
- phase over: dim + GAME OVER + score + hint "click or press a key to play again".
- paused: dim + "PAUSED".
- demo label: in demo mode draw small "DEMO — AI AT THE GRILL" bottom-left? The overlay covers this. skip.
Logo canvas: draw once: mini burger + hearts.
```js
function drawLogo(){
const c=logo.getContext('2d'); const d=2; logo.width=200*d; logo.height=110*d; c.scale(d,d);
// hearts around
heartAt(c, 30,26,10, 340); heartAt(c, 170,18,7, 350); heartAt(c, 180,60,5,330);
c.save(); c.translate(100,54); c.scale(0.62,0.62); drawBurger(c, 0,-30, 150, 0); c.restore();
}
```
drawBurger signature: (ctx, cx, topY, w, squash?) uses ctx param. In-game call drawBurger(ctx, paddleX, PADDLE_Y, PADDLE_W, squash). Tilt applied outside.
Mini burger for lives: simplified:
```js
function miniBurger(g,x,y){ // 22x14
g.fillStyle bun '#efa75c': rr top dome small... simpler: three rr: bun, patty, bun.
}
```
heartPath(g,s) as defined; heartAt(g,x,y,s,hue): save translate rotate slight; fill.
Vignette in bg: radial gradient from center transparent to rgba(0,0,0,0.5) at edges — this is atmosphere shading, fine.
Floor: from y=620: checks 20px: for x step 20: fill alt colors rows 620-640 two rows of 10px. Colors #24121b and #170a10 — hmm too subtle? #2b1520/#180b10 slightly visible. Plus top edge line rgba(255,90,130,.35) 2px. ok.
Also side wall hairlines? skip.
Ambient hearts: 14 hearts: {x,y,s,vy(-8..-16 px/s),sway,phase,alpha .04-.09, hue 335-350}. update y -= vy*dt/60; sway x. wrap to bottom.
Alright — the "hearts" particle explosion count: on each brick 24 hearts + 8 dots + ring + pop. With combos at 60fps multiple bricks/frame possible? one per substep; up to ~3 bricks/frame if lined up → ~90 hearts — fine.
Hearts cap: if parts.length>900 parts.splice(0, parts.length-900).
Performance: drawing 900 bezier hearts per frame ok on modern machines; typical counts lower.
Sounds mapping:
- wall: beep(300,0.04,'triangle',0.05)
- paddle: beep(150,0.09,'square',0.09,-40) + beep(90? )
- brickBreak: beep(440*Math.pow(1.06,S.combo),0.08,'square',0.07, +120) and sparkle beep(1180,0.15,'sine',0.035, +200) slight delay? just both.
- toughHit(crack): beep(220,0.05,'square',0.06)
- lost: beep(220,0.5,'sawtooth',0.09,-160)
- clear: arpeggio [523,659,784,1047] step 0.09s sine .07
- launch: beep(500,0.06,'triangle',0.06,+200)
Volume modest.
Now overlays text in canvas banners use Impact stack.
Also add tiny footer hint in-canvas during user play? Not needed.
Let me now also double check "No horizontal scrollbar": body overflow hidden; canvas CSS sized by min scale — fine.
Meta viewport for mobile.
Prevent text selection: user-select none; -webkit-tap-highlight transparent; canvas touch-action none.
Now write drawBurger carefully with concrete numbers. Let w=PADDLE_W=156, l=cx-w/2, top=PADDLE_Y.
Layer stack (y offsets from top):
- bunTop: h=18. dome path:
```js
g.beginPath();
g.moveTo(l+4, y+18);
g.bezierCurveTo(l+4, y+4, l+w*0.16, y, l+w*0.5, y);
g.bezierCurveTo(l+w*0.84, y, l+w-4, y+4, l+w-4, y+18);
g.closePath();
```
fill #efa75c stroke #a4622b lw 2.5 (lineJoin round).
Seeds at fractions: [0.18,0.62],[0.3,0.42],[0.42,0.6]? Compute seed positions on dome: f in [0.15..0.85], y ~ y + domeheight*(1 - profile(f))... approximate: seeds list [[0.2,10],[0.35,5],[0.5,8],[0.65,5],[0.8,10],[0.28,14]? where second is y offset from top of bun (0..18). Dome top edge at center y=0, edges y=18: ySeed(f)=4+16*(|f-0.5|*2)^1.5 approx. Let me hardcode: [[0.16,13],[0.30,7],[0.46,4],[0.62,5],[0.78,9],[0.36,13],[0.66,12]] → position x=l+w*f, y=y+v. ellipse rx3 ry1.8 rotate f*? small random fixed rotations. fill #ffe8bd.
- lettuce: y2=y+18. Shape with scallops:
```js
g.beginPath();
g.moveTo(l-6, y2);
g.lineTo(l+w+6, y2);
// scallops from right to left
for(let x=l+w+6; x>l-6; x-=10){
g.quadraticCurveTo(x-5, y2+9, x-10, y2+2);
}
g.closePath();
```
fill #7ec850 stroke #4e8a28 lw 2.
- tomato: y3=y2+9: rr(l+10, y3, w-20, 8, 4) fill #e5453c stroke #a92c25.
- cheese: y4=y3+8. drips first: rr at [l+w*0.2-6, y4+2, 12, 10, 5], [l+w*0.5-6,...], [l+w*0.8-6,...] fill #ffc02e stroke none? stroke #d9930b lw2 on both drips and bar: draw drips with fill then bar:
drips: fill only; bar: rr(l+4, y4, w-8, 7, 3) fill #ffc02e stroke #d9930b 2. Then redraw drip tops overlapped by bar; drips outline mismatch: draw drips with stroke first, then bar covers top part of drips outline — remaining visible outline sides bottom. Slight artifacts acceptable at small scale. Alternatively outline drips after bar with a path only along bottom curve — complex. Use strokes carefully: drips stroke then fill; then bar fill+stroke: bar's bottom stroke line will cut across drips — visible line. Hmm: draw bar first (fill+stroke), then drips (fill+stroke) — drip stroke overlaps bar bottom: drip appears attached with its own outline crossing bar — looks like separate segment... At 2px and small sizes it's fine cartoon style. Better: draw cheese as one path? Keep simple: bar then drips, drips stroke thin (1.5) same color as bar stroke — looks like dripping cheese segments. Acceptable.
- patty: y5=y4+7: rr(l+2, y5, w-4, 12, 6) fill #5b3a29 stroke #3a2418; texture: 3 darker dots.
- bottom bun: y6=y5+12: rr(l+8, y6, w-16, 14, 7) fill #efa75c stroke #a4622b; shine: rr(l+18,y6+3, w*0.3,3,2) white .2 alpha.
Total height: 18+9+8+7+12+14 = 68. Paddle visual 566..634. Ball collides at 566 top. Fine. Collision rect uses top region: ball reflects at PADDLE_Y regardless — good.
Hmm PADDLE_H for side collisions: paddle only matters from top; side hits: if ball hits side of paddle... ball comes from above mostly; if ball at paddle side while moving down, |dx| test may pass and tele-port bounce — okay breakout standard.
Shadow: ellipse at (cx, 640-6) rx w*0.42 ry 6 rgba(0,0,0,.35).
Squash transform: translate(cx, 566+68) scale(1+q*.15, 1-q*.22) translate back... apply around bottom:
```js
g.save(); g.translate(cx, PADDLE_Y+68); g.rotate(tilt); g.scale(1+q*0.14, 1-q*0.2); g.translate(-cx, -(PADDLE_Y+68));
drawBurger(g, cx, PADDLE_Y, PADDLE_W);
g.restore();
```
Ball eyes: eyeY = ball.y-3; eyes at ball.x±3.5; pupil offset dir: dx norm*1.1. blink handling: blinkT-=dt/60; if <0 reset 2.4+rand*2.6; blink visible when blinkT<0.1: draw line eyes.
Also ball squash on brick hit? minor, skip.
Trail: store {x,y} each frame (unshift, length 10); draw circles r = BALL_R*(1-i/10)*0.8 alpha 0.05*(1-i/10)? subtle.
Popups draw: font '900 16px Arial Black'; color cream; stroke? use shadowless: fillStyle hsl pink; rising.
Ring: stroke arc expanding r += 3*dt? life -= 0.06*dt; alpha life*0.5, lw 2+? color rgba(255,150,170).
Banner text drawing helper:
```js
function bigText(txt, y, size, fill, shadow){
ctx.font = `${size}px Impact, "Arial Black", sans-serif`;
ctx.textAlign='center';
ctx.fillStyle='rgba(0,0,0,.45)'; ctx.fillText(txt, x+off, y+off);
ctx.fillStyle=fill; ctx.fillText(txt,x,y);
}
```
Impact canvas string: `'700 46px Impact, "Arial Black", sans-serif'` fine.
HUD details:
- Score: label 'SCORE' 11px letterSpacing 3px color #ff8fa8 at (24, 24); value 22px cream at (24, 46). Draw small heart icon left of label? Icon at (30,44)? Place heart icon before value: heartAt(ctx, 34, 38, 7, 344) then value text at x 48 left-align.
- Level center: 'LEVEL 2 · DOUBLE CHEESE' centered cream 15px bold letterSpacing 2. maybe two lines: 'LEVEL 2' small pink, name cream 16px.
- Lives right: '∞'? draw mini burgers: for i<lives: miniBurger(W-30-i*34, 34). In demo, show 'DEMO' instead? Overlay shows demo. Draw lives only user mode; demo: show 'ATTRACT MODE'? skip, blank.
Combo: if S.combo>=2 draw center y=80: `COMBO ×${combo}` pink pop scale: size=18+min(combo,6)*2.
Now ambient hearts drawn with alpha ~0.07 — on dark bg gives subtle depth. Not "low-contrast gray areas" — small accents, fine.
Big banner during clear: "LEVEL CLEAR!" + heart shower.
Level intro banner: name.
Serve hint: user+serve: text above paddle blinking alpha sin.
Game over panel: dim full rect; GAME OVER 54px; "FINAL SCORE {score}" 20px; "CLICK OR PRESS A KEY TO PLAY AGAIN" pulsing 14px; small hearts grey falling spawned once.
Pause: dim + PAUSED + 'press P to resume'.
Edge case: user clicks to launch → anyStart called from pointerdown; if phase==='serve' → launch. But if paused (after blur) clicking resumes? anyStart handles paused toggle. If paused via blur, pointerdown resumes — fine, but togglePause via click could annoy if they click during pause intending stay paused... whatever.
Careful: pointerdown fires on any click including during 'over' before 0.6s? guarded.
Also after startGame from pointerdown, the same click shouldn't also launch (anyStart returns after start). Good.
Space during play does nothing (anyStart: serve→launch else if paused toggle else nothing).
Prevent double-launch: anyStart checks phase==='serve'.
Wait: in demo mode anyStart → startGame() (fresh). Good.
Also keydown any key in demo starts game: keys like Shift? "press a key" — start on any keydown except maybe during... fine: any keydown: if demo → startGame. But that includes 'p'? In demo 'p' shouldn't matter; starting game is the response. ok.
Also ensure preventDefault only on space/arrows so F12 etc unaffected.
Mouse aim for launch angle: launch angle from paddle position? random slight; or aim by mouseX offset? Use rel to paddle center clamp: ang = clamp((mouseX - paddleX)/300, -0.5, 0.5) if mouseX else random. Simple: random ±0.35.
Hmm in user mode after life lost, serve: ball stuck; hint text shows.
Now the logo canvas: I need it sized nicely: 210×120 logical? Style width: 180px; height auto via aspect. Draw hearts floating + burger + maybe text? just burger+hearts.
Implementation of heart drawing shared:
```js
function heartPath(g,s){
g.beginPath();
g.moveTo(0, 0.32*s);
g.bezierCurveTo(0.5*s, -0.42*s, 1.05*s, 0.25*s, 0, s);
g.bezierCurveTo(-1.05*s, 0.25*s, -0.5*s, -0.42*s, 0, 0.32*s);
g.closePath();
}
```
Wait direction: first curve from (0,0.32s) to (0,s) with controls (0.5s,-0.42s) and (1.05s,0.25s): control points order: cp1=(0.5s,-0.42s) near start (rises right), cp2=(1.05s, 0.25s) near end. So the lobe rises up-right from the dip, then curves down to bottom tip at (0,s). Yes heart right half. Left half mirrored: from (0,s) cp1=(-1.05s,0.25s) cp2=(-0.5s,-0.42s) end (0,0.32s). Good.
heartFill(g,x,y,s,rotDeg,h,sat,lit,alpha):
```js
g.save(); g.translate(x,y); g.rotate(rot); g.globalAlpha=alpha; g.fillStyle=`hsl(${h} ${sat}% ${lit}%)`; heartPath(g,s); g.fill();
// shine
g.globalAlpha=alpha*0.8; g.fillStyle='rgba(255,255,255,.5)'; g.beginPath(); g.arc(-s*0.32,-s*0.05,s*0.12,0,7); g.fill();
g.restore();
```
HSL with space syntax: `hsl(340 85% 60%)` modern ok in Chrome.
Particles heart uses hue variance around 342 base or brick hue shifted pink? Hearts should read as hearts — keep them pink/red always (theme), not brick color. Use hue 335-355, sat 75-95, light 52-72. Independent of brick color — hearts are hearts. Dots use brick color.
Grey hearts on death: sat 5, light 62.
Level clear shower: spawn 70 hearts across top with downward gentle fall + sway; life long (life decay slow 0.004) so they rain through the 1.8s clear phase and beyond.
Now assemble the ambient, bg, etc. Also don't forget: `ctx.imageSmoothingEnabled` default fine.
One more consideration: the demo must keep going indefinitely; bricks eventually clear → next level; lives infinite. Good.
update(dt) order summary:
```js
function update(dt){
S.t += dt/60;
if(S.shake>0.05) S.shake*=Math.pow(0.86,dt); else S.shake=0;
squash=Math.max(0,squash-0.08*dt);
tilt = clamp((paddleX-prevPaddleX)*0.035, -0.09, 0.09) smoothed: tilt += (targetTilt-tilt)*0.2*dt; prevPaddleX=paddleX... compute after paddle move.
updateAmbient(dt); updateParts(dt); updatePops(dt); updateRings(dt);
if(S.phase==='clear'){ S.phaseT+=dt/60; if(S.phaseT>1.9){ nextLevel(); } return; }
if(S.phase==='over'){ S.overT+=dt/60; if(S.overT>9){toDemo();} return; }
// bricks spawn
S.levelTime+=dt/60;
for(const b of bricks){ if(!b.dead && b.sp<1 && S.levelTime>b.delay) b.sp=Math.min(1,b.sp+dt*0.05); }
// paddle control
if(S.mode==='demo') demoPaddle(dt); else userPaddle(dt);
paddleX=clamp(...);
tilt update.
if(S.phase==='serve'){ ball.x=paddleX; ball.y=PADDLE_Y-BALL_R-1; if(S.mode==='demo'){S.serveT+=dt/60; if(S.serveT>0.65) launchBall();} }
else stepBall(dt);
blink update.
trail push.
}
```
launchBall sets phase='play' and serveT=0.
nextLevel(){ S.level++; buildLevel(); resetBall(); S.phase='serve'; } For demo auto again. Also during 'clear', ball frozen? Ball keeps flying but bricks gone; freeze ball motion visually? During clear, skip ball physics (return early) — ball frozen mid-air, fine, or hide? Keep frozen; hearts rain. OK.
Level intro banner uses S.levelTime which resets in buildLevel — but resetBall in serve; levelTime starts at build. During serve in user mode waiting, banner shows. good.
resetBall called at buildLevel (initial) — also after life lost (levelTime continues so bricks stay spawned — b.sp already 1, fine).
Wait buildLevel sets S.levelTime=0 → re-run spawn animation after life lost? No: life lost doesn't call buildLevel, only nextLevel/startGame. levelTime persists — sp stays 1. Good.
But `resetBall` sets serveT=0 — used only demo. fine.
Check phase transitions in brickCollide → levelClear when none alive:
```js
function levelClear(){ S.phase='clear'; S.phaseT=0; sfxClear(); showerHearts(); S.combo=0; }
```
Also when last brick broken in demo — same flow.
Game constants speeds: dt frame units; ball step uses speed*dt... S.speed=6.2 → 372px/s. Ball crosses field in ~1.5s. good.
Paddle keyboard speed 8.5/frame = 510px/s. reasonable. Demo paddle 11.
Now, the "showerHearts": for i<60: parts.push heart at x=rand*W, y=-20-rand*200, vy=1+rand*2 (downward), vx sway, s=5..12, decay 0.003, hue pink, g small 0.02? Make them flutter down: vy small 0.5-1.5, gravity 0. Update: hearts standard gravity 0.14 would make them fall fast; for shower set g tiny 0.01, vy initial 0.8. They'll drift down slowly. OK give each particle its own g.
Particle update:
```js
p.vy += p.g*dt; p.x += p.vx*dt + Math.sin(p.wob+=p.wv*dt)*p.wa*dt; p.y += p.vy*dt; p.rot += p.vr*dt; p.life -= p.decay*dt;
```
Air drag: p.vx*=Math.pow(0.985,dt); p.vy*=Math.pow(0.99,dt) for hearts flutter? If g positive terminal velocity emerges via drag: vy capped ~ g/(1-0.99)=g*100 too high; use vy*=0.97 → terminal g/0.03=33g... For burst hearts g=0.13 → terminal 4.3 px/frame — ok hearts rain down at moderate speed, fluttering. good.
Alpha: a = clamp(p.life*2,0,1) * (p.a0||1).
Remove when life<=0 or y>H+40.
Burst hearts initial: vx=cos(a)*sp (sp 1.5-5.5), vy=sin(a)*sp-3 (upward bias), g=0.14±, vr ±0.25, s=4-9, decay 0.009±0.006, wob random, wv 0.08, wa 0.4.
Dots: circle r 2-3.5, color brick, life 0.8 decay 0.03, g 0.2, no flutter.
Sparks (tough crack hit): plus-star white-pink: draw two lines crossing rotated; life .5.
Rings update: r += 3.2*dt; life -= 0.045*dt.
Pops: y += vy*dt (vy -0.7), life -= 0.02*dt; alpha life.
OK. Also add score popup color: cream with pink stroke? simple fill cream, small pink heart before? just text.
Combo increments BEFORE computing pts? Break 1st brick combo=1 → pts=base*1; 2nd without paddle: combo=2 → 100/240. good.
Also cap combo display.
Now, one more feature — heart bursts should also slightly "pop" the HUD score? skip.
Sound arpeggio for level clear uses setTimeout with beeps — but if AC null (demo clears level silently) fine.
Also add background music? No — keep.
Now write CSS:
```css
*{margin:0;padding:0;box-sizing:border-box}
html,body{height:100%}
body{background:#1d0d14;overflow:hidden;font-family:"Trebuchet MS",Verdana,sans-serif;
-webkit-user-select:none;user-select:none;touch-action:none}
#stage{position:fixed;inset:0;display:flex;align-items:center;justify-content:center}
canvas#game{display:block;touch-action:none}
#overlay{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;
background:rgba(20,8,14,.52); transition:opacity .45s ease; }
#overlay.hidden{opacity:0;pointer-events:none}
.card{position:relative;text-align:center;padding:24px;max-width:min(92vw,720px)}
#logo{width:170px;height:auto;display:block;margin:0 auto 4px}
h1{font-family:Impact,"Franklin Gothic Bold","Arial Black",sans-serif;font-weight:400;
font-size:clamp(40px,8.5vw,76px);line-height:.95;letter-spacing:.02em;color:#fff3e0;text-transform:uppercase;
text-shadow:0 4px 0 #c2184f, 0 9px 0 rgba(0,0,0,.35)}
h1 em{font-style:normal;color:#ff7b9c}
.eyebrow{...}
.cta{...}
```
Title text: "LOVE AT FIRST<br><em>BITE</em>"? "LOVE AT FIRST BITE" one line at 76px width ~? "LOVE AT FIRST BITE" 18 chars Impact ~0.45em avg → 18*34=612px fits 720. On 375px: clamp min 40px → 18*18=324 fits 345. ok. Use `BITE` in pink em.
Eyebrow above title: "♥ DEMO MODE — THE CHEF IS PLAYING ♥" small pink letterspaced. The heart char in text is an emoji-ish? "♥" is a text glyph (U+2665), not emoji per se, but the rule about emoji as icons... ♥ in decorative eyebrow — borderline. My taste says avoid emoji as decoration; use CSS/SVG heart instead? Inline SVG tiny heart in eyebrow. I can inline an SVG heart — allowed (self-contained). Let me use small inline SVG hearts in eyebrow flanking text. Or skip hearts there — logo already has hearts. Keep eyebrow text plain: "DEMO MODE · THE CHEF IS PLAYING". Good.
CTA pill: `<div class="cta">CLICK OR PRESS A KEY TO START</div>` background #e8375f; hard shadow #7d1033; animation pulse 1.6s. Font: bold 15px letter-spacing 2px.
Hint: "MOUSE OR ← → TO MOVE · SPACE OR CLICK TO LAUNCH · P TO PAUSE" 11px dim letterspace 1.5.
Order in card: logo, eyebrow, h1, cta, hint. Add small spacing.
Also credit line? skip.
Now writing the JS with care about referenced-before-defined (function decls hoisted — fine).
Let me also handle: overlay hidden during user play — but if user never interacts, demo runs forever behind overlay.
One more: "Something meaningful must be visible on first paint" — canvas paints immediately via rAF; the bg + bricks + burger + overlay visible instantly. The first rAF happens <100ms. Screenshot at 3s shows demo w/ hearts.
Also draw once immediately even before first update? rAF loop starts immediately; first frame includes scene. Good.
Potential pitfall: `ctx.setTransform(dpr...)` then `ctx.drawImage(bg,0,0,W,H)` — bg canvas is W*dpr px; drawImage scales down to W,H under transform → maps to full canvas. good.
buildBG:
```js
let bg=null;
function buildBG(){
bg=document.createElement('canvas');
bg.width=W*dpr; bg.height=H*dpr;
const g=bg.getContext('2d'); g.scale(dpr,dpr);
g.fillStyle='#1d0d14'; g.fillRect(0,0,W,H);
// faint dot grid
g.fillStyle='rgba(255,190,205,0.045)';
for(let x=20;x<W;x+=40)for(let y=76;y<600;y+=40){ g.fillRect(x,y,2,2); }
// floor
for(let r=0;r<2;r++)for(let x=0;x<W;x+=24){ g.fillStyle=(((x/24)|0)+r)%2?'#2a1420':'#160a10'; g.fillRect(x,616+r*12,24,12); }
// floor accent line
g.fillStyle='rgba(255,92,132,0.35)'; g.fillRect(0,614,W,2);
// vignette
const v=g.createRadialGradient(W/2,H*0.45,H*0.25,W/2,H*0.55,H*0.85);
v.addColorStop(0,'rgba(0,0,0,0)'); v.addColorStop(1,'rgba(0,0,0,0.42)');
g.fillStyle=v; g.fillRect(0,0,W,H);
}
```
Floor at y616..640 (2 rows of 12). Paddle bottom 634 overlaps floor — visually paddle sits on counter: shadow ellipse at 636? paddle bottom 566+68=634, floor starts 616 — paddle overlaps floor region — looks like standing in front of counter, fine with shadow.
Hmm, wait: dot grid rows to y<600: paddle zone fine.
HUD strip drawn each frame: fillRect(0,0,W,HUD_H) rgba(12,5,9,.7); hairline bottom rgba(255,92,132,.3) 2px at y=HUD_H-1... draw at 55.
Top wall boundary for ball at TOP=HUD_H=56; ball bounces at y=56 — visually at HUD bottom edge. good.
Now bricks start y=86, below HUD, above paddle: heart level bottom row at 86+9*32=374, +26=400. Ball zone 400..566 ~166px. fine.
Serve hint text at y=PADDLE_Y-30=536.
Banner level name at y=430? center of brick zone ~ 240: draw at y=300 big. During serve it's fine.
Let me now also verify the burger-pattern brick count and look: 15 cols bricks width (960-52-6*14)/15 = (960-52-84)/15 = 824/15 = 54.93. bh 26. Rows y: 86..(9*32+86=374+26=400).
Diamond: 13 cols 64.3w; 9 rows → bottom 86+8*32+26 = 368.
Now HUD lives icons: miniBurger at right: width 26: bun dome rr, patty, bun:
```js
function miniBurger(g,x,y){
g.save(); g.translate(x,y); // center-ish
g.fillStyle='#efa75c'; g.strokeStyle='#a4622b'; g.lineWidth=1.5;
rr(g,-11,-8,22,7,3); g.fill(); g.stroke();
g.fillStyle='#7ec850'; rr(g,-12,-2,24,4,2); g.fill();
g.fillStyle='#5b3a29'; rr(g,-11,1,22,5,2.5); g.fill();
g.fillStyle='#efa75c'; rr(g,-10,5,20,6,3); g.fill(); g.stroke();
g.restore();
}
```
rr helper builds path only (caller fills): define `function rr(g,x,y,w,h,r){g.beginPath(); g.moveTo(x+r,y); ...}` standard; callers do g.fill()/g.stroke() after rr? My snippet above calls rr then fill/stroke — rr creates path; but note rr must beginPath. Implement carefully:
```js
function rr(g,x,y,w,h,r){
r=Math.min(r,w/2,h/2);
g.beginPath();
g.moveTo(x+r,y);
g.arcTo(x+w,y,x+w,y+h,r);
g.arcTo(x+w,y+h,x,y+h,r);
g.arcTo(x,y+h,x,y,r);
g.arcTo(x,y,x+w,y,r);
g.closePath();
}
```
HUD heart icon: heartFill(g, 34, 30, 8, 0, 344, 85, 62, 1). Score value at x=48, baseline 44. Label 'SCORE' at 24, y=22, font 10px letterSpacing.
Level center: label 'LEVEL' tiny pink at (W/2, 22), value `1 · HEARTY` cream 16px at (W/2, 44). textAlign center.
Lives: 'LOVES' label tiny at right? draw label 'LOVES' right-aligned at (W-24,22) pink 10px; icons below at y≈36: for i in 0..lives-1 miniBurger(W-34-i*32, 34). In demo, skip icons and draw nothing (or '∞'). Actually in demo draw text 'DEMO' cream 12px at right — communicates. Add.
Combo text at center y=78.
Now, drawBall eyes look direction: dir=(dx,dy) normalized; pupil offset = dir*1.2. When stuck: look up? look at nearest brick: dir up: (0,-1).
Also blink: implement blinkT countdown; when 0<blinkT<0.12 closed.
Meatball rotation speck: keep simple: 3 small darker specks at fixed angles rotating by accumulated distance: `ballSpin += speed*dt*0.02*sign(dx)`. specks at angle ballSpin + i*2.09: pos r*0.55. Draw ellipse small. Eh, simpler: draw arc "sauce glaze" static highlight — skip rotation specks. Keep highlight + eyes. Good — less clutter.
Now, ensure zero console errors:
- ctx.letterSpacing assignments: fine.
- No AudioContext until gesture.
- All DOM ids exist before script runs (script at body end).
- Careful with `overlay` variable naming conflicts none.
- `Math.hypot` fine.
Also mind: pointermove before first pointerdown (mouse hovering in demo) sets mouseX — harmless since demo AI overrides.
In user mode with phase 'serve': if mousemove sets mouseX, paddle moves even before launch — good.
Launch angle in user: `launchBall(clamp((mouseX??paddleX? ) ...)` Let me: aim = mouseX? clamp((mouseX-paddleX)/400,-0.45,0.45) : (Math.random()*0.7-0.35). If mouse at paddle center, straight up. Fine.
Wait anyStart called by pointerdown also used for launch: user clicks → phase serve → launch with aim from click position: nice.
Also keyboard-only users: space launches random ±0.35.
Now let me think about demo AI quality more: paddle speed 11px/frame vs ball horizontal speed up to sin(1.02)*9.6=8.1 at max speed — AI can reach. Prediction with folding is exact. Add error sin*6 — small. AI should basically never lose. Demo will cycle levels forever — great attract mode.
But one subtlety: predictX only accounts ball above paddle; with substeps the ball might bounce off paddle sides; AI good anyway.
Demo serve: after 0.65s auto launch. During 'serve' phase demo paddle target center; then launch.
Initial page load: toDemo() sets level 0 heart; ball stuck 0.65s then launches. Screenshot at 3s: hearts already bursting.
One more: the clear phase in demo: after 1.9s next level; banner "LEVEL CLEAR!" visible. good.
Hearts on paddle hit ("love tap"): 3 small hearts at contact with low velocity — cute detail. Add hue pink, s 3-5, decay 0.03.
Screen flash on level clear? subtle: draw full rect white alpha 0.15 first 0.2s. ok skip.
Now writing the final code. I'll aim for clean, commented (English comments — user wrote English), organized sections.
Also decide textBaseline defaults; set per use.
Let me write out drawBanner functions:
```js
function drawBanners(){
// level intro
if(S.phase!=='over' && S.levelTime<1.7){
const k=S.levelTime;
const a = k<0.25? k/0.25 : (k>1.3? Math.max(0,(1.7-k)/0.4):1);
const scale = 1+ (1-Math.min(1,k*3))*0.2;
ctx.save(); ctx.globalAlpha=a; ctx.translate(W/2, 300); ctx.scale(scale,scale);
ctx.textAlign='center';
ctx.font='700 15px "Trebuchet MS", Verdana, sans-serif'; try{ctx.letterSpacing='6px';}catch(e){}
ctx.fillStyle='#ff8fa8'; ctx.fillText('LEVEL '+(S.level+1), 0, -26);
ctx.font='700 44px Impact, "Arial Black", sans-serif'; try{ctx.letterSpacing='2px';}catch(e){}
ctx.fillStyle='#fff3e0'; ctx.fillText(name,0,14);
shadow: draw dark offset first.
try{ctx.letterSpacing='0px';}catch(e){}
ctx.restore();
}
...
}
```
Shadow: draw text twice: offset (3,3) rgba(0,0,0,.5) then cream.
Level clear banner: if phase==='clear': "LEVEL CLEAR!" big center + "the hearts approve" small.
Serve hint (user): pulsing "CLICK OR PRESS SPACE TO LAUNCH" at y 536 with alpha 0.55+0.35*sin(t*5).
Over:
```js
ctx.fillStyle='rgba(12,4,9,0.78)'; fillRect all.
big 'GAME OVER' at 300; 'FINAL SCORE '+score at 350; pulsing 'CLICK OR PRESS A KEY TO PLAY AGAIN' at 400; small 'the burger forgives you' pink at 430? cute copy.
```
Paused similar.
Demo bottom hint drawn on canvas? overlay has CTA. skip.
Now the overlay copy: eyebrow "DEMO MODE — THE CHEF IS PLAYING", h1 "LOVE AT FIRST BITE", tagline under title? small line: "hamburger paddle · heart-explosion bricks" pink. then CTA, hint. Good.
Also add `aria-hidden` not needed.
Let me make sure overlay content is legible but demo visible: overlay bg rgba(16,6,11,0.45).
Write the heart logo on logo canvas:
```js
const logo=document.getElementById('logo');
function drawLogo(){
const d=2, w=210,h=120;
logo.width=w*d; logo.height=h*d;
const g=logo.getContext('2d'); g.scale(d,d);
heartFill(g, 26, 30, 11, -0.25, 342, 88, 62, .95);
heartFill(g, 186, 22, 8, 0.2, 350, 90, 68, .95);
heartFill(g, 196, 62, 5, -0.3, 335, 85, 55, .9);
heartFill(g, 12, 74, 6, 0.35, 350, 80, 70, .85);
g.save(); g.translate(105,110); g.scale(0.6,0.6); g.translate(0,-0); drawBurgerAt(g, 0, -68+? );
```
drawBurger expects (g,cx,topY,w): call drawBurger(g, 0, -68, 156)? burger height 68; if translate origin at bottom of burger: drawBurger(g,0,-68,156) draws from y=-68..0; with scale 0.6 → height 41; canvas h=120: place bottom at y=112: translate(105,112) scale .6 → burger occupies 112-41=71..112. Hearts above. Also slight rotate -0.06 for charm.
heartFill signature: (g,x,y,s,rot,h,sat,li,a).
drawBurger(g,cx,y,w) uses squash param optional default 0 (and tilt outside). In-game: wrapper applies transforms then calls drawBurger(ctx, paddleX, PADDLE_Y, PADDLE_W).
One catch: drawBurger draws shadow? Keep shadow separate in game render (not in function).
Now HUD font: `700 16px "Trebuchet MS"...` etc.
Also: `ctx.letterSpacing` persists — reset after use ('0px').
Edge: `Impact` in canvas font string with quotes? Use `'44px Impact, "Arial Black", sans-serif'` — font-family parsing in canvas accepts it. yes.
Now, particles from brick: heart base hue from brick hue? I said keep hearts pink. Use hue = 338+rand*18.
Write burstHearts:
```js
function burstHearts(x,y,n,grey){
for(let i=0;i<n;i++){
const a=Math.random()*Math.PI*2, sp=1.2+Math.random()*4.6;
parts.push({t:'h', x,y, vx:Math.cos(a)*sp, vy:Math.sin(a)*sp-3.1,
g:0.11+Math.random()*0.09, rot:(Math.random()-0.5)*1.6, vr:(Math.random()-0.5)*0.28,
s:3.6+Math.random()*6.4, life:1, dec:0.008+Math.random()*0.009,
hue: grey?0:334+Math.random()*22, sat: grey?6:78+Math.random()*18, lig: grey?60:50+Math.random()*22,
wob:Math.random()*6.3, wv:0.05+Math.random()*0.06, wa:0.25+Math.random()*0.5});
}
}
function burstDots(x,y,n,hsl){
for(let i=0;i<n;i++){ const a=Math.random()*Math.PI*2, sp=1+Math.random()*4.5;
parts.push({t:'d',x,y,vx:Math.cos(a)*sp, vy:Math.sin(a)*sp-2, g:0.22, rot:0,vr:0, s:1.6+Math.random()*2.2, life:1, dec:0.02+Math.random()*0.02, hue:hsl[0],sat:hsl[1],lig:hsl[2], wob:0,wv:0,wa:0});
}
}
function sparks(x,y,n){ ... t:'s' white plus }
function shower(){ for(let i=0;i<70;i++){ parts.push({t:'h', x:Math.random()*W, y:-30-Math.random()*260, vx:(Math.random()-0.5)*0.6, vy:0.4+Math.random()*1.2, g:0.008, rot:Math.random()*6, vr:(Math.random()-0.5)*0.2, s:4+Math.random()*8, life:1, dec:0.0035, hue:334+Math.random()*24, sat:85, lig:55+Math.random()*18, wob:Math.random()*6.3, wv:0.03, wa:0.6}); } }
```
updateParts:
```js
for(let i=parts.length-1;i>=0;i--){const p=parts[i];
p.vy+=p.g*dt; p.vx*=Math.pow(0.99,dt); if(p.t==='h'){p.vy*=Math.pow(0.975,dt);}
p.wob+=p.wv*dt; p.x+=p.vx*dt+Math.sin(p.wob)*p.wa*dt; p.y+=p.vy*dt; p.rot+=p.vr*dt; p.life-=p.dec*dt;
if(p.life<=0||p.y>H+50){parts.splice(i,1);}
}
if(parts.length>950) parts.splice(0,parts.length-950);
```
drawParts: iterate; alpha=Math.min(1,p.life*2.2);
- 'h': heartFill(ctx,p.x,p.y,p.s,p.rot,p.hue,p.sat,p.lig,alpha) — plus scale pop at birth: scale factor = p.life>0.85? 1+(1-(1-(p.life-0.85)/0.15))? overkill; keep simple.
- 'd': fillStyle hsl, circle r=s*alpha?
- 's': plus sign lines white alpha.
Heart shine inside heartFill adds white dot — for hundreds of hearts fine.
Rings: {x,y,r,life}: draw stroke rgba(255,120,150, life*0.5), lw 2.5, arc r. update r+=3.4*dt? plus initial fast: r grows; life-=0.05*dt.
Pops: fill cream text 15px bold, stroke none, at p.x,p.y alpha life.
Now assemble userPaddle:
```js
function userPaddle(dt){
if(keys.left){paddleX-=8.6*dt; mouseX=null;}
if(keys.right){paddleX+=8.6*dt; mouseX=null;}
if(mouseX!=null) paddleX=mouseX;
}
```
clamp after in update: paddleX=clamp(paddleX, PADDLE_W/2+8, W-PADDLE_W/2-8).
tilt: measure movement: `const mv=paddleX-prevPX; prevPX=paddleX; tilt+= ((clamp(mv*0.028,-0.1,0.1))-tilt)*Math.min(1,0.25*dt);`
Alright, also on brick break in user's last life etc no issues.
Level names array index: `LEVELS[S.level % LEVELS.length].name`.
HUD level text: `LEVEL ${S.level+1} · ${name}`.
Score formatting: `String(S.score).padStart(6,'0')`.
Demo HUD right: 'DEMO' text.
Alright, potential issue: In demo mode, overlay covers screen; canvas banner 'level intro' behind overlay — fine, visible through 45% tint.
Now think again about taste check: dark plum bg + candy colors + cream + ketchup accents; no gradients except vignette/ball shading (atmospheric, acceptable); no emoji (using drawn hearts); fonts Impact/Trebuchet (not on banned list); no alerts; interactive everything wired; signature moments: heart-shaped level layouts, burger paddle, meatball with eyes, heart explosions.
Mobile 375px: canvas scaled ~0.39 → tiny but playable; overlay text clamp handles. Fine.
Now write final HTML. Also must double check every function name & variable defined. Let me write the code fully and mentally lint.
Also remember: no `alert`. ok.
Potential subtle bug: `S.phase==='serve'` and demo: demoPaddle handles serve timer & launch. In update I wrote paddle control before stuck-ball follow; demoPaddle sets paddleX; then `if(S.phase==='serve')` follow ball. But launchBall changes phase to 'play' inside demoPaddle — then stuck branch skipped, stepBall runs same frame — fine.
Also make sure `resetBall` sets phase='serve'? It doesn't change phase; callers set. I'll have resetBall set S.phase='serve' and S.serveT=0 to reduce mistakes.
startGame: buildLevel(); resetBall(); (phase serve).
nextLevel: buildLevel(); resetBall().
ballLost user: lives--; if<=0: S.phase='over'; S.overT=0; burst grey hearts at ball pos; else resetBall() (phase serve).
Wait — during 'over' phase the ball is below screen; render ball anyway (offscreen). fine.
toDemo resets: buildLevel, resetBall.
Also when 'over' → auto toDemo after 9s: overlay reappears.
Blur pause: window 'blur' event → if(S.mode==='user' && (S.phase==='play') && !S.paused) S.paused=true. Also serve phase? pause too. If paused during serve, hint hidden behind pause overlay; fine.
Update loop must respect paused:
```js
function frame(ts){
const dt=Math.min(2.5,(ts-last)/16.667); last=ts;
if(!S.paused) update(dt); else S.t+=dt/60; // keep ambient time for pulse
render();
requestAnimationFrame(frame);
}
```
Hmm ambient hearts freeze on pause — acceptable.
First `last` init: `let last=performance.now()`; rAF frame ts param.
Also render uses S.t for pulses.
Check stepBall returns after ballLost to avoid further substeps.
brickCollide: skip if S.phase!=='play'? Only called from stepBall (play). ok.
levelClear check inside hitBrick after dead set: count remaining alive: maintain `aliveCount` var — compute: `if(!bricks.some(b=>!b.dead)) levelClear()`. some over ~120 bricks per break — fine.
Sound functions with guards.
Now, `heartFill` restore alpha etc — always save/restore.
drawAmbient:
```js
function updateAmbient(dt){ for(const a of amb){ a.y-=a.v*dt; a.ph+=0.01*dt; a.x+=Math.sin(a.ph)*0.15*dt; if(a.y<-30){a.y=H+30; a.x=Math.random()*W;} } }
function drawAmbient(){ for(const a of amb){ heartFill(ctx,a.x,a.y,a.s,Math.sin(a.ph*0.5)*0.4,a.hue,a.sat,a.lig,a.al); } }
```
amb init 12: v 0.15-0.4, s 8-22, al 0.05-0.1, hue 335-355, sat 60, lig 45.
drawing heart fill alpha low.
Now logo: after functions defined call drawLogo().
Also handle `document.fonts`? no.
Let me also confirm no leftover `const canvas` conflicts.
Writing sizes: overlay CTA pulse via CSS keyframes:
```css
@keyframes pulse{0%,100%{transform:translateY(0)}50%{transform:translateY(-4px)}}
.cta{display:inline-block;background:#e8375f;color:#fff3e0;font-weight:700;font-size:15px;letter-spacing:2.5px;padding:14px 30px;border-radius:999px;box-shadow:0 5px 0 #7d1033;animation:pulse 1.5s ease-in-out infinite;cursor:pointer;margin-top:22px}
```
h1 margin.
Hint: `.hint{margin-top:16px;font-size:11px;letter-spacing:1.6px;color:rgba(255,220,228,.55)}`.
Eyebrow: `.eyebrow{font-size:12px;letter-spacing:4px;color:#ff8fa8;font-weight:700;margin-bottom:6px}`.
Tagline: `.tag{margin-top:8px;color:rgba(255,214,224,.8);font-size:13px;letter-spacing:1px}`.
Card has no bg (overlay tints). To improve readability add subtle card backdrop? Keep text shadows; overlay tint enough.
I think we're ready to write. Length ~ 850 lines. Let me be meticulous with syntax.
One more check on the heart pattern char rows I listed (13 chars each):
1. "..@@@...@@@.." → 2+3+3+3+2=13 ✓
2. ".@###@.@###@." → 1+1+3+1+1+1+3+1+1=13 ✓
3. "#############" 13 ✓
4. "#############" ✓
5. ".###########." 1+11+1 ✓
6. "..#########.." ✓
7. "...#######..." ✓
8. "....#####...." ✓
9. ".....@@@....." 5+3+5 ✓
10. "......@......" ✓
Burger (15):
1. ".....#####....." 5+5+5 ✓
2. "...#########..." 3+9+3 ✓
3. ".#############." 1+13+1 ✓
4. "###############" 15 ✓
5. "..............." ✓
6. "###############" ✓
7. "###############" ✓
8. "..#...#....#..." 2+1+3+1+4+1+3=15 ✓
9. "@@@@@@@@@@@@@@@" 15 ✓
10. ".#############." ✓
Diamond (13):
"......#......" 6+1+6 ✓
".....@@@....." 5+3+5 ✓
"....#####...." ✓
"...@@@@@@@..." ✓
"..#########.." ✓
"...@@@@@@@..." ✓
"....#####...." ✓
".....###....." ✓
"......#......" ✓
Colors burger map keys: 0,1,2 bun;3 lettuce;5 tomato;6 cheese;7 cheese drip;8 patty;9 bun. Row 4 gap → none.
Diamond color: ring=min(r, 8-r): rings 0..4: hues [340, 18, 46, 100, 196]; sat 85, lig 62; tough rings appear darker via tough marker.
Tough brick drawing: after fill/stroke/highlight, if b.max>1 && b.hp===b.max: draw small heart emblem center: heartFill(ctx, cx, cy-? , 6, 0, hue=348? white-ish? emblem color: hsl(same hue, s, l+25)? Let's draw emblem as darker outline heart: fill 'rgba(0,0,0,0.25)'. Draw heart of size 7 at brick center with alpha .28. When cracked (hp<max): draw cracks: 2-3 line strokes 'rgba(40,10,20,0.55)' lw 2: from center to edges zigzag. Precompute crack path per brick? deterministic from brick position: use b.x seed. Just draw fixed crack shape: moveTo(cx-6,cy-4) lineTo(cx-1,cy) lineTo(cx-4,cy+6); second: moveTo(cx+5,cy-6) lineTo(cx+1,cy-1) lineTo(cx+6,cy+4). ok.
Brick highlight strip: rr(b.x+3,b.y+3,b.w-6,5,2.5) fill rgba(255,255,255,.22)? On light candy colors fine.
Brick stroke: hsl darker: `hsl(h, s%, max(l-18,8)%)` lw 2.
Fill: hsl(h,s%,l%). If tough & full hp: maybe slightly deeper: fine.
Draw bricks with spawn scale: sp eased: e = easeOutBack(sp):
```js
function easeOutBack(x){const c1=1.70158,c3=c1+1;return 1+c3*Math.pow(x-1,3)+c1*Math.pow(x-1,2);}
```
translate(cx,cy) scale(e) translate(-cx,-cy). e may be <1 early (~0). When sp=0 skip drawing (e≈0). ok.
Ball trail: push every frame in play: trail.unshift({x,y}); trail.length=Math.min(trail.length,9). draw i from oldest: alpha (1-i/len)*0.12, r=BALL_R*(1-i/len)*0.85, color '#ffb3c6'? meatball trail pinkish? Meatball brown; trail cream '#ffd9a8' subtle. Hearts pinky trail? Trail as pink hearts would be excessive. Simple circles subtle.
Hmm: hearts everywhere might reduce clarity; small subtle trail is fine.
drawBall:
```js
ctx.save();
// body
const grd? flat: fillStyle '#7a4a2f'; arc r; fill; stroke '#40241a' lw2.
// highlight
arc(ball.x-3, ball.y-3.5, 3.2) fill rgba(255,225,190,0.35)
// glaze arc: stroke arc around upper: skip
// eyes
const ex=ball.x, ey=ball.y-2.5;
white circles r2.4 at ±3.4; pupils r1.15 offset dir*1.1;
blink closed: lines lw2 '#3a2016' horizontal.
mouth tiny 'o'? add small arc smile below? meatball with just eyes is cute. Add tiny mouth: arc(ball.x, ball.y+3, 1.6, 0.15π..0.85π) stroke '#40241a' — a subtle smile. yes cute.
ctx.restore();
```
Eyes look dir: if stuck look up (0,-1) else normalized (vx,vy).
dir-based velocity: ball.dx, ball.dy.
Redefine ball step with dx,dy:
```js
ball.x += ball.dx*S.speed*dt/steps; ball.y += ball.dy*S.speed*dt/steps;
```
walls flip dx/dy. paddle sets (dx,dy)= (sin,−cos). launch similar. brick reflect flips. speed via S.speed.
predictX uses dx*speed.
Also initial S.speed set by buildLevel; launch uses S.speed. Brick break S.speed+=0.035 capped 9.6. Paddle bounce: also slight speedup? S.speed+=0.008? skip.
Ball radius collisions unchanged.
Now hits: hitBrick gets b; compute pts.
sfx:
```js
function sfx(fn){ if(AC && fn) fn(); }
const snd={
wall(){beep(340,0.05,'triangle',0.05);},
paddle(){beep(140,0.1,'square',0.09,-30);beep(240,0.06,'triangle',0.05);},
brick(){beep(420+S.combo*45,0.09,'square',0.06,140);beep(1150,0.14,'sine',0.03,250);},
crack(){beep(230,0.05,'square',0.06,-40);},
lost(){beep(260,0.5,'sawtooth',0.08,-190);},
clear(){[523,659,784,1046].forEach((f,i)=>setTimeout(()=>beep(f,0.18,'triangle',0.07),i*90));},
launch(){beep(480,0.08,'triangle',0.06,220);}
};
```
Guard each: beep checks AC.
setTimeout allowed.
initAudio on pointerdown/keydown once.
Also demo shouldn't beep: guard: only call snd if mode user? After game over auto-demo... AC exists but demo beeps would be odd — silent demo: wrap calls: `if(S.mode==='user') snd.brick()`. I'll add helper `play(name){ if(S.mode==='user') snd[name](); }`.
Now compose everything. Also remember overlay hidden class transition; when returning toDemo remove class.
Let me also double-check `resize` before first render: call resize() at init (sets canvas + bg). buildBG uses dpr.
Also on resize mid-game: canvas re-init clears ctx state — render loop re-sets transform each frame via setTransform at render start. bg rebuilt. fine.
iOS pointer events: use pointermove with `getBoundingClientRect` each event — fine.
Now write the final file. I'll be careful about template literal usage inside hsl strings and quotes.
Also small detail: `canvas.style.width` set in resize; stage flex centers.
Also add `image-rendering` nothing.
Let me write out drawBurger fully with numbers (w=156 default but parameterized; logo uses 156 scaled):
```js
function drawBurger(g,cx,y,w){
const l=cx-w/2;
g.lineJoin='round';
// top bun
g.beginPath();
g.moveTo(l+5, y+19);
g.bezierCurveTo(l+5, y+5, l+w*0.17, y, l+w*0.5, y);
g.bezierCurveTo(l+w*0.83, y, l+w-5, y+5, l+w-5, y+19);
g.closePath();
g.fillStyle='#f2a95e'; g.fill();
g.strokeStyle='#a4622b'; g.lineWidth=2.5; g.stroke();
// seeds
g.fillStyle='#ffe9c2';
const seeds=[[0.18,12],[0.30,7],[0.42,4],[0.58,4],[0.70,7],[0.82,12],[0.26,15],[0.74,15],[0.5,9]];
for(const sd of seeds){
g.save(); g.translate(l+w*sd[0], y+sd[1]); g.rotate((sd[0]-0.5)*0.9);
g.beginPath(); g.ellipse(0,0,2.8,1.7,0,0,Math.PI*2); g.fill(); g.restore();
}
// lettuce
const y2=y+19;
g.beginPath();
g.moveTo(l-7, y2+2);
g.lineTo(l+w+7, y2+2);
for(let x=l+w+7; x>l-7; x-=11){
g.quadraticCurveTo(x-5.5, y2+11, x-11, y2+2);
}
g.closePath();
g.fillStyle='#7ec850'; g.fill();
g.strokeStyle='#4e8a28'; g.lineWidth=2; g.stroke();
// tomato
const y3=y2+9;
rr(g,l+11,y3,w-22,8,4); g.fillStyle='#e5453c'; g.fill();
g.strokeStyle='#a92c25'; g.lineWidth=2; g.stroke();
// cheese bar + drips
const y4=y3+8;
rr(g,l+5,y4,w-10,7,3); g.fillStyle='#ffc22e'; g.fill();
g.strokeStyle='#d9930b'; g.lineWidth=1.8; g.stroke();
for(const f of [0.22,0.52,0.8]){
const dx=l+w*f;
rr(g,dx-6,y4+2,12,9,4.5); g.fillStyle='#ffc22e'; g.fill(); g.strokeStyle='#d9930b'; g.stroke();
}
// patty
const y5=y4+7;
rr(g,l+3,y5,w-6,12,6); g.fillStyle='#5b3a29'; g.fill();
g.strokeStyle='#37221a'; g.lineWidth=2; g.stroke();
g.fillStyle='rgba(0,0,0,0.25)';
for(const f of [0.2,0.42,0.63,0.82]){ g.beginPath(); g.arc(l+w*f, y5+6, 1.7,0,Math.PI*2); g.fill(); }
// bottom bun
const y6=y5+12;
rr(g,l+9,y6,w-18,13,6.5); g.fillStyle='#f2a95e'; g.fill();
g.strokeStyle='#a4622b'; g.lineWidth=2.5; g.stroke();
g.fillStyle='rgba(255,255,255,0.25)';
rr(g,l+20,y6+3,w*0.28,3,1.5); g.fill();
}
```
Height: 19+9+8+7+12+13=68 ✓.
Wait lettuce scallops: from x=l+w+7 going left step 11: number of scallops = (w+14)/11 ≈ 15.4 — not integer, leftover partial: loop while x>l-7, final partial scallop to l-7 might overshoot: last iteration x may be < l-7 already after decrement? Condition x>l-7: start x=l+w+7=163+... For w=156: l+w+7 = l+163; steps 11: 163,152,...,? l-7 boundary: relative distance 170 → 15.45 steps: 15 scallops of 11 = 165 <170 → remainder 5px: loop ends when x=l+? x goes 163→152→...→-2? Sequence x values: 163,152,141,...,x_last where x_last>l-7: x=163-11k. l-7 = -7 relative... set absolute with l: x from l+163 down; condition x>l-7. 163-11k > -7 → 11k<170 → k≤15: at k=15, x=l-2, quad to x-11=l-13 overshoots beyond moveTo start (l-7). closePath handles: lineTo implicit back? After quad end at (l-13, y2+2), closePath draws line back to start (l-7, y2+2) — small backstep artifact: a tiny horizontal backtrack at left edge — barely noticeable 6px. To avoid, compute step = (w+14)/n with n=round((w+14)/11): step=170/15=11.33; loop i in 0..n-1: x=l+w+7-i*step; quad to x-step. Clean. Let me implement that.
```js
const x0=l-7, x1=l+w+7, span=x1-x0, n=Math.round(span/11), st=span/n;
g.moveTo(x0,y2+2); g.lineTo(x1,y2+2);
for(let i=0;i<n;i++){ const xs=x1-i*st; g.quadraticCurveTo(xs-st/2, y2+11, xs-st, y2+2); }
g.closePath();
```
Now stepBall details:
```js
function stepBall(dt){
const steps=Math.max(1, Math.ceil((S.speed*dt)/6));
for(let i=0;i<steps;i++){
ball.x+=ball.dx*S.speed*dt/steps;
ball.y+=ball.dy*S.speed*dt/steps;
if(ball.x<BALL_R){ball.x=BALL_R; if(ball.dx<0){ball.dx=-ball.dx; play('wall');}}
else if(ball.x>W-BALL_R){ball.x=W-BALL_R; if(ball.dx>0){ball.dx=-ball.dx; play('wall');}}
if(ball.y<TOP+BALL_R){ball.y=TOP+BALL_R; if(ball.dy<0){ball.dy=-ball.dy; play('wall');}}
if(ball.dy>0 && ball.y+BALL_R>=PADDLE_Y && ball.y-BALL_R<PADDLE_Y+40 && Math.abs(ball.x-paddleX)<=PADDLE_W/2+BALL_R){
bouncePaddle();
}
brickCollide();
if(ball.y-BALL_R>H+6){ ballLost(); return; }
}
// trail
trail.unshift({x:ball.x,y:ball.y}); if(trail.length>9)trail.pop();
}
```
bouncePaddle:
```js
function bouncePaddle(){
const rel=Math.max(-1,Math.min(1,(ball.x-paddleX)/(PADDLE_W/2)));
const ang=rel*1.02;
ball.dx=Math.sin(ang); ball.dy=-Math.cos(ang);
ball.y=PADDLE_Y-BALL_R-0.5;
S.combo=0; squash=1; play('paddle');
// love tap hearts
for(let k=0;k<3;k++){
parts.push({t:'h',x:ball.x+(Math.random()-0.5)*24,y:PADDLE_Y-4,vx:(Math.random()-0.5)*1.4,vy:-(0.8+Math.random()*1.4),g:0.06,rot:(Math.random()-0.5),vr:0.1,s:2.6+Math.random()*2.4,life:1,dec:0.03,hue:340+Math.random()*16,sat:88,lig:66,wob:Math.random()*6,wv:0.08,wa:0.3});
}
}
```
brickCollide as planned with hitBrick(b).
hitBrick:
```js
function hitBrick(b){
b.hp--;
const cx=b.x+b.w/2, cy=b.y+b.h/2;
if(b.hp<=0){
b.dead=true;
S.combo=Math.min(S.combo+1,9);
const pts=(b.max>1?120:50)*S.combo;
S.score+=pts;
burstHearts(cx,cy,b.max>1?32:24,false);
burstDots(cx,cy,7,b.hsl);
rings.push({x:cx,y:cy,r:4,life:1});
pops.push({x:cx,y:cy-6,txt:'+'+pts,life:1});
S.shake=Math.min(S.shake+2.4,7);
S.speed=Math.min(S.speed+0.03,9.6);
play('brick');
if(!bricks.some(bb=>!bb.dead)) levelClear();
} else {
S.score+=20; pops.push({x:cx,y:cy-6,txt:'+20',life:1});
sparks(cx,cy,6); play('crack'); S.shake=Math.min(S.shake+1,7);
}
}
```
levelClear:
```js
function levelClear(){
S.phase='clear'; S.phaseT=0; shower(); play('clear');
}
```
update clear branch earlier handles next.
nextLevel:
```js
function nextLevel(){ S.level++; S.combo=0; buildLevel(); resetBall(); }
```
ballLost:
```js
function ballLost(){
S.shake=9;
burstHearts(ball.x, Math.min(ball.y,H-20), 18, true);
if(S.mode==='user'){
S.lives--;
if(S.lives<=0){ S.phase='over'; S.overT=0; play('lost'); }
else { play('lost'); resetBall(); }
} else { resetBall(); }
}
```
resetBall:
```js
function resetBall(){ ball.dx=0; ball.dy=-1; ball.stuck=true; ball.x=paddleX; ball.y=PADDLE_Y-BALL_R-1; trail.length=0; S.phase='serve'; S.serveT=0; }
```
Hmm resetBall sets phase serve — but called from nextLevel after buildLevel — fine; also from startGame. But careful: resetBall during 'clear'? no.
Wait: in demo, ballLost → resetBall → phase serve → demoPaddle relaunch after 0.65s. good.
launchBall:
```js
function launchBall(ang){
const a = ang!==undefined?ang:(Math.random()*0.7-0.35);
ball.stuck=false; ball.dx=Math.sin(a); ball.dy=-Math.cos(a);
ball.x=paddleX; ball.y=PADDLE_Y-BALL_R-1;
S.phase='play'; play('launch');
}
```
update serve branch also positions ball at paddle each frame (stuck).
anyStart:
```js
function anyStart(){
if(S.mode==='demo'){ startGame(); return; }
if(S.paused){ S.paused=false; return; }
if(S.phase==='over'){ if(S.overT>0.5) startGame(); return; }
if(S.phase==='serve'){
let a;
if(mouseX!=null){ a=Math.max(-0.5,Math.min(0.5,(mouseX-paddleX)/380)); }
else a=Math.random()*0.7-0.35;
launchBall(a);
}
}
```
Keydown: also handle Escape? no.
Pause toggling 'p':
```js
if(k==='p'||k==='P'){ if(S.mode==='user'&&S.phase!=='over'&&S.phase!=='clear'){ S.paused=!S.paused; } }
```
Blur: `window.addEventListener('blur',()=>{ if(S.mode==='user'&&!S.paused&&(S.phase==='play'||S.phase==='serve')) S.paused=true; });`
Render banners check S.paused overlay.
Also while paused, pointerdown → anyStart: mode user, paused → unpause. But clicking to unpause might also... fine.
Edge: user pauses during serve, clicks → unpauses (not launch) — acceptable.
Now HUD & banners & overlay drawing code. Also "over" shows even while mode user; after 9s → toDemo (mode demo) → overlay DOM appears. good.
Check demo level banner name uses LEVELS[..].name.
Let me now also think about the demo AI paddle speed being framerate scaled: v=clamp(dx, -11*dt, 11*dt) — with dt~1: 11px/frame. good.
userPaddle keyboard 8.6*dt.
paddleX clamp: half= PADDLE_W/2: clamp(paddleX, half+6, W-half-6).
Everything else standard. Also initial `prevPX` init.
For crisp text, avoid fractional font sizes fine.
letterSpacing usage: set and reset around HUD text.
Let me write drawHUD:
```js
function drawHUD(){
ctx.fillStyle='rgba(12,5,9,0.72)'; ctx.fillRect(0,0,W,HUD_H);
ctx.fillStyle='rgba(255,92,132,0.35)'; ctx.fillRect(0,HUD_H-2,W,2);
ctx.textBaseline='alphabetic';
// score
try{ctx.letterSpacing='3px';}catch(e){}
ctx.font='700 10px "Trebuchet MS", Verdana, sans-serif';
ctx.fillStyle='#ff8fa8'; ctx.textAlign='left';
ctx.fillText('SCORE',24,22);
try{ctx.letterSpacing='0px';}catch(e){}
heartFill(ctx,33,40,8,0,342,88,60,1);
ctx.font='900 21px "Arial Black","Trebuchet MS",sans-serif';
ctx.fillStyle='#fff3e0'; ctx.fillText(String(S.score).padStart(6,'0'),48,47);
// level center
ctx.textAlign='center';
try{ctx.letterSpacing='3px';}catch(e){}
ctx.font='700 10px "Trebuchet MS", Verdana, sans-serif';
ctx.fillStyle='#ff8fa8'; ctx.fillText('LEVEL',W/2,20);
try{ctx.letterSpacing='1px';}catch(e){}
ctx.font='700 16px "Trebuchet MS", Verdana, sans-serif';
ctx.fillStyle='#fff3e0';
ctx.fillText((S.level+1)+' · '+LEVELS[S.level%LEVELS.length].name,W/2,42);
// lives / demo
ctx.textAlign='right';
try{ctx.letterSpacing='3px';}catch(e){}
ctx.font='700 10px "Trebuchet MS", Verdana, sans-serif';
ctx.fillStyle='#ff8fa8'; ctx.fillText(S.mode==='user'?'LOVES':'DEMO',W-24,22);
try{ctx.letterSpacing='0px';}catch(e){}
if(S.mode==='user'){ for(let i=0;i<S.lives;i++) miniBurger(ctx,W-36-i*34,36); }
else { ctx.font='700 14px "Trebuchet MS", Verdana, sans-serif'; ctx.fillStyle='rgba(255,243,224,.7)'; ctx.fillText('AI AT THE GRILL',W-24,44); }
// combo
if(S.combo>=2){
ctx.textAlign='center';
const sc=1+Math.min(S.combo,6)*0.06;
ctx.save(); ctx.translate(W/2,84); ctx.scale(sc,sc);
ctx.font='900 17px "Arial Black","Trebuchet MS",sans-serif';
ctx.fillStyle='#ff5d8f'; ctx.fillText('COMBO ×'+S.combo,0,0);
ctx.restore();
}
}
```
miniBurger positions near y=36 with height ~14: occupy 28..42 within HUD 56 ✓.
drawBanners:
```js
function drawBanners(){
const t=S.t;
// level intro
if((S.phase==='serve'||S.phase==='play') && S.levelTime<1.6 && S.phase!=='over'){
const k=S.levelTime;
const a=k<0.2?k/0.2:(k>1.2?Math.max(0,(1.6-k)/0.4):1);
const sc=0.92+Math.min(1,k*4)*0.08;
ctx.save(); ctx.globalAlpha=a; ctx.translate(W/2,285); ctx.scale(sc,sc);
ctx.textAlign='center';
try{ctx.letterSpacing='5px';}catch(e){}
ctx.font='700 13px "Trebuchet MS",Verdana,sans-serif';
ctx.fillStyle='#ff8fa8'; ctx.fillText('LEVEL '+(S.level+1),0,-30);
try{ctx.letterSpacing='2px';}catch(e){}
ctx.font='400 46px Impact,"Arial Black",sans-serif';
ctx.fillStyle='rgba(0,0,0,0.5)'; ctx.fillText(LEVELS[S.level%LEVELS.length].name,3,-30+46+3);
ctx.fillStyle='#fff3e0'; ctx.fillText(LEVELS[S.level%LEVELS.length].name,0,-30+46);
try{ctx.letterSpacing='0px';}catch(e){}
ctx.restore();
}
```
Wait y math: name baseline at y = -30+46 = 16? Let me place: eyebrow at y=-30; name baseline y=16. Then title text position (0,16). Shadow at (3,19). ok rewrite:
```js
ctx.fillText(name shadow at (3,19); real (0,16));
```
Hmm eyebrow baseline -30; name baseline 16: gap 46px — good for 46px font.
// serve hint
```js
if(S.mode==='user'&&S.phase==='serve'&&!S.paused){
ctx.textAlign='center';
const a=0.55+0.4*Math.sin(t*5);
ctx.font='700 13px "Trebuchet MS",Verdana,sans-serif';
try{ctx.letterSpacing='2px';}catch(e){}
ctx.fillStyle=`rgba(255,243,224,${a})`;
ctx.fillText('CLICK OR PRESS SPACE TO LAUNCH',W/2,532);
try{ctx.letterSpacing='0px';}catch(e){}
}
```
// clear banner
```js
if(S.phase==='clear'){
ctx.save(); ctx.textAlign='center';
ctx.font='400 52px Impact,"Arial Black",sans-serif';
ctx.fillStyle='rgba(0,0,0,0.5)'; ctx.fillText('LEVEL CLEAR!',W/2+3,293);
ctx.fillStyle='#fff3e0'; ctx.fillText('LEVEL CLEAR!',W/2,290);
ctx.font='700 14px "Trebuchet MS",Verdana,sans-serif';
try{ctx.letterSpacing='3px';}catch(e){}
ctx.fillStyle='#ff8fa8'; ctx.fillText('THE KITCHEN SWOONS',W/2,318);
try{ctx.letterSpacing='0px';}catch(e){}
ctx.restore();
}
```
// over
```js
if(S.phase==='over'){
ctx.fillStyle='rgba(10,3,8,0.78)'; ctx.fillRect(0,0,W,H);
ctx.textAlign='center';
ctx.font='400 58px Impact,"Arial Black",sans-serif';
ctx.fillStyle='rgba(0,0,0,0.6)'; ctx.fillText('GAME OVER',W/2+4,296);
ctx.fillStyle='#ff5d8f'; ctx.fillText('GAME OVER',W/2,292);
ctx.font='900 22px "Arial Black","Trebuchet MS",sans-serif';
ctx.fillStyle='#fff3e0'; ctx.fillText('FINAL SCORE '+S.score,W/2,340);
const a=0.5+0.4*Math.sin(t*5);
ctx.font='700 14px "Trebuchet MS",Verdana,sans-serif';
try{ctx.letterSpacing='2px';}catch(e){}
ctx.fillStyle=`rgba(255,243,224,${a})`;
ctx.fillText('CLICK OR PRESS A KEY TO PLAY AGAIN',W/2,386);
ctx.fillStyle='rgba(255,143,168,0.8)';
ctx.fillText('THE BURGER FORGIVES YOU',W/2,412);
try{ctx.letterSpacing='0px';}catch(e){}
}
```
// paused
```js
if(S.paused){
ctx.fillStyle='rgba(10,3,8,0.66)'; ctx.fillRect(0,0,W,H);
ctx.textAlign='center';
ctx.font='400 48px Impact,"Arial Black",sans-serif';
ctx.fillStyle='#fff3e0'; ctx.fillText('PAUSED',W/2,300);
ctx.font='700 13px "Trebuchet MS",Verdana,sans-serif';
ctx.fillStyle='rgba(255,243,224,.7)'; ctx.fillText('CLICK OR PRESS P TO RESUME',W/2,330);
}
}
```
Note: clear-phase check earlier: condition order fine.
Hmm, in over phase the canvas dim covers HUD too — ok since drawn in banners after HUD.
Also draw "over" behind? banners drawn last → covers. good.
Now heartFill implement with hsl string: `hsl(${h}, ${sat}%, ${lig}%)` — commas fine.
Ball drawn even when over? It's off-screen. fine.
drawParts:
```js
function drawParts(){
for(const p of parts){
const a=Math.max(0,Math.min(1,p.life*2.2));
if(p.t==='h'){
heartFill(ctx,p.x,p.y,p.s,p.rot,p.hue,p.sat,p.lig,a);
} else if(p.t==='d'){
ctx.globalAlpha=a; ctx.fillStyle=`hsl(${p.hue},${p.sat}%,${p.lig}%)`;
ctx.beginPath(); ctx.arc(p.x,p.y,p.s,0,Math.PI*2); ctx.fill(); ctx.globalAlpha=1;
} else { // spark
ctx.globalAlpha=a; ctx.strokeStyle='#ffd9e4'; ctx.lineWidth=2;
ctx.save(); ctx.translate(p.x,p.y); ctx.rotate(p.rot);
ctx.beginPath(); ctx.moveTo(-p.s*2,0); ctx.lineTo(p.s*2,0); ctx.moveTo(0,-p.s*2); ctx.lineTo(0,p.s*2); ctx.stroke(); ctx.restore(); ctx.globalAlpha=1;
}
}
}
```
heartFill:
```js
function heartFill(g,x,y,s,rot,h,sat,lig,a){
g.save(); g.translate(x,y); g.rotate(rot); g.globalAlpha=a;
g.fillStyle=`hsl(${h},${sat}%,${lig}%)`;
heartPath(g,s); g.fill();
if(s>3){ g.globalAlpha=a*0.7; g.fillStyle='rgba(255,255,255,0.65)';
g.beginPath(); g.arc(-s*0.34,-s*0.02,Math.max(0.7,s*0.13),0,Math.PI*2); g.fill(); }
g.restore();
}
```
rings draw:
```js
for(const r of rings){ ctx.globalAlpha=Math.max(0,r.life)*0.55; ctx.strokeStyle='#ff7ea0'; ctx.lineWidth=2.5; ctx.beginPath(); ctx.arc(r.x,r.y,r.r,0,Math.PI*2); ctx.stroke(); }
ctx.globalAlpha=1;
```
update rings: r.r+=3.6*dt; r.life-=0.05*dt; filter.
pops:
```js
ctx.font='900 15px "Arial Black","Trebuchet MS",sans-serif'; ctx.textAlign='center';
for(const p of pops){ ctx.globalAlpha=Math.max(0,Math.min(1,p.life*1.6)); ctx.fillStyle='#fff3e0'; ctx.fillText(p.txt,p.x,p.y); }
```
update: p.y+=p.vy*dt; p.life-=0.022*dt; remove.
trail draw before ball:
```js
for(let i=trail.length-1;i>=0;i--){ const k=1-i/(trail.length+1); ctx.globalAlpha=k*0.18; ctx.fillStyle='#ff9db8'; ctx.beginPath(); ctx.arc(trail[i].x,trail[i].y,BALL_R*k*0.9,0,Math.PI*2); ctx.fill(); }
```
Hmm color pinkish or cream? The hearts are pink; ball is meatball; trail pink works (love trail!). fine.
drawPaddle:
```js
function drawPaddle(){
// shadow
ctx.fillStyle='rgba(0,0,0,0.35)';
ctx.beginPath(); ctx.ellipse(paddleX,PADDLE_Y+70,PADDLE_W*0.42,6,0,0,Math.PI*2); ctx.fill();
ctx.save();
ctx.translate(paddleX,PADDLE_Y+70);
ctx.rotate(tilt);
ctx.scale(1+squash*0.14,1-squash*0.2);
ctx.translate(-paddleX,-(PADDLE_Y+70));
drawBurger(ctx,paddleX,PADDLE_Y,PADDLE_W);
ctx.restore();
}
```
drawBall:
```js
function drawBall(){
// trail drawn separately
ctx.save();
ctx.fillStyle='#7a4a2f';
ctx.strokeStyle='#3f2519'; ctx.lineWidth=2;
ctx.beginPath(); ctx.arc(ball.x,ball.y,BALL_R,0,Math.PI*2); ctx.fill(); ctx.stroke();
ctx.fillStyle='rgba(255,228,196,0.4)';
ctx.beginPath(); ctx.arc(ball.x-3,ball.y-3.5,3,0,Math.PI*2); ctx.fill();
// eyes
let lx=0,ly=-1;
if(!ball.stuck){ const m=Math.hypot(ball.dx,ball.dy)||1; lx=ball.dx/m; ly=ball.dy/m; }
const closed = ball.blinkT<0.1;
for(const ex of [ball.x-3.6, ball.x+3.6]){
if(closed){
ctx.strokeStyle='#3f2519'; ctx.lineWidth=1.6; ctx.beginPath();
ctx.moveTo(ex-2,ball.y-3); ctx.lineTo(ex+2,ball.y-3); ctx.stroke();
} else {
ctx.fillStyle='#fff6e8'; ctx.beginPath(); ctx.arc(ex,ball.y-3,2.3,0,Math.PI*2); ctx.fill();
ctx.fillStyle='#2b160e'; ctx.beginPath(); ctx.arc(ex+lx*1.1,ball.y-3+ly*1.1,1.15,0,Math.PI*2); ctx.fill();
}
}
// smile
ctx.strokeStyle='#3f2519'; ctx.lineWidth=1.4;
ctx.beginPath(); ctx.arc(ball.x,ball.y+2.2,2.2,Math.PI*0.15,Math.PI*0.85); ctx.stroke();
ctx.restore();
}
```
Smile arc: arc centered at (x, y+2.2) radius 2.2 from 0.15π to 0.85π draws lower arc — smile below center. good.
blink update in update(): ball.blinkT-=dt/60; if(ball.blinkT<-0.02) ball.blinkT=2.2+Math.random()*2.8; closed when 0<blinkT<0.1? Set: when blinkT hits <0 reset to 2.2... define: blinkT counts down; closed when blinkT<0.1 && blinkT>0? On reset 2.2..5. So closed briefly before reset: blinkT in (0,0.1) closed; when ≤0 reset. Implement: if(ball.blinkT<=0) ball.blinkT=2.2+rand; else ball.blinkT-=dt/60; closed = ball.blinkT<0.12. Order: decrement then check reset: if blinkT<0 → reset. closed = blinkT<0.12 (after possible reset → false). fine.
updateAmbient and amb init:
```js
const amb=[];
for(let i=0;i<12;i++) amb.push({x:Math.random()*W,y:Math.random()*H,s:7+Math.random()*16,v:0.12+Math.random()*0.3,ph:Math.random()*6.3,hue:334+Math.random()*22,al:0.05+Math.random()*0.05});
```
Init sequence at bottom:
```js
resize(); toDemo(); drawLogo(); requestAnimationFrame(frame);
```
toDemo: builds level, resetBall, mode demo, overlay show (remove hidden just in case), score 0 etc.
One consideration: `S.levelTime` in banner uses levelTime; during 'clear' phase update returns early before levelTime increment — banner for next level starts after buildLevel resets — good.
In update, the early return for 'clear'/'over' happens before levelTime increment — good.
But note in 'clear' phase parts/rings/pops/ambient already updated before the early return (they're updated at top). Yes I update them before phase branches. Order in update():
1. time, shake, squash decay, ambient, parts, pops, rings
2. phase clear/over handling → return
3. levelTime, brick spawn
4. paddle control, clamp, tilt
5. ball logic
Wait brick spawn uses levelTime — but in serve phase (demo waiting) levelTime increments (spawn anim continues) fine.
Also during phase 'serve' (user waiting to launch), bricks spawn anim; banner shows; nice.
Another subtlety: after ballLost in user mode with lives>0 → resetBall → phase serve; but update's paddle/serve branch runs next frame. good.
During 'over', ball remains where lost; render draws ball below screen — harmless.
Also make sure brickCollide not called when phase serve (ball stuck) — stepBall only in play. ✓.
Guard: stepBall called when `S.phase==='play'`:
```js
if(S.phase==='serve'){ ball.x=paddleX; ball.y=PADDLE_Y-BALL_R-1; if(S.mode==='demo'){...launch} }
else if(S.phase==='play'){ stepBall(dt); }
```
✓.
Now HTML overlay markup:
```html
<div id="overlay">
<div class="card">
<canvas id="logo" width="420" height="240"></canvas>
<div class="eyebrow">DEMO MODE · THE CHEF IS PLAYING</div>
<h1>LOVE AT FIRST <em>BITE</em></h1>
<p class="tag">A burger paddle. A meatball. Bricks that burst into hearts.</p>
<div class="cta">CLICK OR PRESS A KEY TO START</div>
<p class="hint">MOUSE / TOUCH OR ←→ TO MOVE · SPACE TO LAUNCH · P TO PAUSE</p>
</div>
</div>
```
Canvas logo default attributes overridden by drawLogo (sets width/height) — style width via CSS 170px? aspect 210x120 → CSS width:172px; height auto.
drawLogo: hearts + burger.
```js
function drawLogo(){
const d=2, w=210, h=124;
logo.width=w*d; logo.height=h*d;
const g=logo.getContext('2d'); g.scale(d,d);
heartFill(g,24,34,11,-0.22,342,88,60,0.95);
heartFill(g,188,26,8,0.25,352,90,66,0.95);
heartFill(g,178,70,5.5,-0.3,336,85,54,0.9);
heartFill(g,26,84,6,0.3,350,82,70,0.85);
g.save(); g.translate(105,116); g.rotate(-0.05); g.scale(0.62,0.62);
drawBurger(g,0,-68,156);
g.restore();
}
```
Burger at translate(105,116) scaled .62: drawBurger(g,0,-68,156) draws y=-68..0 (w=156 → half 78 fits within ±105? 78*0.62=48 <105 ✓). Visual bottom at 116-? bottom y=0 → 116. Height 68*.62=42 → top at 74. Hearts at 26..84 y overlap top of bun slightly — hearts placed at corners mostly (x 24 & 188), bun spans x 105±48=57..153: hearts at x24/188 outside; heart at (178,70) near right of bun top ok; (26,84) left ok. good.
Now CSS `#logo{width:210px;max-width:60vw;height:auto;filter:drop-shadow(0 6px 0 rgba(0,0,0,.35))}` drop-shadow chunky — nice sticker feel. It's a shadow not glow. fine.
Check the h1 line-height and em color: `em{color:#ff7b9c}` — pink "BITE" with same shadow. text-shadow inherited by em. ok.
One more scan for possible runtime errors:
- `LEVELS[S.level%LEVELS.length].name` with S.level≥0 fine.
- burger color map missing row 4 (gap) — no bricks created there. But map lookup for row 3 present. ✓.
- `def.color(r, pat.length, c)` signature — diamond uses rows param. ✓.
- In brick spawn delay: `delay:(r*0.06 + c*0.012)` for heart 13 cols: max ~0.6+0.15=0.75s; sp rate dt*0.05: reach 1 in 20 frames (0.33s) → done ~1.1s. Banner 1.6s. demo serve launch at 0.65s — some bricks still spawning when ball arrives? Ball reaches bricks ~1.5-1.6s (spawn complete). Collisions skip sp<1 bricks; ball may pass through upper rows mid-spawn — minor, acceptable... To be safe make demo serve wait 1.0s, and sp rate faster: dt*0.08 (12 frames). And delay formula delay:(r*0.05+c*0.01) max ~0.65s + 0.2 = 0.85s done. Set demo serveT>1.0.
- `play()` helper: `function play(n){ if(S.mode==='user') snd[n]&&snd[n](); }` — snd methods call beep which guards AC.
- beep uses exponentialRampToValueAtTime with 0.0001 target ok; freq+slide could be ≤0: clamp: `Math.max(40,freq+slide)`.
- AudioContext creation inside initAudio try/catch.
- keydown listener: `e.key` for arrows preventDefault to stop page scroll (body overflow hidden anyway).
- `pointermove` clamps mouseX to [PADDLE_W/2+8, W-PADDLE_W/2-8] so paddle stays in bounds; also only set if within canvas-ish x range (0..W). We map via rect; if pointer outside canvas (letterbox), still map — clamp anyway; but moving mouse far outside horizontally shouldn't pin paddle at edge unintentionally... Actually clamping means paddle goes to edge when mouse exits right — typical. fine. But only track when pointer over canvas? Letterbox areas exist vertically; simple approach: always track; fine.
However: mousemove outside canvas in demo shouldn't matter; in user mode after start, mouse controls — if user moves mouse away, paddle follows to clamp edge — standard behavior. ok.
- Prevent context menu / drag: not needed.
- `touch-action:none` on body via CSS to let pointermove control paddle on mobile without scrolling.
- iOS Safari: pointer events supported (13+). fine. Latest Chrome target anyway.
Now, one important check: `ctx.letterSpacing` — supported Chrome 99+, but assignment of invalid value silently ignored; setting string '3px' valid. try/catch wraps anyway. After setting, other text calls inherit — I reset to '0px' after each use. Make sure every set resets. I'll write helper:
```js
function ls(v){ try{ ctx.letterSpacing=v; }catch(e){} }
```
Use ls('3px') / ls('0px').
Also textAlign leaks: set per draw. fine.
Now finalize sizes for canvas banner fonts — Impact weight 400 fine.
Potential visual issue: brick highlight on very light bricks (light pink rows with white alpha .22) subtle — ok.
Edge: hearts spawned behind HUD (y<56)? bricks y≥86; hearts burst may fly above HUD area behind HUD strip (HUD drawn after world) — hearts pass under HUD bar: fine (they get covered, looks like going behind the counter).
Ball trail alpha low, ok.
Combo resets on paddle hit AND wall? Keep paddle only (classic). Combo persists across wall bounces — good, encourages bank shots.
Score popup for crack uses cream too.
What about level clear while ball mid-flight (bricks.some check) — phase='clear' freezes ball (update returns early) — ball hangs mid-air with hearts raining — dreamy — good.
After nextLevel → resetBall → serve.
Potential flow bug: 'clear' phase early-return happens BEFORE paddle control — paddle frozen during clear — fine.
During 'clear' or 'over', demoPaddle not called — paddle static — fine.
One more: during 'serve' in user mode with mouse — paddle moves with mouse; ball follows paddle (stuck). ✓.
Font stack for HUD 'Arial Black' — including fallback.
I think the design is solid. Let me also add a tiny detail: ketchup/mustard drips under paddle? no, enough.
Now write everything as final file. Watch out: inside `<script>`, avoid `</script>` sequence — none.
Also the tagline text uses "·" fine.
Let me also double check `hsl(${h},${sat}%,${lig}%)` with fractional h (335+r*2 integer) fine.
Alright — also confirm `parts` cap splice.
Another potential perf: `bricks.some` per break trivial.
Let me write out the heart level color function: `color:(r,rows)=>[335+r*2,86,72-r*3]` r up to 9: hue 353, l=45. Top rows light pink (l 72) to deep red bottom (45) — heart gradient top-light bottom-dark: looks like shaded heart. But visually maybe better deep at top? Heart icons usually solid; fine.
Wait — actually for readability of shape maybe make outline tough bricks darker via emblem anyway. ok.
Also brick 'lig' for patty 28 dark w/ highlight fine.
Let me now also handle the case user wins many levels — speeds cap 9.6.
Final check on requirement "Games must start in a self-playing demo mode behind a 'Click or press a key to start' message" — ✓ overlay CTA text matches.
"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." ✓.
Write final answer now — one fenced code block, complete HTML. I'll add concise comments. Also mention nothing outside code block per rules.
Let me write the code carefully in full.
Additional careful items while writing:
1. `const logo=document.getElementById('logo');` before drawLogo call.
2. Order: script at end of body; DOM ready.
3. `resize()` reads innerWidth/innerHeight — fine.
4. In render, first `ctx.setTransform(dpr,0,0,dpr,0,0)` then clear? We fully redraw bg via drawImage covering all — no clear needed but shake offset may reveal edges: drawImage at (0,0) covers; shaken content translated — edges show previous frame? drawImage bg not translated covers whole → fine.
5. During shake, world drawn translated; bg static — subtle parallax effect — fine.
Let me also add small feature: hearts from broken brick sometimes drift upward? Already upward bias -3.1 initial vy. good.
6. `frame` function: first call `last` undefined → dt clamp: initialize `let last=performance.now();` before rAF.
7. `update` when paused skipped but S.t should still advance for pulse in pause overlay: I'll advance S.t inside render? Simpler: in frame: `if(!S.paused) update(dt); else S.t+=dt/60;`.
8. Any leftover reference: `sparks()` function define.
```js
function sparks(x,y,n){
for(let i=0;i<n;i++){
const a=Math.random()*Math.PI*2, sp=1+Math.random()*3;
parts.push({t:'s',x,y,vx:Math.cos(a)*sp,vy:Math.sin(a)*sp-1,g:0.12,rot:Math.random()*3,vr:0.2,s:1.5+Math.random()*1.5,life:1,dec:0.04,hue:0,sat:0,lig:0,wob:0,wv:0,wa:0});
}
}
```
9. rings/pops update loops with splice.
10. burstDots uses b.hsl array.
Alright, also add "hearts on crack hit"? spark only.
11. In hitBrick tough first-hit: `b.hp` from 2→1: cracked. Draw cracks when `b.max>1&&b.hp<b.max`. Also keep emblem? replace emblem with cracks — emblem only when full hp.
12. Wait: heart-level lobes '@' with tough emblem dark heart icon — thematic.
Let me also confirm `drawBricks` skip dead:
```js
function drawBricks(){
for(const b of bricks){
if(b.dead) continue;
const e=easeOutBack(b.sp); if(e<=0.01) continue;
const cx=b.x+b.w/2, cy=b.y+b.h/2;
ctx.save(); ctx.translate(cx,cy); ctx.scale(e,e); ctx.translate(-cx,-cy);
const [h,s,l]=b.hsl;
rr(ctx,b.x,b.y,b.w,b.h,6);
ctx.fillStyle=`hsl(${h},${s}%,${l}%)`; ctx.fill();
ctx.strokeStyle=`hsl(${h},${Math.max(s-15,30)}%,${Math.max(l-20,10)}%)`; ctx.lineWidth=2; ctx.stroke();
// top highlight
rr(ctx,b.x+3,b.y+3,b.w-6,Math.max(3,b.h*0.28),3);
ctx.fillStyle='rgba(255,255,255,0.22)'; ctx.fill();
if(b.max>1){
if(b.hp===b.max){ heartFill(ctx,cx,cy-1,7,0,h,Math.min(s+10,95),Math.min(l+28,88),0.9); }
else { // cracks
ctx.strokeStyle='rgba(35,8,18,0.6)'; ctx.lineWidth=2;
ctx.beginPath();
ctx.moveTo(cx-8,cy-7); ctx.lineTo(cx-2,cy-1); ctx.lineTo(cx-6,cy+7);
ctx.moveTo(cx+7,cy-8); ctx.lineTo(cx+2,cy-2); ctx.lineTo(cx+7,cy+5);
ctx.stroke();
}
}
ctx.restore();
}
}
```
heartFill emblem light heart on tough brick — like a "sweetheart brick".
Emblem hue same as brick → visible via lightness. ok.
13. easeOutBack overshoot >1 slight scale pop. good.
14. `drawTrail` inside render before ball.
15. userPaddle & clamp & tilt compute after control.
update():
```js
function update(dt){
S.t+=dt/60;
if(S.shake>0.05) S.shake*=Math.pow(0.86,dt); else S.shake=0;
squash=Math.max(0,squash-0.05*dt);
updateAmbient(dt); updateParts(dt); updatePops(dt); updateRings(dt);
if(S.phase==='clear'){ S.phaseT+=dt/60; if(S.phaseT>1.9) nextLevel(); return; }
if(S.phase==='over'){ S.overT+=dt/60; if(S.overT>9) toDemo(); return; }
S.levelTime+=dt/60;
for(const b of bricks){ if(!b.dead&&b.sp<1&&S.levelTime>b.delay) b.sp=Math.min(1,b.sp+dt*0.08); }
if(S.mode==='demo') demoPaddle(dt); else userPaddle(dt);
paddleX=Math.max(PADDLE_W/2+8,Math.min(W-PADDLE_W/2-8,paddleX));
const mv=paddleX-prevPX; prevPX=paddleX;
tilt+=(Math.max(-0.1,Math.min(0.1,mv*0.03))-tilt)*Math.min(1,0.22*dt);
if(ball.blinkT<=0) ball.blinkT=2.2+Math.random()*2.8; else ball.blinkT-=dt/60;
if(S.phase==='serve'){
ball.x=paddleX; ball.y=PADDLE_Y-BALL_R-1;
if(S.mode==='demo'){ S.serveT+=dt/60; if(S.serveT>1.0) launchBall(Math.random()*0.9-0.45); }
} else if(S.phase==='play'){ stepBall(dt); }
}
```
Note ball.blinkT init: define ball object with blinkT:3.
demoPaddle:
```js
function demoPaddle(dt){
let target;
if(S.phase==='serve'){ target=W/2; }
else if(ball.dy>0.05){ target=predictX()+Math.sin(S.t*1.7)*7; }
else { target=ball.x*0.3+(W/2)*0.7+Math.sin(S.t*0.8)*26; }
const v=Math.max(-11*dt,Math.min(11*dt,target-paddleX));
paddleX+=v;
}
```
predictX:
```js
function predictX(){
const sp=S.speed, dy=ball.dy;
if(dy<=0.02) return ball.x;
const t=(PADDLE_Y-BALL_R-ball.y)/(dy*sp);
let px=ball.x+ball.dx*sp*t;
const span=W-2*BALL_R;
px=(px-BALL_R)%(2*span); if(px<0)px+=2*span; if(px>span)px=2*span-px;
return px+BALL_R;
}
```
OK. Writing final now. Also double-check the overlay is `pointer-events:none`? If none, clicks fall to window (still captured by window listener). But CTA cursor pointer visual? If pointer-events none, cursor default. It's fine—whole screen is clickable. Add `#overlay{pointer-events:none}` so nothing blocks; window handles pointerdown. But then `cursor:pointer` on cta does nothing — set body cursor default; okay.
Actually let the overlay keep pointer events default (auto) — window listener still receives (bubbles to window). Either fine. Keep default so hover cursor shows pointer over CTA. window pointerdown listener catches bubbled event. ✓.
Hidden class: opacity 0 + pointer-events none (so gameplay clicks during user mode don't hit invisible overlay — though events bubble to window anyway, pointer-events none avoids cursor weirdness).
Font check h1 fallback ok.
One last: initial `S.levelTime` etc defined in S.
Also `S.serveT` init 0.
Everything set. Write it.
Actually, let me reconsider the brick area for level 1 vs paddle zone for mobile readability — fine.
Also add `title` attribute? no.
Double-check beep signature: beep(f,dur,type,vol,slide).
```js
function beep(f,dur,type,vol,slide){
if(!AC) return;
try{
const t=AC.currentTime;
const o=AC.createOscillator(), g=AC.createGain();
o.type=type; o.frequency.setValueAtTime(f,t);
if(slide) o.frequency.exponentialRampToValueAtTime(Math.max(40,f+slide),t+dur);
g.gain.setValueAtTime(vol,t);
g.gain.exponentialRampToValueAtTime(0.0001,t+dur);
o.connect(g).connect(AC.destination);
o.start(t); o.stop(t+dur+0.05);
}catch(e){}
}
```
`o.connect(g).connect(AC.destination)` — connect returns destination node, chaining works.
initAudio:
```js
let AC=null;
function initAudio(){
if(!AC){ try{ AC=new (window.AudioContext||window.webkitAudioContext)(); }catch(e){ AC=null; } }
if(AC&&AC.state==='suspended'){ AC.resume().catch(()=>{}); }
}
```
Now, also confirm that any pointerdown during demo that starts game also inits audio — yes listener calls initAudio first.
Write final HTML. Also `<meta name="color-scheme" content="dark">`? optional skip.
Also I should double check `padStart` — fine.
Also ensure no `const` reassign errors: paddleX let, etc.
Final: also small favicon? skip (data URI allowed but unnecessary).
Counting overlay demo CTA per spec: text "CLICK OR PRESS A KEY TO START" — matches.
Done planning; produce code.