Hamburger breakoutOne-line
Qwen3.8 Flash
by QwenReasoningClean
Running live in a sandboxed frame. Click inside it before using the keyboard.
How it went
- Total time
- 12 min 11 s
- Cost
- $0.025
- Output tokens
- 52,519
- Spent thinking
- 41,070 tokens
- Speed
- 72.3 tok/s
- Console errors
- 0
- First token after
- 4.2 s
- First visible text after
- 10 min 33 s
- Prompt tokens
- 412
- Code size
- 28.7 KB
- Finish reason
- stop
- Served by
- Alibaba
- Model id
qwen/qwen3.8-flash- Reasoning sent
model default- Max output
- 64,000 tokens
- Generated
- Nov 11, 2026
What it was asked
The prompt
“i want a breakout game where the paddle is a hamburger and every brick you break explodes into tons of little hearts”
System prompt, sent with every prompt
You are generating a self-contained web demo. It will be rendered automatically in a sandboxed iframe and compared side by side with the output of other AI models given the exact same task. Output rules: - Reply with exactly ONE fenced code block containing the complete file: ```html (or ```svg when the task asks for an SVG). No text before or after the code block. - Everything must be inline: CSS in <style>, JavaScript in <script>. No external files, images, fonts or network requests, except the CDN URLs explicitly allowed by the task. - Do not use localStorage, sessionStorage, cookies, IndexedDB, alert(), confirm(), prompt() or window.open(): they are blocked in the sandbox and will throw. - The page must look right in a 1280×800 viewport and adapt gracefully down to 375px wide. No horizontal scrollbar. - Something meaningful must be visible on first paint: a screenshot is taken automatically about 3 seconds after load, without any interaction. - Everything that makes the demo interesting must be visible within the first seconds: nothing hidden behind a high score, a later level or a long play session. - Games must start in a self-playing demo mode (a simple AI plays) behind a "Click or press a key to start" message, so the game can be judged in action without playing. - The iframe only receives keyboard events after the user clicks it: if the demo uses the keyboard, display a clear "Click or press a key to start" message. - The code must run with zero errors in the console of the latest Chrome.
Code run-1.html · 1150 lines
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Hamburger Breakout Heart Explosion</title>
<style>
html, body {
margin: 0;
width: 100%;
height: 100%;
overflow: hidden;
background: #0c0511;
font-family: system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif;
color: #fff;
}
#stage {
position: fixed;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
padding: 12px;
box-sizing: border-box;
overflow: hidden;
}
canvas {
display: block;
width: min(100%, calc((100vh - 26px) * 1.6), 1280px);
max-width: 100%;
height: auto;
aspect-ratio: 16 / 10;
background: #10081a;
border-radius: 14px;
box-shadow:
0 22px 70px rgba(255, 45, 118, 0.18),
inset 0 0 0 1px rgba(255,255,255,0.07);
touch-action: none;
}
#start {
position: fixed;
left: 50%;
bottom: 14px;
transform: translateX(-50%);
width: min(94vw, 760px);
box-sizing: border-box;
text-align: center;
font-weight: 900;
font-size: clamp(15px, 2.2vw, 24px);
line-height: 1.15;
padding: 11px 16px;
border-radius: 999px;
color: #fff;
background:
linear-gradient(90deg, rgba(255,77,109,0.24), rgba(255,150,90,0.18), rgba(255,77,109,0.24));
border: 1px solid rgba(255,255,255,0.26);
box-shadow: 0 12px 34px rgba(0,0,0,0.38), 0 0 30px rgba(255,70,130,0.18);
text-shadow: 0 2px 12px rgba(0,0,0,0.75);
pointer-events: none;
opacity: 1;
transition: opacity 0.35s ease;
}
#start span {
display: block;
margin-top: 3px;
font-weight: 700;
font-size: 0.7em;
color: rgba(255,255,255,0.82);
}
#start.hide {
opacity: 0;
}
@media (max-height: 600px) {
#start {
bottom: 6px;
padding: 7px 12px;
}
}
</style>
</head>
<body>
<div id="stage">
<canvas id="game" width="960" height="600"></canvas>
</div>
<div id="start">
🍔 Click or press a key to start
<span>Self-playing demo · bricks explode into tons of little hearts</span>
</div>
<script>
(function () {
'use strict';
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
const startEl = document.getElementById('start');
const W = 960;
const H = 600;
const TAU = Math.PI * 2;
let dpr = 1;
let time = 0;
let state = 'demo'; // 'demo', 'playing', 'gameover'
let score = 0;
let lives = 3;
let level = 1;
let message = '';
let messageTimer = 0;
const keys = { left: false, right: false };
let inputMode = 'pointer';
let pointerX = W / 2;
let audioCtx = null;
const paddle = {
x: W / 2,
y: H - 95,
w: 174,
h: 76,
squish: 0
};
const ball = {
x: W / 2,
y: H / 2,
r: 12,
vx: 180,
vy: -420,
speed: 430,
angle: 0,
trail: []
};
let bricks = [];
let aliveCount = 0;
const particles = [];
const texts = [];
const bgHearts = [];
const MAX_PARTICLES = 860;
function clamp(v, a, b) {
return v < a ? a : v > b ? b : v;
}
function setSize() {
dpr = Math.min(window.devicePixelRatio || 1, 2);
canvas.width = Math.floor(W * dpr);
canvas.height = Math.floor(H * dpr);
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
}
function ensureAudio() {
try {
if (!audioCtx) {
const AC = window.AudioContext || window.webkitAudioContext;
if (AC) audioCtx = new AC();
}
if (audioCtx && audioCtx.state === 'suspended') {
audioCtx.resume();
}
} catch (e) {
audioCtx = null;
}
}
function tone(freq, dur = 0.12, type = 'sine', vol = 0.04) {
if (!audioCtx) return;
try {
const t = audioCtx.currentTime;
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.type = type;
osc.frequency.setValueAtTime(freq, t);
gain.gain.setValueAtTime(vol, t);
gain.gain.exponentialRampToValueAtTime(0.0001, t + dur);
osc.connect(gain);
gain.connect(audioCtx.destination);
osc.start(t);
osc.stop(t + dur + 0.03);
} catch (e) {}
}
function playBounce() {
if (state === 'playing') tone(250 + Math.random() * 110, 0.05, 'square', 0.018);
}
function playBrick(hue) {
if (state !== 'playing') return;
const f = 430 + (hue % 180) * 3 + Math.random() * 180;
tone(f, 0.11, 'triangle', 0.035);
tone(f * 1.37, 0.08, 'sine', 0.022);
}
function playLose() {
if (state !== 'playing') return;
tone(180, 0.28, 'sawtooth', 0.05);
}
function playLevel() {
if (state !== 'playing') return;
tone(520, 0.12, 'triangle', 0.04);
setTimeout(() => tone(760, 0.16, 'sine', 0.04), 90);
}
function updateOverlay() {
if (state === 'demo') {
startEl.innerHTML =
'🍔 Click or press a key to start' +
'<span>Self-playing demo · bricks explode into tons of little hearts</span>';
startEl.classList.remove('hide');
} else if (state === 'gameover') {
startEl.innerHTML =
'💔 Game Over — Score ' + score +
'<span>Click or press any key to play again</span>';
startEl.classList.remove('hide');
} else {
startEl.innerHTML =
'🍔 Mouse / touch / ← → to move' +
'<span>Every brick bursts into hearts</span>';
startEl.classList.add('hide');
}
}
function setMessage(text, seconds = 1.5) {
message = text;
messageTimer = seconds;
}
function drawRoundRect(x, y, w, h, r) {
r = Math.min(r, Math.abs(w) / 2, Math.abs(h) / 2);
if (w <= 0 || h <= 0 || r < 0) {
ctx.beginPath();
return;
}
ctx.beginPath();
ctx.moveTo(x + r, y);
ctx.lineTo(x + w - r, y);
ctx.quadraticCurveTo(x + w, y, x + w, y + r);
ctx.lineTo(x + w, y + h - r);
ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
ctx.lineTo(x + r, y + h);
ctx.quadraticCurveTo(x, y + h, x, y + h - r);
ctx.lineTo(x, y + r);
ctx.quadraticCurveTo(x, y, x + r, y);
ctx.closePath();
}
function drawHeart(x, y, s, rot, fill) {
if (s <= 0) return;
ctx.save();
ctx.translate(x, y);
ctx.rotate(rot);
const k = s / 18;
ctx.scale(k, k);
ctx.beginPath();
ctx.moveTo(0, -3);
ctx.bezierCurveTo(0, -7, -6, -10, -9, -6);
ctx.bezierCurveTo(-12, -1, -8, 5, 0, 11);
ctx.bezierCurveTo(8, 5, 12, -1, 9, -6);
ctx.bezierCurveTo(6, -10, 0, -7, 0, -3);
ctx.closePath();
ctx.fillStyle = fill;
ctx.fill();
ctx.restore();
}
function addText(x, y, text, hue) {
if (texts.length > 45) texts.splice(0, 12);
texts.push({
x: x,
y: y,
vy: -54,
text: text,
life: 1.2,
max: 1.2,
hue: hue
});
}
function makeBricks(lv) {
bricks.length = 0;
aliveCount = 0;
const cols = 12;
const rows = Math.max(4, Math.min(8, 4 + Math.floor((lv - 1) / 2)));
const gap = 7;
const side = 22;
const top = 86;
const bh = 30;
const bw = (W - side * 2 - gap * (cols - 1)) / cols;
const pattern = (lv - 1) % 5;
function pushBrick(r, c, include, hpOverride) {
if (!include) return;
const hp = hpOverride || (lv >= 3 && r === 0 ? 2 : 1);
const hue = (335 + r * 12 + c * 6 + lv * 21) % 360;
const value = 10 * (rows - r) * (hp > 1 ? 2 : 1);
bricks.push({
x: side + c * (bw + gap),
y: top + r * (bh + gap),
w: bw,
h: bh,
hue: hue,
value: value,
hp: hp,
maxHp: hp,
alive: true,
seed: Math.random() * TAU
});
aliveCount++;
}
for (let r = 0; r < rows; r++) {
for (let c = 0; c < cols; c++) {
let include = true;
if (pattern === 1) {
include = ((r + c) % 2) === 0;
} else if (pattern === 2) {
include = (c % 3 !== r % 3);
} else if (pattern === 3) {
include = !(r === rows - 1 && (c % 5 === 0 || c % 5 === 1));
} else if (pattern === 4) {
include = ((r * 7 + c * 11) % 9) !== 0;
}
pushBrick(r, c, include);
}
}
if (aliveCount < 12) {
bricks.length = 0;
aliveCount = 0;
for (let r = 0; r < rows; r++) {
for (let c = 0; c < cols; c++) {
pushBrick(r, c, true, 1);
}
}
}
}
function resetBall() {
ball.x = paddle.x;
ball.y = paddle.y - paddle.h / 2 - ball.r - 6;
const dir = Math.random() < 0.5 ? -1 : 1;
const a = dir * (0.18 + Math.random() * 0.45);
ball.vx = Math.sin(a) * ball.speed;
ball.vy = -Math.abs(Math.cos(a) * ball.speed);
ball.trail.length = 0;
}
function resetGame() {
score = 0;
lives = 3;
level = 1;
ball.speed = 430;
particles.length = 0;
texts.length = 0;
paddle.x = W / 2;
paddle.y = H - 95;
paddle.squish = 0;
makeBricks(level);
resetBall();
updateOverlay();
}
function beginPlaying(mode) {
ensureAudio();
if (state !== 'playing') {
resetGame();
state = 'playing';
setMessage('Play! 🍔', 1.2);
}
inputMode = mode;
updateOverlay();
}
function getPointerLogicalX(e) {
const rect = canvas.getBoundingClientRect();
if (rect.width <= 0) return W / 2;
const x = ((e.clientX - rect.left) / rect.width) * W;
return clamp(x, 0, W);
}
function spawnHearts(b, big) {
const count = big ? 68 + Math.floor(Math.random() * 30) : 12;
while (particles.length + count > MAX_PARTICLES) {
particles.splice(0, 24);
}
for (let i = 0; i < count; i++) {
const a = Math.random() * TAU;
const speed = big ? 40 + Math.random() * 340 : 25 + Math.random() * 140;
const hueBase = big ? b.hue + Math.random() * 70 - 35 : b.hue + Math.random() * 40 - 20;
let hue = (hueBase + 360) % 360;
if (Math.random() < (big ? 0.42 : 0.22)) {
hue = (330 + Math.random() * 45) % 360;
}
const life = big ? 0.75 + Math.random() * 1.55 : 0.25 + Math.random() * 0.55;
particles.push({
x: b.x + Math.random() * b.w,
y: b.y + Math.random() * b.h,
vx: Math.cos(a) * speed,
vy: Math.sin(a) * speed - (big ? 85 : 45),
size: big ? 4 + Math.random() * 19 : 3 + Math.random() * 9,
rot: Math.random() * TAU,
spin: (Math.random() - 0.5) * 8,
life: life,
maxLife: life,
hue: hue,
sat: 88 + Math.random() * 12,
light: 48 + Math.random() * 34,
grav: 150 + Math.random() * 290,
drag: 1.5 + Math.random() * 1.5
});
}
}
function spawnMissHearts(x, y) {
for (let i = 0; i < 24; i++) {
if (particles.length >= MAX_PARTICLES) break;
const a = Math.random() * Math.PI;
const speed = 70 + Math.random() * 220;
const life = 0.55 + Math.random() * 0.9;
particles.push({
x: x + (Math.random() - 0.5) * 60,
y: y + (Math.random() - 0.5) * 24,
vx: Math.cos(a) * speed,
vy: -Math.abs(Math.sin(a) * speed + 60),
size: 5 + Math.random() * 16,
rot: Math.random() * TAU,
spin: (Math.random() - 0.5) * 7,
life: life,
maxLife: life,
hue: (330 + Math.random() * 45) % 360,
sat: 55,
light: 72,
grav: 130 + Math.random() * 220,
drag: 1.8
});
}
}
function destroyBrick(b) {
if (!b.alive) return;
b.alive = false;
aliveCount--;
score += b.value;
addText(b.x + b.w / 2, b.y + b.h / 2, '+' + b.value, b.hue);
spawnHearts(b, true);
playBrick(b.hue);
}
function normalizeBall() {
if (!isFinite(ball.vx) || !isFinite(ball.vy) || ball.vx === 0 && ball.vy === 0) {
ball.vx = 0;
ball.vy = -ball.speed;
}
const maxTilt = 1.18;
const minVy = Math.cos(maxTilt) * ball.speed;
if (Math.abs(ball.vy) < minVy) {
ball.vy = (ball.vy < 0 ? -1 : 1) * minVy;
}
const len = Math.hypot(ball.vx, ball.vy);
ball.vx = ball.vx / len * ball.speed;
ball.vy = ball.vy / len * ball.speed;
}
function collideWalls() {
if (ball.x - ball.r < 0) {
ball.x = ball.r;
ball.vx = Math.abs(ball.vx);
playBounce();
}
if (ball.x + ball.r > W) {
ball.x = W - ball.r;
ball.vx = -Math.abs(ball.vx);
playBounce();
}
if (ball.y - ball.r < 0) {
ball.y = ball.r;
ball.vy = Math.abs(ball.vy);
playBounce();
}
}
function collidePaddle() {
if (ball.vy <= 0) return;
const left = paddle.x - paddle.w / 2;
const right = paddle.x + paddle.w / 2;
const top = paddle.y - paddle.h / 2;
const bottom = paddle.y + paddle.h / 2;
if (
ball.y + ball.r >= top &&
ball.y - ball.r <= bottom &&
ball.x >= left &&
ball.x <= right
) {
const hit = clamp((ball.x - paddle.x) / (paddle.w / 2), -1, 1);
ball.speed = Math.min(ball.speed + 12, 830);
const angle = hit * 0.95;
ball.vx = Math.sin(angle) * ball.speed;
ball.vy = -Math.abs(Math.cos(angle) * ball.speed);
ball.y = top - ball.r;
paddle.squish = 1;
normalizeBall();
playBounce();
}
}
function collideBricks() {
for (let i = 0; i < bricks.length; i++) {
const b = bricks[i];
if (!b.alive) continue;
const cx = b.x + b.w / 2;
const cy = b.y + b.h / 2;
const dx = Math.abs(ball.x - cx);
const dy = Math.abs(ball.y - cy);
if (dx <= b.w / 2 + ball.r && dy <= b.h / 2 + ball.r) {
const overlapX = b.w / 2 + ball.r - dx;
const overlapY = b.h / 2 + ball.r - dy;
if (overlapX < overlapY) {
if (ball.x < cx) {
ball.vx = -Math.abs(ball.vx);
ball.x = cx - b.w / 2 - ball.r - 1;
} else {
ball.vx = Math.abs(ball.vx);
ball.x = cx + b.w / 2 + ball.r + 1;
}
} else {
if (ball.y < cy) {
ball.vy = -Math.abs(ball.vy);
ball.y = cy - b.h / 2 - ball.r - 1;
} else {
ball.vy = Math.abs(ball.vy);
ball.y = cy + b.h / 2 + ball.r + 1;
}
}
ball.speed = Math.min(ball.speed + 4, 860);
normalizeBall();
b.hp--;
if (b.hp <= 0) {
destroyBrick(b);
} else {
spawnHearts(b, false);
addText(b.x + b.w / 2, b.y + b.h / 2, '+5', b.hue);
score += 5;
tone(260 + b.hue * 0.7, 0.08, 'sine', 0.025);
}
return true;
}
}
return false;
}
function loseBall() {
if (state === 'gameover') return;
spawnMissHearts(ball.x, H - 34);
if (state === 'demo') {
setMessage('Miss! Demo continues', 1.1);
resetBall();
return;
}
lives = Math.max(0, lives - 1);
playLose();
if (lives <= 0) {
state = 'gameover';
setMessage('Game Over 💔', 3);
updateOverlay();
} else {
setMessage('Lost a heart!', 1.4);
resetBall();
}
}
function levelClear() {
if (state === 'gameover') return;
level++;
ball.speed = Math.min(430 + level * 32, 850);
makeBricks(level);
resetBall();
setMessage('Level ' + level + ' — More Hearts! 🍔❤️', 2.2);
playLevel();
for (let i = 0; i < 8; i++) {
addText(W / 2 + (Math.random() - 0.5) * 320, H * 0.32 + (Math.random() - 0.5) * 90, '❤', (330 + Math.random() * 50) % 360);
}
}
function updateBackgroundHearts(dt) {
for (const b of bgHearts) {
b.y -= (b.vy + Math.sin(time * 0.8 + b.phase) * 7) * dt;
b.x += (b.vx + Math.cos(time * 0.55 + b.phase) * 9) * dt;
b.rot += b.spin * dt;
if (b.y < -b.size * 1.3) {
b.y = H + b.size * 0.8;
b.x = Math.random() * W;
}
if (b.x < -b.size) b.x = W + b.size * 0.5;
if (b.x > W + b.size) b.x = -b.size * 0.5;
}
}
function updateParticles(dt) {
for (let i = particles.length - 1; i >= 0; i--) {
const p = particles[i];
p.life -= dt;
if (p.life <= 0 || p.x < -90 || p.x > W + 90 || p.y > H + 90 || p.y < -120) {
particles[i] = particles[particles.length - 1];
particles.pop();
continue;
}
p.vy += p.grav * dt;
const drag = Math.exp(-p.drag * dt);
p.vx *= drag;
p.vy *= drag;
p.x += p.vx * dt;
p.y += p.vy * dt;
p.rot += p.spin * dt;
}
}
function updateTexts(dt) {
for (let i = texts.length - 1; i >= 0; i--) {
const t = texts[i];
t.life -= dt;
if (t.life <= 0) {
texts[i] = texts[texts.length - 1];
texts.pop();
continue;
}
t.y += t.vy * dt;
t.vy *= Math.exp(-2.3 * dt);
}
}
function updatePaddle(dt) {
paddle.squish *= Math.exp(-8 * dt);
if (state === 'demo') {
const target = ball.x + ball.vx * 0.055;
const dx = target - paddle.x;
const max = 950 * dt;
paddle.x += clamp(dx, -max, max);
} else if (state === 'playing') {
if (inputMode === 'pointer' && !keys.left && !keys.right) {
paddle.x += (pointerX - paddle.x) * Math.min(1, 18 * dt);
}
if (inputMode === 'keyboard' || keys.left || keys.right) {
const speed = 640;
if (keys.left) paddle.x -= speed * dt;
if (keys.right) paddle.x += speed * dt;
}
}
paddle.x = clamp(paddle.x, paddle.w / 2 + 10, W - paddle.w / 2 - 10);
}
function updateBall(dt) {
if (state === 'gameover') return;
const steps = Math.max(1, Math.ceil(ball.speed * dt / 6));
const sdt = dt / steps;
for (let s = 0; s < steps; s++) {
ball.x += ball.vx * sdt;
ball.y += ball.vy * sdt;
collideWalls();
collidePaddle();
if (collideBricks()) break;
}
ball.angle += (ball.vx > 0 ? 4 : -4) * dt;
ball.trail.push({
x: ball.x,
y: ball.y,
life: 0.32,
max: 0.32,
size: ball.r * 1.85,
rot: ball.angle
});
while (ball.trail.length > 22) ball.trail.shift();
for (let i = ball.trail.length - 1; i >= 0; i--) {
ball.trail[i].life -= dt;
if (ball.trail[i].life <= 0) ball.trail.splice(i, 1);
}
if (ball.y > H + ball.r * 4) {
loseBall();
}
if (aliveCount === 0) {
levelClear();
}
}
function update(dt) {
time += dt;
if (messageTimer > 0) messageTimer -= dt;
updateBackgroundHearts(dt);
updatePaddle(dt);
updateBall(dt);
updateParticles(dt);
updateTexts(dt);
}
function drawBackground() {
const bg = ctx.createLinearGradient(0, 0, 0, H);
bg.addColorStop(0, '#18091e');
bg.addColorStop(0.42, '#22102e');
bg.addColorStop(1, '#0c0511');
ctx.fillStyle = bg;
ctx.fillRect(0, 0, W, H);
ctx.save();
ctx.globalCompositeOperation = 'lighter';
const glow = ctx.createRadialGradient(W / 2, H * 0.28, 10, W / 2, H * 0.28, H * 0.62);
glow.addColorStop(0, 'rgba(255, 85, 140, 0.17)');
glow.addColorStop(0.55, 'rgba(255, 140, 90, 0.06)');
glow.addColorStop(1, 'rgba(255, 140, 90, 0)');
ctx.fillStyle = glow;
ctx.fillRect(0, 0, W, H);
ctx.restore();
}
function drawBackgroundHearts() {
for (const b of bgHearts) {
ctx.globalAlpha = b.alpha;
drawHeart(b.x, b.y, b.size, b.rot, 'hsl(' + b.hue + ', 72%, 58%)');
}
ctx.globalAlpha = 1;
}
function drawBricks() {
for (const b of bricks) {
if (!b.alive) continue;
ctx.save();
ctx.fillStyle = 'rgba(0,0,0,0.18)';
drawRoundRect(b.x + 4, b.y + 6, b.w, b.h, 8);
ctx.fill();
const hpR = b.hp / b.maxHp;
const grad = ctx.createLinearGradient(b.x, b.y, b.x, b.y + b.h);
grad.addColorStop(0, 'hsl(' + b.hue + ', 96%, ' + (70 + hpR * 4) + '%)');
grad.addColorStop(0.52, 'hsl(' + b.hue + ', 98%, ' + (56 + hpR * 4) + '%)');
grad.addColorStop(1, 'hsl(' + ((b.hue + 22) % 360) + ', 90%, 33%)');
ctx.fillStyle = grad;
drawRoundRect(b.x, b.y, b.w, b.h, 8);
ctx.fill();
ctx.fillStyle = 'rgba(255,255,255,0.34)';
drawRoundRect(b.x + 4, b.y + 3, b.w - 8, b.h * 0.31, 5);
ctx.fill();
ctx.globalAlpha = 0.18;
drawHeart(b.x + b.w / 2, b.y + b.h / 2 + 1, Math.min(14, b.h * 0.52), b.seed, 'rgba(255,255,255,1)');
ctx.globalAlpha = 1;
if (b.hp < b.maxHp) {
ctx.strokeStyle = 'rgba(255,255,255,0.45)';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(b.x + b.w * 0.28, b.y + b.h * 0.42);
ctx.lineTo(b.x + b.w * 0.47, b.y + b.h * 0.56);
ctx.lineTo(b.x + b.w * 0.72, b.y + b.h * 0.29);
ctx.stroke();
}
ctx.restore();
}
}
function drawPaddle() {
const pw = paddle.w;
const ph = paddle.h;
const hw = pw / 2;
const hh = ph / 2;
const sx = 1 + paddle.squish * 0.08;
const sy = 1 - paddle.squish * 0.06;
ctx.save();
ctx.translate(paddle.x, paddle.y);
ctx.scale(sx, sy);
ctx.globalAlpha = 0.28;
ctx.fillStyle = '#000';
ctx.beginPath();
ctx.ellipse(0, hh + 10, hw + 14, 11, 0, 0, TAU);
ctx.fill();
ctx.globalAlpha = 1;
let g = ctx.createLinearGradient(0, hh - 20, 0, hh);
g.addColorStop(0, '#c8814b');
g.addColorStop(1, '#7d4621');
ctx.fillStyle = g;
drawRoundRect(-hw + 10, hh - 20, pw - 20, 20, 10);
ctx.fill();
g = ctx.createLinearGradient(0, 0, 0, 18);
g.addColorStop(0, '#743f2b');
g.addColorStop(1, '#391d14');
ctx.fillStyle = g;
drawRoundRect(-hw + 5, 0, pw - 10, 18, 8);
ctx.fill();
ctx.fillStyle = '#f4c15d';
drawRoundRect(-hw + 10, -8, pw - 20, 10, 3);
ctx.fill();
ctx.fillStyle = '#ffe18d';
for (let i = 0; i < 6; i++) {
const x = -hw + 18 + i * ((pw - 36) / 5);
ctx.beginPath();
ctx.moveTo(x, 2);
ctx.lineTo(x + 7, 2);
ctx.lineTo(x + 3.5, 13);
ctx.closePath();
ctx.fill();
}
ctx.fillStyle = '#68c35e';
ctx.beginPath();
const steps = 22;
const l = -hw + 4;
const r = hw - 4;
const topL = -24;
ctx.moveTo(l, topL);
for (let i = 0; i <= steps; i++) {
const t = i / steps;
ctx.lineTo(l + (r - l) * t, topL + Math.sin(t * Math.PI * 5) * 3);
}
for (let i = steps; i >= 0; i--) {
const t = i / steps;
ctx.lineTo(l + (r - l) * t, topL + 14 + Math.sin(t * Math.PI * 5 + 1) * 2);
}
ctx.closePath();
ctx.fill();
ctx.fillStyle = '#ef3b5f';
drawRoundRect(-hw + 18, -34, pw - 36, 10, 5);
ctx.fill();
ctx.fillStyle = 'rgba(255,255,255,0.18)';
drawRoundRect(-hw + 22, -32, pw - 44, 4, 2);
ctx.fill();
g = ctx.createLinearGradient(0, -hh, 0, -24);
g.addColorStop(0, '#e6b174');
g.addColorStop(1, '#b27840');
ctx.fillStyle = g;
ctx.beginPath();
ctx.moveTo(-hw + 6, -24);
ctx.quadraticCurveTo(-hw + 5, -hh + 1, 0, -hh + 1);
ctx.quadraticCurveTo(hw - 5, -hh + 1, hw - 6, -24);
ctx.closePath();
ctx.fill();
ctx.fillStyle = 'rgba(255,255,255,0.22)';
ctx.beginPath();
ctx.ellipse(-hw * 0.27, -hh * 0.62, hw * 0.28, 4.5, -0.28, 0, TAU);
ctx.fill();
ctx.fillStyle = '#fff0cf';
const seeds = [
[-0.29, -0.73],
[-0.16, -0.82],
[0.00, -0.86],
[0.16, -0.82],
[0.29, -0.73],
[-0.08, -0.74],
[0.08, -0.74]
];
for (const s of seeds) {
ctx.save();
ctx.translate(s[0] * pw, s[1] * hh);
ctx.rotate(s[0] * 0.7);
ctx.beginPath();
ctx.ellipse(0, 0, 3.2, 1.9, 0, 0, TAU);
ctx.fill();
ctx.restore();
}
ctx.restore();
}
function drawBall() {
for (const t of ball.trail) {
const a = clamp(t.life / t.max, 0, 1);
ctx.globalAlpha = a * 0.25;
drawHeart(t.x, t.y, t.size * (0.68 + 0.55 * a), t.rot, 'hsl(330, 100%, 78%)');
}
ctx.globalAlpha = 1;
ctx.save();
ctx.globalCompositeOperation = 'lighter';
ctx.globalAlpha = 0.23;
ctx.fillStyle = 'rgba(255,115,175,0.8)';
ctx.beginPath();
ctx.arc(ball.x, ball.y, ball.r * 1.85, 0, TAU);
ctx.fill();
ctx.restore();
const grad = ctx.createRadialGradient(
ball.x - ball.r * 0.35,
ball.y - ball.r * 0.35,
ball.r * 0.2,
ball.x,
ball.y,
ball.r
);
grad.addColorStop(0, '#ffffff');
grad.addColorStop(0.34, '#ffb9d8');
grad.addColorStop(1, '#ff2d78');
ctx.fillStyle = grad;
ctx.beginPath();
ctx.arc(ball.x, ball.y, ball.r, 0, TAU);
ctx.fill();
ctx.fillStyle = 'rgba(255,255,255,0.74)';
drawHeart(ball.x, ball.y + 1, ball.r * 1.18, ball.angle, 'rgba(255,255,255,0.74)');
}
function drawParticles() {
for (const p of particles) {
const a = clamp(p.life / p.maxLife, 0, 1);
if (a <= 0) continue;
const alpha = a * a;
ctx.globalAlpha = alpha;
const size = p.size * (0.62 + 0.72 * a);
drawHeart(p.x, p.y, size, p.rot, 'hsl(' + p.hue + ', ' + p.sat + '%, ' + p.light + '%)');
}
ctx.globalAlpha = 1;
}
function drawTexts() {
for (const t of texts) {
const a = clamp(t.life / t.max, 0, 1);
ctx.globalAlpha = a;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.font = '900 22px system-ui';
ctx.fillStyle = 'hsl(' + t.hue + ', 95%, 75%)';
ctx.fillText(t.text, t.x, t.y);
}
ctx.globalAlpha = 1;
}
function drawUI() {
ctx.save();
ctx.textBaseline = 'middle';
ctx.fillStyle = 'rgba(255,255,255,0.08)';
drawRoundRect(12, 12, 230, 50, 25);
ctx.fill();
ctx.textAlign = 'left';
ctx.font = '700 13px system-ui';
ctx.fillStyle = 'rgba(255,255,255,0.68)';
ctx.fillText('SCORE', 28, 28);
ctx.font = '900 22px system-ui';
ctx.fillStyle = '#fff';
ctx.fillText(score, 28, 48);
for (let i = 0; i < lives; i++) {
drawHeart(132 + i * 30, 37, 23, Math.sin(time * 2 + i) * 0.12, '#ff4b6d');
}
ctx.fillStyle = 'rgba(255,255,255,0.08)';
drawRoundRect(W - 126, 12, 114, 50, 25);
ctx.fill();
ctx.textAlign = 'right';
ctx.font = '700 13px system-ui';
ctx.fillStyle = 'rgba(255,255,255,0.68)';
ctx.fillText('LEVEL', W - 26, 28);
ctx.font = '900 22px system-ui';
ctx.fillStyle = '#fff';
ctx.fillText(level, W - 26, 48);
if (state === 'demo') {
ctx.textAlign = 'center';
ctx.font = '800 14px system-ui';
ctx.fillStyle = 'rgba(255,255,255,0.62)';
ctx.fillText('SELF-PLAYING DEMO', W / 2, 32);
}
ctx.restore();
}
function drawMessage() {
if (messageTimer <= 0) return;
const a = clamp(messageTimer / 0.8, 0, 1);
ctx.save();
ctx.globalAlpha = a;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.font = '900 40px system-ui';
ctx.fillStyle = '#fff';
ctx.shadowColor = 'rgba(255, 70, 135, 0.85)';
ctx.shadowBlur = 28;
ctx.fillText(message, W / 2, H * 0.48);
ctx.restore();
}
function draw() {
ctx.clearRect(0, 0, W, H);
drawBackground();
drawBackgroundHearts();
drawBricks();
drawPaddle();
drawBall();
drawParticles();
drawTexts();
drawUI();
drawMessage();
}
function initBackgroundHearts() {
for (let i = 0; i < 28; i++) {
bgHearts.push({
x: Math.random() * W,
y: Math.random() * H,
vx: (Math.random() - 0.5) * 14,
vy: 8 + Math.random() * 24,
size: 24 + Math.random() * 78,
hue: 325 + Math.random() * 50,
rot: Math.random() * TAU,
spin: (Math.random() - 0.5) * 0.7,
phase: Math.random() * TAU,
alpha: 0.035 + Math.random() * 0.065
});
}
}
window.addEventListener('resize', setSize);
setSize();
window.addEventListener('keydown', function (e) {
const moveLeft = e.code === 'ArrowLeft' || e.code === 'KeyA';
const moveRight = e.code === 'ArrowRight' || e.code === 'KeyD';
if (moveLeft || moveRight || e.code === 'Space' || e.code === 'Enter') {
e.preventDefault();
}
if (moveLeft) keys.left = true;
if (moveRight) keys.right = true;
if (state !== 'playing') {
beginPlaying('keyboard');
} else if (moveLeft || moveRight) {
inputMode = 'keyboard';
}
}, { passive: false });
window.addEventListener('keyup', function (e) {
if (e.code === 'ArrowLeft' || e.code === 'KeyA') keys.left = false;
if (e.code === 'ArrowRight' || e.code === 'KeyD') keys.right = false;
});
canvas.addEventListener('pointerdown', function (e) {
pointerX = getPointerLogicalX(e);
if (state !== 'playing') {
beginPlaying('pointer');
} else {
inputMode = 'pointer';
}
if (e.pointerType === 'touch') e.preventDefault();
}, { passive: false });
window.addEventListener('pointermove', function (e) {
if (state === 'playing') {
pointerX = getPointerLogicalX(e);
inputMode = 'pointer';
}
}, { passive: true });
let last = 0;
function frame(ts) {
if (!last) last = ts;
let dt = (ts - last) / 1000;
last = ts;
if (dt > 0.05) dt = 0.05;
update(dt);
draw();
requestAnimationFrame(frame);
}
initBackgroundHearts();
resetGame();
updateOverlay();
requestAnimationFrame(frame);
})();
</script>
</body>
</html>Reasoning trace (41,070 tokens)
We need answer user's request: generate self-contained web demo: breakout game where paddle is hamburger and every brick you break explodes into tons of little hearts. Need output one html code block only, no text. Need ensure visible in first seconds, self-playing demo behind start message. Inline. No external. Need probably canvas. Need design responsive. Must run in Chrome no console errors. Need keyboard/mouse. Need start overlay message. Since self-playing AI before click. User can click or press key. Need avoid localStorage etc.
We need produce complete HTML. Consider requirements:
- Breakout game: paddle shaped like hamburger, bricks, ball(s). Bricks break into little hearts particles.
- Paddle is a hamburger: draw buns, lettuce, tomato, patty.
- Every brick broken explodes into tons of little hearts. Use particles.
- self-playing demo behind "Click or press a key to start". AI controls paddle to track ball until user interacts. Then user controls mouse/touch/keyboard.
- visible on first paint: overlay maybe canvas game running with demo.
- adapt down to 375.
- no horizontal scrollbar.
- Need maybe sound? Could use WebAudio oscillator for bounces/destroy? Nice. But no external. Ensure no errors if audio unavailable; can start audio on user gesture only. Demo mode maybe silent.
- Full code.
Need decide implementation details.
Structure:
<html><head><style>...
<body><div id="app"><canvas></canvas><div id="overlay">...
<script>...
</script></body></html>
Responsive: canvas logical size maybe 960x600, scale to fit container with CSS. Use devicePixelRatio for crisp. Draw using logical coordinates.
Game loop requestAnimationFrame with dt. State: mode 'demo' or 'playing'. On click/keydown -> if demo start playing, if game over restart? Need message.
- Could overlay with instruction and score. Need hide overlay? The user wants start message. We can render in DOM overlay or canvas. Simpler: DOM overlay with "Click or press a key to start". It can fade when start. But to be visible first and self-playing behind? If overlay covers entire screen, can't see game. Could have semi-transparent overlay at bottom or top. Use a banner overlay with message, game still visible. Maybe central but transparent? We can use pointer-events none except start message? Actually click anywhere in canvas starts. Use overlay positioned at bottom, pointer-events none.
- Need no blocking? "Click or press a key to start" must receive? We can attach listeners to window/canvas. Overlay pointer-events none so clicks reach canvas or window.
- Maybe show at top: "🍔 Breakout • Self-playing demo — Click or press any key to start".
Brick layout: rows x cols. Colors: maybe heart-themed: red/pink/orange. Each brick has color, hp maybe 1. For explosion hearts.
Heart particles: array. On brick break, spawn N hearts (e.g. 60) with velocity random, size, color, rotate, gravity? Since hearts float upward? "explodes into tons" maybe burst outward with gravity and fade. Could add some upward drift. Particles heart shape.
Ball: maybe heart? or tomato? Could be a glowing heart ball? Need physics.
Paddle: hamburger. Collision: use ball reflection. Paddle drawn with elliptical buns etc. Need ensure collision rectangle? Use bounding box. Add angle based on hit point.
Controls:
- Mouse/touch: move paddle horizontally with pointer. Use rect. In playing mode. Keyboard: arrow left/right, maybe A/D.
- Demo: AI moves paddle toward ball. Also if ball below etc. Ensure AI not perfect but enough. Could set paddle x = ball.x target with easing, clamp. Serve ball at start.
Game states:
- start ball at top? Demo ball maybe moving slowly. Need immediate action: bricks visible, ball bouncing.
- When all bricks destroyed -> next level? Need something meaningful early. Could generate levels quickly: after clear, maybe spawn new bricks with slight harder. But within 3 sec enough hearts from first brick? Could ensure ball hits brick quickly by initial trajectory down. Self-playing demo should start moving.
- Lives? 3 hearts. Score. If lose life, ball reset near paddle. In demo continues.
Performance: particle max maybe limit 600. Use simple fill.
Drawing hearts:
function drawHeart(ctx, x,y,size, rot, color) {ctx.save(); translate rotate scale; beginPath(); moveTo(0, -0.4*size); bezierCurveTo(0.4*size, -0.8*size, 1.2*size, -0.1*size, 0,0.8*size) ... Need reliable heart. Use parametric: heart(x,y,s): ctx.moveTo(0, s*0.25); ctx.bezierCurveTo(0,0, -s*0.5,-s*0.25, -s*0.5,0);? Let's recall good heart. Use canvas path:
ctx.beginPath();
const w = s, h = s;
ctx.moveTo(0, h/4);
ctx.quadraticCurveTo(0, 0, w/4, 0);
ctx.quadraticCurveTo(w/2, 0, w/2, h/4);
ctx.quadraticCurveTo(w/2, h/2, 0, h);
ctx.quadraticCurveTo(-w/2, h/2, -w/2, h/4);
ctx.quadraticCurveTo(-w/2, 0, -w/4, 0);
ctx.quadraticCurveTo(0, 0, 0, h/4);
But centered. Simpler use known heart shape:
function heartPath(ctx, s) {
ctx.moveTo(0, s * 0.25);
ctx.bezierCurveTo(0, -s*0.1, -s*0.5, -s*0.2, -s*0.5, s*0.1);
ctx.bezierCurveTo(-s*0.5, s*0.45, -s*0.15, s*0.6, 0, s*0.85);
ctx.bezierCurveTo(s*0.15, s*0.6, s*0.5, s*0.45, s*0.5, s*0.1);
ctx.bezierCurveTo(s*0.5, -s*0.2, 0, -s*0.1, 0, s*0.25);
}
Need closepath fill. This draws heart with top lobes y -0.2 and bottom y 0.85, not centered. Could draw centered by y offset. Or use scale -1? Let's test mentally. For size 10: start (0,2.5) lower center? top left curve to (-5,-2), then to bottom (0,8.5), etc. Maybe heart upside? We want point down. Start at cleft top (0,0)? Path:
beginPath();
moveTo(x, y - size/4) // top center cleft?
bezierCurveTo(x, y - size/2, x - size/2, y - size/2, x - size/2, y - size/4);
bezierCurveTo(x - size/2, y, x, y + size/2, x, y + size/2);
bezierCurveTo(x, y + size/2, x + size/2, y, x + size/2, y - size/4);
bezierCurveTo(x + size/2, y - size/2, x, y - size/2, x, y - size/4);
fill. Need point down? Top cleft y - 0.25? bottom point y +0.5? Maybe okay.
Alternatively use text emoji ❤ with font. But tons particles maybe slow? Could use fillText hearts. Better path.
Need ensure no errors with ctx transform and reset.
Visual polish:
- Background gradient, vignette. Maybe stars/bokeh? Bricks have highlight. Paddle drawn hamburger: lower bun ellipse/rectangle, patty, cheese, lettuce, tomato, upper bun. Add sesame seeds.
- Ball maybe a glowing heart or cherry? Could draw heart ball to match. But physics easier circle. Draw circle with heart? Maybe ball is a tomato/heart. Use radial gradient. Or a glowing pink heart. Collision circle.
Brick explosion hearts: For each destroyed brick, spawn maybe 35 hearts plus debris colored hearts. Could have tons: 50-80. Particles: x,y, vx,vy, size, life, maxLife, hue, rot, spin, gravity, drag, float.
- Colors from brick hue. Also pink/red hearts.
- Use additive? Draw globalCompositeOperation 'lighter' for glowing. Maybe okay but could overbright. Use 'source-over' for hearts.
Particles include heart shape and sparkles. Limit to 900. If max, remove older.
Sound:
- Web Audio simple: beep for brick: oscillator, gain envelope, frequency random. Start audioContext on user gesture only, so if demo no sound. When user clicks to start, can play sounds. But if first brick before user gesture? Could no sound. Fine. Or create audio context after gesture. Need not use external.
- Use try/catch.
Game logic details:
- canvas logical width = 960, height = 600. Paddle width maybe 160, height 60? Hamburger bounding box. Ball radius 10. Speed base 380 px/s. On level increases.
- Bricks: rows = maybe 4 initially, cols = 12, brick width ~ (width - padding*2 - gap*cols)/cols, height 30. Top margin 90. Colors by row. Score per brick.
- Collision:
ball movement in steps sub-stepping to prevent tunneling? Use simple collision with AABB.
Paddle collision: if ball within expanded rect and moving down: reflect vy, adjust vx based on intersection.
Walls: bounce.
Bricks: find nearest row/col via grid if structured? We can just iterate alive bricks. Since 48 bricks, fine. Use AABB circle. If collision, destroy, reflect. To avoid multiple hits, destroy brick.
- After destroy, maybe split ball? Could power-ups? Not necessary. But "tons hearts" only.
- Lives: top hearts UI? Draw lives as hearts.
- Game over: message in overlay; if click restart. Need demo continues after gameover? Maybe if lives 0, reset demo/playing. Simpler: after gameover, ball reset with lives 3 and bricks new if in demo or playing? Could always restart level. For user, click to start if demo, click if game over restart. Need no confusing. Use gameState: 'demo', 'playing', 'gameover', 'levelcomplete'? If demo and ball lost, reset life and continue. If playing and ball lost, lives--. If lives 0 -> state 'gameover', ball maybe paused? We can show overlay and wait for click/key to reset to playing.
- Level clear: bricks gone -> state 'levelup' maybe spawn new bricks after delay? To keep action, automatically generate next level and continue without stopping? If playing, just create new bricks, level++, message. In demo also.
- Serve ball: from paddle top or brick area? For immediate bricks: In demo, serve ball from top moving downward toward bricks? But ball can't go below paddle? If start at top center, bounce off walls, hit bricks. Could serve from paddle top moving down (angle upward? In breakout, ball starts near paddle and goes up. Need action within seconds. Make start at top y=300 maybe with vy=-speed/2 to bricks? Actually bricks at top y 80-200, ball moving up. If serve from paddle, it goes up after bounce. For immediate, set ball at (width/2, height/2) vy=-speed, vx random. Or serve on top moving down? Gravity? Breakout ball bounces. Start at bottom moving up toward bricks; first hit around 0.5 sec. Good.
Self-playing: In demo, AI should hit ball reliably. Use target = ball.x + ball.vx*0.05. Paddle easing maybe 0.08. Need avoid weird when ball hidden. Add small prediction: if ball.vy > 0 and y > paddle.y - 100, target ball.x else center? It can still catch. Use paddle speed max 900 px/s in demo. Easing with clamp. Could use direct x=ball.x if demo.
- Need demo start maybe user not click; no overlay blocking. Show top banner. Could auto-play forever.
Input handling:
- window.addEventListener('pointerdown', startOrResume). Also keydown.
- On pointermove/touchmove, update targetX.
- On keydown left/right set keys.
- On keyup clear.
- If state demo, on first input state playing.
- If state playing, keyboard move.
- If gameover, input restarts to playing. Need maybe prevent page scroll with space? Use e.preventDefault on arrows/space? Keydown for any key starts.
DOM overlay:
- #wrap full screen flex. canvas display block width 100vw max 1280? CSS aspect ratio? Use canvas width/height attributes = logical * dpr. CSS width 100%; height auto. Need fit in viewport without scroll. Use max-height 100vh.
- For mobile: 375. Need controls maybe touch. Paddle follows finger.
- Start message: fixed bottom? Use div overlay pointer-events none. Contains text and controls. Could show score? Game score drawn canvas. Maybe DOM top banner: "🍔 Hamburger Breakout — hearts everywhere".
- Need screenshot first 3s sees game and message. Use central translucent card? If central covers, not see behind? Could semi-transparent enough. Maybe bottom bar with message. But requirement: display clear "Click or press a key to start". Put bottom center large.
- Overlay can have CSS animation pulse.
HTML:
<!doctype html><html lang=en><meta charset=utf-8><meta viewport>
<title>
<style>
html,body{margin:0;height:100%;background:#120a14;overflow:hidden;font-family...}
#stage{position:fixed;inset:0;display:flex;align-items:center;justify-content:center;padding:8px}
canvas{width:100%; height:100%; max-width:1280px; max-height:800px; aspect-ratio: 16/10; background:transparent; border-radius:16px; box-shadow... touch-action:none}
#start{position:fixed;left:50%; bottom:18px; transform:translateX(-50%); pointer-events:none; color...; padding... background rgba(255,255,255,0.12); backdrop-filter? maybe avoid unsupported? Use rgba. border-radius; text...; max-width 92vw; text-align:center}
@media (min-height) ...
</style>
<body><div id=stage><canvas id=game></canvas></div><div id=start>...</div><script>
...
</script></body>
Need avoid horizontal scrollbar. stage overflow hidden.
Canvas sizing:
const W=960, H=600;
function resize(){ const dpr=Math.min(window.devicePixelRatio||1,2); const rect=canvas.getBoundingClientRect(); canvas.width=Math.round(rect.width*dpr); canvas.height=Math.round(rect.height*dpr); scale? }
Better: set canvas.width = W*dpr, height=H*dpr, ctx.setTransform(dpr,0,0,dpr,0,0). CSS handles scaling. Use logical W/H. On resize if rect changes no need. But for responsive, use CSS max. So canvas.width fixed 960*dpr; ctx transform. But if canvas width changes? CSS scales. Good. Need set dpr. Could use ctx.setTransform(canvas.width/W,0,0,canvas.height/H,0,0).
- If devicePixelRatio changes? not needed.
Game variables:
let state='demo', score=0, lives=3, level=1, message='', messageTimer=0;
ball = {x,y,r,vx,vy,speed, attached:false? maybe no attach. Could have serve delay}
paddle = {x,y,w,h,speed,targetX, vx}
keys = {left,right}
particles=[]
heartsUI? maybe not.
balls array? Could powerups? Maybe multiple balls? The task only breakout. Could add occasional split? Keep simple. Maybe when ball hits special brick? Not necessary.
For more fun, every level has different brick patterns? We can generate rows/cols varying. But immediate.
- Bricks: array object {x,y,w,h,color,alive,hp}. Need maybe pattern with gaps? For hearts, use colors.
- generateBricks(level): rows = min(5+level,8), cols=12; top margin 110. Some patterns: full, stripes, alternating, pyramid. Use patterns:
if level % 4 == 1 full rectangle
%2 maybe checker
%3 diamond? Could simple. Need visible first: full.
But after clear in demo, generate new.
- Color: hue = (row*20 + col*10 + level*30)%360 or palette.
- Brick score = 10* (row+1) maybe.
- Destroy: spawnParticles(x,y,w,h,color,hue). score += value; message? maybe show floating text.
Floating score texts: small array texts {x,y,text,life,color}. When brick destroyed, add "+20" plus hearts.
Particle functions:
spawnHearts(x,y, hueCount) maybe spawn 60:
for i<50:
ang = -Math.PI/2 + random(-1.2,1.2)? explosion outward.
speed random 50..350.
vx = cos(ang)*speed; vy=sin(ang)*speed - 60 maybe upward.
size=4+rand*14.
life=0.8+rand*1.2.
rot random -1..1 spin.
color = hsl(hue + rand*40 -20, 100%, 55+rand20).
Add gravity 500? If explode outward, gravity makes fall. Could hearts rise like confetti: vy negative then gravity. Set gravity=220, drag=1.5. Some sparkle.
For tons: 70 hearts per brick. If 48 destroyed could be thousands. Need limit. But per brick enough. Use global max 1200. If too many, remove oldest. For performance, use simple.
- On brick hit, also spawn few rectangular debris maybe hearts enough. Maybe add "burst ring" effect.
Particles update:
p.vy += p.grav*dt; p.vx *= (1 - p.drag*dt) clamp; p.x += p.vx*dt; p.y += p.vy*dt; p.life -= dt; p.rot += p.spin*dt.
If y > H+30 remove. If x out remove.
Draw hearts: ctx.globalAlpha = alpha; color; path.
Maybe use shadowBlur for glow but expensive. Could skip.
Use composite 'lighter' for sparkles only. But hearts source-over.
Draw ball:
- Draw circle with radial gradient maybe cherry/heart. Use ball as glowing heart with motion trail? Could store trail array of past positions for nice effect. Trail length 12, draw fading hearts behind ball? Could be extra but nice.
- Ball shape: draw small heart rotated according to velocity? Use drawHeart(ball.x, ball.y, ball.r*2, rot, pink). But collision uses circle. If heart ball, visual okay. Could draw heart in ball. But breakout ball should be circle. Maybe draw circle with heart pattern: radial gradient and small white highlight. Or make ball a heart: path heart size radius*1.6. Hearts particle theme.
- Use trail: array of {x,y,life}. Add ball position each frame. Draw trail hearts small fading. Nice but not needed.
Collision details:
function collideWalls()
if x-r<0 {x=r; vx=abs(vx); playPing; }
if x+r>W {x=W-r; vx=-abs(vx)}
if y-r<0 {y=r; vy=abs(vy)}
if y-r>H+? lose life if y > H + r maybe. Actually if bottom below H: ball lost. Check y - r > H? maybe.
Paddle collision:
const px=paddle.x - paddle.w/2, py=paddle.y - paddle.h/2, pw=paddle.w, ph=paddle.h.
if ball.vy >0 and y+r >= py and y-r <= py+ph and x >= px-r and x <= px+pw+r:
hitPos = (x - paddle.x)/(paddle.w/2) clamp -1..1; angle = hitPos * max angle (1.2 rad); speed=ball.speed; vx = sin(angle)*speed; vy = -abs(cos(angle)*speed); y=py-r; if demo maybe sound if state playing.
Potential stuck: move ball outside.
Brick collision:
for brick alive: if ball intersects AABB. Need reflection axis. Use closest point: cx=clamp(ball.x,b.x,b.x+b.w), cy=clamp(ball.y,b.y,b.y+b.h); dx=ball.x-cx; dy=ball.y-cy; dist2=dx*dx+dy*dy; if <= r*r. Determine overlap: If dist==0 or center inside brick? Use prevY? Simpler:
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 both >0, collision. If overlapX < overlapY: horizontal reflect else vertical reflect. But if ball corner, either. Destroy.
Need ensure reflection out of brick. Set ball.x += vx direction sign*overlap? Could just bounce: if reflect x, vx = -vx; else vy=-vy; then move ball a bit. Since destroying brick, less stuck.
Potential multiple bricks in frame. Use loop over bricks; if collision and not destroyed, destroy and reflect. Only one per frame? Good.
- If ball speed high, substeps: update ball in N = Math.ceil(speed*dt / 8). For each substep collide. This improves. We'll implement moveBall(dt) with substeps.
Lose life:
if y > H + ball.r*3:
lives--; spawn some hearts at bottom? message. If lives <=0: state='gameover'; message='Game Over...'. In demo: reset? If state demo lives-- and if 0 reset to 3? Maybe for demo, keep infinite lives. Do: if demo and lose, ball reset, no lives change? Or show hearts but not gameover. We'll maintain lives only in playing. But in demo draw lives? Could show 3. Better: in demo lives=3 and lose resets ball but no decrement? But user expects maybe lives. Could decrement in demo too, if 0 reset level. Simpler: if state=='demo' {resetBall();} if playing lives-- etc.
- Reset ball: attach to paddle? In demo: set at paddle top y=paddle.y-paddle.h/2 - r, x=paddle.x, vy=-speed, vx random. Or set from bottom? For action, serve from bottom moving up? Actually if reset after lose, ball near bottom and moving up, immediate brick hit. Good.
Level clear:
if no bricks: level++, message "Level 2: more hearts!", generateBricks(level), resetBall, ball.speed increase? Maybe +8.
- score maybe no reset.
Input to start:
function startGame(){ if state==='demo' { state='playing'; hide overlay; init? message='Play!' ; if audio unlock; } else if state==='playing' { // maybe if overlay hidden do nothing } else if state==='gameover' { reset(); state='playing'; }
}
- If click/keydown any, if state demo -> playing. If already playing and user presses maybe ignore. If pointerdown on canvas and state playing, move paddle? We need pointer events update. For touch, pointerdown then pointermove sets targetX. If state demo click starts but should also set paddle target from pointer? yes.
Message overlay update:
- DOM #start text: If state demo, show "Click or press any key to start".
If playing: maybe hide? Could show "Mouse/touch/←→ to move" but requirement only start. Hide after start? If hidden, no message. Screenshot at 3s sees game and maybe still overlay because no interaction. Good. After interaction, hide.
If gameover: show "Game Over — Click to play again". Need DOM update.
Use startEl.style.opacity = (state==='demo'||state==='gameover')?'1':'0'. Also pointer-events none.
Audio:
let audioCtx=null;
function ensureAudio(){ if(!audioCtx){try{audioCtx=new (window.AudioContext||window.webkitAudioContext)()}catch(e){}} if(audioCtx && audioCtx.state==='suspended') audioCtx.resume();}
function tone(freq,dur,type='sine',gain=0.05){ if(!audioCtx) return; const o=audioCtx.createOscillator(); const g=audioCtx.createGain(); o.type=type; o.frequency.value=freq; g.gain.setValueAtTime(0,audioCtx.currentTime); g.gain.linearRampToValueAtTime(gain,audioCtx.currentTime+0.01); g.gain.exponentialRampToValueAtTime(0.001,audioCtx.currentTime+dur); o.connect(g); g.connect(audioCtx.destination); o.start(); o.stop(audioCtx.currentTime+dur+0.02);}
Call only if state playing? For bounce maybe. In demo silent. But if ensureAudio called before gesture? It's okay maybe context not running? Actually audioCtx created inside gesture start only. For brick before start no sound.
- Brick destroy: tone(400+random*800, 0.12, 'triangle'); maybe multiple random for heart.
Drawing bricks:
Each brick: fill with rounded rect. Use gradient top/bottom. Add small heart pattern? Maybe every brick is a burger? No, brick explosion into hearts. Could make bricks like cheese/tomato slices. Draw rounded rect with color, inner shine, small heart icon. Need tons hearts on break.
- Draw outline glow. If brick hp? maybe not.
function roundRect(ctx,x,y,w,h,r){...}
Brick colors palette: ['#ff4b64','#ff8c66','#ffd166','#ef476f','#ff70a6','#9d4edd'].
Use hue stored. Draw heart small on each brick to hint? Could.
Drawing hamburger paddle:
Need bounding box: width maybe 180, height 80. Draw bottom bun: rounded brown ellipse/rect. Patty: dark brown rounded. Cheese: yellow zigzag? Lettuce: green wavy. Tomato: red circles. Upper bun: tan dome with sesame. Add bounce on collision? Maybe squish animation (paddle.squish). Add on hit. Could draw with ctx.save translate rotate? Simpler static relative to center. Need ensure ball collides with top of paddle. Use paddle.y is center. Bounding top = y - h/2. Draw shapes within box.
- Hamburger visual:
bottomY = y + h*0.35. bottom bun: roundRect from x-w/2 to x+w/2, height 18, radius 9, brown gradient.
patty: height 16 at y + 0.05h? dark brown.
cheese: yellow with triangular drips? draw rectangle then triangles along bottom. Or skip.
lettuce: green wavy using path along middle.
tomato: red circles? Maybe tomato slices as ellipses across upper middle.
upper bun: ellipse/dome height 30, radius; tan; sesame seeds small white ellipses. Add highlight.
- Could be too complex. Use layered rounded rects:
paddle.h=64, w=170.
bottom bun: rect h=14 at y+24, r=8, brown #a9683f.
patty: rect h=16 at y+8, #5a3625.
cheese: rect h=8 at y-2, #f4d06f with triangles.
lettuce: path wavy at y-12, #69b755.
tomato: circles? red rect? Use 3 red ellipses at y-16, #ef5350.
upper bun: ellipse arc from x-w/2 to x+w/2, top y-38 to y-26, #d69d66 with seeds. Need draw path:
ctx.beginPath(); ctx.moveTo(x-w/2, top); ctx.quadraticCurveTo(x-w/2, y-44, x, y-46); ctx.quadraticCurveTo(x+w/2, y-44, x+w/2, top); ctx.closePath(); fill. Where top = y-24? Good.
Seeds: small ellipses at positions.
Need ensure collision rectangle aligns top of hamburger: top = y - h/2 = y -32 if h=64. Upper bun top maybe y - 46 if visual exceeds collision. Better paddle.h=76 visual top y-38. But collision box top y-38. Set h=76. Visual fits within. Let's define paddle.h = 72, half=36. Draw from -36 to +36. upper bun top at -32. Good.
- Paddle bounce: if hit, squish = 1, decays. scaleX = 1 + squish*0.15, scaleY = 1 - squish*0.1. Or just visual.
Background:
- Dark gradient with soft radial glow. Maybe "kitchen" pattern? Add floating hearts in background? Could add slow background hearts (parallax) to enhance theme. But not too heavy. Use a few large translucent hearts drifting. Or sparkles.
- Draw floor? A plate? Could add subtle horizontal line under paddle.
UI:
- Score top-left, lives hearts top-left? Level top-right. Message center with fade.
- Lives: draw hearts count (lives) top left with score. Use drawHeart for life icons. If lives <=0 none.
- Score: font.
- Message: center text with shadow.
Need ensure first paint: game starts immediately with requestAnimationFrame. Overlay visible. Bricks.
Potential issues:
- CSS aspect-ratio maybe supported in Chrome; ok.
- pointer events: canvas touch-action none.
- window keydown may be blocked until iframe clicked. The overlay says click. Good.
- On keydown any key to start. If state demo, start. If playing and key pressed maybe use as movement. We should not prevent all keys. For arrows/space prevent default if needed. For any key, if state demo start. If state gameover restart.
- Need pointerdown inside canvas maybe also start if clicking anywhere? window pointerdown. But if state playing, pointerdown might set target. Fine.
Testing mentally code:
Variables W,H, canvas, ctx.
init functions: createBricks(level), resetBall(), start particles.
Let's design code thoroughly.
HTML skeleton:
```html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Hamburger Breakout Hearts</title>
<style>
html,body {margin:0;width:100%;height:100%;overflow:hidden;background:#0d0710;font-family: system-ui, sans-serif;}
#stage{position:fixed; inset:0; display:flex; align-items:center; justify-content:center; padding:10px; box-sizing:border-box;}
canvas{display:block; width:100%; max-width:1280px; height:auto; max-height:800px; aspect-ratio: 16 / 10; background:#0d0710; border-radius:14px; box-shadow:0 20px 80px rgba(255,80,130,.18), inset 0 0 0 1px rgba(255,255,255,.07); touch-action:none;}
#start{position:fixed; left:50%; bottom:16px; transform:translateX(-50%); width:min(92vw,760px); pointer-events:none; text-align:center; color:#fff; font-weight:800; font-size:clamp(15px,2.4vw,24px); line-height:1.1; background:linear-gradient(90deg, rgba(255,77,109,.22), rgba(255,140,102,.18)); border:1px solid rgba(255,255,255,.25); box-shadow:0 10px 30px rgba(0,0,0,.35); padding:10px 16px; border-radius:999px; opacity:1; transition:opacity .35s ease; text-shadow:0 2px 12px rgba(0,0,0,.7);}
#start.hide{opacity:0;}
#start span{display:block; font-weight:700; font-size:.72em; color:rgba(255,255,255,.82);}
@media (max-height:620px){#start{bottom:6px; font-size:15px; padding:7px 10px}}
</style>
</head>
<body>
<div id="stage"><canvas id="game" width="960" height="600"></canvas></div>
<div id="start">🍔 Click or press a key to start <span>Mouse / touch / ← →</span></div>
<script>
...
</script>
</body>
</html>
```
Need ensure canvas width/height attr fixed but we may set dpr. If we set canvas.width = 960*dpr, attribute changes. CSS aspect ratio. In canvas element width=960 height=600. Later set to dpr.
```
const canvas=document.getElementById('game'); const ctx=canvas.getContext('2d'); const W=960,H=600;
function setSize(){ const dpr=Math.min(window.devicePixelRatio||1,2); canvas.width=Math.floor(W*dpr); canvas.height=Math.floor(H*dpr); ctx.setTransform(dpr,0,0,dpr,0,0);}
window.addEventListener('resize',setSize); setSize();
```
If devicePixelRatio 1, ok. CSS max-height 800 but aspect ratio; on small heights, canvas scales down? width:100%, height:auto; max-height:800px; aspect-ratio ensures. If window height 375? stage padding, max-height maybe 100vh? CSS max-height:800px not fit. Better canvas max-height: calc(100vh - 20px). width auto? Use:
canvas { width: min(100%, 1280px); height: auto; max-height: calc(100vh - 20px); aspect-ratio:16/10; }
But if width from 100% and height auto, aspect-ratio determines height; max-height can scale. Need ensure no overflow. Use both:
canvas { display:block; width:100%; max-width:1280px; max-height:calc(100vh - 20px); aspect-ratio:16/10; }
In flex, if max-height triggers, browser adjusts width? aspect-ratio and max-height should. But width:100% might force overflow if height capped? CSS can reduce height but width remains causing aspect mismatch? Actually aspect-ratio with width and max-height may cause height capped and aspect ratio ignored if width constrained? Might distort? But canvas has intrinsic? We set canvas.width/height fixed; CSS aspect ratio. If max-height constrains, width remains 100%? Could distort. Better use JavaScript sizing or use container with max width and height.
Simpler:
#stage{position:fixed; inset:0; display:grid; place-items:center; padding:10px; overflow:hidden;}
canvas{width:auto; height:auto; max-width:100%; max-height:100%; aspect-ratio:16/10;}
But width auto with aspect-ratio? It will take intrinsic? Set canvas CSS width/height? Could do: canvas {max-width:100%; max-height:100%; width:auto; height:auto; aspect-ratio:16/10;} In grid, aspect-ratio might need one dimension. If both auto, intrinsic size from attributes (960x600) and max scales proportionally? Yes intrinsic ratio preserved if width/height auto and max constraints? For replaced elements, if only max-width/height, it scales preserving aspect if one dimension auto? Could be. But canvas has intrinsic width attr in CSS pixels? We set attr to dpr? Might intrinsic pixel dimensions large; max-width scales. Better set CSS width/height via JS: fit canvas size to container preserving aspect. But we can keep logical fixed and use CSS transform? Let's implement robust:
CSS:
#stage {position:fixed; inset:0; overflow:hidden; display:flex; align-items:center; justify-content:center;}
canvas { display:block; background...; width: 960px; height:600px; max-width:100%; max-height:100%; }
But if max-height, width remains 960px causing horizontal? max-width:100% limits, but ratio not automatic unless aspect-ratio. Use aspect-ratio and width:100%; but height overflow. Alternative:
canvas { aspect-ratio:16/10; width: min(100vw - 20px, (100vh - 20px) * 1.6, 1280px); height:auto; }
This uses viewport units: CSS `width: min(calc(100vw - 20px), calc((100vh - 20px) * 1.6), 1280px);` and height auto with aspect-ratio. Good. No horizontal overflow. Need support min/calc; Chrome yes. Use:
canvas { width:min(100%, calc((100vh - 20px) * 1.6), 1280px); height:auto; aspect-ratio:16/10; max-width:100%; }
`100%` of stage which has padding. Good. But CSS width min with % and vh units. For 375x667, min(375-20=355, (667-20)*1.6=1035, 1280) => 355. height 222. Fine. For short viewport 1280x400, min(1280-20=1260, (400-20)*1.6=608) =>608. Good.
Need account #stage padding maybe width min(100%) inside stage. #stage padding 10. canvas max-width:100%. Use CSS as above.
But if canvas width CSS less than intrinsic? It scales. Canvas internal resolution set to W*dpr but CSS display size smaller. ctx transform? We set canvas.width=960*dpr, height=600*dpr. But CSS scales. Good.
Potential crispness: If canvas displayed 355px width but internal 1920? dpr max2 => internal 1920? Overkill but okay. Could set internal to 960*dpr independent display. Fine.
Need pointer mapping:
Use canvas.getBoundingClientRect(). targetX logical = (clientX - rect.left)/rect.width*W. Since CSS scaled. Good.
Game loop:
let last=0; function frame(ts){ if(!last)last=ts; let dt=(ts-last)/1000; last=ts; if(dt>0.05)dt=0.05; update(dt); draw(); requestAnimationFrame(frame); }
requestAnimationFrame(frame);
Start immediately.
Need `requestAnimationFrame` in sandbox ok.
Update:
- update input paddle.
- if state != 'gameover', update ball.
- update particles, texts, background hearts.
- messageTimer.
Paddle movement:
If state demo: targetX = ball.x; easing.
If playing: if mouse/touch use pointerTarget if not null? keyboard overrides? We'll set targetX from pointer if pointerActive. Keyboard move.
Paddle physics: x += (targetX - x)*0.18 maybe speed clamp. Or use vx for inertia. Simpler:
let target = pointerActive ? pointerX : paddle.x;
if keys.left target = paddle.x - 260*dt*speed? Use paddle.x += keyAccel.
Use both:
let tx = paddle.x;
if(pointerActive) tx = pointerX;
if(keys.left) tx -= 200; if(keys.right) tx += 200; // not good for touch
Maybe keyboard sets velocity. Use:
if(state==='playing'){
if(keys.left) paddle.x -= 620*dt;
if(keys.right) paddle.x += 620*dt;
pointerActive: paddle.x += (pointerX - paddle.x) * Math.min(1, 18*dt);
}
Demo: paddle.x += (ball.x - paddle.x) * Math.min(1, 12*dt); but ball may move faster. Use direct clamp speed max=900. target=ball.x + ball.vx*0.05; dx=target-paddle.x; maxMove=900*dt; paddle.x += clamp(dx, -maxMove, maxMove); Then clamp.
Clamp paddle x between w/2+8 and W-w/2-8.
pointerActive false initially. On pointerdown/move set true. If using keyboard maybe pointerActive remains true and conflicts. Could set pointerActive false on keydown? Maybe. Better use pointer position only if pointer is over canvas or after touch? We can use `pointerTarget` always set by mouse; mousemove updates. If keyboard pressed, temporarily ignore mouse? Not necessary.
Input listeners:
```
let pointerX = W/2, pointerActive=false;
function getPointerX(e){const r=canvas.getBoundingClientRect(); return ((e.clientX - r.left) / r.width) * W;}
canvas.addEventListener('pointerdown', e=>{ ensureAudio(); if(state==='demo'||state==='gameover'){startOrRestart();} pointerActive=true; pointerX=getPointerX(e); });
canvas.addEventListener('pointermove', e=>{ if(e.buttons||state==='playing'||true) { pointerActive=true; pointerX=getPointerX(e); } });
window.addEventListener('pointerup', e=>{ if(e.pointerType==='touch') pointerActive=false? For mouse keep.});
```
Actually for mouse, pointermove should move even without click. But if user hasn't clicked, demo shouldn't be controlled? Requirement self-playing behind message. If mouse moves over canvas before click, should not override AI? Maybe it can but still demo. If pointerActive true, but state demo AI overrides? We can only use pointer when state playing. Good.
- On touch move, if state playing, update pointer. If not, on pointerdown start.
- On mousemove: if state playing, pointerX = getPointerX(e). If state demo, not control. So no need pointerActive. Use state check in update: if playing and mouse/touch used flag. Set inputMode='pointer' on pointermove/down in playing? Or always pointerX.
Simpler:
`pointerX` always updated by window pointermove. In update, if state playing, paddle.x += (pointerX - paddle.x)*... unless keyboard? Could also keyboard add. That means if mouse stationary from previous, okay. In mobile, after finger up pointerX remains last, paddle stays. Fine.
Keyboard: keys update. If keyboard input, maybe still pointer influence; but pointerX could be center? If user uses keyboard only, no mousemove, pointerX default W/2 so paddle will always drift to center conflicting. So use `inputMode` default 'keyboard'? Let's do:
`let inputMode='demo';` On pointermove or pointerdown while state playing, set inputMode='pointer'; On keydown left/right while playing, inputMode='keyboard'. In update if playing: if inputMode==='pointer' ease to pointerX; if keyboard, accelerate based on keys; if none, no move? For mobile pointer after down.
Need still click to start sets state playing but inputMode remains pointer from click. Good.
If keydown any starts demo and sets inputMode keyboard? If key is arrow. If any key not movement? Could set pointer? Better if state demo and keydown, start and inputMode='keyboard'.
- On pointermove over canvas before start (state demo), don't change inputMode. On pointerdown to start, state playing, inputMode='pointer' and pointerX. Good.
Keyboard update:
```
const paddleSpeed=640;
if(inputMode==='keyboard' || keys.left || keys.right){
if(keys.left) paddle.x -= paddleSpeed*dt;
if(keys.right) paddle.x += paddleSpeed*dt;
}
if(inputMode==='pointer'){
paddle.x += (pointerX - paddle.x) * Math.min(1, 20*dt);
}
```
But if inputMode pointer and keys still pressed? Not issue. Could combine.
Ball:
```
const ball={x:W/2,y:H*0.62,r:11,vx:180,vy:-400,speed:440,trail:[]};
function resetBall(){ball.x=paddle.x; ball.y=paddle.y - paddle.h/2 - ball.r - 1; const angle = (Math.random()*0.9 -0.45) - Math.PI/2? Need moving upward. Set vx = Math.sin(angle)*ball.speed; vy = -Math.cos(angle? hmm). Simpler: const dir = (Math.random()<.5?-1:1); const speed=ball.speed; ball.vx=dir*(speed*0.4+Math.random()*120); ball.vy=-speed*0.95; normalize; ball.speed = Math.hypot; }
```
Actually if reset from paddle top, ball near bottom and moving up to bricks. Good. If y above bricks? bricks top 110; paddle top 500, so moving up.
Speed: ball.speed = 420 + level*25 maybe. On brick hit maybe slight speedup.
Need ensure ball doesn't serve stuck with brick collision immediate? If brick layout top margin 110, ball y=500, fine.
Ball update substeps:
```
function updateBall(dt){
const steps=Math.max(1, Math.ceil(ball.speed*dt / 6));
const sdt=dt/steps;
for(let i=0;i<steps;i++){
ball.x += ball.vx*sdt; ball.y += ball.vy*sdt;
collideWalls(); collidePaddle(); if(collideBricks()) break; // maybe still continue? if destroyed only one
}
if(ball.y > H + 50) loseBall();
}
```
collideBricks loops alive. If collision: destroyBall? no. Reflect. Return true.
Need maybe if ball stuck inside brick? destroy so no.
Bricks:
```
function makeBricks(level){ bricks=[]; rows=Math.min(6 + Math.floor(level/2),9); cols=12; gap=8; top=100; side=20; bw=(W-side*2 - gap*(cols-1))/cols; bh=28; palette=['#ff5f6d','#ffc269','#ff1f6d','#845ec2','#00c9ff']; for row... col... choose if pattern. alive true; hue. }
```
Need pattern with maybe top full. Simpler:
```
for (let r=0;r<rows;r++) for(c=0;c<cols;c++) {
let skip = (level%3==1 && (r+c)%2==0) || ...; // may reduce bricks, but need enough. If skip, skip some.
if(skip) continue;
const hue = (r*18 + c*9 + level*40)%360; const sat = 95; const light = 52+Math.sin(c/cols*Math.PI)*8;
bricks.push({x: side + c*(bw+gap), y: top + r*(bh+gap), w:bw, h:bh, hue, color:hsl, alive:true, value: 10*(rows-r), seed:Math.random()});
}
```
If skip too many? Keep some full. Use rows=5 cols=12 => 60. If checker skip half => 30. Fine. Need if no bricks? if skip all? no.
- Maybe add special "heart brick" hp? Not needed.
- Bricks have `w,h`. If checker pattern skip (r%2==0&&c%2==0). Need not leave empty.
- Could have bricks with hp 2 on later levels? For explosion tons each hit? If hp >1, only explode when destroyed. Could make bricks require multiple hits. For first demo, one hit.
- Add `hp` for some bricks: if level>2 and row=0 maybe hp=2. On hit decrement; small hearts; if hp=0 destroy with tons. Nice. Need collision reflect and maybe no destroy if hp>0. For simplicity all hp=1. Could hp=1 only.
Particle spawn:
```
function destroyBrick(b, impact){ b.alive=false; score+=b.value; addText(b.x+b.w/2,b.y+b.h/2, '+'+b.value, b.hue); spawnHeartBurst(b, impact); if(state==='playing') tone(...); // maybe no sound in demo? if audioCtx exists.
}
```
`spawnHeartBurst` uses center maybe.
```
function spawnHeartBurst(b, impactX){
const cx=b.x+b.w/2, cy=b.y+b.h/2;
const count=55 + Math.floor(Math.random()*20);
for(let i=0;i<count;i++){
if(particles.length>MAX_PARTICLES) particles.shift();
const a = -Math.PI/2 + (Math.random()*1.6 - 0.8);
const spd = 70 + Math.random()*320;
const hue = (b.hue + Math.random()*60 -30 + (Math.random()<0.35? 330:0))%360;
particles.push({x:cx + (Math.random()-0.5)*b.w, y:cy+(Math.random()-0.5)*b.h*0.8, vx:Math.cos(a)*spd + impactX, vy:Math.sin(a)*spd - 50, size:5+Math.random()*16, rot:Math.random()*Math.PI, spin:(Math.random()-0.5)*6, life:0.8+Math.random()*1.4, maxLife:1.6, hue, sat:90, light:50+Math.random()*30, grav:180+Math.random()*260, drag:1.8});
}
// ring maybe sparkles
for (let i=0;i<20;i++){ ... }
}
```
Need hearts maybe some rise up, some explode outward. Could include upward bias.
If ball velocity impactX? Add ball.vx*0.1.
- Use p.life / maxLife for alpha.
- Update p:
```
p.vy += p.grav*dt; p.vx -= p.vx*p.drag*dt; p.vy -= p.vy*p.drag*dt? drag reduces both.
p.x += p.vx*dt; p.y += p.vy*dt; p.rot += p.spin*dt; p.life -=dt;
if(p.life<=0 || p.x<-60 || p.x>W+60 || p.y>H+80) remove.
```
- Draw with alpha easeOut? `alpha=Math.max(0,Math.min(1,p.life/(p.maxLife*0.7)))` maybe.
- Could add scale shrink as life ends.
Use array with splice? For performance use filter? `for(let i=particles.length-1;i>=0;i--) if dead swap remove`. We'll implement.
Heart drawing function:
Need centered heart maybe. Let's design robust path centered at origin with size s and y point down.
Use:
```
function heartPath(ctx, s) {
const top = -s * 0.35;
const bottom = s * 0.45;
const side = s * 0.5;
ctx.beginPath();
ctx.moveTo(0, top + s*0.2); // cleft? maybe top center y -0.2s? Let's define.
```
Let's derive using known cubic:
```
function heartPath(ctx, s) {
ctx.beginPath();
const w = s;
ctx.moveTo(0, w * 0.25);
ctx.bezierCurveTo(0, -w * 0.15, -w * 0.5, -w * 0.45, -w * 0.5, w * 0.1);
ctx.bezierCurveTo(-w * 0.5, w * 0.55, 0, w * 0.75, 0, w * 1.1);
ctx.bezierCurveTo(0, w * 0.75, w * 0.5, w * 0.55, w * 0.5, w * 0.1);
ctx.bezierCurveTo(w * 0.5, -w * 0.45, 0, -w * 0.15, 0, w * 0.25);
ctx.closePath();
}
```
This starts at center between lobes? y = 0.25w; then curves to top left (-0.5,-0.45) then to bottom (0,1.1). That seems heart extends from -0.45w to 1.1w, vertical height 1.55w. For size parameter maybe w=16 => -7 to 17. Not centered. For drawHeart(x,y,s) maybe translate to x,y and offset y -0.33s? Let's create function to draw centered:
Use size as half-width maybe. Simpler known from stack:
```
function drawHeart(ctx, x, y, size, rotation, fill) {
ctx.save(); ctx.translate(x,y); ctx.rotate(rotation); ctx.scale(size/10, size/10); ctx.beginPath(); ctx.moveTo(0,-2); ctx.bezierCurveTo(0,-6, -6,-8, -6,-2); ctx.bezierCurveTo(-6,3, -1,6, 0,9); ctx.bezierCurveTo(1,6, 6,3, 6,-2); ctx.bezierCurveTo(6,-8, 0,-6, 0,-2); ctx.closePath(); ctx.fillStyle=fill; ctx.fill(); ctx.restore();
}
```
Let's test: starts at (0,-2), left lobe to (-6,-8), bottom (0,9), right (6,-2). It's centered around y ~1.8? Actually min -8 max 9; center y=0.5. With scale size/10, vertical height 17 units, size 20 maybe. Good enough. We can offset by -0.3*size? If we scale by size/20? Let's define heart size relative to radius. Use:
```
function drawHeart(x,y,s,rot,color) {
ctx.save(); ctx.translate(x,y); ctx.rotate(rot); ctx.scale(s/20,s/20); // if path spans 20x? top -8 bottom 9, width 12. So s=20 height 17. Fine.
ctx.beginPath(); ... fill; restore;
}
```
Maybe center vertical by translate(0,-1) after scale? Add offset -2? Use ctx.translate(0, -1) before scale? For path center near y=0.5. Draw at y with size roughly bounding radius. Good.
Let's use a known parametric heart? Could use:
```
for (t) x=16 sin^3(t), y=13 cos t -5 cos 2t -2 cos3t - cos4t; scaled.
```
This gives nice heart centered origin but many points, for tons particles maybe slow. But 70* maybe okay? Need path simple.
Use cubic heart.
Draw ball as circle maybe not heart. We'll add trail of hearts.
Background floating hearts:
Initialize 30 bgHearts with x,y,speed, size, hue, rot, phase. Update slow. Draw alpha .12 behind. Could create "tons" feel.
Level message:
`function setMessage(text, time){message=text; messageTimer=time;}`
Draw center text if messageTimer>0. Use font bold, glow. Fade.
Score/lives drawing:
- `drawHeart` for lives at top-left: for i<lives draw at 20+i*28, 32, size=20, rot=0, color red.
- Text score: `ctx.font='800 22px system-ui'; fillStyle #fff; fillText('Score '+score, 110? maybe after lives).
- Level top-right.
Need avoid overlap with message.
Game reset:
```
function resetGame(full){ score=0; lives=3; level=1; makeBricks(level); paddle.w=170; paddle.h=72; paddle.x=W/2; paddle.y=H-90; resetBall(); particles=[]; texts=[]; }
```
Paddle y: H - 90 = 510 if H=600. Bottom of hamburger visual? h/2=36, y=510 => bottom 546, top 474. Ball lose if y>H. Good.
`makeBricks` top=100. Rows up to 9 => height 9*36=324 -> y=424 below paddle top? Actually top=100, rows=9, bottom=100+8*36+28=416, paddle top 474, okay.
For rows=9, bricks bottom 416, space 58, fine.
Start/Restart:
```
function startPlaying(){ ensureAudio(); if(state==='demo'){ state='playing'; setMessage('Let’s go! 🍔❤️',2); inputMode='pointer'; } else if(state==='gameover'){ resetGame(); state='playing'; setMessage('Again!',1.5); inputMode='pointer'; } startEl.classList.add('hide'); }
```
For keydown: if state demo/playing/gameover: startPlaying? If already playing no effect. If gameover restart. If demo start. Set inputMode='keyboard' if key was arrow? else pointer? Could set if state demo start and inputMode='keyboard'. If playing and user pressed key, inputMode='keyboard'. If key is not arrow, maybe inputMode unchanged.
`document.addEventListener('keydown', e=>{ if(['ArrowLeft','ArrowRight','Space','Enter'].includes(e.code)) e.preventDefault(); if(state==='demo'||state==='gameover') startPlaying(); inputMode='keyboard'; keys.left=...; });`
But if keydown space to start, inputMode keyboard and then no movement. ok.
Need if keydown during gameover after click? ok.
- On pointerdown, startPlaying but inputMode pointer.
- On pointermove: if state playing and (e.buttons? or touch) set inputMode pointer, pointerX. Actually mousemove no buttons? We want mouse control always after start. Use if state==='playing' { inputMode='pointer'; pointerX=getPointerX(e); }
`hideStart`: If state playing, overlay hidden. If state gameover, show restart text. Use update DOM maybe every frame? Could update when state changes. Simpler in draw? DOM not. We'll call updateOverlay in state changes and frame? Use function.
```
function updateOverlay(){
if(state==='demo') { text...; hide false }
else if(state==='gameover') { text "Game Over — score ... Click or press key to restart"; hide false }
else hide true;
}
```
Need set startEl.innerHTML. If gameover, maybe show score. In demo, "🍔 Click or press a key to start". Could include self-playing note.
Potential problem: If state playing but no user interaction? not. Good.
Demo AI details:
In update, if state==='demo':
```
const target = ball.x + ball.vx*0.05;
const dx=target - paddle.x;
const max = 900*dt;
paddle.x += Math.max(-max, Math.min(max, dx));
```
But if ball moving down and near bottom, good. If ball near bricks moving up, target ball.x; fine. Need avoid losing? If ball goes past paddle? Could AI fail sometimes; if lives in demo? We'll not decrement. We can reset ball. But if AI catches every time, no explosion maybe? It will hit bricks. Good.
Could make AI intentionally let ball pass to show lose? Not necessary. But maybe if ball goes behind paddle due to corner, reset. Good.
If ball.x near edges, target. If ball.y below paddle? reset.
Collision with paddle angle. Need paddle.h maybe visual. Use top collision. If ball.vy >0 and y+r >= paddle.y - paddle.h/2 && y <= paddle.y + paddle.h/2? Ball center could be inside paddle. Use `if (ball.vy > 0 && ball.y + ball.r >= paddleTop && ball.y - ball.r <= paddleBottom && ball.x >= paddleLeft - ball.r && ball.x <= paddleRight + ball.r)`.
Then set y = paddleTop - ball.r.
- If paddle moves fast, ball could hit side? Breakout usually only top. Fine.
Collide walls:
```
if(ball.x - ball.r < 0){ ball.x=ball.r; ball.vx=Math.abs(ball.vx); sound; trail? }
if(ball.x + ball.r > W){...}
if(ball.y - ball.r < 0){...}
```
Bottom: no wall.
Brick collision reflect with hp:
```
function collideBricks(){
for(const b of bricks) if(b.alive){
const bCenterX=b.x+b.w/2, bCenterY=b.y+b.h/2;
const dx=Math.abs(ball.x - bCenterX), dy=Math.abs(ball.y - bCenterY);
if(dx <= b.w/2+ball.r && dy <= b.h/2+ball.r){
// determine side by overlaps
const overlapX = b.w/2+ball.r - dx;
const overlapY = b.h/2+ball.r - dy;
if(overlapX < overlapY){
ball.vx = -ball.vx; ball.x = ball.x < bCenterX ? b.x - ball.r : b.x + b.w + ball.r;
} else {
ball.vy = -ball.vy; ball.y = ball.y < bCenterY ? b.y - ball.r : b.y + b.h + ball.r;
}
b.hp--; spawnSmallHearts; if(b.hp<=0) destroyBrick(b);
// increase ball speed slightly
ball.speed = Math.min(ball.speed+3, 760); maybe normalize.
return true;
}
}
return false;
}
```
But if ball moving horizontally and hits side, reflect vx. If ball speed normalized, okay. However if ball inside due to corner, overlapX maybe smaller. Good. Need set ball velocity normalized? We can after bounce keep magnitude ball.speed by normalizing:
```
function normalizeBall(){ const len=Math.hypot(ball.vx,ball.vy); if(len) {ball.vx=ball.vx/len*ball.speed; ball.vy=ball.vy/len*ball.speed;} }
```
Call after bounce.
For paddle bounce, set ball.speed.
`ball.speed` can increase. Initial 420.
If hp >1, small hearts spawn maybe. But all hp 1. We can implement hp. `b.hp = (level>2 && r==0 ? 2 : 1)`. For hp2, need show damage. On hit decrement, small burst 10 hearts. When destroyed, huge burst. Good. But initial first level full.
Brick generation hp. Need if hp2 maybe color lighter? Draw cracks? Use `b.hp` and maxHp. Draw inner opacity based hp.
Sound:
```
function playBounce(){ if(audioCtx) tone(220+rand*60,0.06,'square',.03) }
function playBrick(){ if(audioCtx) { tone(330+Math.random()*650,0.15,'triangle',.05); tone(660+... 0.08) } }
function playLose(){ tone(150,.3,'sawtooth') }
```
Only call if state playing? But audioCtx only exists after start. In demo, audioCtx null. Fine. Could call in playing only.
Heart burst with tons: Could be too many for performance if multiple destroyed. Use MAX 800? But "tons". 600 okay. If many bricks destroyed, old removed. Use 700. On mobile, maybe 500. Could set `const MAX_PARTICLES = window.innerWidth < 600 ? 500 : 900;` But canvas fixed, performance. Use 800. For tons, 70 per brick. If multiple within seconds, may remove. Good.
Need draw order: background, bricks, particles, paddle, ball, UI. Particles behind/above? Hearts explosion should be over bricks/paddle/ball? Maybe draw particles after bricks before UI, so above. If ball collides, hearts could obscure ball? fine. Draw paddle before particles? Better: background, bricks, paddle, ball/trail, particles on top, UI. Or particles top.
Trail:
```
ball.trail=[]; add every frame: trail.push({x:ball.x,y:ball.y,life:0.25}); update life. Draw trail hearts small behind ball.
```
If particles on top, trail behind.
`updateTrail`: trail.unshift? We can push and remove. In draw, alpha=life/0.25.
Draw ball:
```
ctx.save(); ctx.globalCompositeOperation='lighter'; for t of ball.trail draw small heart color rgba pink; ctx.restore();
// ball circle with radial gradient and heart
const grad = ctx.createRadialGradient(ball.x-4,ball.y-4,1,ball.x,ball.y,ball.r+3); grad.addColorStop(0,'#fff'); grad.addColorStop(.4,'#ff9ec7'); addColorStop(1,'#ff2d77'); ctx.fillStyle=grad; ctx.beginPath(); ctx.arc(...); ctx.fill();
// heart icon on ball
drawHeart(ball.x, ball.y, ball.r*1.4, ball.angle, rgba white .8?) But white heart may hide circle. Maybe draw pink heart over ball? Hmm. Could use drawHeart for ball itself instead of circle: fill red heart. Then collision circle. Use `drawHeart(ball.x, ball.y, ball.r*2.2, Math.atan2(ball.vy,ball.vx)+Math.PI/2, '#ff4d6d')` with gradient? If heart extends slightly beyond r but collision circle r okay. Better ball is heart. But for physics, if heart visual larger? Use collision r=11. draw size maybe 26. Good.
```
Need ball spin angle: `ball.angle += ball.vx * 0.001?` For rotation.
Draw paddle:
Could be too detailed. Let's implement `drawHamburger(px,py,w,h,squish)`.
Use ctx.save translate px py scale sx sy.
Coordinates relative:
- top = -h/2 = -36, bottom=36.
Upper bun:
```
ctx.fillStyle= gradient #e6b374 to #c9854c;
ctx.beginPath();
ctx.moveTo(-w/2+4, -h*0.15); // maybe -11
ctx.quadraticCurveTo(-w/2+4, -h*0.48, 0, -h*0.48);
ctx.quadraticCurveTo(w/2-4, -h*0.48, w/2-4, -h*0.15);
ctx.closePath(); fill.
// seeds
ctx.fillStyle='#fff6d8'; for pos array draw ellipse rotate.
```
Need bun top curve. h=72: -h*0.48=-34.6, -h*0.15=-10.8. Good.
- Tomato: red rounded rect height 8 at y -4:
```
ctx.fillStyle='#ff4b64'; roundRect(-w/2+12, -6, w-24, 12, 6);
```
- Cheese: yellow rect at y 2 with drips: roundRect(-w/2+8,0,w-16,10,3) fill.
Drips: triangles bottom? maybe.
- Patty: dark brown at y 12: roundRect(-w/2+6,10,w-12,14,6).
- Lettuce: green wavy above patty maybe between tomato and patty? At y 6? Let's layer top to bottom: upper bun top -34 to -10, tomato -10 to 0, cheese 0 to 8, lettuce 8 to 14 (wavy), patty 14 to 26, bottom bun 26 to 36.
Actually h=72, center 0. bottom bun from 24 to 36. patty from 8 to 24? Let's do simpler.
Draw bottom bun first:
```
roundRect(-w/2+8, h/2-18, w-16, 18, 9) fill #a9683f
```
Patty:
```
roundRect(-w/2+4, h/2-34, w-8, 16, 7) fill #5b3826
```
Cheese:
```
roundRect(-w/2+8, h/2-42, w-16, 10, 3) fill #f4c65b
```
Lettuce:
wavy path around y = h/2-46 = -10? Let's compute h/2=36; -46? Wait:
bottom bun y from 18 to 36 (h/2-18 to h/2)
patty y 2 to 18? if h/2-34=2, h/2-18=18. Good.
cheese y -6 to 4 (h/2-42=-6)
tomato y -16 to -8 (h/2-52=-16)
lettuce y -22 to -14 wavy
upper bun y -36 to -20 dome.
This fits.
```
ctx.fillStyle='#69b755'; path lettuce: begin; moveTo(-w/2+4,-22); for i 0..n: quadratic wavy; close to y -14.
```
Simplify: draw green rounded rect with scalloped bottom:
```
ctx.beginPath(); const steps=9; const y=-18; const amp=4; for i=0..steps: x=-w/2+6 + (w-12)*i/steps; ctx.lineTo(x, y - (i%2?0:amp)); ctx.lineTo(x, y+6); ... hmm.
```
Maybe skip lettuce? Hamburger can have tomato/cheese. But need recognizable. Draw lettuce as green wavy band using sine:
```
ctx.beginPath();
ctx.moveTo(-w/2+4, -22);
for(let i=0;i<=24;i++){ const t=i/24; const x=-w/2+4 + (w-8)*t; const y=-20 + Math.sin(t*Math.PI*6)*3; ctx.lineTo(x,y); }
for(let i=24;i>=0;i--){ ... y=-14 + Math.sin(...)*2 }
ctx.closePath(); fill.
```
Could work.
Tomato: red circles across? Use roundRect with red, then darker slice arcs?
Upper bun: path:
```
ctx.beginPath();
ctx.moveTo(-w/2+4, -22);
ctx.bezierCurveTo(-w/2+4, -38, -w*0.28, -40, 0, -40);
ctx.bezierCurveTo(w*0.28, -40, w/2-4, -38, w/2-4, -22);
ctx.closePath(); fill;
```
Seeds: positions array [-w*0.28,-30], [ -0.1w, -35], etc. Draw ellipse.
Need collision top maybe visual upper bun top -40 if h/2=36 -> outside. Use paddle.h=80, top=-40. Then visual fits. Set paddle.h=80. Bottom 40, top -40. Paddle y = H-90 => top 470, bottom 550. Bricks bottom max? For rows 9 top 100 -> 416, gap okay. If rows 9 and top=100 bottom=100+8*36+28? If bh=28 gap=8, bottom=100+8*(36)+28? Actually last y=100+8*36=388, h=28 =>416. Paddle top 470. Good.
Use paddle.h=76? top=-38, upper bun -36. Good. Collision h=76. Use `paddle.h=74`, top -37. Draw upper bun -34. ok.
Let's set h=76. Visual: top=-38. Upper bun y -34 to -20. collision top -38. Fine.
Paddle collision box maybe narrower than visual? Use `paddle.w=170`, h=74. Visual w 170. Good.
Draw burger squish:
`const sx=1+paddle.squish*0.08, sy=1-paddle.squish*0.06; paddle.squish*=Math.exp(-dt*8);`
Scale ctx.
RoundRect function for bricks and burger:
```
function roundRectPath(x,y,w,h,r){ r=Math.min(r,w/2,h/2); ctx.beginPath(); ctx.moveTo(x+r,y); ctx.lineTo(x+w-r,y); ctx.quadraticCurveTo(x+w,y,x+w,y+r); ... }
```
Bricks drawing:
```
for b alive:
const x=b.x, y=b.y, w=b.w, h=b.h;
const hpRatio=b.hp/b.maxHp;
const light = 40+hpRatio*25; const hue=b.hue;
const grad = ctx.createLinearGradient(x,y,x,y+h); grad.addColorStop(0,hsl(hue,95,70)); grad.addColorStop(1,hsl(hue,100,48));
ctx.fillStyle=grad; roundRectPath(x,y,w,h,8); ctx.fill();
ctx.fillStyle='rgba(255,255,255,.35)'; roundRectPath(x+3,y+3,w-6,h*0.35,6); ctx.fill(); // shine
// small heart
ctx.fillStyle='rgba(255,255,255,.15)'; drawHeart(x+w/2,y+h/2-1, Math.min(14,h-8), b.seed*10, 'rgba(255,255,255,.2)');
if(hpRatio<1) draw crack? maybe alpha.
```
Need drawHeart with absolute coords. Our drawHeart takes center x,y.
For brick hearts maybe hidden under fill. Could set after gradient.
Particles drawing:
`drawHeart(p.x,p.y,p.size,p.rot,color)` But p.size is size, path scale s/20. Good. Use fill hsl.
Alpha = p.life/p.maxLife. Maybe multiply.
If many, save/restore each call can be expensive. Could implement drawHeartWithoutSave? Need rotate. Could use transform. For 800 particles okay. Optimize: use ctx.setTransform? We'll use save/restore maybe okay at 60fps. But if many, could degrade. Alternative draw heart with current transform by manually translate/rotate? We can do `ctx.save(); ctx.translate...` for each. 800 okay maybe. Could reduce max. Use 600.
Need "tons little hearts" visual. 70 per brick. Could be okay.
Use `ctx.globalAlpha = alpha; drawHeart(...); ctx.globalAlpha=1;`
Update overlay text after state changes. Also message.
Gameover:
If lives 0 state gameover. Stop ball update? In update if state gameover skip ball. But particles continue. Show message. Overlay shows game over score. Click restart.
Need if level clear: if state playing or demo: level++, generate, reset ball, ball.speed increase. In demo continues.
Potential issue: `bricks.length` after clear check. In update after destroy maybe if all destroyed. Could check after loop. We'll have `if (bricks.every(b=>!b.alive)) levelClear();` But every on 60 each frame okay. Or track aliveCount. Simpler `if (!bricks.some(b=>b.alive))`. But called every frame after ball update. Fine. Use `aliveBricks` count decrement. On destroy aliveBricks--. If 0 levelClear. Need reset aliveBricks in makeBricks.
Need handle `collideBricks` with substeps: if returns true, break substeps. But there may be multiple. ok.
Substep collision with brick can destroy multiple if ball passes through? Since destroy brick, maybe only one. Fine.
Initial demo setup: state 'demo', score maybe 0, lives 3, level 1. ResetBall with ball moving. AI will catch. Bricks generate. Need message? "Self-playing demo". Overlay says. Could also canvas message "Demo: AI playing". Draw in UI maybe.
First paint before requestAnimationFrame? The script initializes and draws. Need call `resetGame(); requestAnimationFrame`. Good.
No external fonts. Use system-ui.
Now code details:
```
const startEl=document.getElementById('start');
function setOverlay(text){ startEl.innerHTML=text; }
function updateOverlay(){
if(state==='demo'){ setOverlay('🍔 Click or press a key to start<br><span>Self-playing hamburger breakout demo — bricks explode into hearts</span>'); startEl.classList.remove('hide'); }
else if(state==='gameover'){ setOverlay('💔 Game Over — Score '+score+'<br><span>Click or press any key to play again</span>'); startEl.classList.remove('hide'); }
else { setOverlay('🍔 Mouse / touch / ← → to move'); startEl.classList.add('hide'); }
}
```
Need hide via class opacity 0. Even if hidden pointer-events none. Good.
Initial overlay: set demo.
Keyboard prevent default only for game keys to avoid scroll: `if (['ArrowLeft','ArrowRight','Space','Enter'].includes(e.code)) e.preventDefault();` But if any key starts demo, don't prevent? Fine.
Touch: pointerdown on canvas only? Use window pointerdown. But if overlay pointer-events none, ok. Use canvas.addEventListener('pointerdown',...). If user clicks outside canvas? Probably stage only. But requirement click/press. Use window pointerdown? If pointerdown outside, still start. But getPointerX relative to canvas if outside maybe clamp. Use canvas.addEventListener('pointerdown', start, false); If user clicks page outside maybe not. But full stage. Maybe use window for keyboard, canvas for pointer. But overlay may cover bottom? pointer-events none so canvas receives. Good. Use canvas listener. Also on stage? Maybe if user clicks overlay text pointer-events none. Good. If user clicks outside canvas? no. Fine.
Need pointermove on canvas only? If mouse leaves canvas and playing, target should maybe clamp. Use window pointermove, compute relative and clamp.
```
function updatePointer(e){ const rect=canvas.getBoundingClientRect(); const x=(e.clientX-rect.left)/rect.width*W; pointerX=clamp(x,0,W); if(state==='playing') inputMode='pointer'; }
window.addEventListener('pointermove', updatePointer, {passive:true});
canvas.addEventListener('pointerdown', e=>{ ensureAudio(); if(state!=='playing'){ startPlaying(); } updatePointer(e); inputMode='pointer'; if(e.pointerType==='touch'){ e.preventDefault? Need allow? pointerdown passive? Use not passive. } });
```
But pointermove before start won't set inputMode; pointerdown after start will. In update if state playing and inputMode pointer, control. If mouse never moved, pointerX maybe default? set to paddle.x.
For keydown:
```
window.addEventListener('keydown', e=>{
if(e.repeat) { if state demo? no repeat? still? maybe.
ensureAudio();
if (['ArrowLeft','ArrowRight','Space','Enter','KeyA','KeyD'].includes(e.code)) e.preventDefault();
if (state==='demo'||state==='gameover') { startPlaying(); }
if(['ArrowLeft','KeyA'].includes(e.code)) {keys.left=true; inputMode='keyboard';}
if(['ArrowRight','KeyD'].includes(e.code)) {keys.right=true; inputMode='keyboard';}
});
window.addEventListener('keyup',...);
```
If keydown any key to start (e.g. 'KeyH') start but no inputMode. If key not movement, maybe set inputMode keyboard? Could. If state demo and key not arrow, after start maybe no control. But user will use mouse. Maybe set inputMode pointer? Not if no pointer. But demo starts, paddle stationary? Need ensure can play. Could if any key start and no movement, inputMode 'pointer' default W/2, paddle moves to pointer? We can set pointerX = paddle.x to prevent. But if no pointer move, inputMode pointer keeps. Maybe better after key start set inputMode='keyboard' but keys false => paddle stays. User can press arrows. Fine.
Need `startPlaying` not call ensureAudio multiple times? okay.
AudioContext resume: Some browsers require gesture. We call in pointerdown/keydown. Good. If no AudioContext (blocked?), try catch.
Draw functions:
```
function draw(){
ctx.clearRect(0,0,W,H);
// background
const bg=ctx.createLinearGradient(0,0,0,H); bg.addColorStop(0,'#170b1d'); bg.addColorStop(.45,'#1e1028'); bg.addColorStop(1,'#0f0612'); ctx.fillStyle=bg; ctx.fillRect(0,0,W,H);
// radial glow behind paddle/bricks maybe
ctx.save(); ctx.globalCompositeOperation='lighter'; const rg=ctx.createRadialGradient(W/2,H*0.25,10,W/2,H*0.25,H*0.6); rg.addColorStop(0,'rgba(255,70,120,0.16)'); rg.addColorStop(1,'rgba(255,70,120,0)'); ctx.fillStyle=rg; ctx.fillRect(0,0,W,H); ctx.restore();
drawBackgroundHearts();
drawBricks();
drawPaddle();
drawBallTrail(); drawBall(); drawParticles(); drawTexts(); drawUI(); drawMessage();
}
```
Need order: particles top? If particles on top of ball? Maybe hearts explosion should be on top. Draw particles after paddle/ball. UI after. Background hearts first.
Background hearts:
```
const bgHearts=[]; for i=0..24 bgHearts.push({x:randW,y:randH,size:20+rand*80,vy:-8-8*rand,rot:rand, spin, hue, alpha:.04+.08});
update: y += vy*dt; if y<-100 reset bottom. x += sin(t+phase)*0.2? Maybe.
draw alpha .06 hsl(330,80,60).
```
Need update even demo. Good.
Draw ball trail:
```
for t of ball.trail { ctx.globalAlpha=t.life/0.28*0.35; drawHeart(t.x,t.y,t.size, t.rot, 'rgba(255,110,170,1)') }
```
Need not use globalAlpha with drawHeart fill? It multiplies fillStyle alpha? Canvas globalAlpha works.
Ball trail update: each frame push if state != gameover. `ball.trail.push({x:ball.x,y:ball.y,life:0.3,size:ball.r*1.4,rot:ball.angle})`; update life. Remove.
Draw ball:
If heart ball: `ctx.save(); ctx.translate(ball.x, ball.y); ctx.scale(1+?); const gradient? For heart path we need fill gradient maybe set fillStyle = grad. Need path at origin. We'll implement drawHeart with fill color/gradient? It can accept any fill. It uses ctx.fill. But if we pass gradient with coordinates? Need create gradient relative to ball before save? `const ballGrad=ctx.createRadialGradient(ball.x-4, ball.y-4, 1, ball.x, ball.y, ball.r+4);` Then drawHeart(ball.x, ball.y, ball.r*2.3, ball.angle, ballGrad). drawHeart sets fillStyle=fill; ctx.fill(); okay.
```
ball.angle += (ball.vx > 0 ? 0.06 : -0.06) maybe dt: ball.angle += ball.vx*0.0006 + ball.vy*0.0003? Use dt.
```
Draw particle hearts with colors: `p.color = 'hsl('+hue+' '+sat+'% '+light+'%)'`; If want pink hearts, use hue = 340 + random*40. For brick hue. Use `hsl(hue, 92%, 62%)`.
RoundRect helper:
```
function roundRect(x,y,w,h,r){ ... }
```
Need ensure if h negative? no.
GenerateBricks details:
```
function makeBricks(level){
bricks=[]; aliveCount=0;
const cols=12;
const rows = Math.max(4, Math.min(8, 4 + Math.floor(level/2)));
const gap=7;
const side=24;
const top=96;
const bh=30;
const bw=(W - side*2 - gap*(cols-1))/cols;
const pattern = (level-1)%5;
for r... for c...
let include=true;
if(pattern===1 && (r+c)%2===0) include=false;
if(pattern===2 && c===0 && r%2===1) include=false; // stripes
if(pattern===3 && r%2===1 && (c===0 || c===cols-1)) include=false;
if(pattern===4 && ((r*c)%7===0)) include=false; // random-ish but need enough. Maybe skip.
if(!include) continue;
const maxHp = level >=3 && r===0 ? 2 : 1;
const hue = (340 + r*12 + c*6 + level*30) % 360; // pinks reds/oranges
const light = 52 - r*2 + Math.sin(c)*?; // maybe.
const sat = 92;
bricks.push({...});
aliveCount++;
}
}
```
Need first level include maybe all. If pattern 1 skip half, still enough. If all bricks skip? no.
Maybe colors not pink? Task hearts. Use hue range 320-40 (reds/pinks). `hue = (330 + Math.sin(c)*20 + r*8 + level*15)%360`. Good.
Score texts:
```
function addText(x,y,text,hue){ texts.push({x,y,vy:-45,text:'+'+score, life:1.2,max:1.2,hue}); }
```
Draw with alpha and font. Maybe heart symbol? "❤ +20"? Use text. `ctx.font = '800 20px system-ui'; ctx.textAlign='center';`
UI:
```
ctx.textAlign='left'; ctx.textBaseline='middle';
ctx.font='800 20px system-ui'; ctx.fillStyle='#fff';
ctx.fillText('SCORE ' + score, 84, 32);
ctx.textAlign='right'; ctx.fillText('LEVEL ' + level, W-24,32);
// lives hearts at x=28 + i*30, y=30
for i=0;i<lives;i++ drawHeart(30+i*30,32,22,0,'#ff4d6d');
// state text? In demo small top center: "DEMO" maybe.
ctx.textAlign='center'; ctx.font='700 14px system-ui'; ctx.fillStyle='rgba(255,255,255,.55)'; ctx.fillText(state==='demo'?'SELF-PLAYING DEMO':'', W/2, 32);
```
Need score text offset from lives. Use left margin.
Message draw:
```
if(messageTimer>0){ messageTimer-=dt; const a=Math.min(1,messageTimer/0.8); ctx.globalAlpha=a; ctx.textAlign='center'; ctx.font='900 clamp?' fixed 34px; ctx.fillStyle='#fff'; ctx.shadowColor='rgba(255,70,130,.9)'; ctx.shadowBlur=25; ctx.fillText(message,W/2,H*0.45); ... }
```
Need reset shadow after.
Lose ball:
```
function loseBall(){ if(state==='demo'){ setMessage('Miss! Demo continues',1.2); spawnMissHearts?; resetBall(); } else { lives--; playLose; if(lives<=0){ state='gameover'; setMessage('Game Over',2.5); updateOverlay(); } else { setMessage('Lost a heart! ❤️',1.5); resetBall(); } } }
```
If state playing, lives. If state gameover, ball not updated.
Miss hearts: maybe spawn few white hearts at bottom.
Level clear:
```
function levelClear(){ level++; setMessage('Level '+level+' — more hearts! 🍔',2); makeBricks(level); ball.speed=Math.min(650+level*25,820); resetBall(); // spawn celebration hearts from bricks? already all gone. Maybe big burst.
if(state==='playing'){ // score bonus
}
}
```
Need if state demo continues. Good.
Ensure after resetBall, ball not inside new bricks? New bricks top max bottom maybe 424, ball from paddle top 510, okay.
Update function:
```
function update(dt){
updateBackground(dt);
if(state !== 'gameover') {
updatePaddle(dt);
updateBall(dt);
}
updateParticles(dt);
updateTexts(dt);
if(messageTimer>0) messageTimer-=dt;
}
```
Need after ball update, if aliveCount==0 levelClear? But if levelClear inside ball update? We'll call check after. However if all bricks destroyed in a frame, make new bricks and resetBall, then later bottom check may lose ball from reset? reset sets y top, moving up, no lose.
Need if `collideBricks` destroys and aliveCount=0. In update after `updateBall`, check `if(aliveCount ===0) levelClear();` But if gameover? no.
Maybe levelClear called multiple? makeBricks sets aliveCount. ok.
UpdatePaddle:
```
function updatePaddle(dt){
if(state==='demo'){ const target=ball.x + ball.vx*0.06; const dx=target-paddle.x; const max=950*dt; paddle.x += clamp(dx,-max,max); }
else if(state==='playing'){
if(inputMode==='pointer' && !keys.left && !keys.right) paddle.x += (pointerX - paddle.x)*Math.min(1, 16*dt);
if(inputMode==='keyboard' || keys.left || keys.right){ if(keys.left) paddle.x-=520*dt; if(keys.right) paddle.x+=520*dt; }
}
paddle.x=clamp(paddle.x,paddle.w/2+10,W-paddle.w/2-10);
// squish decay
}
```
If inputMode pointer but keyboard left pressed, both may combine. Fine. But if keys pressed inputMode keyboard, pointer influence disabled unless keys? The condition above with pointer && !keys. Good.
If pointerX updated only when state playing. In mobile after touch end, pointerX remains last. Good.
Pointer start with touch: On pointerdown, ensureAudio, state start, updatePointer(e), inputMode='pointer'. If pointerType mouse, no need track buttons. On pointermove if state playing set inputMode pointer. But if user uses keyboard after mouse, keydown sets keyboard. Good.
Potential problem: On iframe, keyboard events only after click. We handle pointerdown on canvas, but if user clicks overlay? pointer-events none, so canvas. If they press key without clicking? iframe not focused maybe no key. Overlay says click. Good.
No errors:
- Need `e.preventDefault()` on pointerdown for touch? If passive listener by default? `addEventListener('pointerdown', handler, {passive:false})` then can prevent default. To avoid scrolling? body overflow hidden. Could not need. But use passive false maybe.
- Keydown e.repeat: If state demo and held key? start. ok.
- AudioContext: if not user gesture, don't create? We create in gesture. But if startPlaying called from keydown before gesture? keydown is gesture. Good. But if state demo initial update calls playBounce? No audioCtx null, tone returns. But we only call tone if audioCtx. Good.
Need ensure `window.AudioContext||window.webkitAudioContext` typed? In Chrome fine.
Potential optimization: drawHeart uses save/restore per particle and brick heart. Could be many. But ok.
Let's make heart path optimized with `ctx.beginPath` inside drawHeart and `ctx.fill`. Use `ctx.save()` for transform. Could use manual path with current transform? Need translate/rotate. We'll accept.
Need maybe avoid globalCompositeOperation leftover. Use save/restore for glow.
Let's write drawHeart:
```
function drawHeart(x,y,s,rot,fill){
if (s <= 0) return;
ctx.save();
ctx.translate(x,y);
ctx.rotate(rot);
const k = s/18; // scale
ctx.scale(k,k);
ctx.beginPath();
ctx.moveTo(0,-3);
ctx.bezierCurveTo(0,-7, -6,-10, -9,-6);
ctx.bezierCurveTo(-12,-1, -8,5, 0,11);
ctx.bezierCurveTo(8,5, 12,-1, 9,-6);
ctx.bezierCurveTo(6,-10, 0,-7, 0,-3);
ctx.closePath();
if(typeof fill === 'string') ctx.fillStyle=fill; else ctx.fillStyle=fill;
ctx.fill();
ctx.restore();
}
```
Test: path top cleft y -3, side lobes y -10 to -6, bottom y 11, width 18. Center around 1. So if s=20 scale=20/18=1.1, height ~22, point y 12, top -11. Good.
For particles p.size=6+... => path scaled ~size. Good.
For brick hearts s=12.
Maybe heart path could look weird. Let's test cubic: Start at cleft (0,-3). Left upper: bezier (0,-3)->control (0,-7),( -6,-10)->(-9,-6). That's upper left lobe. Then (-9,-6)->(-12,-1),( -8,5)->0,11 left side to bottom. Symmetric. Seems heart.
Draw burger with roundRect. Need helper `drawRoundRect(x,y,w,h,r)`:
```
function drawRoundRect(x,y,w,h,r){...}
```
Use in bricks/burger.
Paddle draw code more precise:
```
function drawPaddle(){
const px=paddle.x, py=paddle.y, pw=paddle.w, ph=paddle.h;
const sx=1+paddle.squish*.08, sy=1-paddle.squish*.07;
ctx.save(); ctx.translate(px,py); ctx.scale(sx,sy);
// shadow
ctx.fillStyle='rgba(0,0,0,.25)'; ctx.beginPath(); ctx.ellipse(0, ph/2+8, pw/2+10,10,0,0,Math.PI*2); ctx.fill();
// bottom bun
let grd=ctx.createLinearGradient(0,ph/2-18,0,ph/2); grd.addColorStop(0,'#b07543'); grd.addColorStop(1,'#7b4a26'); ctx.fillStyle=grd; drawRoundRect(-pw/2+8, ph/2-20, pw-16, 20, 10); ctx.fill();
// patty
grd=ctx.createLinearGradient(0, ph/2-36,0, ph/2-16); grd.addColorStop(0,'#78442f'); grd.addColorStop(1,'#432117'); ctx.fillStyle=grd; drawRoundRect(-pw/2+4, ph/2-36, pw-8, 18, 9); ctx.fill();
// cheese
ctx.fillStyle='#f3c55c'; drawRoundRect(-pw/2+8, ph/2-44, pw-16, 10, 3); ctx.fill();
// cheese drips
ctx.fillStyle='#ffd76a'; for i=0;i<5;i++ { const x=-pw/2+20+i*(pw-40)/4; ctx.beginPath(); ctx.moveTo(x, ph/2-34); ctx.lineTo(x+6, ph/2-34); ctx.lineTo(x+3, ph/2-24); ctx.fill(); }
// lettuce
ctx.fillStyle='#7ac15e'; ctx.beginPath(); const lx=-pw/2+4, rx=pw/2-4, ty=ph/2-56? Wait ph=76, half=38, ph/2-56=-18. bottom? Let's define from -24 to -18? Need coordinates relative center 0, half=38. bottom bun ph/2-20 = 18. patty ph/2-36=2? That seems patty from 2 to20, bottom bun from18 to38 overlap. Let's recalc.
ph=76, half=38.
bottom bun: y=ph/2-20=18, h20 -> bottom 38.
patty: y=ph/2-38=0, h18 -> bottom 18.
cheese: y=ph/2-46=-8, h10 -> 2.
lettuce: y=ph/2-58=-20, h? maybe 12 -> -8.
tomato: y=ph/2-68=-30, h10 -> -20.
upper bun: top around -38.
Need use ph/2 =38. Let's define explicit:
```
bottomY=18, pattyY=0, cheeseY=-8, lettuceY=-20, tomatoY=-30, bunY=-38? Actually upper bun from -38 to -28.
```
Good.
Code:
- bottom bun: y=18, h=20
- patty: y=0,h=18
- cheese: y=-8,h=10; drips from y=2 down? cheese bottom 2, drips to 10.
- lettuce: from -22 to -12 (sine). y base -16 maybe.
- tomato: y=-30,h=10 (bottom -20)
- upper bun: y=-38 to -28 curve.
Need collision top y=-38 (paddle.h/2). ok.
Let's implement:
```
// bottom bun
drawRoundRect(-pw/2+8, 18, pw-16, 20, 10);
// patty
drawRoundRect(-pw/2+4, 0, pw-8, 18, 9);
// cheese
drawRoundRect(-pw/2+8, -8, pw-16, 10, 2);
// cheese drips triangles y 2 to 12
// lettuce band
ctx.fillStyle = '#79c45f'; ctx.beginPath(); const steps=20; const left=-pw/2+4,right=pw/2-4; const top=-24; ctx.moveTo(left,top); for i=0..steps x left+(right-left)*i/steps; y = top + Math.sin(i/steps*Math.PI*6)*3; lineTo(x,y); for i=steps..0 y = top+12 + Math.sin(...)*3; lineTo; close; fill.
// tomato slice red rounded rect with seeds? drawRoundRect(-pw/2+14, -34, pw-28, 10,5); maybe tomato behind bun? y -34 to -24, overlaps lettuce top? okay. Use -34.
// upper bun
ctx.fillStyle gradient; ctx.beginPath(); ctx.moveTo(-pw/2+4, -28); ctx.bezierCurveTo(-pw/2+4,-44,-pw*0.26,-46,0,-46); ctx.bezierCurveTo(pw*0.26,-46,pw/2-4,-44,pw/2-4,-28); ctx.closePath(); fill;
```
Collision top -38, but upper bun visual top -46 exceeds box. If ball collides at -38, it bounces above visual? Actually top visual extends above collision, ball may appear to hit lower than top. Could set collision h=94? But then paddle area bigger. Or draw burger within -38. Let's constrain upper bun top -36. Use h=78, top=-39. Upper bun -36. Good. Set paddle.h=80? top=-40, burger top -36. okay. Use h=80. Then bottom 40. Paddle y=H-95? If H=600, y=505, top=465, bottom=545. Good. Bricks bottom 416, gap 49. Fine.
Use explicit constants inside scaled context not depend h? Could hardcode within [-36,38]. But paddle.h=76, half=38. Let's use hardcoded for visual, collision h=76. Upper bun top -34, okay inside -38. Use:
- bottom bun y=18,h20 -> bottom 38.
- patty y=0,h18
- cheese y=-8,h10
- lettuce y=-22,h12
- tomato y=-32,h10 -> -22
- upper bun from -34 to -24. Good. Collision top -38, slightly above burger but fine.
Need scale with h? Use constants relative to paddle.h maybe not.
```
const hw=pw/2;
drawRoundRect(-hw+8, paddle.h/2-20, pw-16, 20,10);
paddle.h/2 =38 -> 18.
patty: paddle.h/2-38 =0
cheese: -8
lettuce: -22
tomato: -32
upper bun: -34
```
Ok.
- If h=76. Use `paddle.h=76`. Good.
- Tomato: red rounded rect y=-34? h10 -> -24. Use -34, h10.
- Upper bun: y -36 to -26. Collision top -38. Good.
Seeds at y -30.
- Need drawRoundRect uses relative coordinates after translate. Good.
```
// tomato
ctx.fillStyle='#ef4b5f'; drawRoundRect(-hw+18, -34, pw-36, 10,5); ctx.fill();
// maybe tomato seeds? skip
```
Upper bun gradient:
```
ctx.beginPath(); ctx.moveTo(-hw+6, -26); ctx.bezierCurveTo(-hw+6,-42,-pw*0.26,-42,0,-42); ctx.bezierCurveTo(pw*0.26,-42,hw-6,-42,hw-6,-26); ctx.lineTo(hw-6,-26); ctx.closePath(); fill;
```
Top at -42 > collision -38. Too much. Use top -38:
```
moveTo(-hw+6, -28); bezierCurveTo(-hw+6,-38,-pw*0.26,-38,0,-38); bezier... close.
```
Ok. Seeds y -33.
Draw bun highlight: ellipse white alpha.
Add sesame positions:
```
for pos [[-0.28w,-33], [-0.14w,-35], [0,-36], [0.14w,-35], [0.28w,-33], [-0.22,-31], [0.22,-31]] draw ellipse 3x2 rotated.
```
Need draw shadow under paddle maybe.
Brick collision top uses paddle.y - paddle.h/2. If visual upper bun top -38, collision top -38. good.
Particle burst direction: maybe hearts explode outward and then float up. If grav positive, fall. Could make some hearts have negative gravity (rise) or low gravity. Use grav = 60 + rand*120, drag 1.5, initial vy -150.. -350 (up) plus sideways. They will rise, fall, fade. This looks tons.
```
vy = -180 - Math.random()*220 + ball.vy*0.2? if brick broken by ball downward? ball.vy negative hitting brick bottom. Use explosion upward from brick? Let's set vy = -120 + Math.random()*250? For outward. If want hearts fall after explosion: vy initial random -250..150, grav 320. Nice.
```
We want "explodes into tons of little hearts" maybe outward burst from brick. Use random angles full 2pi but upward bias. `ang = Math.random()*Math.PI*2; spd=50+300; vy = Math.sin(ang)*spd - 50;`. Good.
Need spawn hearts with brick dimensions: distribute inside rect.
Potential issue with `MAX_PARTICLES` and shift causing O(n). Use if particles.length > max, remove 50 instead of shift. Simpler:
```
while(particles.length > MAX_PARTICLES) particles.splice(0,20);
```
Ok.
Texts:
```
function addText(x,y,text,hue){ if(texts.length>40) texts.splice(0,10); texts.push(...)}
update: y += vy*dt; vy *= (1 - 2*dt); life-=dt; if <=0 remove swap.
draw: alpha, font bold, fill hsl, stroke black? Add text shadow.
```
Need maybe hearts from ball trail: `trail` update after ball move. If state gameover skip? okay.
Implementation with arrays and swap remove:
```
for (let i=particles.length-1;i>=0;i--){ const p=particles[i]; ... if dead { particles[i]=particles[particles.length-1]; particles.pop(); } }
```
But need update first. Good.
Bricks alive count: use array and set alive false. `aliveCount--`.
Sound functions: ensureAudio only after gesture. If state becomes playing, `ensureAudio()`. But if user only presses key (gesture), ok. Could call in keydown/pointerdown before start.
```
function ensureAudio(){ try { if(!audioCtx) audioCtx = new (window.AudioContext || window.webkitAudioContext)(); if(audioCtx.state==='suspended') audioCtx.resume(); } catch(e){} }
```
`tone` check if `!audioCtx || audioCtx.state !== 'running'`? If suspended, resume may be async; can still schedule. Use if no audioCtx return. If audioCtx state suspended, resume.
```
function tone(freq,dur=0.1,type='sine',vol=0.04){ if(!audioCtx)return; const t=audioCtx.currentTime; const osc=audioCtx.createOscillator(); const gain=audioCtx.createGain(); osc.type=type; osc.frequency.setValueAtTime(freq,t); gain.gain.setValueAtTime(vol,t); gain.gain.exponentialRampToValueAtTime(0.0001,t+dur); osc.connect(gain).connect(audioCtx.destination); osc.start(t); osc.stop(t+dur+0.02); }
```
- Multiple oscillators per heart? only brick, few. ok.
Call tone in playing only? In collideWalls and paddle and brick, if audioCtx not null (user started) can play even if state demo? If audioCtx exists from previous gameover? Not. Use `if (state==='playing' && audioCtx) play...` But if startPlaying state playing and ensureAudio, ok. In demo, state 'demo', no sound. If gameover, no ball. Good.
Need `state` global. Functions can access.
Potential bug: `updateOverlay` called during reset? If startPlaying calls reset? no. If gameover updateOverlay. Good.
Now write code.
Double-check collision brick reflection and normalization:
```
function normalizeBall(){ const len=Math.hypot(ball.vx, ball.vy); if(len ===0){ ball.vy=-ball.speed; return;} ball.vx=ball.vx/len*ball.speed; ball.vy=ball.vy/len*ball.speed; }
```
After bounce, if ball.vy becomes 0 due angle? ensure not zero horizontal causing no brick? In wall collision: if abs(vx)<50 after paddle? We can set. In normalize, if len small set upward. After paddle bounce, `if(Math.abs(ball.vy) < 120) ball.vy = -120;` Then normalize. For wall bottom not.
Paddle bounce:
```
const hit=(ball.x-paddle.x)/(paddle.w/2); const angle=hit*0.9; ball.speed=Math.min(800, ball.speed+10); ball.vx=Math.sin(angle)*ball.speed; ball.vy=-Math.cos(angle)*ball.speed; ball.y=paddleTop-ball.r; normalize? Already speed.
```
If hit near edge cos 0.62 => vy -500? fine. Ensure vy negative.
Brick reflect:
```
if(overlapX < overlapY){ ball.vx = Math.abs(ball.vx) * (ball.x < bCenterX ? -1 : 1); ball.x = ball.x < bCenterX ? b.x - ball.r : b.x+b.w+ball.r; } else { ball.vy = Math.abs(ball.vy) * (ball.y < bCenterY ? -1 : 1); ball.y = ball.y < bCenterY ? b.y - ball.r : b.y+b.h+ball.r; }
normalizeBall();
```
Need set sign based on velocity direction? If ball inside, above formula ensures moving away. Good.
For hp >1, don't destroy but reflect.
If ball hits brick from side while moving downward, reflect side maybe. Good.
Could ball stuck between brick and paddle? Destroy brick no. If hp >1 maybe reflect. ok.
Substep collisions: if collideBricks returns true, break; but after break ball may be near. Next frame. ok.
Need after brick bounce check bottom? In substep, ball y maybe set. After loop, if ball.y > H+60 lose.
Level clear: makeBricks while particles from old. Good.
Initial demo: `startEl` text. Could update every state change. Also maybe message at start: `setMessage('Demo: AI is playing', 2.2)`. But overlay has. Canvas message maybe too much. Fine.
Potential issue: `canvas.width = W*dpr` and CSS width may cause internal transform? We use `ctx.setTransform(dpr,0,0,dpr,0,0)`. All drawing logical 0..960. Good. If dpr changes resize call.
Need no horizontal scrollbar: body overflow hidden.
Testing code mentally for syntax:
- Use template literals? OK.
- Use `const palette`? fine.
Now compose final HTML in one code block. Need no explanation. Ensure valid.
Let's write full code:
```
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Hamburger Breakout Heart Explosion</title>
<style> ... </style>
</head>
<body>
<div id="stage"><canvas id="game" width="960" height="600"></canvas></div>
<div id="start">...</div>
<script>
(function(){
'use strict';
const canvas=..., ctx=..., W=960,H=600;
let dpr = Math.min(window.devicePixelRatio || 1, 2);
function resizeCanvas(){ dpr=Math.min(window.devicePixelRatio||1,2); canvas.width=Math.floor(W*dpr); canvas.height=Math.floor(H*dpr); ctx.setTransform(dpr,0,0,dpr,0,0); }
window.addEventListener('resize', resizeCanvas); resizeCanvas();
...
})();
</script>
</body>
</html>
```
Need CSS canvas width min uses `--`? Let's implement:
```
#stage {position:fixed; inset:0; display:flex; align-items:center; justify-content:center; padding:10px; box-sizing:border-box; overflow:hidden;}
canvas { display:block; width:min(100%, calc((100vh - 24px) * 1.6), 1280px); max-width:100%; height:auto; aspect-ratio:16 / 10; background:#0d0710; border-radius:14px; box-shadow:...; touch-action:none; }
```
`height:auto` with aspect-ratio. Should work. On very wide short viewport, `calc((100vh-24px)*1.6)` maybe min. But if canvas aspect-ratio height auto, width chosen. Need `max-height: calc(100vh - 24px)` too? Could include:
```
max-height: calc(100vh - 24px);
```
But if max-height and width min? It might. We'll add max-height. If max-height applies, aspect-ratio may preserve by reducing height but width still min? For aspect-ratio with max-height and width:100%, width can exceed? Let's rely on width formula. For 375x667, min(375-20=355, (667-24)*1.6=1029, 1280)=355 height=222. ok. For 1280x800: min(1260,1241,1280)=1241 height=776. plus padding 24 ok. For 1280x360: min(1260,537,1280)=537 height=336 plus padding 24 fits. Good.
Need use `100vw` vs stage 100%? width min 100% of stage (viewport minus padding), okay. But if viewport has scrollbar? no. Use `min(100%, calc((100vh - 24px) * 1.6), 1280px)`. Good.
Overlay CSS: pointer-events none.
```
#start { position:fixed; left:50%; bottom:14px; transform:translateX(-50%); width:min(94vw, 760px); ... }
```
If gameover, width ok.
Need hide class: `.hide{opacity:0;}`.
Now JS. Need ensure variables declared before functions? Hoisting for function declarations okay. Use let for state.
Implementation details code:
```
let state = 'demo';
let score = 0, lives = 3, level = 1;
let message = '', messageTimer = 0;
let audioCtx = null;
const keys = {left:false,right:false};
let inputMode = 'pointer';
let pointerX = W/2;
const paddle = {x:W/2, y:H-95, w:174, h:76, squish:0};
const ball = {x:W/2, y:H/2, r:12, vx:180, vy:-400, speed:430, angle:0, trail:[]};
let bricks=[];
let aliveCount=0;
const particles=[];
const texts=[];
const bgHearts=[];
const MAX_PARTICLES = 850;
const PALETTE = [...]
```
`resetGame`:
```
function resetGame(){ score=0; lives=3; level=1; particles.length=0; texts.length=0; ball.trail.length=0; ball.speed=430; makeBricks(level); paddle.x=W/2; resetBall(true); message=''; messageTimer=0; updateOverlay(); }
```
`resetBall`:
```
function resetBall(serve){ ball.x=paddle.x; ball.y=paddle.y - paddle.h/2 - ball.r - 4; const dir=Math.random()<.5?-1:1; const a=dir*(0.18+Math.random()*0.42); ball.vx=Math.sin(a)*ball.speed; ball.vy=-Math.abs(Math.cos(a))*ball.speed; ball.trail.length=0; }
```
Angle: if a 0.2 => vx 86, vy -424. ok.
Need ensure ball not above bricks? y=505-38-16=451, bricks bottom maybe up to 416, 35 gap. Fine.
If bricks bottom higher than 451? rows max? top 96 + (rows-1)*(30+gap). gap 7, rows8 => y last=96+7*37=355+30=385. Good. If rows 8. We'll use rows <=8. bottom max 385. gap 66. Good.
`makeBricks`:
```
function makeBricks(lv){ bricks=[]; aliveCount=0; const cols=12, rows=Math.max(4, Math.min(8, 4+Math.floor(lv/2))); const gap=7, side=24, top=90, bh=30; const bw=(W-side*2-gap*(cols-1))/cols; const pattern=(lv-1)%5;
for(let r=0;r<rows;r++){ for(let c=0;c<cols;c++){ let include=true; if(pattern===1) include=(r+c)%2===1; else if(pattern===2) include=(r%3!==1); else if(pattern===3) include=!(r===rows-1 && (c%4===0||c%4===1)); else if(pattern===4) include=(Math.abs(r*rows + c*cols)%11!==0); if(!include) continue; const hp=(lv>=3 && r===0) ? 2 : 1; const hue=(335 + r*11 + c*5 + lv*23) % 360; const value=10*(rows-r); bricks.push({x:side+c*(bw+gap), y:top+r*(bh+gap), w:bw, h:bh, hue, value, hp, maxHp:hp, alive:true, seed:Math.random()*Math.PI*2}); aliveCount++; }}
if(aliveCount===0) { // fallback add middle bricks
for(let c=0;c<cols;c++){ ... }
}
}
```
Pattern could skip too many? pattern4 `(r*11+c*7)%13 !== 0` ok. Need if all false? no. Maybe simpler pattern include true for pattern0. Use:
```
if(pattern===1) include = ((r+c)%2) === 0; // checker keeps half.
if(pattern===2) include = r < 4 || c % 3 !== 0; //
```
Need ensure enough. Let's write simpler:
```
if(pattern === 1) include = ((r+c)&1) === 0;
else if(pattern === 2) include = (c % 3 !== (r % 3));
else if(pattern === 3) include = (r < 3 || (c + r) % 4 !== 0);
else if(pattern === 4) include = ((r * 5 + c * 3) % 7 !== 0);
```
Checker include if true, but if pattern1 include false when odd? It will have half. Good. For fallback if aliveCount<10, add all missing? Simpler: after loops if aliveCount < rows*cols/3, set all include. But we need code. Let's do:
```
if(aliveCount < 15) { bricks=[]; for rows/cols all hp1; }
```
But for first pattern? Level1 pattern0 all. ok.
Colors: `const light = 55 - r*2 + (hp>1?5:0)`. Store color? Draw dynamic.
`drawBricks`:
```
function drawBricks(){ for(const b of bricks){ if(!b.alive) continue; const x=b.x,y=b.y,w=b.w,h=b.h; ctx.save(); ctx.fillStyle='rgba(0,0,0,.18)'; drawRoundRect(x+3,y+5,w,h,8); ctx.fill(); const g=ctx.createLinearGradient(x,y,x,y+h); const hpR=b.hp/b.maxHp; g.addColorStop(0,`hsl(${b.hue} 95% ${68+hpR*4}%)`); g.addColorStop(.55,`hsl(${b.hue} 98% ${54+hpR*5}%)`); g.addColorStop(1,`hsl(${(b.hue+360+20)%360} 90% ${34}%)`); ctx.fillStyle=g; drawRoundRect(x,y,w,h,8); ctx.fill(); ctx.fillStyle='rgba(255,255,255,.32)'; drawRoundRect(x+4,y+3,w-8,h*0.32,6); ctx.fill(); ctx.fillStyle='rgba(255,255,255,.12)'; drawHeart(x+w/2, y+h/2, Math.min(14, h*0.55), b.seed, 'rgba(255,255,255,.25)'); if(b.hp < b.maxHp){ ctx.strokeStyle='rgba(255,255,255,.45)'; ctx.lineWidth=2; ctx.beginPath(); ctx.moveTo(x+w*0.3,y+h*0.45); ctx.lineTo(x+w*0.5,y+h*0.55); ctx.lineTo(x+w*0.7,y+h*0.3); ctx.stroke(); } ctx.restore(); } }
```
Need drawHeart fill string but globalAlpha? It uses fillStyle. ok.
`drawRoundRect` as function without ctx param uses global ctx. Use `function drawRoundRect(x,y,w,h,r){...}`. If w negative? no. If r> w/2 clamp.
`destroyBrick`:
```
function destroyBrick(b){ if(!b.alive) return; b.alive=false; aliveCount--; score += b.value; addText(b.x+b.w/2,b.y+b.h/2,'+'+b.value,b.hue); spawnHearts(b, true); if(state==='playing'){ playBrickSound(b.hue); } }
```
`hitBrick` for hp>1 small hearts, sound. In collideBricks after `b.hp--`, if b.hp<=0 destroy else `spawnHearts(b,false)`? Need not reflect before? yes. Small hearts 12. Add `b.value`? No score. Maybe small.
```
if(b.hp <= 0) destroyBrick(b); else { spawnSmallHearts(b); if(state==='playing') tone(...); }
```
Need `b.value` score maybe add on hit? Could add 5. But if destroy add full? Keep simple score only on destroy.
`spawnHearts(b, big)`:
```
function spawnHearts(b,big){ const count= big ? 70 + Math.floor(Math.random()*30) : 12; const cx=b.x+b.w/2, cy=b.y+b.h/2; for i... if particles length > MAX remove. const a=Math.random()*Math.PI*2; const speed=big? 40+Math.random()*330 : 30+Math.random()*120; const hue = (b.hue + (Math.random()*70-35) + (Math.random()<0.3?330:0)) %360; particles.push({x: b.x + Math.random()*b.w, y: b.y+Math.random()*b.h, vx:Math.cos(a)*speed, vy:Math.sin(a)*speed - (big?70:30), size:big?5+Math.random()*18:3+Math.random()*9, rot:Math.random()*6.28, spin:(Math.random()-.5)*8, life:big?0.8+Math.random()*1.3:0.3+Math.random()*0.5, maxLife:1.6, hue:hue, sat:92, light:52+Math.random()*25, grav:160+Math.random()*260, drag:1.8}); }
```
If hue calculation with 330 may exceed, use ((hue%360)+360)%360.
Could add ring sparkles with bigger, but hearts enough.
`spawnMissHearts(x,y)` maybe small gray hearts.
`updateParticles`:
```
for i=particles.length-1... { p.life-=dt; if(p.life<=0...){ swap remove continue} p.vy += p.grav*dt; const drag=1 - Math.min(0.3, p.drag*dt); p.vx *= drag; p.vy *= drag; p.x += p.vx*dt; p.y += p.vy*dt; p.rot += p.spin*dt; if(p.x<-80... ) remove }
```
Need drag not negative: use `const factor = Math.exp(-p.drag*dt); p.vx*=factor; p.vy*=factor;` More stable. Use exp. ok.
Draw particles:
```
for p of particles { const alpha=Math.max(0,Math.min(1,p.life/(p.maxLife*0.75))); ctx.globalAlpha=alpha; const scale = alpha*? maybe p.size*(0.75+0.5*alpha); drawHeart(p.x,p.y,p.size*(0.6+0.8*alpha), p.rot, `hsl(${p.hue} ${p.sat}% ${p.light}%)`); }
ctx.globalAlpha=1;
```
Need p.maxLife for big 1.6, small maybe life > max? set maxLife=p.life initial. So alpha p.life / p.maxLife. Use clamp.
Text update:
```
for i... { t.y += t.vy*dt; t.vy *= Math.exp(-2*dt); t.life-=dt; if dead remove }
draw: ctx.globalAlpha=t.life/t.max; ctx.fillStyle=`hsl(${t.hue} 95% 72%)`; ctx.font='800 22px ...'; textAlign center; fillText(t.text,t.x,t.y); also small heart? ok.
```
Background hearts:
```
for i<26 bgHearts.push({x:Math.random()*W, y:Math.random()*H, vy:6+Math.random()*18, vx:(Math.random()-.5)*6, size:25+Math.random()*70, hue:330+Math.random()*35, rot:Math.random()*6.28, spin:(Math.random()-.5)*.5, phase:Math.random()*6.28, alpha:.04+Math.random()*.07});
update: b.y += (b.vy + Math.sin(time+b.phase)*8) * dt; b.x += b.vx*dt + Math.cos(time*0.7+b.phase)*8*dt; if b.y > H+b.size { b.y=-b.size; b.x=Math.random()*W; } if b.x < -b.size ...
draw: ctx.globalAlpha = b.alpha; drawHeart(..., `hsl(${b.hue} 70% 60%)`); ctx.globalAlpha=1;
```
Need global time variable. Use `let time=0; time += dt` in update.
Message draw: Use time? `if(messageTimer>0)`.
`drawPaddle` detailed code:
```
function drawPaddle(){
const pw=paddle.w, ph=paddle.h, hw=pw/2, hh=ph/2;
const sx=1+paddle.squish*.08, sy=1-paddle.squish*.06;
ctx.save(); ctx.translate(paddle.x,paddle.y); ctx.scale(sx,sy);
// shadow
ctx.globalAlpha=.28; ctx.fillStyle='#000'; ctx.beginPath(); ctx.ellipse(0,hh+10,hw+14,12,0,0,Math.PI*2); ctx.fill(); ctx.globalAlpha=1;
// bottom bun
let g=ctx.createLinearGradient(0,hh-20,0,hh); g.addColorStop(0,'#c8814b'); g.addColorStop(1,'#7d4621'); ctx.fillStyle=g; drawRoundRect(-hw+10,hh-20,pw-20,20,10); ctx.fill();
// patty
g=ctx.createLinearGradient(0,0,0,18); g.addColorStop(0,'#6d3b28'); g.addColorStop(1,'#3a1e14'); ctx.fillStyle=g; drawRoundRect(-hw+5,0,pw-10,18,8); ctx.fill();
// cheese
ctx.fillStyle='#f6c35a'; drawRoundRect(-hw+10,-8,pw-20,10,3); ctx.fill();
ctx.fillStyle='#ffe087'; for(let i=0;i<6;i++){ const x=-hw+18+i*(pw-36)/5; ctx.beginPath(); ctx.moveTo(x,2); ctx.lineTo(x+7,2); ctx.lineTo(x+3.5,12); ctx.closePath(); ctx.fill(); }
// lettuce
ctx.fillStyle='#66c05d'; ctx.beginPath(); const steps=22; const l=-hw+4, r=hw-4, top=-24; ctx.moveTo(l,top); for(let i=0;i<=steps;i++){ const t=i/steps; ctx.lineTo(l+(r-l)*t, top+Math.sin(t*Math.PI*5)*3); } for(let i=steps;i>=0;i--){ const t=i/steps; ctx.lineTo(l+(r-l)*t, top+13+Math.sin(t*Math.PI*5+1)*2); } ctx.closePath(); ctx.fill();
// tomato
ctx.fillStyle='#ef3b5f'; drawRoundRect(-hw+18,-34,pw-36,10,5); ctx.fill(); ctx.fillStyle='rgba(255,255,255,.18)'; drawRoundRect(-hw+22,-32,pw-44,4,2); ctx.fill();
// upper bun
g=ctx.createLinearGradient(0,-hh-3,0,-24); g.addColorStop(0,'#e5ae6e'); g.addColorStop(1,'#b77a42'); ctx.fillStyle=g; ctx.beginPath(); ctx.moveTo(-hw+6,-24); ctx.quadraticCurveTo(-hw+4,-hh-2,0,-hh-2); ctx.quadraticCurveTo(hw-4,-hh-2,hw-6,-24); ctx.closePath(); ctx.fill();
// bun highlight
ctx.fillStyle='rgba(255,255,255,.2)'; ctx.beginPath(); ctx.ellipse(-hw*0.25,-hh*0.72,hw*0.3,4,-0.3,0,Math.PI*2); ctx.fill();
// sesame seeds
ctx.fillStyle='#fff0cf'; const seeds=[[-.29,-.76],[-.16,-.86],[0,-.9],[.16,-.86],[.29,-.76],[-.08,-.79],[.08,-.79]]; for(const s of seeds){ ctx.save(); ctx.translate(s[0]*pw, s[1]*hh); ctx.rotate(s[0]*.7); ctx.beginPath(); ctx.ellipse(0,0,3.2,2,0,0,Math.PI*2); ctx.fill(); ctx.restore(); }
ctx.restore();
}
```
Potential issue quadratic curve top at y=-hh-2 = -40 for hh=38, collision top -38, visual top -40 outside. Use `-hh`? If top at -38. Use `quadraticCurveTo(-hw+4, -hh, 0, -hh);` and start at -24. But control at -hh maybe top -38. Good. Seeds y -0.9*hh = -34.2 inside. Bun highlight. Let's adjust:
```
ctx.moveTo(-hw+6,-24); ctx.quadraticCurveTo(-hw+5,-37,0,-37); ctx.quadraticCurveTo(hw-5,-37,hw-6,-24); close.
```
Collision top -38, visual top -37. ok.
`seeds` y relative to hh? use s[1]*hh. With s -0.9 -> -34, fine.
Upper bun top from y -37, collision -38. okay.
Need `drawRoundRect` before fill uses beginPath. ok.
`drawBall`:
```
function drawBall(){ // trail
for t: ctx.globalAlpha = Math.max(0,t.life/0.32)*0.32; drawHeart(t.x,t.y,t.size,t.rot,`hsla(335,100%,70%,${ctx.globalAlpha})`); ...
ctx.globalAlpha=1;
const grad=ctx.createRadialGradient(ball.x-4,ball.y-4,1,ball.x,ball.y,ball.r*1.8);
grad.addColorStop(0,'rgba(255,255,255,.95)'); grad.addColorStop(.25,'#ffd1e5'); grad.addColorStop(1,'#ff2d78');
drawHeart(ball.x,ball.y, ball.r*2.45, ball.angle, grad);
// small glow
ctx.save(); ctx.globalCompositeOperation='lighter'; ctx.globalAlpha=.3; ctx.beginPath(); ctx.arc(ball.x,ball.y,ball.r*1.6,0,6.28); ctx.fillStyle='rgba(255,80,140,.35)'; ctx.fill(); ctx.restore();
}
```
Need drawHeart uses fillStyle=fill; for gradient fill okay. But `drawHeart` uses `ctx.fillStyle = fill` not `if typeof` ok.
Trail update:
```
function updateBall(dt){
// move substeps
ball.trail.push({x:ball.x,y:ball.y,life:0.35,size:ball.r*1.7,rot:ball.angle});
if(ball.trail.length>25) ball.trail.shift();
for(let i=ball.trail.length-1;i>=0;i--){ ball.trail[i].life-=dt; if(ball.trail[i].life<=0) ball.trail.splice(i,1); }
}
```
But push before move? after move ok. If state demo, trail. Good.
Need ball.angle update before draw: `ball.angle += (ball.vx*0.0003 + ball.vy*0.0002) + dt*0.5;` maybe. Use dt.
`ball.angle += (ball.vx + ball.vy) * 0.0008 * dt? Actually vx/vy px/s, dt s => units px? no. Use `ball.angle += dt*2.5 * (ball.vx>=0?1:-1);` simple spin.
```
ball.angle += (ball.vx > 0 ? 4 : -4)*dt;
```
`collideWalls`:
```
function collideWalls(){
if(ball.x-ball.r<0){ball.x=ball.r; ball.vx=Math.abs(ball.vx); if(state==='playing')tone(...)}
if(ball.x+ball.r>W){...}
if(ball.y-ball.r<0){ball.y=ball.r; ball.vy=Math.abs(ball.vy); ...}
}
```
Need if ball.y+ball.r < H? no.
`updateBall`:
```
function updateBall(dt){
if(state==='gameover') return;
const steps = Math.max(1, Math.ceil(ball.speed*dt/6));
const sdt = dt/steps;
for(let s=0;s<steps;s++){
ball.x += ball.vx*sdt; ball.y += ball.vy*sdt;
collideWalls();
collidePaddle();
if(collideBricks()) break;
}
ball.angle += (ball.vx > 0 ? 3 : -3)*dt;
// trail update separate before or after.
if(ball.y > H + ball.r*4) loseBall();
if(state==='playing' && aliveCount===0) levelClear(); else if(state==='demo' && aliveCount===0) levelClear();
}
```
Level clear in update after ball. If state gameover and alive 0? no.
Maybe if gameover, ball not update, but alive 0 from previous? Not.
`collidePaddle`:
```
function collidePaddle(){
if(ball.vy <= 0) return;
const left=paddle.x-paddle.w/2, right=paddle.x+paddle.w/2, top=paddle.y-paddle.h/2, bottom=paddle.y+paddle.h/2;
if(ball.y+ball.r >= top && ball.y-ball.r <= bottom && ball.x+ball.r >= left && ball.x-ball.r <= right){
const hit=clamp((ball.x-paddle.x)/(paddle.w/2),-1,1);
ball.speed=Math.min(ball.speed+12, 820);
const angle=hit*0.95;
ball.vx=Math.sin(angle)*ball.speed;
ball.vy=-Math.abs(Math.cos(angle)*ball.speed);
ball.y=top-ball.r;
paddle.squish=1;
if(state==='playing'){ tone(180+Math.abs(hit)*120, .07, 'square', .025); }
}
}
```
Need if angle near 0.95 cos 0.58 -> vy -500 ok. If ball hits side near bottom? no.
`collideBricks`:
```
function collideBricks(){
for(const b of bricks){
if(!b.alive) continue;
const cx=b.x+b.w/2, cy=b.y+b.h/2;
const dx=Math.abs(ball.x-cx), dy=Math.abs(ball.y-cy);
if(dx <= b.w/2+ball.r && dy <= b.h/2+ball.r){
const overlapX=b.w/2+ball.r-dx; const overlapY=b.h/2+ball.r-dy;
if(overlapX < overlapY){
if(ball.x < cx){ ball.vx=-Math.abs(ball.vx); ball.x=cx - b.w/2 - ball.r - 1; } else { ball.vx=Math.abs(ball.vx); ball.x=cx + b.w/2 + ball.r + 1; }
} else {
if(ball.y < cy){ ball.vy=-Math.abs(ball.vy); ball.y=cy - b.h/2 - ball.r - 1; } else { ball.vy=Math.abs(ball.vy); ball.y=cy + b.h/2 + ball.r + 1; }
}
ball.speed=Math.min(ball.speed+4, 840); normalizeBall();
b.hp--;
if(b.hp <= 0) destroyBrick(b); else { spawnHearts(b,false); if(state==='playing') tone(260+b.hue, .08,'sine',.03); }
return true;
}
}
return false;
}
```
Use ball.x relative to brick center for horizontal reflect. If ball came from right, center > x? Set correctly. But if ball moving right and hits left side, ball.x < cx, vx=-abs => bounce left, set ball.x left. If ball moving left and hits right side, ball.x > cx, vx=abs => right, set right. Good. Vertical similar. If corner and overlapX smaller, side. Good.
`normalizeBall`: if len <1 set upward. After brick reflect, if vy sign set.
`loseBall`:
```
function loseBall(){ if(state==='gameover') return; if(state==='demo'){ resetBall(); spawnMissHearts(ball.x,H-30); return; } lives=Math.max(0,lives-1); playLoseSound(); spawnMissHearts(ball.x,H-30); if(lives<=0){ state='gameover'; setMessage('Game Over 💔',3); updateOverlay(); } else { setMessage('Lost a heart!',1.4); resetBall(); updateOverlay? no; } }
```
Need ball y after reset not >H. ok.
If state playing and level clear? no.
`spawnMissHearts`: white/pink hearts.
`levelClear`:
```
function levelClear(){ if(state==='gameover') return; level++; setMessage('Level '+level+' — Extra Hearts!',2.2); ball.speed=Math.min(850, 430 + level*30); makeBricks(level); for(let i=0;i<5;i++) addText(rand..., '❤'); resetBall(); if(state==='playing') tone chord; }
```
Could set `ball.speed` before reset. Good.
`startPlaying`:
```
function startPlaying(){ ensureAudio(); if(state==='gameover'){ resetGame(); state='playing'; inputMode='pointer'; setMessage('Go!',1.2); } else if(state==='demo'){ state='playing'; inputMode='pointer'; setMessage('Go! 🍔',1.2); } updateOverlay(); }
```
But resetGame calls updateOverlay after state? It sets score/lives. If state gameover, after resetGame state playing, updateOverlay called in reset? It may see state playing? We need resetGame not change state. `resetGame()` only data. Then set state. updateOverlay.
```
function startPlaying(){ ensureAudio(); if(state!=='playing'){ resetGame(); state='playing'; } else { ...? } inputMode = (last input was key? pointer?) maybe set to pointer on pointerdown. startPlaying doesn't know. Use set inputMode outside.
```
But if state demo start and user used key, we want keyboard not pointer. If pointerdown, pointer. Since startPlaying called inside handlers after inputMode maybe set? We can set before? Simpler:
- In keydown handler: if state !== 'playing', resetGame(); state='playing'; updateOverlay(); inputMode='keyboard'; keys maybe set after.
- In pointerdown handler: if state !== 'playing', resetGame(); state='playing'; updateOverlay(); inputMode='pointer'; pointerX = getPointerX(e);
No separate startPlaying. But ensureAudio.
```
function beginPlaying(mode){ ensureAudio(); if(state!=='playing'){ resetGame(); state='playing'; } else { setMessage('Go!',0.8); } inputMode=mode; updateOverlay(); }
```
If already playing and keydown, no reset but set inputMode. For pointermove, if state playing set inputMode pointer, pointerX; no begin.
- For keydown any key if state not playing: beginPlaying('keyboard'). If already playing and arrow: beginPlaying('keyboard')? That would not reset but set inputMode. Good. But if key not arrow, set inputMode keyboard maybe. ok.
- For pointerdown if state not playing: beginPlaying('pointer').
- For pointermove if state not playing: do not begin (only pointerdown or key starts), but can update pointerX? Not needed. If state demo and mouse moves, ignore. But if state gameover and mouse moves before click? Could not start. okay.
`beginPlaying`:
```
function beginPlaying(mode){ ensureAudio(); if(state!=='playing'){ resetGame(); state='playing'; inputMode=mode; setMessage('Play!',1.2); } else { inputMode=mode; } updateOverlay(); }
```
If user presses key when state playing, resetGame not called. Good.
But if state demo, AI moving paddle. User clicks pointer, resetGame sets paddle center, ball reset, level 1. This interrupts demo action but okay.
ResetGame should not set state. It should generate level1 and reset. If called from begin for gameover, reset. Good.
Need avoid resetting when already playing and keydown movement? begin called with 'keyboard' on keydown? In handler: `if(['ArrowLeft','ArrowRight','KeyA','KeyD'].includes(e.code) || state!=='playing') beginPlaying(e.code...?'keyboard':'keyboard');` If keydown space while playing? state==='playing', beginPlaying sets inputMode keyboard no reset. ok. If keydown 'KeyF' not movement while playing? Could set inputMode keyboard causing paddle freeze? Maybe not. Only call begin if state != playing. For movement keys while playing, inputMode keyboard. Code:
```
window.addEventListener('keydown', e=>{
const moving = e.code==='ArrowLeft'||e.code==='ArrowRight'||e.code==='KeyA'||e.code==='KeyD';
if(moving) e.preventDefault();
if(state!=='playing'){ ensureAudio(); beginPlaying('keyboard'); }
else if(moving){ inputMode='keyboard'; }
if(e.code==='ArrowLeft'||e.code==='KeyA') keys.left=true;
if(e.code==='ArrowRight'||e.code==='KeyD') keys.right=true;
});
```
But `beginPlaying` calls ensureAudio itself. If state! playing. For pointerdown: `if(state!=='playing') beginPlaying('pointer'); else { updatePointer; inputMode='pointer'; }`
Need if state demo and pointerdown, begin resets and inputMode pointer, pointerX updated after? We'll call updatePointer(e) after begin? `beginPlaying('pointer')` doesn't know x. In handler:
```
function getPointerLogicalX(e){...}
const x=getPointerLogicalX(e); pointerX=x;
if(state!=='playing') beginPlaying('pointer'); else inputMode='pointer';
```
Good.
Keydown if state demo: beginPlaying('keyboard') sets inputMode keyboard, keys false. User can press arrow. If they clicked? ok.
UpdateOverlay:
```
function updateOverlay(){
if(state==='demo'){ startEl.innerHTML='🍔 Click or press a key to start<br><span>Self-playing demo · bricks explode into hearts</span>'; startEl.classList.remove('hide'); }
else if(state==='gameover'){ startEl.innerHTML='💔 Game Over — Score '+score+'<br><span>Click or press any key to play again</span>'; startEl.classList.remove('hide'); }
else { startEl.classList.add('hide'); startEl.innerHTML='🍔 Mouse / touch / ← → to move'; }
}
```
Need if hidden but innerHTML changed; ok.
Potential: On gameover, updateOverlay called. If state demo initial, overlay visible. Good.
`drawMessage` should not use shadowBlur maybe expensive but okay.
```
function drawMessage(){ if(messageTimer<=0)return; const a=Math.min(1,messageTimer); ctx.save(); ctx.globalAlpha=a; ctx.textAlign='center'; ctx.textBaseline='middle'; ctx.font='900 36px system-ui'; ctx.fillStyle='#fff'; ctx.shadowColor='rgba(255,70,130,.9)'; ctx.shadowBlur=24; ctx.fillText(message,W/2,H*0.5); ctx.shadowBlur=0; ctx.restore(); }
```
Need H*0.5 might cover paddle? okay.
`drawUI`:
```
function drawUI(){
// score panel maybe semi rect
ctx.save(); ctx.textBaseline='middle'; ctx.fillStyle='rgba(255,255,255,.08)'; drawRoundRect(10,10,210,48,24); ctx.fill(); // maybe
ctx.fillStyle='rgba(255,255,255,.75)'; ctx.font='700 13px system-ui'; ctx.textAlign='left'; ctx.fillText('SCORE', 22,25); ctx.fillStyle='#fff'; ctx.font='800 20px system-ui'; ctx.fillText(score,22,45);
for i lives drawHeart(120+i*28,32,22,0,'#ff4b6d');
ctx.textAlign='right'; ctx.fillStyle='rgba(255,255,255,.08)'; drawRoundRect(W-120,10,110,48,24); ctx.fill(); ctx.fillStyle='rgba(255,255,255,.75)'; ctx.font='700 13px system-ui'; ctx.fillText('LEVEL',W-20,25); ctx.fillStyle='#fff'; ctx.font='800 20px system-ui'; ctx.fillText(level,W-20,45);
if(state==='demo'){ ctx.textAlign='center'; ctx.font='700 13px system-ui'; ctx.fillStyle='rgba(255,255,255,.6)'; ctx.fillText('SELF-PLAYING DEMO', W/2, 32); }
ctx.restore();
}
```
Lives hearts might overlap score if lives=3 at x120. ok. If lives >3? max3. Use score panel 230. ok.
Need drawRoundRect uses current path? It begins path. Good.
`drawBackgroundHearts` before UI. Need time variable.
Time update: `time += dt` at start update.
`updatePaddle` decay squish: `paddle.squish *= Math.exp(-dt*8);` after collisions? Do in updatePaddle. If hit set squish=1.
In demo, if ball is below paddle but not lost? loseBall after y>H. AI might fail if ball goes under? It should catch if target. If ball hits side and goes down past? Reset if lost. Good.
Could ball get stuck horizontal? `ball.vy` near 0? normalize ensures. In walls if vx becomes too small? We can add if abs(ball.vy)<100 set vy sign. After normalize? Could if after wall horizontal only, vy=0. Paddle bounce ensures vy. But brick reflection can make vy=0 if hit side? It reflects vx, vy could be small. To avoid endless horizontal, in normalizeBall if Math.abs(ball.vy) < 90, set `ball.vy = Math.sign(ball.vy||1)*Math.max(130, ball.speed*0.3)`? But if ball moving downward and side reflect, vy could be positive small? Actually vy unchanged from previous. If vy small because too horizontal, adjust upward/downward? We want keep downward maybe. Use:
```
if(Math.abs(ball.vy) < 100) ball.vy = (ball.y < H/2 ? -1 : 1) * Math.max(120, ball.speed*0.28);
```
Call in normalize? But if ball above middle and vy positive small maybe should go downward? Hmm. Simpler: after normalize if `Math.abs(ball.vy) < 70` set `ball.vy = ball.vy >=0 ? 90 : -90`; not too vertical. But horizontal speed maybe > speed? normalize. Let's do:
```
function normalizeBall(){ let len=Math.hypot(ball.vx,ball.vy); if(!isFinite(len)||len<1){ ball.vx=0; ball.vy=-ball.speed; len=ball.speed; } const maxAngle=1.2; const vyMin=Math.cos(maxAngle)*ball.speed; // vy magnitude at least cos(1.2)*speed ~0.36*speed
if(Math.abs(ball.vy) < vyMin){ ball.vy = Math.sign(ball.vy||-1)*vyMin; }
len=Math.hypot(ball.vx,ball.vy); ball.vx=ball.vx/len*ball.speed; ball.vy=ball.vy/len*ball.speed; }
```
But if ball near bottom, we want vy positive? sign preserved. For paddle, vy negative. If side brick with vy positive small, sign positive -> vyMin positive (downward). Good.
Need `Math.sign(0)` 0; use `ball.vy < 0 ? -1 : 1`. If 0, maybe -1? If ball above bricks? okay.
Wall collision if vy too small? normalize after paddle/brick. ok.
Potential: If ball is below bricks and moving up, hits bottom edge of brick? It reflects vy positive? Actually collision if ball.y<cy -> from below? In our brick vertical reflect: if ball.y < cy (above center) set ball.vy=-abs (upward). If ball is below brick center but moving upward, ball.y > cy? then set ball.vy=abs (downward). Good. So after hitting bottom of brick from below, ball goes downward. Then paddle bounce sends upward. Good.
Need collision detection for moving up and brick: if ball below center? `ball.y < cy` means above center, set upward. Wait if moving up and hits bottom edge, ball center might be below brick center? Brick y 100, h30 center115. Ball moving up y=120 below center; collision, ball.y < cy false (120<115 false), so vy=abs downward. Correct (bounce down). If ball moving up but center above brick top y=90 < cy true, set vy=-abs upward? If hitting top from above? If ball center above brick and moving up, collision likely from below? Not. But ok.
Substep collision if ball inside brick: dx <=..., dy<=... set reflect based center. Good.
Level clear after destroy brick; `aliveCount` decremented. If multiple in same frame? only one due break. If all destroyed via one? one. Then next frame check. ok.
Sound frequencies:
```
function playBounce(){ if(state==='playing') tone(340+Math.random()*120,0.05,'square',0.018);}
function playBrickSound(hue){ if(state==='playing'){ const f=520 + Math.random()*520; tone(f,0.13,'triangle',0.035); tone(f*1.5,0.09,'sine',0.02); } }
function playLoseSound(){ if(state==='playing') tone(190,0.28,'sawtooth',0.05); }
```
Need call wall sound inside `playBounce`. If state demo no audioCtx? Could call `if(state==='playing' && audioCtx)`. In wall function use `if(state==='playing') playBounce()`.
Maybe add sound for level clear:
```
function playLevel(){ if(state==='playing'){ tone(660,0.12); tone(880,0.18); } }
```
Now potential syntax: `hsl(${b.hue} 95% 68%)` valid? CSS hsl with spaces requires modern. Chrome supports `hsl(335 95% 68%)`. Or use commas for compatibility: `hsl(${b.hue}, 95%, ${...}%)`. Use commas safer.
`hsla(${p.hue}, 92%, ${p.light}%, ${alpha})` if want. But drawHeart fill string. Use hsl.
`drawHeart` uses `ctx.fillStyle=fill;` If fill is gradient object created before with coordinates in global? If we use gradient with coords relative to ball and then save translate in drawHeart, fill gradient is in user space? Canvas gradients are defined in user space at creation, unaffected by transform? Actually gradient coordinates are in user space coordinate system at time of fill? It uses current transform? For fill, gradient is mapped by current transform? If created in global coords then drawHeart transforms, gradient coordinates not transformed? Might be wrong. Let's avoid gradient for heart; use solid color with glow. For ball, could draw circle gradient then small heart overlay. Simpler.
```
ctx.save(); ctx.beginPath(); ctx.arc(ball.x,ball.y,ball.r,0,6.28); grad; ctx.fill(); ctx.restore();
drawHeart(ball.x, ball.y, ball.r*1.8, ball.angle, 'rgba(255,255,255,.22)')
```
But ball visual heart? We can draw circle gradient plus heart white inside. Good. Avoid gradient in transformed save. Draw circle then heart with white. Ball is circle? Requirement not. But heart theme. Maybe draw circle with heart pattern. Good.
Let's implement ball:
```
// glow
ctx.save(); ctx.globalCompositeOperation='lighter'; ctx.globalAlpha=.22; ctx.beginPath(); ctx.arc(ball.x,ball.y,ball.r*1.8,0,TAU); ctx.fillStyle='rgba(255,120,180,.5)'; ctx.fill(); ctx.restore();
const grad = ctx.createRadialGradient(ball.x-ball.r*0.35, ball.y-ball.r*0.35, ball.r*0.2, ball.x, ball.y, ball.r);
grad.addColorStop(0,'#fff'); grad.addColorStop(.35,'#ffb8d6'); grad.addColorStop(1,'#ff2f7b');
ctx.fillStyle=grad; ctx.beginPath(); ctx.arc(ball.x,ball.y,ball.r,0,TAU); ctx.fill();
ctx.fillStyle='rgba(255,255,255,.75)'; drawHeart(ball.x, ball.y+1, ball.r*1.2, ball.angle, 'rgba(255,255,255,.75)');
```
drawHeart with solid string okay.
Trail hearts solid color.
Particle solid color string okay.
If `drawHeart` called with fill object in brick? We use string. Good.
Need ensure `drawHeart` restores and doesn't alter globalAlpha inside. It uses save. Good.
Potential: `drawRoundRect` leaves path. Need call ctx.fill/stroke after. Good.
Let's write `drawRoundRect`:
```
function drawRoundRect(x,y,w,h,r){ r=Math.min(r,Math.abs(w)/2,Math.abs(h)/2); if(w<0||h<0||r<0) { ctx.beginPath(); return; } ctx.beginPath(); ctx.moveTo(x+r,y); ctx.lineTo(x+w-r,y); ctx.quadraticCurveTo(x+w,y,x+w,y+r); ctx.lineTo(x+w,y+h-r); ctx.quadraticCurveTo(x+w,y+h,x+w-r,y+h); ctx.lineTo(x+r,y+h); ctx.quadraticCurveTo(x,y+h,x,y+h-r); ctx.lineTo(x,y+r); ctx.quadraticCurveTo(x,y,x+r,y); ctx.closePath(); }
```
Need `clamp`:
```
function clamp(v,a,b){return v<a?a:v>b?b:v;}
```
`spawnMissHearts`:
```
function spawnMissHearts(x,y){ for(let i=0;i<18;i++){ particles.push({..., hue:340+Math.random()*20, light:70, grav:-40? maybe rise, life:0.8, size:4+rand*12, vy:-200...}) } }
```
`spawnCelebration` maybe level clear: spawn hearts around screen? Could be nice. In levelClear: `for i<200 spawn random top?` But limit. Use maybe 60.
Now ensure no variables undefined. Let's write code in final carefully.
One more thought: The paddle hamburger could be drawn as collision box, but ball might pass side due collision only top. If ball moving down and x within left/right, hit top. If ball approaches side below top, maybe collide due box sides? Our condition includes ball.y-ball.r <= bottom and x within sides. It triggers if ball center within paddle vertical range and horizontal range, even moving down. If ball is beside paddle and moving down, condition x+ r >= left and x-r <= right. If x outside by r, yes, might bounce horizontally? But condition doesn't check vy >0 only. If ball is next to side and within vertical range, y+r >= top maybe yes if not too low. Could reflect top incorrectly, making ball pop to top side. In breakout, side collisions can happen. But simple top-only can cause weird if ball near edge. It checks x within [left-r,right+r], so if ball just to left of paddle by <=r and y below top, it bounces to top? Set y=top-r; ball appears jump sideways? Actually y set top, x unchanged, then vx unchanged? We set vy negative; ball may bounce upward even though it hit side. Could be okay. To avoid, maybe only if ball.y < top + 8? But if ball center y < bottom, x outside by <=r. Could set if `ball.x >= left-r && ball.x <= right+r` and `ball.y + ball.r >= top && ball.y - ball.r < top + 8`? More realistic top only. But paddle is burger, side collisions maybe. Let's implement top collision only to avoid weird:
```
if (ball.vy > 0 && ball.y + ball.r >= top && ball.y + ball.r <= top + ball.vy*dt*? no) { if x between left-r, right-r? }
```
But with substeps, ball may be slightly below top. Use if center `ball.y >= top - ball.r && ball.y <= top + paddle.h*0.55`? Hmm.
Better use AABB but if ball center within horizontal bounds (not expanded) and ball.y+ball.r>=top. If ball.x within paddle width (not r), top collision. If ball.x outside width, ignore (it will fall/lose or hit brick). Side collision not necessary. Use `ball.x >= left && ball.x <= right`. Then no side pop. However ball radius can overlap side but no collision; ball may pass visually near edge but fine. Use expanded by r? Could allow edge catch. Let's use `ball.x >= left - ball.r*0.4 && ball.x <= right + ball.r*0.4`. okay. But if x outside, bounce side? Maybe ignore. Simpler use top collision only when ball.x between left and right. Good.
```
if(ball.vy > 0 && ball.y + ball.r >= top && ball.y - ball.r <= top + paddle.h*0.7 && ball.x >= left && ball.x <= right) { ... }
```
But if ball moves fast and in substep y passes below top inside paddle, still y <= top + h*0.7 (e.g. top+53) catches if horizontal. Good.
No side collision. This can allow ball to pass sides but acceptable. Add if ball.x just outside, misses. Good.
Similarly brick collision uses AABB circle, okay.
Update ball trail: after substeps, push. But if collideBricks breaks, ball.y set near brick, trail ok.
`updatePaddle`: pointer mode easing. If state playing and inputMode pointer, but pointerX maybe not updated if window pointermove outside? okay. On mobile, pointermove with finger sets pointerX; after lift, stays. Good.
If keyboard input and inputMode pointer, we disabled pointer if keys left/right. But if key pressed and released, keys false, inputMode keyboard, pointer disabled? In update:
```
if (state==='playing') {
if(inputMode==='pointer' && !keys.left && !keys.right) ease;
if(keys.left) x-=; if(keys.right) x+=;
}
```
This means if inputMode keyboard, no ease. If keys false, paddle stays. If user moves mouse later, pointermove sets inputMode pointer. Good.
If inputMode pointer but key left held, ease disabled, key moves. Good. If key released, pointer resumes to pointerX (old). ok.
Need pointermove handler: `if(state==='playing'){ inputMode='pointer'; pointerX=getPointerLogicalX(e); }` If pointer is over UI? It's okay. Use window.
`getPointerLogicalX`: if rect width 0? no. Use `clamp`.
```
function getPointerLogicalX(e){ const rect=canvas.getBoundingClientRect(); if(rect.width===0) return W/2; const x=((e.clientX-rect.left)/rect.width)*W; return clamp(x,0,W); }
```
Touch pointerdown: maybe prevent scrolling. Use `{ passive: false }` and `e.preventDefault()` if pointerType==='touch'. But body overflow hidden, no scroll. Could call `e.preventDefault()` on pointerdown to avoid focus? ok. But may block? No. Use try. If passive false.
Need initial `updateOverlay()` called. Good.
Now let's craft final code. Need no comments? Comments okay but code length. Ensure no syntax mistakes.
Potential issue: `state` used in `resetGame`? no.
Let's write:
```
function resetGame(){ score=0; lives=3; level=1; ball.speed=430; particles.length=0; texts.length=0; ball.trail.length=0; makeBricks(level); paddle.x=W/2; paddle.squish=0; resetBall(); }
```
If called when state playing from gameover, resets. If demo start, resets from current demo score? In begin for demo, reset score to 0; ok. Could be surprising but starts user game fresh.
`beginPlaying`:
```
function beginPlaying(mode){ ensureAudio(); if(state!=='playing'){ resetGame(); state='playing'; setMessage('Play! 🍔',1.2); } inputMode=mode; updateOverlay(); }
```
If already playing and mode keyboard, doesn't reset. Good.
If state gameover and keydown any: reset. If pointerdown: reset.
`messageTimer` decrement in update if >0. If levelClear sets message.
Need `time` for background. Initialize 0.
`drawBackgroundHearts` uses time.
Let's write full JS with all functions. Use IIFE.
Check `spawnHearts` for hp>1 false: small hearts; need color `p.hue`. p.maxLife initial p.life. Good.
```
function spawnHearts(b, big){ const count=big ? 68+Math.floor(Math.random()*28) : 12; while(particles.length + count > MAX_PARTICLES) particles.splice(0, 24); for... }
```
If max less count, while can infinite? If MAX_PARTICLES > count. yes. Or if particles length near max, remove enough. If count > max? no.
`particles.splice(0, 24)` modifies. ok.
`updateParticles`:
```
for(let i=particles.length-1;i>=0;i--){ const p=particles[i]; p.life-=dt; if(p.life <=0 || p.x<-100 ...){ particles[i]=particles[particles.length-1]; particles.pop(); continue;} p.vy += p.grav*dt; const factor=Math.exp(-p.drag*dt); p.vx *= factor; p.vy *= factor; p.x += p.vx*dt; p.y += p.vy*dt; p.rot += p.spin*dt; }
```
Need if p.vy negative and grav positive; okay.
`drawParticles`:
```
for(const p of particles){ const a=clamp(p.life/p.maxLife,0,1); const alpha=a*a; if(alpha<=0)continue; ctx.globalAlpha=alpha; const size=p.size*(0.7+0.6*a); drawHeart(p.x,p.y,size,p.rot,`hsl(${p.hue}, ${p.sat}%, ${p.light}%)`); }
ctx.globalAlpha=1;
```
This uses drawHeart save/restore which may reset globalAlpha? drawHeart doesn't change globalAlpha. Good.
`drawTexts` similarly.
`addText` for score maybe `text = '+' + b.value`. Need if b.value large. ok.
`drawBall`: trail hearts solid `hsla(335,100%,75%,1)` but globalAlpha set. drawHeart fill with hsl; globalAlpha applies.
```
for(const t of ball.trail){ const a=clamp(t.life/t.max,0,1); ctx.globalAlpha=a*0.25; drawHeart(t.x,t.y,t.size*(0.7+0.5*a), t.rot, 'hsl(330,100%,78%)'); }
ctx.globalAlpha=1;
```
Need t.max. push with life 0.32, max 0.32.
`updateBall` trail push after move:
```
ball.trail.push({x:ball.x, y:ball.y, life:0.32, max:0.32, size:ball.r*1.8, rot:ball.angle});
while(ball.trail.length>22) ball.trail.shift();
for(let i=ball.trail.length-1;i>=0;i--){ ball.trail[i].life-=dt; if(ball.trail[i].life<=0) ball.trail.splice(i,1); }
```
Use shift? small. ok.
Need ball.angle update before push for trail? yes.
`collidePaddle` set ball.y=top-ball.r; If top collision only, ball may be below top by large if moving fast and substep caught, set to top. Good.
`loseBall` if state demo: `spawnMissHearts(ball.x, H-30); resetBall();` But if state demo and lives? lives unchanged. ok.
If playing: lives--. If lives <=0 set gameover. Does resetBall? no. Overlay shows. In gameover, ball not update. If user starts reset. Good.
`levelClear` check after ball update. If all bricks destroyed, make new bricks. Could if state demo and AI? ok. If ball speed increase. `for(let i=0;i<3;i++) addText(W/2 + rand, H*0.5, 'LEVEL UP', ...)`; maybe too much. Use message.
Need `aliveCount` in `makeBricks`. If bricks empty? fallback. Implement fallback robust:
```
if(aliveCount ===0){ for(let r=0;r<rows;r++) for(let c=0;c<cols;c++){ bricks.push(...); aliveCount++; } }
```
But if we already created some, don't duplicate? If aliveCount 0. Fine.
Need helper to create brick to avoid duplication:
```
function addBrick(...){ ... }
```
Could be overkill. Use loops.
MakeBricks code:
```
function makeBricks(lv) {
bricks.length=0; aliveCount=0;
const cols=12;
const rows=Math.max(4, Math.min(8, 4 + Math.floor((lv-1)/2)));
const gap=7; const side=22; const top=86; const bh=30;
const bw=(W-side*2-gap*(cols-1))/cols;
const pattern=(lv-1)%5;
for (let r=0; r<rows; r++) {
for (let c=0; c<cols; c++) {
let include=true;
if(pattern===1) include=((r+c)%2===0);
else if(pattern===2) include=(c%3!==(r%3));
else if(pattern===3) include=!(r===rows-1 && (c%5===0 || c%5===1));
else if(pattern===4) include=(((r*7 + c*11) % 9) !== 0);
if(!include) continue;
const hp=(lv>=3 && r===0) ? 2 : 1;
const hue=(335 + r*12 + c*6 + lv*21) % 360;
const value=10*(rows-r);
bricks.push({...}); aliveCount++;
}
}
if(aliveCount < 12) {
bricks.length=0; aliveCount=0;
for(let r=0;r<rows;r++) for(let c=0;c<cols;c++) { ... include all hp1 }
}
}
```
Pattern2 may skip columns but alive okay.
Need if pattern1 include even => half, for rows4 cols12 => 24 alive. ok.
For lv>=3 hp row0, but if hp2 need maxHp. Draw damage. Destroy after 2 hits. In destroy, b.hp=0. ok.
Score value if hp2 maybe double. Set `value=10*(rows-r)*(hp>1?2:1)`.
`hitBrick` small hearts: if hp >1 after decrement, maybe spawn 10 hearts. If b.hp=1, destroyed spawn big. Good. But if hp2 first hit b.hp becomes 1, spawn small. If hp1 first hit b.hp 0, destroy. Good.
Need `destroyBrick` checks b.alive false; if hp<=0 call. But in hitBrick if b.hp<=0 and b.alive true, destroy. If already false no.
`collideBricks` uses for(const b of bricks). If `destroyBrick` removes hearts and maybe sets b.alive false. It returns true after destroying first. If hp>1 returns true. If b.hp>0. Good. But if b.hp becomes 0, we call destroyBrick and then return true. ok.
Potential bug: `b.hp` decrement before if `b.alive`? If ball hits same brick multiple times within same frame due substeps? We break after first. Next frame maybe ball position set outside. Good.
Sound in `destroyBrick`: if state playing tone. If state demo no. If state gameover no. If user started but audio suspended, `tone` checks.
`playLevel` on levelClear if state playing.
Need `TAU` const. `Math.PI*2`.
Now code final. Ensure no use of `alert`. Good.
Let's assemble. Need ensure all functions inside IIFE and no missing semicolons.
Could there be an issue with `ctx.setTransform` and `clearRect`? Since transform dpr, clearRect 0,0,W,H clears logical area. Good. But canvas internal size W*dpr; after setTransform, clearRect fills logical. Good.
`ctx.createLinearGradient` coordinates logical. Good.
CSS: `background` canvas maybe `#0e0713`. The body background. Good.
Need border-radius clipping? Canvas drawing fills all; border radius CSS. Good.
Check overlay z-index: start fixed, bottom. pointer-events none. Good.
Now final answer code block only. Need no text before/after.
Double-check syntax:
- In drawPaddle seeds loop: `for (const s of seeds) { ... }` ok.
- `seeds` relative uses `pw` and `hh`: x positions as fractions. y `s[1]*hh` with s[1] negative. Good.
- Upper bun path uses `ctx.quadraticCurveTo(-hw + 6, -hh, 0, -hh);` Wait start at (-hw+6,-24), control (-hw+5,-hh) maybe too flat? Good.
- drawRoundRect inside save scaled, uses logical coords. Good.
- drawHeart with fill string gradient? no.
Potential: In drawPaddle lettuce path:
```
ctx.moveTo(l, top);
for(i=0;i<=steps;i++){ ctx.lineTo(... top + sin...) }
for(i=steps;i>=0;i--){ ctx.lineTo(... top+13 + sin...)}
```
Need last point? top+13. Good. The path maybe self. ok.
Potential: Cheese drips: `ctx.fill()` after each triangle? We set fillStyle and beginPath each triangle. ok.
Potential: `ctx.ellipse` in drawPaddle seed positions: `ctx.ellipse(0,0,3.2,2,0,0,Math.PI*2)` ok.
Potential: In `drawUI`, `drawRoundRect` and fill. If panel width 230. ok.
Potential: `updateOverlay`: if state playing sets startEl.innerHTML each call. Could repeatedly? Only state changes. In `beginPlaying` called movement key every time? If state playing and moving, updateOverlay called, changes innerHTML to hidden text. Minor. Could avoid. But okay. Maybe updateOverlay called on every keydown repeat? If holding key, keydown repeats, updateOverlay sets innerHTML each time while playing, causing hidden overlay still innerHTML. No visible issue but DOM churn. Could call only if state changes or mode? Simpler: In keydown handler, if state!=='playing' begin; else if moving set inputMode; no updateOverlay. For pointermove no. For beginPlaying update. Good. We'll write handlers accordingly.
Handlers:
```
window.addEventListener('keydown', function(e){
const moveLeft = e.code==='ArrowLeft' || e.code==='KeyA';
const moveRight = e.code==='ArrowRight' || e.code==='KeyD';
if(moveLeft || moveRight || e.code==='Space' || e.code==='Enter') e.preventDefault();
if(e.code === 'ArrowLeft' || moveLeft) keys.left = true;
if(e.code === 'ArrowRight' || moveRight) keys.right = true;
if(state !== 'playing'){
beginPlaying('keyboard');
} else if(moveLeft || moveRight){
inputMode='keyboard';
}
}, {passive:false});
```
But first line: `if (e.code === 'ArrowLeft' || moveLeft)` redundant; `ArrowLeft` is moveLeft. ok.
If state not playing and keydown any starts; keys set maybe if arrow. Good.
Keyup:
```
if (e.code==='ArrowLeft'||e.code==='KeyA') keys.left=false; if right false.
```
Pointerdown:
```
canvas.addEventListener('pointerdown', function(e){ const x=getPointerLogicalX(e); pointerX=x; if(state!=='playing') beginPlaying('pointer'); else inputMode='pointer'; if(e.pointerType==='touch') e.preventDefault(); }, {passive:false});
window.addEventListener('pointermove', function(e){ if(state==='playing'){ pointerX=getPointerLogicalX(e); inputMode='pointer'; } }, {passive:true});
```
What if state gameover and user moves pointer? state not playing so no control. Click restarts. Good.
If state demo and user moves mouse? no control. If pointerdown starts. Good.
`beginPlaying` calls ensureAudio and resetGame, sets state, inputMode, message, updateOverlay. If already playing (not used by pointermove but could from pointerdown if state playing? In handler else not begin). So begin only state not playing. Good.
Need maybe if keydown state not playing, beginPlaying keyboard and `resetGame` resets level to 1. If initial demo had score/particles; user start fresh. ok.
Potential: After gameover, keydown restarts. Good.
Now code final. Ensure no unused variables? ok.
Let's produce final answer.