Hamburger breakoutOne-line
MiMo-V2.6-Pro
by Xiaomireasoning defaultClean
Running live in a sandboxed frame. Click inside it before using the keyboard.
How it went
- Total time
- 8 min 44 s
- Cost
- $0.024
- Output tokens
- 27,636
- Spent thinking
- 15,053 tokens
- Speed
- 52.8 tok/s
- Console errors
- 0
- First token after
- 1.0 s
- First visible text after
- 5 min 57 s
- Prompt tokens
- 360
- Code size
- 35.7 KB
- Finish reason
- stop
- Served by
- Xiaomi
- Model id
xiaomi/mimo-v2.6-pro- Reasoning sent
model default- Max output
- 131,072 tokens
- Generated
- Nov 13, 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 · 1107 lines
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>Burger Breakout — smash bricks, shower hearts</title>
<style>
*{box-sizing:border-box}
html,body{margin:0;padding:0;overflow-x:hidden}
body{
min-height:100vh;
min-height:100dvh;
background:
radial-gradient(1100px 620px at 50% -12%, #4a1a58 0%, #2a1236 42%, #170c22 100%);
color:#fff;
font-family:"Trebuchet MS","Segoe UI",system-ui,-apple-system,sans-serif;
display:flex;flex-direction:column;align-items:center;
padding:18px 18px 14px;
gap:14px;
-webkit-font-smoothing:antialiased;
}
/* ---------- header ---------- */
.topbar{
width:min(1180px,100%);
display:flex;align-items:center;justify-content:space-between;
gap:18px;flex-wrap:wrap;
}
.brand{display:flex;align-items:center;gap:14px;min-width:0}
.burger-badge{
position:relative;width:52px;height:46px;flex:none;
filter:drop-shadow(0 6px 14px rgba(255,120,60,.35));
}
.burger-badge span{position:absolute;left:2px;right:2px;display:block}
.bun-top{
top:0;height:20px;border-radius:22px 22px 8px 8px;
background:linear-gradient(180deg,#FFD28A,#E19631);
}
.fill{
top:18px;height:12px;border-radius:5px;
background:linear-gradient(180deg,#7BC950 0 5px,#FFC53D 5px 8px,#7A3E1D 8px 12px);
}
.bun-bot{
top:29px;height:15px;border-radius:7px 7px 16px 16px;
background:linear-gradient(180deg,#E9A64B,#B96C24);
}
.brand-text h1{
margin:0;font-size:clamp(19px,3.4vw,30px);letter-spacing:.11em;line-height:1;
background:linear-gradient(92deg,#FFF3D6,#FFB0D0 42%,#FF6F9E 78%,#FFD98A);
-webkit-background-clip:text;background-clip:text;color:transparent;
}
.brand-text p{
margin:6px 0 0;font-size:clamp(10px,1.7vw,13px);
letter-spacing:.22em;text-transform:uppercase;color:rgba(255,214,232,.62)
}
.stats{display:flex;gap:10px;flex-wrap:wrap}
.stat{
background:rgba(255,255,255,.07);
border:1px solid rgba(255,255,255,.13);
border-radius:15px;padding:8px 16px;min-width:88px;
box-shadow:inset 0 1px 0 rgba(255,255,255,.12),0 10px 22px rgba(0,0,0,.22);
}
.stat .label{
display:block;font-size:10px;letter-spacing:.2em;text-transform:uppercase;
color:rgba(255,220,236,.62);margin-bottom:2px
}
.stat strong{font-size:20px;letter-spacing:.04em;line-height:1.1}
.stat.combo strong{color:#FFD166}
.stat.lives strong{color:#FF6B9D;letter-spacing:.12em;font-size:19px}
.stat.lives i{font-style:normal}
.stat.lives i.off{opacity:.28}
/* ---------- stage ---------- */
.stage{
position:relative;
width:min(100%, calc((100vh - 190px) * 1.5385));
width:min(100%, calc((100dvh - 190px) * 1.5385));
aspect-ratio:1000 / 650;
border-radius:26px;overflow:hidden;
background:#2A1236;
box-shadow:
0 34px 80px rgba(0,0,0,.55),
0 0 0 1px rgba(255,255,255,.10),
0 0 110px rgba(255,90,160,.16);
}
canvas{display:block;width:100%;height:100%;touch-action:none;cursor:none}
.overlay{
position:absolute;inset:0;display:grid;place-items:center;text-align:center;
padding:18px;
background:radial-gradient(120% 100% at 50% 45%, rgba(24,9,32,.32), rgba(20,7,28,.72));
transition:opacity .3s ease;
}
.overlay.hidden{opacity:0;pointer-events:none}
.panel{
max-width:min(620px,92%);
background:rgba(38,15,50,.82);
border:1px solid rgba(255,150,200,.32);
border-radius:24px;padding:clamp(18px,3.4vw,32px) clamp(18px,4vw,40px);
box-shadow:0 28px 70px rgba(0,0,0,.5),0 0 60px rgba(255,110,175,.22),
inset 0 1px 0 rgba(255,255,255,.16);
backdrop-filter:blur(6px);-webkit-backdrop-filter:blur(6px);
}
.eyebrow{
font-size:clamp(9px,1.6vw,11px);letter-spacing:.32em;text-transform:uppercase;
color:#FFB3D2;margin-bottom:12px
}
.panel h2{
margin:0 0 12px;font-size:clamp(20px,4.4vw,38px);line-height:1.08;letter-spacing:.05em;
text-transform:uppercase;
background:linear-gradient(100deg,#FFF6E4,#FFB6D5 45%,#FF6B9D);
-webkit-background-clip:text;background-clip:text;color:transparent;
text-shadow:0 0 34px rgba(255,110,170,.28);
}
.panel p{margin:0 0 20px;font-size:clamp(12px,2.3vw,16px);line-height:1.65;color:rgba(255,232,244,.82)}
.panel p b{color:#FFD98A}
.cta{
appearance:none;border:0;cursor:pointer;
padding:14px 30px;border-radius:999px;
font:inherit;font-size:clamp(12px,2.2vw,15px);font-weight:bold;
letter-spacing:.19em;text-transform:uppercase;color:#3A0B25;
background:linear-gradient(135deg,#FFD98A,#FF7FAB 60%,#FF5D8F);
box-shadow:0 16px 34px rgba(255,90,150,.42),inset 0 1px 0 rgba(255,255,255,.55);
transition:transform .16s ease,box-shadow .16s ease;
}
.cta:hover{transform:translateY(-2px) scale(1.02);box-shadow:0 22px 44px rgba(255,90,150,.5)}
.cta:active{transform:translateY(1px) scale(.99)}
/* ---------- footer ---------- */
.controls{
width:min(1180px,100%);display:flex;align-items:center;justify-content:space-between;
gap:14px;flex-wrap:wrap;
}
.hints{
display:flex;align-items:center;gap:8px;flex-wrap:wrap;
font-size:12px;letter-spacing:.13em;text-transform:uppercase;color:rgba(255,222,238,.62)
}
.key{
display:inline-grid;place-items:center;min-width:28px;height:26px;padding:0 8px;
border-radius:8px;font-size:11px;letter-spacing:.08em;color:#FFE7F1;
background:rgba(255,255,255,.09);border:1px solid rgba(255,255,255,.16);
box-shadow:0 3px 0 rgba(0,0,0,.25)
}
.sep{opacity:.4}
.buttons{display:flex;gap:10px}
.ghost{
appearance:none;cursor:pointer;font:inherit;font-size:11px;letter-spacing:.16em;
text-transform:uppercase;color:#FFDDEE;background:rgba(255,255,255,.07);
border:1px solid rgba(255,255,255,.16);border-radius:999px;padding:10px 18px;
transition:background .18s ease,transform .18s ease
}
.ghost:hover{background:rgba(255,120,175,.22);transform:translateY(-1px)}
@media (max-width:760px){
body{padding:12px 12px 10px;gap:10px}
.stat{min-width:70px;padding:7px 12px;border-radius:12px}
.stat strong{font-size:16px}
.hints{font-size:10px;letter-spacing:.1em}
.stage{border-radius:18px}
}
</style>
</head>
<body>
<header class="topbar">
<div class="brand">
<div class="burger-badge" aria-hidden="true">
<span class="bun-top"></span><span class="fill"></span><span class="bun-bot"></span>
</div>
<div class="brand-text">
<h1>BURGER BREAKOUT</h1>
<p>smash the bricks · shower the hearts</p>
</div>
</div>
<div class="stats">
<div class="stat"><span class="label">Score</span><strong id="score">0</strong></div>
<div class="stat"><span class="label">Level</span><strong id="level">1</strong></div>
<div class="stat combo"><span class="label">Combo</span><strong id="combo">x1.0</strong></div>
<div class="stat lives"><span class="label">Lives</span><strong id="lives"><i>♥</i><i>♥</i><i>♥</i></strong></div>
</div>
</header>
<main class="stage" id="stage">
<canvas id="game"></canvas>
<div class="overlay" id="overlay">
<div class="panel">
<div class="eyebrow" id="ov-eyebrow">Demo mode · auto-playing</div>
<h2 id="ov-title">Click or press a key to start</h2>
<p id="ov-text">Move the burger paddle with your mouse, your finger, or the <b>← →</b> keys.<br>
Press <b>Space</b> to launch the ball and watch every brick burst into hearts.</p>
<button class="cta" id="ov-btn">Start game</button>
</div>
</div>
</main>
<footer class="controls">
<div class="hints">
<span class="key">←</span><span class="key">→</span> move
<span class="sep">•</span>
<span class="key">Space</span> launch / pause
<span class="sep">•</span>
<span class="key">P</span> pause
<span class="sep">•</span>
drag anywhere on the field
</div>
<div class="buttons">
<button class="ghost" id="restart">New game</button>
<button class="ghost" id="mute">Sound: on</button>
</div>
</footer>
<script>
(function(){
'use strict';
/* ============================================================
Setup
============================================================ */
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
const stage = document.getElementById('stage');
const overlay = document.getElementById('overlay');
const ovEyebrow= document.getElementById('ov-eyebrow');
const ovTitle = document.getElementById('ov-title');
const ovText = document.getElementById('ov-text');
const ovBtn = document.getElementById('ov-btn');
const elScore = document.getElementById('score');
const elLevel = document.getElementById('level');
const elCombo = document.getElementById('combo');
const elLives = document.getElementById('lives');
const btnMute = document.getElementById('mute');
const btnRestart = document.getElementById('restart');
const W = 1000, H = 650, GRAV = 560;
const clamp = (v,a,b)=>Math.max(a,Math.min(b,v));
const PALETTE = [
['#FF7A9E','#E23E6B'],
['#FFC75C','#E39222'],
['#8FE06A','#4FA83C'],
['#FF9A5C','#E4622B'],
['#C58BFF','#8648D8'],
['#63D6E8','#2A93B8'],
['#FF6FB5','#C22C7A'],
['#F5E27A','#C9A227']
];
const HEART_COLORS = ['#FF4D7D','#FF7BA9','#FF2D55','#FFA6C9','#FFD1E3','#FF6B9D','#FFFFFF','#FF8FC0'];
let scale = 1, dpr = 1;
function resize(){
const r = canvas.getBoundingClientRect();
if(!r.width || !r.height) return;
dpr = Math.min(window.devicePixelRatio || 1, 2);
canvas.width = Math.round(r.width * dpr);
canvas.height = Math.round(r.height * dpr);
scale = r.width / W;
}
if(window.ResizeObserver){ new ResizeObserver(resize).observe(canvas); }
window.addEventListener('resize', resize);
resize();
function roundRect(x,y,w,h,r){
r = Math.min(r, Math.abs(w)/2, Math.abs(h)/2);
ctx.beginPath();
ctx.moveTo(x+r,y);
ctx.arcTo(x+w,y,x+w,y+h,r);
ctx.arcTo(x+w,y+h,x,y+h,r);
ctx.arcTo(x,y+h,x,y,r);
ctx.arcTo(x,y,x+w,y,r);
ctx.closePath();
}
function heartPath(s){
ctx.beginPath();
ctx.moveTo(0, s*0.60);
ctx.bezierCurveTo(-s*0.16, s*0.40, -s*0.62, s*0.12, -s*0.62, -s*0.16);
ctx.bezierCurveTo(-s*0.62, -s*0.52, -s*0.16, -s*0.58, 0, -s*0.26);
ctx.bezierCurveTo(s*0.16, -s*0.58, s*0.62, -s*0.52, s*0.62, -s*0.16);
ctx.bezierCurveTo(s*0.62, s*0.12, s*0.16, s*0.40, 0, s*0.60);
ctx.closePath();
}
/* ============================================================
Audio (generated, no files)
============================================================ */
let actx = null, muted = false;
function initAudio(){
try{
if(!actx){
const AC = window.AudioContext || window.webkitAudioContext;
if(AC) actx = new AC();
}
if(actx && actx.state === 'suspended') actx.resume();
}catch(e){}
}
function tone(freq, dur, type, vol, slideTo, delay){
if(!actx || muted) return;
try{
const t = actx.currentTime + (delay||0);
const o = actx.createOscillator(), g = actx.createGain();
o.type = type || 'sine';
o.frequency.setValueAtTime(freq, t);
if(slideTo) o.frequency.exponentialRampToValueAtTime(Math.max(30, slideTo), t + dur);
g.gain.setValueAtTime(0.0001, t);
g.gain.exponentialRampToValueAtTime(vol, t + 0.01);
g.gain.exponentialRampToValueAtTime(0.0001, t + dur);
o.connect(g); g.connect(actx.destination);
o.start(t); o.stop(t + dur + 0.03);
}catch(e){}
}
function sfx(name, arg){
if(!actx || muted) return;
if(name === 'brick'){
const p = 1 + Math.min(arg||0, 14) * 0.09;
tone(230*p, .11, 'square', .05, 330*p);
tone(760*p, .07, 'triangle', .035, 1150*p);
tone(1500, .05, 'sine', .022, 900);
} else if(name === 'wall'){
tone(200, .045, 'square', .026, 150);
} else if(name === 'paddle'){
tone(300, .10, 'triangle', .06, 240);
tone(120, .12, 'sine', .04, 90);
} else if(name === 'launch'){
tone(360, .16, 'sine', .07, 900);
} else if(name === 'life'){
tone(320, .34, 'sawtooth', .05, 110);
tone(160, .40, 'triangle', .05, 70, .06);
} else if(name === 'level'){
tone(440, .12, 'triangle', .06, 440);
tone(554, .12, 'triangle', .06, 554, .11);
tone(659, .18, 'triangle', .07, 659, .22);
tone(880, .30, 'sine', .05, 880, .33);
}
}
/* ============================================================
Game state
============================================================ */
const game = {
state:'demo', score:0, lives:3, level:1,
combo:0, comboTimer:0, shake:0, time:0
};
const paddle = { x:W/2 - 88, y:H - 90, w:176, h:64, speed:820, targetX:null };
const ball = {
x:W/2, y:H-140, vx:0, vy:0, r:11, speed:430,
attached:true, autoLaunch:0.9, trail:[]
};
let bricks = [];
const particles = [];
const texts = [];
const bgHearts = [];
for(let i=0;i<9;i++){
bgHearts.push({
x:Math.random()*W, y:Math.random()*H,
size:26 + Math.random()*54,
speed:8 + Math.random()*16,
alpha:.035 + Math.random()*.05,
rot:(Math.random()-.5)*1.2,
phase:Math.random()*Math.PI*2
});
}
/* ============================================================
Level
============================================================ */
function buildLevel(){
bricks.length = 0;
const cols = 10;
const rows = Math.min(4 + Math.floor(game.level * 0.75), 8);
const gapX = 10, gapY = 12, marginX = 42, top = 108, bh = 36;
const bw = (W - marginX*2 - gapX*(cols-1)) / cols;
for(let r=0;r<rows;r++){
for(let c=0;c<cols;c++){
const pal = PALETTE[(r + game.level - 1) % PALETTE.length];
const tough = (game.level > 1 && r < 2 && (c + r) % 3 === 0) ? 2 : 1;
bricks.push({
x: marginX + c*(bw+gapX),
y: top + r*(bh+gapY),
w: bw, h: bh,
c1: pal[0], c2: pal[1],
hp: tough, maxHp: tough,
special: Math.random() < 0.12,
dead: false, flash: 0
});
}
}
}
function resetBall(){
ball.attached = true;
ball.vx = 0; ball.vy = 0;
ball.x = paddle.x + paddle.w/2;
ball.y = paddle.y - ball.r - 8;
ball.trail.length = 0;
ball.autoLaunch = (game.state === 'demo') ? 0.75 : 0;
}
function launchBall(){
if(!ball.attached) return;
ball.attached = false;
const ang = (Math.random()*0.72 - 0.36);
ball.vx = Math.sin(ang) * ball.speed;
ball.vy = -Math.abs(Math.cos(ang) * ball.speed);
sfx('launch');
}
/* ============================================================
Particles
============================================================ */
class Particle{
constructor(x,y,vx,vy,life,color,size,type){
this.x=x; this.y=y; this.vx=vx; this.vy=vy;
this.life=life; this.maxLife=life;
this.color=color; this.size=size; this.type=type||'heart';
this.rot=Math.random()*Math.PI*2;
this.vr=(Math.random()-.5)*7;
this.phase=Math.random()*Math.PI*2;
this.wob=2 + Math.random()*4;
this.seed=Math.random();
}
update(dt){
this.life -= dt;
if(this.type === 'ring'){
this.size += 320*dt;
return;
}
if(this.type === 'text'){
this.y += this.vy*dt;
this.vy *= (1 - 1.6*dt);
return;
}
this.vy += GRAV * dt * (this.type === 'spark' ? 0.45 : 1);
this.vx *= (1 - 1.15*dt);
this.x += this.vx*dt + Math.sin(this.phase)*26*dt*(this.type==='heart'?1:0.2);
this.y += this.vy*dt;
this.rot += this.vr*dt;
this.phase += this.wob*dt;
}
draw(){
const t = clamp(this.life/this.maxLife, 0, 1);
if(this.type === 'ring'){
ctx.globalAlpha = t*0.55;
ctx.strokeStyle = this.color;
ctx.lineWidth = 3.2*t + 0.6;
ctx.beginPath(); ctx.arc(this.x,this.y,this.size,0,Math.PI*2); ctx.stroke();
ctx.globalAlpha = 1;
return;
}
if(this.type === 'text'){
ctx.globalAlpha = clamp(t*1.5,0,1);
ctx.font = 'bold 21px "Trebuchet MS", sans-serif';
ctx.textAlign = 'center';
ctx.lineWidth = 5;
ctx.strokeStyle = 'rgba(40,8,32,.65)';
ctx.strokeText(this.text, this.x, this.y);
ctx.fillStyle = this.color;
ctx.fillText(this.text, this.x, this.y);
ctx.globalAlpha = 1;
return;
}
if(this.type === 'spark'){
ctx.globalAlpha = clamp(t*1.3,0,1);
ctx.fillStyle = this.color;
const s = this.size * (0.4 + t*0.8);
ctx.save();
ctx.translate(this.x,this.y); ctx.rotate(this.rot);
ctx.fillRect(-s/2,-s/2,s,s);
ctx.restore();
ctx.globalAlpha = 1;
return;
}
const pop = 1 + (1-t)*0.25;
const s = this.size * pop;
ctx.globalAlpha = clamp(t*1.6, 0, 1);
ctx.save();
ctx.translate(this.x, this.y);
ctx.rotate(this.rot * 0.35);
ctx.fillStyle = this.color;
heartPath(s);
ctx.fill();
ctx.globalAlpha = clamp(t*1.2,0,1)*0.55;
ctx.fillStyle = 'rgba(255,255,255,.85)';
heartPath(s*0.36);
ctx.fill();
ctx.restore();
ctx.globalAlpha = 1;
}
}
function addText(x,y,str,color){
const p = new Particle(x,y,0,-52,1.1,color,10,'text');
p.text = str;
texts.push(p);
if(texts.length > 40) texts.shift();
}
function spawnBurst(x,y,count,color,special){
const total = special ? count*2 : count;
for(let i=0;i<total;i++){
const a = Math.random()*Math.PI*2;
const sp = 70 + Math.random()*420;
particles.push(new Particle(
x, y,
Math.cos(a)*sp, Math.sin(a)*sp - 90 - Math.random()*120,
1.05 + Math.random()*1.25,
HEART_COLORS[(Math.random()*HEART_COLORS.length)|0],
6 + Math.random()*11,
'heart'
));
}
for(let i=0;i<12;i++){
const a = Math.random()*Math.PI*2, sp = 120 + Math.random()*380;
particles.push(new Particle(x,y,Math.cos(a)*sp,Math.sin(a)*sp,0.32+Math.random()*0.3,color,3+Math.random()*6,'spark'));
}
particles.push(new Particle(x,y,0,0,0.5,color,16,'ring'));
const cap = 1050;
if(particles.length > cap) particles.splice(0, particles.length - cap);
}
/* ============================================================
Input
============================================================ */
const keys = { left:false, right:false };
function toWorld(clientX){
const r = canvas.getBoundingClientRect();
return (clientX - r.left) / (r.width / W);
}
stage.addEventListener('pointermove', e=>{
if(game.state !== 'playing') return;
paddle.targetX = clamp(toWorld(e.clientX) - paddle.w/2, 8, W - paddle.w - 8);
}, {passive:true});
stage.addEventListener('pointerdown', e=>{
initAudio();
if(game.state === 'demo' || game.state === 'gameover'){
startGame();
} else if(game.state === 'paused'){
togglePause();
} else {
paddle.targetX = clamp(toWorld(e.clientX) - paddle.w/2, 8, W - paddle.w - 8);
if(ball.attached) launchBall();
}
});
window.addEventListener('keydown', e=>{
const k = e.key;
if(k === 'ArrowLeft' || k === 'ArrowRight' || k === ' ' || k === 'Spacebar') e.preventDefault();
initAudio();
if(k === 'ArrowLeft' || k === 'a' || k === 'A') { keys.left = true; paddle.targetX = null; }
if(k === 'ArrowRight'|| k === 'd' || k === 'D') { keys.right = true; paddle.targetX = null; }
if(game.state === 'demo' || game.state === 'gameover'){
startGame();
return;
}
if(k === ' ' || k === 'Spacebar'){
if(game.state === 'paused'){ togglePause(); return; }
if(ball.attached) launchBall(); else togglePause();
}
if(k === 'p' || k === 'P') togglePause();
});
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;
});
ovBtn.addEventListener('click', e=>{
e.stopPropagation();
initAudio();
if(game.state === 'paused') togglePause();
else startGame();
});
btnRestart.addEventListener('click', ()=>{
initAudio();
startGame();
});
btnMute.addEventListener('click', ()=>{
initAudio();
muted = !muted;
btnMute.textContent = 'Sound: ' + (muted ? 'off' : 'on');
if(!muted) tone(520,.09,'triangle',.05,760);
});
/* ============================================================
Flow
============================================================ */
function setOverlay(eyebrow, title, text, btnText){
ovEyebrow.textContent = eyebrow;
ovTitle.textContent = title;
ovText.innerHTML = text;
ovBtn.textContent = btnText;
overlay.classList.remove('hidden');
}
function hideOverlay(){ overlay.classList.add('hidden'); }
function updateHUD(){
elScore.textContent = game.score.toLocaleString();
elLevel.textContent = game.level;
elCombo.textContent = 'x' + (1 + Math.min(game.combo, 25) * 0.12).toFixed(1);
let h = '';
for(let i=0;i<3;i++){
h += (i < game.lives) ? '<i>♥</i>' : '<i class="off">♡</i>';
}
elLives.innerHTML = h;
}
function startGame(){
game.state = 'playing';
game.score = 0; game.lives = 3; game.level = 1;
game.combo = 0; game.comboTimer = 0; game.shake = 0;
particles.length = 0; texts.length = 0;
ball.speed = 430;
buildLevel();
resetBall();
hideOverlay();
updateHUD();
initAudio();
}
function togglePause(){
if(game.state === 'playing'){
game.state = 'paused';
setOverlay('Break time', 'Paused', 'The burger is holding your spot.<br>Press <b>Space</b> or tap to keep smashing.', 'Resume');
} else if(game.state === 'paused'){
game.state = 'playing';
hideOverlay();
}
}
function endGame(){
game.state = 'gameover';
setOverlay('Kitchen closed', 'Game over',
'Final score <b>' + game.score.toLocaleString() + '</b> · Level <b>' + game.level + '</b><br>Every brick you broke filled the sky with hearts.',
'Play again');
overlay.classList.remove('hidden');
sfx('life');
}
function nextLevel(){
game.level++;
ball.speed = Math.min(430 + (game.level-1)*30, 760);
buildLevel();
resetBall();
addText(W/2, H*0.52, 'LEVEL ' + game.level, '#FFD98A');
spawnBurst(W/2, H*0.45, 90, '#FF6B9D', true);
sfx('level');
updateHUD();
}
/* ============================================================
Update
============================================================ */
function updateAI(dt){
if(ball.attached){
ball.autoLaunch -= dt;
if(ball.autoLaunch <= 0) launchBall();
paddle.targetX = W/2 - paddle.w/2 + Math.sin(game.time*0.7)*220;
return;
}
if(ball.vy > 0){
let t = (paddle.y - ball.y) / ball.vy;
t = clamp(t, 0, 3.2);
let pred = ball.x + ball.vx * t;
let guard = 0;
while((pred < 0 || pred > W) && guard++ < 6){
if(pred < 0) pred = -pred;
if(pred > W) pred = 2*W - pred;
}
paddle.targetX = clamp(pred - paddle.w/2 + (Math.random()-0.5)*10, 8, W - paddle.w - 8);
} else {
paddle.targetX = clamp(W/2 - paddle.w/2 + Math.sin(game.time*0.9)*240, 8, W - paddle.w - 8);
}
}
function updatePaddle(dt){
const maxSpeed = (game.state === 'demo') ? 1250 : 980;
if(paddle.targetX !== null){
const diff = paddle.targetX - paddle.x;
const step = clamp(diff, -maxSpeed*dt, maxSpeed*dt);
paddle.x += step;
if(Math.abs(diff) < 1.4) paddle.x = paddle.targetX;
}
if(keys.left){ paddle.x -= paddle.speed*dt; paddle.targetX = null; }
if(keys.right){ paddle.x += paddle.speed*dt; paddle.targetX = null; }
paddle.x = clamp(paddle.x, 8, W - paddle.w - 8);
if(ball.attached){
ball.x = paddle.x + paddle.w/2;
ball.y = paddle.y - ball.r - 8;
}
}
function hitBrick(b){
b.hp--;
b.flash = 1;
const cx = clamp(ball.x, b.x, b.x + b.w);
const cy = clamp(ball.y, b.y, b.y + b.h);
if(b.hp <= 0){
b.dead = true;
game.combo++;
game.comboTimer = 2.4;
const mult = 1 + Math.min(game.combo, 25) * 0.12;
const pts = Math.round(100 * mult * (b.special ? 2.5 : 1) * (b.maxHp > 1 ? 1.5 : 1));
game.score += pts;
spawnBurst(cx, cy, b.special ? 54 : 40, b.special ? '#FF5E9C' : b.c1, b.special);
addText(cx, cy - 6, '+' + pts, b.special ? '#FFB3D2' : '#FFE3A8');
game.shake = Math.max(game.shake, b.special ? 12 : 8);
sfx('brick', game.combo);
} else {
spawnBurst(cx, cy, 14, b.c1, false);
game.shake = Math.max(game.shake, 4);
sfx('wall');
}
updateHUD();
}
function ballStep(dt){
ball.x += ball.vx * dt;
ball.y += ball.vy * dt;
if(ball.x < ball.r){ ball.x = ball.r; ball.vx = Math.abs(ball.vx); sfx('wall'); }
if(ball.x > W - ball.r){ ball.x = W - ball.r; ball.vx = -Math.abs(ball.vx); sfx('wall'); }
if(ball.y < ball.r + 4){ ball.y = ball.r + 4; ball.vy = Math.abs(ball.vy); sfx('wall'); }
// paddle
const pTop = paddle.y - 4, pBot = paddle.y + paddle.h*0.52;
if(ball.vy > 0 &&
ball.x + ball.r > paddle.x && ball.x - ball.r < paddle.x + paddle.w &&
ball.y + ball.r > pTop && ball.y - ball.r < pBot){
const rel = clamp((ball.x - (paddle.x + paddle.w/2)) / (paddle.w/2), -1, 1);
const ang = rel * (Math.PI/3);
const sp = Math.min(Math.hypot(ball.vx, ball.vy) * 1.018, 900);
ball.vx = Math.sin(ang) * sp;
ball.vy = -Math.abs(Math.cos(ang) * sp);
ball.y = pTop - ball.r - 1;
game.combo = 0;
game.shake = Math.max(game.shake, 5);
sfx('paddle');
spawnBurst(ball.x, pTop, 8, '#FFC7DE', false);
updateHUD();
}
// bricks
for(let i=0;i<bricks.length;i++){
const b = bricks[i];
if(b.dead) continue;
const nx = clamp(ball.x, b.x, b.x + b.w);
const ny = clamp(ball.y, b.y, b.y + b.h);
const dx = ball.x - nx, dy = ball.y - ny;
if(dx*dx + dy*dy <= ball.r*ball.r){
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 < 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);
break;
}
}
if(ball.y - ball.r > H + 24) loseLife();
}
function loseLife(){
spawnBurst(ball.x, Math.min(ball.y, H - 30), 30, '#FF6B9D', false);
game.lives--;
game.combo = 0;
sfx('life');
if(game.lives <= 0 && game.state !== 'demo'){
updateHUD();
endGame();
return;
}
resetBall();
updateHUD();
}
function update(dt){
game.time += dt;
game.shake = Math.max(0, game.shake - dt*30);
if(game.comboTimer > 0){
game.comboTimer -= dt;
if(game.comboTimer <= 0){ game.combo = 0; updateHUD(); }
}
if(game.state === 'demo') updateAI(dt);
else if(game.state === 'playing' && ball.attached && ball.autoLaunch > 0){
ball.autoLaunch -= dt;
if(ball.autoLaunch <= 0) launchBall();
}
if(game.state !== 'paused'){
updatePaddle(dt);
if(!ball.attached && game.state !== 'gameover'){
const speed = Math.hypot(ball.vx, ball.vy) || 1;
const steps = Math.max(1, Math.ceil(speed * dt / 7));
for(let s=0;s<steps;s++) ballStep(dt / steps);
} else if(ball.attached){
ball.x = paddle.x + paddle.w/2;
ball.y = paddle.y - ball.r - 8;
}
ball.trail.push({x:ball.x, y:ball.y});
if(ball.trail.length > 12) ball.trail.shift();
for(let i=particles.length-1;i>=0;i--){
const p = particles[i];
p.update(dt);
if(p.life <= 0) particles.splice(i,1);
}
for(let i=texts.length-1;i>=0;i--){
const p = texts[i];
p.update(dt);
if(p.life <= 0) texts.splice(i,1);
}
for(const h of bgHearts){
h.y += h.speed * dt;
h.phase += dt*0.45;
if(h.y - h.size > H){ h.y = -h.size; h.x = Math.random()*W; }
}
for(const b of bricks){ if(b.flash > 0) b.flash = Math.max(0, b.flash - dt*3.2); }
if(game.state === 'playing' && bricks.length && bricks.every(b=>b.dead)) nextLevel();
}
}
/* ============================================================
Draw
============================================================ */
function drawBackground(){
const g = ctx.createLinearGradient(0,0,0,H);
g.addColorStop(0,'#2B1338');
g.addColorStop(.52,'#3B1743');
g.addColorStop(1,'#22102B');
ctx.fillStyle = g; ctx.fillRect(0,0,W,H);
const rg = ctx.createRadialGradient(W/2, H*0.30, 20, W/2, H*0.30, W*0.74);
rg.addColorStop(0,'rgba(255,110,170,.18)');
rg.addColorStop(1,'rgba(255,110,170,0)');
ctx.fillStyle = rg; ctx.fillRect(0,0,W,H);
ctx.fillStyle = 'rgba(255,255,255,.055)';
for(let x=20;x<W;x+=40){
for(let y=20;y<H;y+=40) ctx.fillRect(x,y,2.2,2.2);
}
for(const h of bgHearts){
ctx.save();
ctx.globalAlpha = h.alpha;
ctx.translate(h.x, h.y);
ctx.rotate(Math.sin(h.phase)*0.22 + h.rot);
ctx.fillStyle = '#FF87B6';
heartPath(h.size*0.5);
ctx.fill();
ctx.restore();
}
ctx.globalAlpha = 1;
const fg = ctx.createLinearGradient(0, H-170, 0, H);
fg.addColorStop(0,'rgba(255,90,150,0)');
fg.addColorStop(1,'rgba(255,90,150,.15)');
ctx.fillStyle = fg; ctx.fillRect(0,H-170,W,170);
ctx.strokeStyle = 'rgba(255,180,215,.16)';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(0, H-16); ctx.lineTo(W, H-16);
ctx.stroke();
}
function drawBrick(b){
const r = 11;
const g = ctx.createLinearGradient(0, b.y, 0, b.y + b.h);
g.addColorStop(0, b.c1);
g.addColorStop(1, b.c2);
roundRect(b.x, b.y, b.w, b.h, r);
ctx.fillStyle = g; ctx.fill();
ctx.save();
roundRect(b.x, b.y, b.w, b.h, r);
ctx.clip();
const gloss = ctx.createLinearGradient(0, b.y, 0, b.y + b.h*0.5);
gloss.addColorStop(0,'rgba(255,255,255,.34)');
gloss.addColorStop(1,'rgba(255,255,255,0)');
ctx.fillStyle = gloss;
ctx.fillRect(b.x, b.y, b.w, b.h*0.52);
ctx.fillStyle = 'rgba(0,0,0,.16)';
ctx.fillRect(b.x, b.y + b.h - 8, b.w, 8);
if(b.maxHp > 1 && b.hp > 1){
ctx.strokeStyle = 'rgba(255,255,255,.5)';
ctx.lineWidth = 3;
ctx.beginPath();
ctx.moveTo(b.x + b.w*0.28, b.y + 5);
ctx.lineTo(b.x + b.w*0.42, b.y + b.h*0.5);
ctx.lineTo(b.x + b.w*0.32, b.y + b.h - 5);
ctx.moveTo(b.x + b.w*0.68, b.y + 5);
ctx.lineTo(b.x + b.w*0.56, b.y + b.h*0.5);
ctx.lineTo(b.x + b.w*0.70, b.y + b.h - 5);
ctx.stroke();
}
if(b.flash > 0){
ctx.fillStyle = 'rgba(255,255,255,' + (b.flash*0.55) + ')';
ctx.fillRect(b.x, b.y, b.w, b.h);
}
ctx.restore();
roundRect(b.x, b.y, b.w, b.h, r);
ctx.lineWidth = 2;
ctx.strokeStyle = 'rgba(255,255,255,.30)';
ctx.stroke();
if(b.special){
ctx.save();
ctx.translate(b.x + b.w/2, b.y + b.h/2 + 1);
ctx.fillStyle = 'rgba(255,255,255,.92)';
heartPath(11);
ctx.fill();
ctx.restore();
}
}
function drawPaddle(){
const x = paddle.x, y = paddle.y, w = paddle.w, h = paddle.h;
const cx = x + w/2;
ctx.fillStyle = 'rgba(0,0,0,.30)';
ctx.beginPath();
ctx.ellipse(cx, y + h + 10, w*0.44, 11, 0, 0, Math.PI*2);
ctx.fill();
// bottom bun
const bg = ctx.createLinearGradient(0, y + h*0.66, 0, y + h);
bg.addColorStop(0,'#F0AE55'); bg.addColorStop(1,'#B96C24');
ctx.fillStyle = bg;
roundRect(x + 3, y + h*0.66, w - 6, h*0.36, 14);
ctx.fill();
// patty
const pg = ctx.createLinearGradient(0, y + h*0.50, 0, y + h*0.76);
pg.addColorStop(0,'#7E4220'); pg.addColorStop(1,'#47220F');
ctx.fillStyle = pg;
roundRect(x - 3, y + h*0.50, w + 6, h*0.27, 11);
ctx.fill();
// cheese + drips
ctx.fillStyle = '#FFC53D';
roundRect(x + 1, y + h*0.40, w - 2, h*0.15, 7);
ctx.fill();
for(let i=0;i<6;i++){
const dx = x + 16 + i*(w - 32)/5;
ctx.beginPath();
ctx.moveTo(dx - 9, y + h*0.50);
ctx.lineTo(dx + 9, y + h*0.50);
ctx.lineTo(dx, y + h*0.63);
ctx.closePath();
ctx.fill();
}
// lettuce
ctx.fillStyle = '#69C34A';
ctx.beginPath();
ctx.moveTo(x - 5, y + h*0.42);
for(let i=0;i<=10;i++){
const lx = x - 5 + (w + 10)*i/10;
const ly = y + h*0.42 + ((i % 2 === 0) ? 6 : -3);
ctx.lineTo(lx, ly);
}
ctx.lineTo(x + w + 5, y + h*0.31);
ctx.lineTo(x - 5, y + h*0.31);
ctx.closePath();
ctx.fill();
// top bun
const tg = ctx.createLinearGradient(0, y - 2, 0, y + h*0.42);
tg.addColorStop(0,'#FFD294'); tg.addColorStop(.55,'#F0A94D'); tg.addColorStop(1,'#D98B2C');
ctx.fillStyle = tg;
ctx.beginPath();
ctx.ellipse(cx, y + h*0.40, w/2, h*0.44, 0, Math.PI, Math.PI*2);
ctx.lineTo(x + w, y + h*0.44);
ctx.lineTo(x, y + h*0.44);
ctx.closePath();
ctx.fill();
// gloss on bun
ctx.strokeStyle = 'rgba(255,255,255,.40)';
ctx.lineWidth = 5;
ctx.lineCap = 'round';
ctx.beginPath();
ctx.ellipse(cx, y + h*0.40, w*0.31, h*0.29, 0, Math.PI*1.12, Math.PI*1.72);
ctx.stroke();
// sesame seeds
ctx.fillStyle = '#FFF4D8';
const seeds = [[-0.28,0.16],[-0.06,0.05],[0.17,0.12],[0.33,0.23],[-0.4,0.28],[0.05,0.28]];
for(const s of seeds){
ctx.save();
ctx.translate(cx + s[0]*w, y + h*0.16 + s[1]*h);
ctx.rotate(s[0]*1.6);
ctx.beginPath();
ctx.ellipse(0,0,6.5,3.4,0,0,Math.PI*2);
ctx.fill();
ctx.restore();
}
}
function drawBall(){
for(let i=0;i<ball.trail.length;i++){
const t = ball.trail[i];
const a = (i / ball.trail.length) * 0.24;
ctx.fillStyle = 'rgba(255,130,180,' + a + ')';
ctx.beginPath();
ctx.arc(t.x, t.y, ball.r * (0.30 + 0.65*i/ball.trail.length), 0, Math.PI*2);
ctx.fill();
}
const g = ctx.createRadialGradient(
ball.x - ball.r*0.38, ball.y - ball.r*0.45, ball.r*0.12,
ball.x, ball.y, ball.r
);
g.addColorStop(0,'#FFFFFF');
g.addColorStop(.35,'#FF8CB8');
g.addColorStop(1,'#D3255F');
ctx.fillStyle = g;
ctx.beginPath();
ctx.arc(ball.x, ball.y, ball.r, 0, Math.PI*2);
ctx.fill();
ctx.lineWidth = 1.6;
ctx.strokeStyle = 'rgba(255,255,255,.55)';
ctx.stroke();
if(ball.attached && game.state !== 'demo'){
ctx.globalAlpha = .55 + Math.sin(game.time*5)*0.22;
ctx.font = 'bold 17px "Trebuchet MS", sans-serif';
ctx.textAlign = 'center';
ctx.fillStyle = '#FFE1EE';
ctx.fillText('SPACE / TAP TO LAUNCH', ball.x, paddle.y - 74);
ctx.globalAlpha = 1;
}
}
function draw(){
ctx.setTransform(dpr*scale, 0, 0, dpr*scale, 0, 0);
const sh = game.shake;
if(sh > 0) ctx.translate((Math.random()-.5)*sh, (Math.random()-.5)*sh);
drawBackground();
for(const b of bricks){ if(!b.dead) drawBrick(b); }
drawPaddle();
drawBall();
for(const p of particles) p.draw();
for(const t of texts) t.draw();
// vignette
const vg = ctx.createRadialGradient(W/2, H/2, H*0.30, W/2, H/2, H*0.92);
vg.addColorStop(0,'rgba(0,0,0,0)');
vg.addColorStop(1,'rgba(10,2,16,.42)');
ctx.fillStyle = vg;
ctx.fillRect(0,0,W,H);
}
/* ============================================================
Loop
============================================================ */
let last = performance.now();
function loop(now){
const dt = Math.min((now - last) / 1000, 0.034);
last = now;
update(dt);
draw();
requestAnimationFrame(loop);
}
// initial demo scene
game.state = 'demo';
game.score = 0; game.lives = 3; game.level = 1;
buildLevel();
resetBall();
updateHUD();
for(let i=0;i<3;i++){
spawnBurst(180 + i*300, 220 + (i%2)*90, 26, '#FF6B9D', false);
}
requestAnimationFrame(loop);
})();
</script>
</body>
</html>Reasoning trace (15,053 tokens)
We need to produce a complete self-contained HTML game. Breakout with hamburger paddle, hearts particle explosions. Must start in self-playing demo mode with "Click or press a key to start" overlay. Canvas game, responsive.
Let me design carefully.
Canvas game with:
- Background: diner-ish, warm gradient, maybe subtle pattern.
- HUD: score, lives, level, combo.
- Paddle: drawn hamburger (bun bottom, patty, lettuce wavy, cheese, bun top with sesame seeds) — drawn with canvas paths.
- Ball: maybe a tomato slice / sesame? Let's do a glossy red ball with highlight.
- Bricks: various colors (ketchup red, mustard yellow, pickle green, cheese, etc.) with rounded rect and gloss.
- Every brick break: "tons of little hearts" — spawn ~40 hearts with physics, gravity, rotation, colors pink/red/white, plus maybe floating score text and a shockwave ring.
- Sound: WebAudio generated blips (no external files). Nice touch. Must be created on user gesture though; can init audio on click.
- Controls: mouse/touch move paddle, arrow keys / A D, space to launch/pause.
- Demo mode: AI follows ball automatically; overlay message "Click or press a key to start".
- Responsive: canvas fills container, game world fixed logical size (e.g. 960x600) scaled to fit; use device pixel ratio.
- Prevent horizontal scrollbar.
Important: everything inline. No external fonts — use system font stack.
Let me write the code.
Structure:
```html
<!DOCTYPE html>
<html lang="en">
<head>... style ...</head>
<body>
<div class="wrap">
<header class="topbar"> title + score chips </header>
<div class="stage"><canvas id="game"></canvas><div class="overlay" id="overlay">...</div></div>
<footer class="hints">...</footer>
</div>
<script>...</script>
</body>
</html>
```
Rendering: use canvas with logical width 960, height 620. Resize canvas to container size with DPR, and use scale factor.
Actually simpler: canvas backing size = container CSS size * dpr; ctx.setTransform(scale...) where scale = cssWidth / 960 ... but then aspect changes. Better: set logical W/H fixed ratio960x620 and letterbox via container aspect ratio CSS: `.stage { aspect-ratio: 960/620; max-height: ...}`. Hmm, at 1280x800 we want it to fit nicely. Let's compute layout: topbar ~72px, footer ~56px, padding. Stage available height maybe 800-72-56-32 = 640. Width1280-64 = 1216. Ratio 960/620=1.548. For width 1216 → height 785 too tall. So constrain stage by height: max-height. Use `aspect-ratio: 48/31; width: min(100%, calc((100vh - 190px) * 1.548))`. Simpler: make stage flex:1 and use JS to compute canvas CSS size keeping ratio. Hmm.
Alternative: let canvas fill available area and compute logical layout dynamically (brick grid adapts to width). That's more work but more robust responsive. Let's do: logical canvas size = actual CSS size in CSS pixels (scaled by DPR internally), and game layout computed from canvas size. Bricks: columns computed based on width, e.g. cols = clamp(8..14). Paddle width proportional. Physics in CSS-pixel coords. This adapts down to 375px width nicely.
But careful: on very small screens, we need paddle & bricks sized reasonably. Let's define a scale factor s = clamp(width/960, 0.55, 1.25)? Hmm, then on 375 wide, s=0.55 → paddle 110px wide... The stage width375 - padding = ~343. Ball radius 7*0.55 ≈ 4. Fine.
Actually simpler and robust: keep world coordinates fixed at W=1000, H=650 and scale canvas drawing to fit container with uniform scale and letterbox (centered). Container has aspect-ratio set via JS to match? No — just use `object-fit`-like: compute scale = min(cw/1000, ch/650), translate to center. Unused space = background of stage (same color-ish). Since stage CSS aspect-ratio: 1000/650 is fixed, there's no letterbox anyway if we set aspect-ratio in CSS. And to fit vertically at 1280x800, we constrain stage height.
Let me do: `.stage { position:relative; width:100%; aspect-ratio: 1000/650; max-height: calc(100dvh - 170px); }` — but with aspect-ratio + max-height, the width would exceed? With `aspect-ratio` and `max-height`, the box shrinks height but width stays 100% → distortion of ratio. Use `margin-inline:auto` plus width:min(100%, calc((100dvh - 170px) * 1000/650)). That works: `width: min(100%, calc((100dvh - 172px) * 1.538))`. Then aspect-ratio gives height. Good. Canvas fills stage at 100%x100%, and drawing uses scale = canvasCSSWidth/1000.
Let's just set canvas internal width = cssW*dpr, and ctx.scale(dpr*cssW/1000, same) — uniform since ratio preserved.
Now game logic in world coords 1000x650.
Layout:
- Playfield top area: bricks y from 90 to 260 approx.
- Paddle at y = 590, width 150, height 46 (hamburger shape ~ 56 tall visually maybe).
- Ball radius 11.
Bricks: 5 rows x 10 cols, each 88 wide with 8 gap: 10*88 + 9*8 = 880+72 = 952, margin 24 each side. Good. Brick height 34, gap 10.
Levels: after clearing, next level with more rows / faster ball.
Hearts: particle system with types: hearts (small, drawn with path), tiny dots, sparkle. "tons of little hearts" — spawn 45-60 per brick. Performance: cap total particles ~1200. Drawing many heart paths each frame is okay-ish. 1200 hearts path per frame at 60fps... probably fine but let's cap at 900 and use simpler drawing.
Heart drawing: two arcs + triangle, or use a bezier path. Let's make a heart path function scaled.
Sound: AudioContext with oscillators for bounce, brick break (a little "pop" with pitch up by combo), paddle hit, wall, level up. Init on first user interaction.
Game states: 'demo' (autoplay with AI), 'playing', 'paused', 'levelclear' (brief), 'gameover'.
Demo mode: AI paddle tracks ball with slight lag/imperfection; ball auto-launches. Overlay shows "Click or press a key to start" with a pulsing style, plus small instructions.
On user start: reset score/lives/level to fresh game? Probably start fresh game at level 1. Keep it simple: start new game.
Keyboard: left/right/A/D move, space = launch/pause, P pause. Mouse move controls paddle (relative to canvas), click launches.
Touch: touchmove moves paddle.
Ball attached to paddle before launch (sticky) — show "LAUNCH" hint.
Let's handle collision:
- Ball vs walls: x < r → x=r, vx=abs; x > W-r → reflect; y < r → reflect.
- Ball vs paddle: if vy>0 and ball bottom within paddle top region and x overlap → set vy negative, vx based on hit offset (angle up to 60°), speed slightly increase.
- Ball vs bricks: iterate active bricks, AABB with circle: find nearest point; if distance <= r → determine bounce axis via penetration depths; destroy brick; spawn hearts; add score, combo.
Lives: 3. When ball y > H + 40 → lose life, if 0 → gameover overlay with restart button.
Now the hamburger paddle drawing. Draw at (px, py) with width w, height h. Approx:
- bottom bun: rounded rect, golden #E8A33D with darker bottom, sesame? bottom bun usually plain.
- patty: dark brown rounded rect.
- lettuce: green wavy strip (sine).
- cheese: yellow with drips (small triangles).
- top bun: dome shape (bezier) golden with sesame seeds.
Total visual height maybe 62 with paddle hitbox height 18 near the top? Hmm — collisions should use a hitbox that's roughly the top surface. Let's set paddle hit rect = {x, y, w, h} where y is the top of the top bun and h ~ 30. Fine; ball collides with the rounded top.
Let's define drawPaddle(x, y, w, h) where (x,y) top-left of bounding box of width w, height h (h≈54). Collision rect: x..x+w, y+8 .. y+30 maybe. Simpler: use full bbox for collision but bounce only when vy>0. Acceptable.
Actually visually it'll look right: hamburger sits with its top bun at paddle top.
Bricks drawing: rounded rect with gradient, inner highlight line, and a subtle icon? Maybe each row has a texture: sesame dots on top row, etc. Keep it: gradient fill + top gloss + dark border + tiny decorative marks. Colors per row:
1 #FF4D6D (tomato red)
2 #FFB703 (mustard/cheese)
3 #7BC950 (pickle green)
4 #FF8C42 (orange)
5 #C86BFA (berry purple)
6 #4DD0E1 (soda blue) for higher levels.
Also maybe some bricks are "double" (2 hits) at higher levels — nice but optional. Let's include tougher bricks on later rows in level 2+ with hp 2 and a small shield icon/ cracked look.
Background: draw a dark-ish warm gradient? For readability let's use a deep plum/night diner background with subtle radial glow and dot grid, so the bright bricks/hearts pop. Header bar styled in dark with pink accents.
Let's pick overall page theme: dark aubergine background (#1b1024 → #2a1230), canvas background gradient #2b1533 → #451c3f with a soft glow, and neon pink accents. Title "HEARTBREAKER" or "BURGER BREAKOUT". Let's call it "BURGER BREAKOUT" with subtitle "every bite breaks hearts". Nice.
HUD chips: SCORE, LIVES (heart icons), LEVEL, COMBO.
Now, first paint: game canvas drawn immediately with bricks, paddle, ball, hearts decorative, plus overlay. Screenshot at 3s: demo mode running with hearts flying — great.
Let's write the JS carefully.
```js
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
const W = 1000, H = 650;
let dpr = 1, cssW = 0, scale = 1;
function resize(){
const rect = canvas.getBoundingClientRect();
dpr = Math.min(window.devicePixelRatio || 1, 2);
canvas.width = Math.round(rect.width*dpr);
canvas.height = Math.round(rect.height*dpr);
scale = rect.width / W; // uniform
}
```
Draw: `ctx.setTransform(dpr*scale,0,0,dpr*scale,0,0)`.
Input coordinates: `const x = (e.clientX - rect.left)/scale`.
Game object.
Particles:
```js
class Heart {
constructor(x,y,opts){...}
update(dt){ vy += gravity*dt; x+=vx*dt; ... life -= dt; rotation += vr*dt; }
draw(ctx){...}
}
```
Use arrays with `life`/`maxLife`. Remove when life<=0.
Heart path function:
```js
function heartPath(ctx, s){
ctx.beginPath();
ctx.moveTo(0, s*0.35);
ctx.bezierCurveTo(0, s*0.05, -s*0.5, -s*0.05, -s*0.5, s*0.35*?)...
}
```
Simpler standard heart:
```js
function heart(ctx, s){
ctx.beginPath();
ctx.moveTo(0, s*0.75);
ctx.bezierCurveTo(-s*1.1, -s*0.1, -s*0.55, -s*0.95, 0, -s*0.3);
ctx.bezierCurveTo(s*0.55, -s*0.95, s*1.1, -s*0.1, 0, s*0.75);
ctx.closePath();
}
```
Let's verify: heart top with two lobes... Standard:
```
moveTo(0, 0.35s)
bezierCurveTo(-0.1s, 0.1s, -0.6s, -0.05s, -0.6s, 0.25s) // left lobe
bezierCurveTo(-0.6s, 0.55s, -0.1s, 0.8s, 0, 1.0s) // left bottom
bezierCurveTo(0.1s, 0.8s, 0.6s, 0.55s, 0.6s, 0.25s)
bezierCurveTo(0.6s, -0.05s, 0.1s, 0.1s, 0, 0.35s)
```
That gives heart pointing down with center at ~ (0, 0.5s). Fine — I'll use this with translate(0,-0.5s) or just draw around origin roughly.
Let me define heart centered:
```js
function heartPath(ctx, s) {
const y = s * 0.15;
ctx.beginPath();
ctx.moveTo(0, s * 0.62 - y);
ctx.bezierCurveTo(-s*0.12, s*0.42-y, -s*0.62, s*0.12-y, -s*0.62, -s*0.16-y);
ctx.bezierCurveTo(-s*0.62, -s*0.52-y, -s*0.16, -s*0.58-y, 0, -s*0.26-y);
ctx.bezierCurveTo(s*0.16, -s*0.58-y, s*0.62, -s*0.52-y, s*0.62, -s*0.16-y);
ctx.bezierCurveTo(s*0.62, s*0.12-y, s*0.12, s*0.42-y, 0, s*0.62-y);
ctx.closePath();
}
```
Good enough.
Explosion: `spawnHearts(x, y, count, color)`.
Also a shockwave ring particle and "score text" floating.
Screen shake on brick break: shake magnitude small, applied to world transform.
Combo: increments per brick within a 2s window; multiplier = 1 + combo*0.1 capped.
Now demo AI:
```js
function demoAI(){
if(ball.attached) { launch after short delay }
const target = ball.x + ball.vx * ((paddle.y - ball.y)/Math.max(ball.vy, 1)) ...
```
Simple: predict x at paddle y with wall reflections:
```js
let t = (paddle.y - ball.y)/ball.vy; if(t>0 && t<3){ pred = ball.x + ball.vx*t; reflect pred within [0,W] } else pred = ball.x;
```
Then move paddle toward pred with max speed. Add small noise. Also make demo deliberately decent (never lose). If ball missed, it's fine — lives aren't shown in demo? We can still show lives; if demo loses a ball, respawn. Keep demo from game over by auto-respawning (gameOver not triggered in demo; just respawn).
Level clear in demo: advance level.
Now the overlay: shown in demo state with headline "CLICK OR PRESS A KEY TO START", subtext "Move with mouse, touch, or ← → · Space to launch". Must be visible but not block the gameplay view too much — semi-transparent panel in center, game running behind. Good.
Also paused overlay & game over overlay: reuse `#overlay` with different content via JS (set innerHTML / classes). Use a state-driven render function.
Careful with "no alert/confirm/prompt", "no localStorage". Fine.
Audio:
```js
let audioCtx=null;
function initAudio(){ if(!audioCtx) audioCtx = new (window.AudioContext||window.webkitAudioContext)(); if(audioCtx.state==='suspended') audioCtx.resume(); }
function beep(freq, dur, type, gain, slideTo){...}
```
Sound on brick break: two quick oscillators (square+triangle) with pitch based on row/combo. Plus a soft noise burst? Keep simple.
Mute toggle button in header. Since audio only starts after gesture, fine.
Now, let's make sure "no horizontal scrollbar": html,body{overflow-x:hidden}, layout uses max-width.
Responsiveness: header wraps; footer hints hide some on small. Stage width formula uses100dvh — maybe use `min(100%, calc((100dvh - 180px) * 1.538))`. On 1280x800: (800-180)*1.538 = 953.9 → width 954, height 620. Header ~76, footer ~52, paddings... total 76+620+52+ gaps ≈ 780. Fits 800. Good. Let me measure more concretely: body padding 18px top/bottom = 36; header 62; gap 14; stage 620; gap 12; footer 44 → 788. OK, use 100dvh - 190px → (800-190)*1.538 = 938 → height 610. Total 36+62+14+610+12+44 = 778. Fits.
On narrow: 375 width → stage width = 100% = ~343, height 223. That's small but playable-ish. Maybe on narrow screens allow taller stage? Not needed. But 223px height means bricks tiny. Acceptable graceful adaptation. Alternatively use aspect ratio 16/10 on small screens... Let's keep uniform scaling for correctness.
Hmm, at 375 wide the game field is 343x223 CSS px — the bricks are 88/1000*343 = 30px wide, 12px tall. Playable. Paddle 51px wide. Ball radius 3.8px. Ok.
Let me write the full HTML now.
Details for drawing bricks:
```js
function drawBrick(b){
const r = 10;
roundRect(ctx, b.x, b.y, b.w, b.h, r);
const g = ctx.createLinearGradient(0,b.y,0,b.y+b.h);
g.addColorStop(0, b.c1); g.addColorStop(1, b.c2);
ctx.fillStyle=g; ctx.fill();
// gloss
ctx.save(); ctx.clip();
ctx.fillStyle='rgba(255,255,255,.22)';
roundRect(ctx,b.x+4,b.y+3,b.w-8,b.h*0.38,r*0.6); ctx.fill();
ctx.fillStyle='rgba(0,0,0,.14)';
ctx.fillRect(b.x,b.y+b.h-7,b.w,7);
ctx.restore();
ctx.lineWidth=2; ctx.strokeStyle='rgba(255,255,255,.28)'; stroke roundrect
if(b.hp>1){ draw shield lines / darker overlay }
}
```
Also add little decorative seeds? skip.
Ball drawing: radial gradient circle with white highlight + soft glow (shadowBlur). shadowBlur with many particles could be slow; use sparingly (only ball and paddle glow).
Particles: hearts with color from palette; also small "spark" squares and rings.
Now code for update loop with fixed-ish dt: `dt = Math.min((t-last)/1000, 0.033)`.
Ball speed: base 420 px/s in world coords (world 1000x650). Paddle speed for keyboard: 720 px/s.
Ball-brick collision: iterate bricks; use simple AABB overlap with circle:
```js
const nx = clamp(ball.x, b.x, b.x+b.w), ny = clamp(ball.y, b.y, b.y+b.h);
const dx = ball.x-nx, dy=ball.y-ny;
if(dx*dx+dy*dy <= r*r){ collide }
```
Determine reflection: compute overlap on both axes using ball position and previous position. Use approach: if (ball.prevY + r <= b.y) → hit top: ball.y = b.y - r; vy = -abs(vy). etc. Store prev position each frame.
Simpler: compute penetration:
```js
const overlapX = (b.w/2 + r) - Math.abs(ball.x - (b.x+b.w/2));
const overlapY = (b.h/2 + r) - Math.abs(ball.y - (b.y+b.h/2));
if(overlapX < overlapY) { vx = -vx; ball.x += Math.sign(ball.x - center)*overlapX }
else { vy = -vy; ball.y += Math.sign(ball.y - center)*overlapY }
```
Good.
Only one brick collision per frame (break out after handling), to avoid weirdness.
Ball speed increases slightly on brick hits and paddle hits, capped.
Now level generation:
```js
function buildLevel(level){
bricks=[];
const cols = 10;
const rows = Math.min(4 + level, 8);
const gapX = 10, gapY = 12;
const marginX = 40;
const brickW = (W - marginX*2 - gapX*(cols-1))/cols;
const brickH = 34;
const top = 110;
...
}
```
Rows colored by palette index (level offset for variety). hp: rows beyond3 in level>1 get 2 hp sometimes.
Also a "heart brick" special: random bricks are gold with a heart icon giving bonus? Nice touch:1-in-9 bricks is a "LOVE brick" (pink with heart) that spawns extra hearts & bonus 250 points. Let's implement `b.special = 'heart'`.
Lives display: hearts in header via HTML.
Let's now think about the header markup:
```html
<header>
<div class="brand">
<div class="logo">🍔</div> <!-- emoji? Might be fine, no external. Use emoji for the burger logo. But could look inconsistent across platforms. Maybe draw a small canvas? Emoji is okay. Actually let's avoid emoji in the title and instead use CSS-drawn burger? Simpler: use emoji 🍔 in a badge — acceptable, renders fine in Chrome. Hmm, "self-contained", emoji is system font. Fine. -->
<div><h1>BURGER BREAKOUT</h1><p>smash bricks · shower hearts</p></div>
</div>
<div class="stats">
<div class="stat"><span>SCORE</span><strong id="score">0</strong></div>
...
</div>
</header>
```
Footer: control hints + mute button + restart button.
Now the start overlay content and interaction. On click on canvas or any key: startGame(). Also keydown handler: if state==='demo' → startGame() and also apply the key.
Let me write `startGame()`:
```js
state='playing'; score=0; lives=3; level=1; combo=0; buildLevel(1); resetBall(); hide overlay;
```
Pause when tab hidden: `document.addEventListener('visibilitychange', ...)` pause. Fine but careful not to leave it paused when screenshot happens; visibility is fine.
Overlay function:
```js
function showOverlay(kind){
if(kind==='demo'){...}
}
```
I'll just write HTML inside overlay for demo, and dynamically set for gameover/paused.
Now write the code fully and carefully.
Heart colors: ['#FF4D7D','#FF7BA9','#FF2D55','#FFA6C9','#FFD1E3','#FF6B9D','#FFFFFF'].
Particles cap:1100. If exceeding, splice oldest.
Per brick: 42 hearts + 8 sparks + 1 ring. That's 51 particles * ~12 bricks alive = fine.
Now let's write draw order: background (grid + glow), bricks, paddle, ball, particles, floating texts, vignette? Keep.
Background drawn in canvas:
```js
const bg = ctx.createLinearGradient(0,0,0,H); ...radial glow at (W/2, H*0.35)
dot grid: for loops x every 40, y every 40 small rgba circles (alpha .04) — 25*17 = 425 dots per frame. Fine but could cache to offscreen canvas. Let's just draw them — it's fine (425 fillRect/circles). Actually use a cached offscreen pattern canvas created once at resize. Simpler: draw with fillRect 2x2 in a loop — cheap.
Also decorative big hearts in the background at low alpha, slowly floating? Nice ambience. Add 6 slow floating hearts with alpha .06.
Screen shake: apply translate before world drawing.
Now the AI demo needs to look good. Also make demo spawn a big heart burst occasionally even without bricks? No, bricks break constantly.
Let's ensure the ball never gets stuck in horizontal loops: add slight random on paddle bounce.
Time to write.
One more: `requestAnimationFrame` loop and `resize` via ResizeObserver on stage.
Careful: canvas.getBoundingClientRect each resize.
Let me write the final code.
```js
// ---------- helpers ----------
const clamp=(v,a,b)=>Math.max(a,Math.min(b,v));
function roundRect(ctx,x,y,w,h,r){ r=Math.min(r,w/2,h/2); ctx.beginPath(); ctx.moveTo(x+r,y); ctx.arcTo(x+w,y,x+w,y+h,r); ctx.arcTo(x+w,y+h,x,y+h,r); ctx.arcTo(x,y+h,x,y,r); ctx.arcTo(x,y,x+w,y,r); ctx.closePath(); }
```
Game state object:
```js
const game = {
state:'demo', score:0, lives:3, level:1, combo:0, comboTimer:0, shake:0, time:0, muted:false
};
const paddle = { x: W/2-85, y: H-86, w:170, h:58, speed:760, targetX:null };
const ball = { x:0,y:0, vx:0,vy:0, r:11, speed:430, attached:true, trail:[] };
```
Paddle draw with w=170,h=58: hamburger layers:
- total height 58. Top bun: from y to y+26 (dome).
- cheese/lettuce: y+24..y+38
- patty: y+32..y+44
- bottom bun: y+42..y+58.
Let me draw layered:
```js
function drawPaddle(){
const {x,y,w,h}=paddle;
const cx=x+w/2;
ctx.save();
// shadow
ctx.fillStyle='rgba(0,0,0,.28)'; ctx.beginPath(); ctx.ellipse(cx, y+h+6, w*0.46, 10, 0,0,Math.PI*2); ctx.fill();
// bottom bun
const bg=ctx.createLinearGradient(0,y+h*0.68,0,y+h);
bg.addColorStop(0,'#E9A64B'); bg.addColorStop(1,'#B96C24');
ctx.fillStyle=bg;
roundRect(ctx,x+2,y+h*0.68,w-4,h*0.32,12); ctx.fill();
// patty
const pg=ctx.createLinearGradient(0,y+h*0.52,0,y+h*0.78);
pg.addColorStop(0,'#7A3E1D'); pg.addColorStop(1,'#4B2410');
ctx.fillStyle=pg; roundRect(ctx,x-2,y+h*0.52,w+4,h*0.26,10); ctx.fill();
// cheese
ctx.fillStyle='#FFC53D'; roundRect(ctx,x+2,y+h*0.42,w-4,h*0.14,7); ctx.fill();
// cheese drips
ctx.fillStyle='#FFC53D';
for(let i=0;i<5;i++){ const dx=x+18+i*(w-36)/4; ctx.beginPath(); ctx.moveTo(dx-8,y+h*0.5); ctx.lineTo(dx+8,y+h*0.5); ctx.lineTo(dx,y+h*0.62); ctx.closePath(); ctx.fill(); }
// lettuce wavy
ctx.fillStyle='#69C34A';
ctx.beginPath(); ctx.moveTo(x-4,y+h*0.40);
for(let i=0;i<=12;i++){ const lx=x-4+(w+8)*i/12; const ly=y+h*0.40+ (i%2? 5:-2); ... }
```
Hmm, lettuce wavy: draw a path across with bumps then close along top. Let's do:
```js
ctx.beginPath();
ctx.moveTo(x-4, y+h*0.42);
for(let i=0;i<=10;i++){
const lx=x-4+(w+8)*i/10;
const ly=y+h*0.42+((i%2===0)?6:-3);
ctx.lineTo(lx,ly);
}
ctx.lineTo(x+w+4, y+h*0.32);
ctx.lineTo(x-4, y+h*0.32);
ctx.closePath(); ctx.fill();
```
Approximately wavy strip. Good.
- top bun dome:
```js
const tg=ctx.createLinearGradient(0,y,0,y+h*0.42);
tg.addColorStop(0,'#FFC978'); tg.addColorStop(1,'#E0922F');
ctx.fillStyle=tg;
ctx.beginPath();
ctx.moveTo(x, y+h*0.40);
ctx.quadraticCurveTo(x, y-2, cx, y-2); // hmm
```
Better dome: `ctx.ellipse(cx, y+h*0.40, w/2, h*0.42, 0, Math.PI, 0)` → arc from left to right over the top, then close along bottom. Use:
```js
ctx.beginPath();
ctx.ellipse(cx, y+h*0.40, w/2, h*0.42, 0, Math.PI, 2*Math.PI);
ctx.lineTo(x+w, y+h*0.44); ctx.lineTo(x, y+h*0.44); ctx.closePath(); ctx.fill();
```
Then sesame seeds: small white ellipses rotated at few positions.
Then a white gloss arc on top bun: `ctx.strokeStyle='rgba(255,255,255,.35)'; ellipse arc`.
Ball: maybe a red tomato-ish ball with a shine. Let's draw as a glossy red sphere with a white highlight and a tiny sesame? Keep sphere.
Okay. Also the ball trail: store last ~8 positions, draw fading circles. Nice motion.
Now let's handle the "LAUNCH" state: ball attached to paddle, positioned above top bun center. `resetBall()` sets attached true. In demo, auto-launch after 0.6s.
Keyboard:
```js
window.addEventListener('keydown', e=>{
if(['ArrowLeft','ArrowRight','Space',' '].includes(e.key)) e.preventDefault();
if(game.state==='demo' || game.state==='gameover'){ startGame(); return; } // careful: for gameover maybe require click? Allow key to restart.
if(e.key===' '){ if(ball.attached) launchBall(); else togglePause(); }
if(e.key==='p'||e.key==='P') togglePause();
keys[e.key.toLowerCase()]=true;
});
```
Careful: `keys` set after the state check — if starting game with ArrowRight, also set key. Let's restructure: set keys first, then state handling.
Mouse move on canvas → paddle target x; also on mousemove anywhere over stage. Use pointer events on canvas with `pointerdown` to start.
Paddle movement: if mouse control active (`pointerActive`), lerp paddle.x toward targetX; else keyboard velocity.
I'll use: `paddle.targetX` set on pointermove; each frame, if targetX != null move toward it with speed limit (so keyboard and pointer blend). Keyboard sets targetX = null? Simpler: keyboard adds velocity to paddle.x directly and sets targetX=null; pointer sets targetX.
Demo AI sets targetX too (no pointer). But if user pointermove happens... in demo state we don't need pointer. Fine.
Now the floating score text particles: `texts` array {x,y,vy,life,text,color}.
Ring particle: {x,y,r,maxR,life,color}.
Let's write `explode(b)`:
```js
function explode(x,y,color,count,special){
for(let i=0;i<count;i++){
const a=Math.random()*Math.PI*2, sp=60+Math.random()*380;
particles.push(new Heart(x,y,Math.cos(a)*sp, Math.sin(a)*sp-40, ...));
}
...
}
```
Gravity ~520 px/s². Hearts drift and fall. Life1.1–2.2s, fade out.
Also spawn some "confetti hearts" that flutter with sine motion: `wobble` factor.
Now let's write the code fully and carefully.
Also need `game.comboTimer` decreasing, and combo resets on paddle hit? Typical: combo resets when ball hits paddle. Let's reset combo on paddle hit and on life lost.
Scoring: `points = 100 * (1 + Math.min(combo,20)*0.15) * levelMultiplier`. Keep integer.
HUD updates: update DOM only when values change (or every frame is fine, cheap-ish but avoid layout thrash; do on change).
Let me write out the HTML/CSS.
CSS:
```css
*{box-sizing:border-box}
html,body{margin:0;padding:0;overflow-x:hidden}
body{
min-height:100dvh;
background: radial-gradient(1200px 700px at 50% -10%, #3b1747 0%, #20102b 45%, #140a1c 100%);
color:#fff;
font-family: "Trebuchet MS", "Segoe UI", system-ui, sans-serif;
display:flex; flex-direction:column; align-items:center;
padding:18px 18px 14px;
gap:14px;
}
header{width:min(1180px,100%); display:flex; align-items:center; justify-content:space-between; gap:18px; flex-wrap:wrap;}
```
Stat chips with pink glow.
Stage:
```css
.stage{position:relative; width:min(100%, calc((100dvh - 190px)*1.5385)); aspect-ratio:1000/650; border-radius:26px; overflow:hidden; box-shadow: 0 30px 80px rgba(0,0,0,.55), 0 0 0 1px rgba(255,255,255,.09), 0 0 90px rgba(255,77,125,.18);}
canvas{display:block;width:100%;height:100%;touch-action:none;}
```
Overlay:
```css
.overlay{position:absolute; inset:0; display:grid; place-items:center; text-align:center; background:rgba(20,8,26,.42); backdrop-filter: blur(2px);}
.overlay.hidden{display:none}
```
Overlay panel with big headline. For demo, add `pointer-events:none` so clicks reach canvas? Actually click handler on canvas or stage; overlay click also should start. Attach pointerdown to stage. Fine.
Note: `backdrop-filter` is okay in Chrome.
Now footer: hints row.
Also mute button and restart button in footer with `onclick` handlers (not inline? inline is fine).
Let me now write everything concretely. I'll be careful about the `100dvh` calc for width — use `calc((100dvh - 190px) * 1.5385)`. If dvh unsupported (old), fallback `vh` first in a separate declaration.
Now writing the JS in full detail.
```js
(function(){
'use strict';
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
const W=1000, H=650;
const els = {
score: document.getElementById('score'),
level: document.getElementById('level'),
combo: document.getElementById('combo'),
lives: document.getElementById('lives'),
overlay: document.getElementById('overlay'),
stage: document.getElementById('stage'),
mute: document.getElementById('mute'),
};
```
resize:
```js
let scale=1, dpr=1;
function resize(){
const r = canvas.getBoundingClientRect();
if(!r.width || !r.height) return;
dpr = Math.min(window.devicePixelRatio||1, 2);
canvas.width = Math.round(r.width*dpr);
canvas.height = Math.round(r.height*dpr);
scale = r.width / W;
}
new ResizeObserver(resize).observe(canvas);
resize();
```
Draw transform each frame:
```js
ctx.setTransform(dpr*scale,0,0,dpr*scale,0,0);
```
But shake: `ctx.translate(shakeX, shakeY)` after setTransform.
OK now the code. I'll write it out in the answer. Let me make sure there are no syntax errors.
Key functions list:
- roundRect
- heartPath
- clamp
- resize
- buildLevel
- resetBall, launchBall
- startGame, togglePause, gameOver
- spawnBurst (hearts), addText
- update(dt), updateDemoAI- draw(), drawBackground, drawBricks, drawPaddle, drawBall, drawParticles
- loop(t)
- input handlers
- audio functions
I'll write the code now mentally complete, then output.
Regarding `state` values: 'demo', 'playing', 'paused', 'gameover'.
In demo mode, lives shouldn't hit 0. In `loseLife`, if state==='demo', just resetBall and keep going (no game over), score continues (score shown but resets at start).
Level clear: when bricks array all dead → level++ , buildLevel, resetBall, flash text "LEVEL n", maybe briefly slow. Keep playing (or in demo too).
Let me handle `game.state==='paused'` update skip.
Ball update:
```js
function updateBall(dt){
if(ball.attached){
ball.x = paddle.x + paddle.w/2;
ball.y = paddle.y - ball.r - 6;
ball.trail.length=0;
return;
}
const steps = Math.ceil(Math.max(Math.abs(ball.vx),Math.abs(ball.vy))*dt/8) || 1;
for(let s=0;s<steps;s++) updateBallStep(dt/steps);
}
```
Substeps improve collision at high speed. Good.
updateBallStep:
```js
ball.x += ball.vx*step; ball.y += ball.vy*step;
// walls
if(ball.x < ball.r){ ball.x=ball.r; ball.vx=Math.abs(ball.vx); sfx('wall'); }
if(ball.x > W-ball.r){ ball.x=W-ball.r; ball.vx=-Math.abs(ball.vx); sfx('wall'); }
if(ball.y < ball.r+6){ ball.y=ball.r+6; ball.vy=Math.abs(ball.vy); sfx('wall'); }
// paddle
if(ball.vy>0 && ball.x+ball.r>paddle.x && ball.x-ball.r<paddle.x+paddle.w && ball.y+ball.r>paddle.y && ball.y-ball.r<paddle.y+paddle.h*0.6){
const rel=(ball.x-(paddle.x+paddle.w/2))/(paddle.w/2);
const ang=rel*Math.PI/3;
const sp=Math.min(Math.hypot(ball.vx,ball.vy)*1.015, 900);
ball.vx=Math.sin(ang)*sp; ball.vy=-Math.abs(Math.cos(ang)*sp);
ball.y=paddle.y-ball.r-1;
game.combo=0; updateHUD(); sfx('paddle'); shake(3);
}
// bricks
for(const b of bricks){ if(b.dead) continue; ... collision ... break; }
// bottom
if(ball.y-ball.r>H+20){ loseLife(); }
```
Note paddle collision box: use `paddle.y+...` top of the hamburger; the visual top bun top is at paddle.y. Use rect y = paddle.y - 2 to paddle.y + h*0.55.
Trail push in updateBall after steps.
`shake(amount)` sets game.shake = Math.max(game.shake, amount), decays in update.
Particles update: `p.update(dt)`. Use plain objects with type field and switch in draw. Let's define a class Heart, Ring, Text.
Simplify: one array `particles` of objects with `.type` and fields, plus `update(dt)` inline switch.
I'll implement as classes for clarity.
```js
class Particle {
constructor(x,y,vx,vy,life,color,size,type){...; this.rot=Math.random()*Math.PI*2; this.vr=(Math.random()-0.5)*6; this.wob=Math.random()*Math.PI*2; this.wobSpeed=2+Math.random()*4; }
update(dt){
this.life-=dt;
this.vy += GRAV*dt;
this.vx *= (1 - 1.2*dt); // drag
this.x += this.vx*dt + Math.sin(this.wob)*20*dt;
this.y += this.vy*dt;
this.rot += this.vr*dt;
this.wob += this.wobSpeed*dt;
}
}
```
Draw heart with alpha = clamp(life/maxLife*1.6,0,1) and scale grows at start.
Spark type: small rotated squares with fast fade, gravity low.
Ring type: radius expands, alpha fades.
Text type: floats up.
Good.
Now `drawParticles()` groups by save/restore per particle —900 saves/restores per frame; acceptable but maybe slow-ish. Alternative: set transform manually with translate/rotate per particle (that's what save/restore does). It's fine; canvas can handle ~1000 simple paths at 60fps typically. To be safe, cap hearts at 700 and sparks 250.
Let me cap: in spawnBurst, if particles.length > 1000, remove oldest extras.
Now audio:
```js
let actx=null, muted=false;
function initAudio(){
if(!actx){ const AC=window.AudioContext||window.webkitAudioContext; if(AC) actx=new AC(); }
if(actx && actx.state==='suspended') actx.resume();
}
function tone(freq, dur, type, vol, slideTo, delay=0){
if(!actx||muted) return;
const t=actx.currentTime+delay;
const o=actx.createOscillator(), g=actx.createGain();
o.type=type; o.frequency.setValueAtTime(freq,t);
if(slideTo) o.frequency.exponentialRampToValueAtTime(Math.max(30,slideTo), t+dur);
g.gain.setValueAtTime(0.0001,t);
g.gain.exponentialRampToValueAtTime(vol,t+0.008);
g.gain.exponentialRampToValueAtTime(0.0001,t+dur);
o.connect(g); g.connect(actx.destination); o.start(t); o.stop(t+dur+0.02);
}
function sfx(name, arg){ ... }
```
brick: `tone(220+arg*40, .12,'square',.06, 320+arg*60)` + `tone(880,.06,'triangle',.04,1200)`.
wall: tone(180,.05,'square',.03,140)
paddle: tone(300,.09,'triangle',.06,240)
launch: tone(400,.16,'sine',.07,900)
level: sequence.
life: tone(300,.4,'sawtooth',.06,90)
Fine.
Now let me write the final HTML. I need to be thorough but concise.
One more consideration: "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." — our overlay does exactly that.
Also `window.addEventListener('keydown')` — before click, keys won't reach, that's fine.
Let me write it.
Regarding DOM `lives` display: `<span class="hearts">` filled with ♥ characters colored. Use text '♥' × lives and '♡' for lost. Simple.
Now writing final output. Let me carefully produce the code.
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>Burger Breakout</title>
<style>...</style>
</head>
<body>
<header class="topbar">
<div class="brand">
<div class="burger-badge" aria-hidden="true">
<span class="bun-top"></span><span class="fill"></span><span class="bun-bot"></span>
</div>
<div class="brand-text">
<h1>BURGER BREAKOUT</h1>
<p>smash the bricks · shower the hearts</p>
</div>
</div>
<div class="stats">
<div class="stat"><span class="label">Score</span><strong id="score">0</strong></div>
<div class="stat"><span class="label">Level</span><strong id="level">1</strong></div>
<div class="stat combo"><span class="label">Combo</span><strong id="combo">x1</strong></div>
<div class="stat lives"><span class="label">Lives</span><strong id="lives">♥♥♥</strong></div>
</div>
</header>
<main class="stage" id="stage">
<canvas id="game"></canvas>
<div class="overlay" id="overlay">
<div class="panel">
<div class="eyebrow">DEMO MODE · AUTO-PLAYING</div>
<h2 id="overlay-title">CLICK OR PRESS A KEY TO START</h2>
<p id="overlay-text">Move the burger paddle with your mouse, touch, or ← → keys.<br>Press <b>Space</b> to launch the ball.</p>
<button class="cta" id="overlay-btn">START GAME</button>
</div>
</div>
</main>
<footer class="controls">
<div class="hints">
<span class="key">←</span><span class="key">→</span> move
<span class="sep">•</span>
<span class="key">Space</span> launch / pause
<span class="sep">•</span>
<span class="key">P</span> pause
<span class="sep">•</span> drag on the field </div>
<div class="buttons">
<button id="restart" class="ghost">New game</button>
<button id="mute" class="ghost">Sound: on</button>
</div>
</footer>
<script>...</script>
</body>
</html>
```
CSS burger-badge: a small44x36 CSS burger with layered rounded divs. Nice.
Now JS. Let me write it out fully and carefully, then double check syntax.
Potential issue: `ResizeObserver` callback calls resize which reads getBoundingClientRect — fine.
`updateHUD()` called when values change.
Let me define `buildLevel`:
```js
function buildLevel(){
bricks.length=0;
const cols=10;
const rows=Math.min(4+Math.floor((game.level-1)*0.8), 8);
const gapX=10, gapY=12, marginX=42, top=112, bh=36;
const bw=(W-marginX*2-gapX*(cols-1))/cols;
for(let r=0;r<rows;r++){
for(let c=0;c<cols;c++){
const pal=PALETTE[(r+game.level-1)%PALETTE.length];
const hp=(game.level>1 && r<2 && (c+r)%3===0)?2:1;
bricks.push({
x:marginX+c*(bw+gapX), y:top+r*(bh+gapY), w:bw, h:bh,
c1:pal[0], c2:pal[1], hp, maxHp:hp,
special: Math.random()<0.12,
dead:false, hitFlash:0
});
}
}
}
```
Rows max 8 → top 112 + 8*48 = 496 → too low (paddle at 564). 8 rows: 112+8*48=496 bottom. paddle.y = H-86=564. OK gap of 68. Fine.
Level 1: rows =4+0 = 4 → wait `Math.min(4+Math.floor(0*0.8),8)` = 4. Good. Level 2: 4+0.8→floor 0 → 4. Hmm floor(1*0.8)=0. Level 3: floor(2*.8)=1 → 5 rows. Let's use `Math.min(4 + Math.floor(game.level*0.75), 8)`: level1→4, l2→5, l3→6, l4→7, l5+→8. Good.
Ball speed: `ball.speed = 430 + (game.level-1)*28`, capped 720.
Now `resetBall()`:
```js
function resetBall(){
ball.attached=true;
ball.vx=0; ball.vy=0;
ball.x=paddle.x+paddle.w/2; ball.y=paddle.y-ball.r-8;
ball.trail.length=0;
ball.autoLaunch = (game.state==='demo') ? 0.7 : 0;
}
```
In update, if attached and game.state==='demo' → countdown and launch. In playing, wait for input (but maybe auto-launch after 2.5s to avoid idle screenshots... screenshot at 3s happens in demo mode anyway since user hasn't clicked). Fine; but let's also auto-launch in playing after 2.2s? No, better to keep user control. Actually if a user clicks to start and doesn't press space, ball sits. That's standard. But add hint text drawn on canvas near paddle: "PRESS SPACE / TAP TO LAUNCH".
`launchBall()`:
```js
function launchBall(){
ball.attached=false;
const ang=(Math.random()*0.7-0.35);
ball.vx=Math.sin(ang)*ball.speed;
ball.vy=-Math.cos(ang)*ball.speed;
sfx('launch');
}
```
loseLife:
```js
function loseLife(){
spawnBurst(ball.x, Math.min(ball.y, H-40), 26, '#FF6B9D');
game.lives--; game.combo=0;
sfx('life');
if(game.lives<=0 && game.state!=='demo'){ endGame(); }
else { resetBall(); updateHUD(); }
}
```
endGame: state='gameover', show overlay with score and "PLAY AGAIN" button.
`startGame()`: state='playing'; score=0; lives=3; level=1; combo=0; particles.length=0; texts.length=0; buildLevel(); resetBall(); hide overlay; updateHUD(); initAudio().
`startDemo()` initial: state='demo'; level=1; score=0; lives=3; buildLevel(); resetBall(); show demo overlay.
Toggle pause: if playing → paused + overlay "PAUSED"; if paused → resume.
Overlay show/hide: `els.overlay.classList.toggle('hidden', !show)`.
Overlay content setter:
```js
function setOverlay(eyebrow,title,text,btnText){...}
```
Now the demo AI in update:
```js
function updateAI(dt){
if(ball.attached){ aiTimer-=dt; if(aiTimer<=0) launchBall(); return; }
const t=(paddle.y-ball.y)/Math.max(ball.vy,1);
let pred=ball.x;
if(t>0&&t<4&&ball.vy>0){
pred=ball.x+ball.vx*t;
// reflect
const period=...; simpler: while(pred<0||pred>W){ if(pred<0) pred=-pred; if(pred>W) pred=2*W-pred; }
} else {
pred=W/2 + Math.sin(game.time*0.8)*180;
}
const target=clamp(pred-paddle.w/2*0, 0, W-paddle.w); // careful
paddle.targetX = clamp(pred - paddle.w/2 + (Math.random()-0.5)*8, 0, W-paddle.w);
}
```
Then generic paddle movement handles targetX with speed limit (max speed for AI, so it can miss occasionally... but better to make AI reliable). Set AI paddle speed high enough.
Paddle movement update:
```js
function updatePaddle(dt){
const maxSpeed = (game.state==='demo'? 1200 :900);
if(paddle.targetX!==null){
const diff=paddle.targetX-paddle.x;
const step=clamp(diff, -maxSpeed*dt, maxSpeed*dt);
paddle.x+=step;
if(Math.abs(diff)<1.2) paddle.x=paddle.targetX;
}
if(keys.left) { paddle.x-=paddle.speed*dt; paddle.targetX=null; }
if(keys.right){ paddle.x+=paddle.speed*dt; paddle.targetX=null; }
paddle.x=clamp(paddle.x, 8, W-paddle.w-8);
if(ball.attached){ ball.x=paddle.x+paddle.w/2; }
}
```
Note: `paddle.targetX` for player pointer set to absolute x - w/2.
Now the drawing of the background hearts: `bgHearts` array with slow drift, generated once.
Alright, writing the final answer now. I'll be meticulous with syntax.
For `roundRect`, note `ctx.roundRect` exists in modern Chrome but I'll use my own helper for safety.
Text drawing for floating score: `ctx.font = 'bold 22px Trebuchet MS, sans-serif'`.
Also draw a top "banner" in canvas? Not needed.
Draw `drawPaddle` — ensure gradient creation each frame is fine.
Let me finalize numbers:
- paddle.w=176, paddle.h=62, paddle.y=H-88=562.
- ball.r=11.
Wait: paddle bottom = 562+62 = 624, H=650. Ball falls to bottom → lost at y>H+20. Good.
Bricks top 112, so there's space at top for HUD? We don't draw HUD in canvas (HTML header does). Fine — maybe draw a thin neon line at top? Not needed.
Let me add a subtle "kitchen counter" floor gradient at the bottom of the canvas for depth.
Now, code writing time. I'll produce it.
Also need to ensure `particles` removal: filter with a loop backwards splicing. Fine.
Final check on the overlay for demo: `.panel` with button "START GAME" — clicking button triggers startGame (and stopPropagation not needed).
I'll attach `els.stage.addEventListener('pointerdown', ...)` → initAudio(); if state demo/gameover → startGame(); else if ball.attached → launchBall(); else if paused → resume.
`pointermove` on stage → set paddle target (only when state==='playing'), compute from rect.
Also `pointerdown` then drag: set `dragging=true`, pointermove while dragging updates. Just update always on pointermove (no button check) — simpler and works with mouse hover.
For touch, pointermove works with `touch-action:none` on canvas.
Now writing.
Regarding `game.time` increment for AI and background.
`combo` display: `x${1+Math.min(game.combo,25)*0.1}` maybe show multiplier like "x1.4". Let's show `x${(1+Math.min(game.combo,25)*0.12).toFixed(1)}`.
Score on brick: `Math.round(100 * mult * (b.special?2.5:1) * (b.maxHp>1?1.5:1))`.
OK writing now for real. I'll write the JS in one go, carefully.
Also: `updateHUD()` sets textContent.
Potential subtle bug: `Math.min(4 + Math.floor(game.level*0.75), 8)` at level 1 = 4+0 = 4 ✓.
Let me write the `drawBall` with trail:
```js
function drawBall(){
for(let i=0;i<ball.trail.length;i++){
const t=ball.trail[i], a=(i/ball.trail.length)*0.22;
ctx.fillStyle=`rgba(255,120,170,${a})`;
ctx.beginPath(); ctx.arc(t.x,t.y,ball.r*(0.35+0.6*i/ball.trail.length),0,Math.PI*2); ctx.fill();
}
const g=ctx.createRadialGradient(ball.x-ball.r*0.35, ball.y-ball.r*0.45, ball.r*0.15, ball.x, ball.y, ball.r);
g.addColorStop(0,'#FFF2F6'); g.addColorStop(.45,'#FF6FA5'); g.addColorStop(1,'#D62562');
ctx.fillStyle=g; ctx.beginPath(); ctx.arc(ball.x,ball.y,ball.r,0,Math.PI*2); ctx.fill();
ctx.strokeStyle='rgba(255,255,255,.5)'; ctx.lineWidth=1.5; ctx.stroke();
}
```
Trail push: `ball.trail.push({x:ball.x,y:ball.y}); if(ball.trail.length>12) ball.trail.shift();` once per frame in updateBall.
Good. Write it.
One more thought on performance: gradients created per brick per frame (up to 80 bricks). That's 80 gradient objects/frame — acceptable. Could cache per row. I'll cache gradient creation using a map keyed by color? Gradients depend on brick y. Instead use solid color fill + overlay gradient... Simplify: create gradient per brick is fine (Chrome handles it).
Actually to be safer with 80 bricks × 60fps, I'll precompute each brick's gradient once at build time and store it (`b.grad = ctx.createLinearGradient(...)`). Gradients are tied to the context but reusable. Do it in buildLevel. But ctx state at creation only matters for the coordinate space — createLinearGradient uses current transform? Gradients are in user space at draw time, so creating during buildLevel with the same transform scale is fine as long as we don't rely on a different transform. Since draw uses setTransform each frame with the same scale, gradient coords (world units) work. Good — but resize changes scale... gradient coords are in world space so they remain valid.
Hmm, careful: gradient coordinates are interpreted in the user space *at draw time*, so storing them is fine.
I'll just create them per draw call for simplicity and safety; 80 is nothing.
Final: write the answer.
Let me write the whole script text now (drafting):
```js
(function(){
'use strict';
const canvas=document.getElementById('game');
const ctx=canvas.getContext('2d');
const stage=document.getElementById('stage');
const overlay=document.getElementById('overlay');
const ovEyebrow=document.getElementById('ov-eyebrow');
const ovTitle=document.getElementById('ov-title');
const ovText=document.getElementById('ov-text');
const ovBtn=document.getElementById('ov-btn');
const elScore=document.getElementById('score');
const elLevel=document.getElementById('level');
const elCombo=document.getElementById('combo');
const elLives=document.getElementById('lives');
const btnMute=document.getElementById('mute');
const btnRestart=document.getElementById('restart');
const W=1000,H=650;
const GRAV=560;
const PALETTE=[
['#FF7A9E','#E23E6B'],
['#FFC75C','#E39222'],
['#8FE06A','#4FA83C'],
['#FF9A5C','#E4622B'],
['#C58BFF','#8648D8'],
['#63D6E8','#2A93B8'],
['#FF6FB5','#C22C7A'],
['#F5E27A','#C9A227']
];
```
`clamp`.
Now writing the real thing in the answer. I'll include comments sparsely.
For the `hearts` lives display: `elLives.innerHTML` with spans colored: `'<i class="on">♥</i>'.repeat(lives) + '<i class="off">♡</i>'.repeat(3-lives)`.
Time to output. I need to double check every `{`/`}` mentally as I write.
Also: no `alert`. OK.
One risk: `new ResizeObserver(resize).observe(canvas)` — canvas is display:block width 100% height 100%; fine.
Another: initial `resize()` called before layout? Script at end of body, so layout exists. Good.
Let's write out the final code.
I also want the demo overlay text to be readable over the moving game. Use a dark panel with border and glow.
Write the answer now.
For `drawBackground`:
```js
function drawBackground(){
const g=ctx.createLinearGradient(0,0,0,H);
g.addColorStop(0,'#2A1236'); g.addColorStop(.55,'#3A1641'); g.addColorStop(1,'#24102C');
ctx.fillStyle=g; ctx.fillRect(0,0,W,H);
const rg=ctx.createRadialGradient(W/2,H*0.32,20,W/2,H*0.32,W*0.72);
rg.addColorStop(0,'rgba(255,110,170,.16)'); rg.addColorStop(1,'rgba(255,110,170,0)');
ctx.fillStyle=rg; ctx.fillRect(0,0,W,H);
// grid dots
ctx.fillStyle='rgba(255,255,255,.05)';
for(let x=20;x<W;x+=40) for(let y=20;y<H;y+=40) ctx.fillRect(x,y,2,2);
// ambient hearts
for(const h of bgHearts){ ... }
// floor glow
const fg=ctx.createLinearGradient(0,H-140,0,H);
fg.addColorStop(0,'rgba(255,90,150,0)'); fg.addColorStop(1,'rgba(255,90,150,.13)');
ctx.fillStyle=fg; ctx.fillRect(0,H-140,W,140);
}
```
grid dots: 25*17=425 fillRects — fine.
bgHearts: 7 hearts with {x,y,size,speed,alpha,rot}.
OK, output.
Let me write drawBrick with hitFlash decay handled in update.
`b.hitFlash` decrement in update loop.
Now finally write. Also handle `game.shake` decay: `game.shake=Math.max(0,game.shake-dt*28)`, and in draw: `ctx.translate((Math.random()-.5)*game.shake, (Math.random()-.5)*game.shake)`.
Writing the answer!
Also `document.addEventListener('visibilitychange', ()=>{ if(document.hidden && game.state==='playing') togglePause(); })` — this could cause the screenshot to show paused if hidden... it's fine.
Hmm, actually careful: if the iframe becomes hidden during screenshot? Unlikely. I'll skip the visibility pause to be safe. Actually keep it simple: skip.
Final code output now.
Regarding the CTA button styling and `:focus-visible`. Fine.
Let me write out everything.
One more check on `heartPath` used by particles and background and the special brick icon.
```js
function heartPath(ctx,s){
ctx.beginPath();
ctx.moveTo(0,s*0.60);
ctx.bezierCurveTo(-s*0.16,s*0.40,-s*0.62,s*0.12,-s*0.62,-s*0.16);
ctx.bezierCurveTo(-s*0.62,-s*0.52,-s*0.16,-s*0.58,0,-s*0.26);
ctx.bezierCurveTo(s*0.16,-s*0.58,s*0.62,-s*0.52,s*0.62,-s*0.16);
ctx.bezierCurveTo(s*0.62,s*0.12,s*0.16,s*0.40,0,s*0.60);
ctx.closePath();
}
```
Looks like a heart. Good.
Now the answer.