Hamburger breakoutOne-line
MiniMax M3.1 Flash
by MiniMaxReasoningClean
Running live in a sandboxed frame. Click inside it before using the keyboard.
How it went
- Total time
- 1 min 23 s
- Cost
- $0.012
- Output tokens
- 19,089
- Spent thinking
- 7,204 tokens
- Speed
- 231 tok/s
- Console errors
- 0
- First token after
- 621 ms
- First visible text after
- 49.7 s
- Prompt tokens
- 528
- Code size
- 33.5 KB
- Finish reason
- stop
- Served by
- DigitalOcean
- Model id
minimax/minimax-m3.1-flash- Reasoning sent
{"effort":"high"}- 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 · 1126 lines
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<title>Heartbreaker — Burger Breakout</title>
<style>
* { box-sizing: border-box; }
html, body {
margin: 0;
width: 100%;
height: 100%;
overflow: hidden;
background: #080511;
color: #fff;
font-family: "Trebuchet MS", "Arial Rounded MT Bold", system-ui, sans-serif;
-webkit-user-select: none;
user-select: none;
}
body::before {
content: "";
position: fixed;
inset: 0;
pointer-events: none;
z-index: 2;
background:
radial-gradient(ellipse at 18% 20%, rgba(255, 44, 130, .13), transparent 48%),
radial-gradient(ellipse at 82% 72%, rgba(116, 64, 255, .13), transparent 46%);
}
body::after {
content: "";
position: fixed;
inset: 0;
pointer-events: none;
z-index: 3;
background: radial-gradient(ellipse at center, transparent 48%, rgba(3, 2, 9, .62) 100%);
}
canvas {
position: fixed;
inset: 0;
width: 100%;
height: 100%;
touch-action: none;
z-index: 1;
}
.hud {
position: fixed;
z-index: 12;
top: 0;
left: 0;
right: 0;
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 12px;
padding: 18px 24px;
pointer-events: none;
}
.brand { display: flex; align-items: center; gap: 11px; }
.logo {
width: 42px;
height: 42px;
border-radius: 13px;
background: linear-gradient(145deg, rgba(255,255,255,.14), rgba(255,255,255,.03));
border: 1px solid rgba(255,255,255,.13);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 2px;
box-shadow: 0 8px 22px rgba(0,0,0,.3), inset 0 1px 0 rgba(255,255,255,.15);
flex: 0 0 auto;
}
.logo i { display: block; border-radius: 4px; }
.logo .bun { width: 24px; height: 8px; background: linear-gradient(#ffd776, #e98d3c); }
.logo .lett { width: 27px; height: 4px; background: #63d34e; border-radius: 6px 6px 3px 3px; }
.logo .meat { width: 25px; height: 6px; background: linear-gradient(#7c3b2d, #4a211c); }
.logo .base { width: 23px; height: 6px; background: linear-gradient(#ef9b45, #c9682b); }
.brand b {
display: block;
font-size: 21px;
letter-spacing: 1.8px;
line-height: 1;
background: linear-gradient(90deg, #fff 15%, #ff72b5 90%);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
text-shadow: 0 0 26px rgba(255, 80, 150, .2);
}
.brand small {
display: block;
margin-top: 5px;
color: rgba(255,255,255,.46);
font-size: 10px;
letter-spacing: 3px;
font-weight: bold;
}
.stats { display: flex; gap: 9px; align-items: stretch; }
.stat {
min-width: 82px;
padding: 8px 13px 9px;
border-radius: 15px;
background: linear-gradient(145deg, rgba(255,255,255,.105), rgba(255,255,255,.035));
border: 1px solid rgba(255,255,255,.11);
box-shadow: 0 10px 26px rgba(0,0,0,.24), inset 0 1px 0 rgba(255,255,255,.11);
backdrop-filter: blur(9px);
}
.stat label {
display: block;
color: rgba(255,255,255,.43);
font-size: 9px;
font-weight: bold;
letter-spacing: 2px;
margin-bottom: 3px;
}
.stat span {
display: block;
font-size: 18px;
font-weight: 900;
letter-spacing: 1.2px;
font-variant-numeric: tabular-nums;
}
.stat.score span { color: #ffdf84; }
.stat.level span { color: #8fe9ff; }
.hearts { display: flex; gap: 3px; height: 22px; align-items: center; }
.hearts i {
font-style: normal;
color: #ff3d7f;
font-size: 19px;
line-height: 1;
text-shadow: 0 0 10px rgba(255, 61, 127, .7);
transition: opacity .2s, transform .2s;
}
.hearts i.lost { opacity: .18; color: #fff; text-shadow: none; transform: scale(.82); }
.start-overlay {
position: fixed;
inset: 0;
z-index: 20;
display: grid;
place-items: center;
padding: 16px;
background: radial-gradient(ellipse at center, rgba(9,4,20,.12), rgba(6,3,14,.44));
transition: opacity .45s ease, visibility .45s;
}
.start-overlay.hidden { opacity: 0; visibility: hidden; pointer-events: none; }
.start-card {
width: min(520px, calc(100vw - 30px));
padding: 30px 34px 28px;
border-radius: 29px;
text-align: center;
background: linear-gradient(155deg, rgba(37,16,57,.86), rgba(12,6,25,.8));
border: 1px solid rgba(255, 133, 190, .3);
box-shadow:
0 28px 80px rgba(0,0,0,.55),
0 0 70px rgba(255, 48, 125, .14),
inset 0 1px 0 rgba(255,255,255,.14);
backdrop-filter: blur(12px);
animation: cardFloat 3.2s ease-in-out infinite;
}
.demo-tag {
display: inline-block;
padding: 7px 13px;
border-radius: 999px;
font-size: 10px;
letter-spacing: 2.4px;
font-weight: 900;
color: #ffb3d5;
background: rgba(255, 65, 139, .12);
border: 1px solid rgba(255, 105, 169, .34);
}
.demo-tag::before {
content: "";
display: inline-block;
width: 7px;
height: 7px;
margin-right: 8px;
border-radius: 50%;
background: #ff4b91;
box-shadow: 0 0 10px #ff4b91;
animation: blink 1.1s infinite;
}
h1 {
margin: 15px 0 8px;
font-size: clamp(42px, 8vw, 64px);
line-height: .86;
letter-spacing: 1px;
font-weight: 900;
background: linear-gradient(180deg, #fff 5%, #ff5d9f 58%, #9d55ff 100%);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
filter: drop-shadow(0 8px 24px rgba(255, 55, 135, .28));
}
.start-card p {
margin: 14px auto 20px;
max-width: 370px;
color: rgba(255,255,255,.66);
font-size: 14px;
line-height: 1.55;
}
.cta {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 10px;
padding: 14px 19px;
border-radius: 15px;
font-size: 12px;
font-weight: 900;
letter-spacing: 1.25px;
color: #230817;
background: linear-gradient(100deg, #ff7fb6, #ffc7e1);
box-shadow: 0 10px 28px rgba(255, 73, 145, .3), inset 0 1px 0 rgba(255,255,255,.65);
animation: pulse 1.7s ease-in-out infinite;
}
.mouse {
width: 15px;
height: 20px;
border: 2px solid #230817;
border-radius: 8px;
position: relative;
}
.mouse::after {
content: "";
position: absolute;
width: 2px;
height: 5px;
border-radius: 2px;
background: #230817;
left: 50%;
top: 3px;
transform: translateX(-50%);
}
.start-card small {
display: block;
margin-top: 15px;
color: rgba(255,255,255,.36);
font-size: 11px;
letter-spacing: .4px;
}
.toast {
position: fixed;
z-index: 30;
left: 50%;
top: 104px;
transform: translate(-50%, -12px);
padding: 10px 17px;
border-radius: 999px;
font-size: 12px;
font-weight: 900;
letter-spacing: 1.5px;
color: #fff;
background: rgba(19,9,34,.82);
border: 1px solid rgba(255,255,255,.14);
box-shadow: 0 12px 35px rgba(0,0,0,.35);
opacity: 0;
pointer-events: none;
transition: opacity .25s, transform .25s;
white-space: nowrap;
}
.toast.show { opacity: 1; transform: translate(-50%, 0); }
.controls {
position: fixed;
z-index: 11;
bottom: 10px;
left: 50%;
transform: translateX(-50%);
color: rgba(255,255,255,.25);
font-size: 10px;
letter-spacing: 1.5px;
font-weight: bold;
pointer-events: none;
text-align: center;
}
@keyframes blink { 50% { opacity: .25; } }
@keyframes pulse { 50% { transform: scale(1.025); box-shadow: 0 12px 36px rgba(255,73,145,.45); } }
@keyframes cardFloat { 50% { transform: translateY(-7px); } }
@media (max-width: 620px) {
.hud { padding: 13px 12px; gap: 8px; }
.brand { gap: 8px; }
.logo { width: 36px; height: 36px; border-radius: 11px; }
.brand b { font-size: 15px; letter-spacing: 1px; }
.brand small { font-size: 7px; letter-spacing: 1.6px; margin-top: 4px; }
.stats { gap: 5px; }
.stat { min-width: 0; padding: 7px 8px; border-radius: 12px; }
.stat label { font-size: 7px; letter-spacing: 1px; }
.stat span { font-size: 13px; letter-spacing: .5px; }
.stat.lives { padding-right: 7px; }
.hearts { gap: 1px; height: 17px; }
.hearts i { font-size: 14px; }
.start-card { padding: 24px 20px 22px; border-radius: 24px; }
.start-card p { font-size: 12px; margin: 12px auto 17px; }
.cta { font-size: 10px; padding: 13px 14px; letter-spacing: .8px; }
.controls { display: none; }
.toast { top: 82px; font-size: 10px; }
}
</style>
</head>
<body>
<canvas id="game"></canvas>
<header class="hud">
<div class="brand">
<div class="logo"><i class="bun"></i><i class="lett"></i><i class="meat"></i><i class="base"></i></div>
<div><b>HEARTBREAKER</b><small>BURGER BREAKOUT</small></div>
</div>
<div class="stats">
<div class="stat score"><label>SCORE</label><span id="score">000000</span></div>
<div class="stat level"><label>LEVEL</label><span id="level">01</span></div>
<div class="stat lives"><label>BURGERS</label><div class="hearts" id="lives"><i>♥</i><i>♥</i><i>♥</i></div></div>
</div>
</header>
<div class="start-overlay" id="startOverlay">
<div class="start-card">
<span class="demo-tag">SELF-PLAYING DEMO</span>
<h1>HEART<br>BREAKER</h1>
<p>Smash every brick into a spectacular burst of tiny hearts. Your paddle is a hamburger, so naturally it is deliciously bouncy.</p>
<div class="cta"><span class="mouse"></span>CLICK OR PRESS A KEY TO START</div>
<small>Move with your mouse, touch, or the ← → arrow keys.</small>
</div>
</div>
<div class="toast" id="toast"></div>
<div class="controls">MOUSE / TOUCH / ARROW KEYS</div>
<script>
(() => {
"use strict";
const canvas = document.getElementById("game");
const ctx = canvas.getContext("2d");
const scoreEl = document.getElementById("score");
const levelEl = document.getElementById("level");
const livesEl = document.getElementById("lives");
const overlay = document.getElementById("startOverlay");
const toastEl = document.getElementById("toast");
let W = 0, H = 0, DPR = 1;
let left = 20, right = 20, playTop = 100, paddleY = 700;
let mode = "demo";
let score = 0, level = 1, lives = 3, combo = 0, comboTimer = 0;
let bricks = [], particles = [], rings = [], floaters = [], ambient = [];
let shake = 0, elapsed = 0, levelState = "play", levelTimer = 0;
let targetX = null, keys = {}, toastTimer = 0, trailTimer = 0, paddleFlash = 0;
let ready = false;
const paddle = { x: 300, y: 700, w: 120, h: 30, speed: 880 };
const ball = { x: 0, y: 0, vx: 0, vy: 0, r: 8, speed: 440, active: false, delay: 0 };
const trail = [];
const palettes = [
["#ff4f91", "#c91863"],
["#ff6d5c", "#d32e54"],
["#ff9b54", "#e35d32"],
["#b366ff", "#6f35ca"],
["#4ed9ee", "#2776c9"]
];
const heartColors = ["#ff3d7f", "#ff77b0", "#ffadc9", "#d85cff", "#ff5e68", "#ffd166", "#fff0f6"];
const rand = (a, b) => a + Math.random() * (b - a);
const clamp = (v, a, b) => Math.max(a, Math.min(b, v));
const pick = arr => arr[(Math.random() * arr.length) | 0];
function rr(x, y, w, h, r) {
r = Math.min(r, Math.abs(w) / 2, Math.abs(h) / 2);
ctx.beginPath();
ctx.moveTo(x + r, y);
ctx.arcTo(x + w, y, x + w, y + h, r);
ctx.arcTo(x + w, y + h, x, y + h, r);
ctx.arcTo(x, y + h, x, y, r);
ctx.arcTo(x, y, x + w, y, r);
ctx.closePath();
}
function resize() {
W = window.innerWidth;
H = window.innerHeight;
DPR = Math.min(2, window.devicePixelRatio || 1);
canvas.width = Math.round(W * DPR);
canvas.height = Math.round(H * DPR);
canvas.style.width = W + "px";
canvas.style.height = H + "px";
ctx.setTransform(DPR, 0, 0, DPR, 0, 0);
left = W < 600 ? 12 : 26;
right = W - left;
playTop = W < 600 ? 88 : (H < 560 ? 84 : 102);
paddle.w = W < 600 ? 88 : 120;
paddle.h = W < 600 ? 25 : 30;
paddle.y = H - (W < 600 ? 38 : 50);
ball.r = W < 600 ? 7 : 8;
ball.speed = clamp(W * 0.42, 350, 480);
paddle.x = clamp(paddle.x, left, right - paddle.w);
if (ready && bricks.length) {
buildLevel(level);
}
}
function buildLevel(n) {
level = n;
const cols = W < 600 ? 7 : (W < 900 ? 9 : 10);
const rows = W < 600 ? 6 : 5;
const gap = W < 600 ? 5 : 8;
const margin = left + 4;
const total = right - margin - 4;
const bw = (total - gap * (cols - 1)) / cols;
const bh = W < 600 ? 20 : 27;
const top = playTop + 17;
bricks = [];
for (let r = 0; r < rows; r++) {
for (let c = 0; c < cols; c++) {
const p = palettes[r % palettes.length];
let hp = r < 2 ? 1 : (r < 4 ? 2 : 3);
if ((r + c) % 7 === 0 && r > 1) hp++;
bricks.push({
x: margin + c * (bw + gap),
y: top + r * (bh + gap),
w: bw,
h: bh,
hp,
maxHp: hp,
color: p[0],
color2: p[1],
flash: 0,
seed: Math.random() * 10
});
}
}
levelState = "play";
levelTimer = 0;
resetBall(0.75);
updateHud();
}
function resetBall(delay) {
ball.x = paddle.x + paddle.w / 2;
ball.y = paddle.y - ball.r - 2;
ball.vx = rand(-100, 100);
ball.vy = -ball.speed * .8;
ball.delay = delay;
ball.active = false;
trail.length = 0;
}
function startPlayer() {
if (mode === "player") return;
mode = "player";
overlay.classList.add("hidden");
showToast("YOU'RE COOKING! ♥");
}
function showToast(text) {
toastEl.textContent = text;
toastEl.classList.add("show");
clearTimeout(toastTimer);
toastTimer = setTimeout(() => toastEl.classList.remove("show"), 1500);
}
function updateHud() {
scoreEl.textContent = String(score).padStart(6, "0");
levelEl.textContent = String(level).padStart(2, "0");
const icons = livesEl.children;
for (let i = 0; i < icons.length; i++) {
icons[i].classList.toggle("lost", i >= lives);
}
}
function addFloater(x, y, text, color, size) {
floaters.push({ x, y, text, color, size: size || 18, life: 1.25, max: 1.25 });
}
function spawnBurst(x, y, count, huge) {
count = count || 30;
for (let i = 0; i < count; i++) {
const a = rand(0, Math.PI * 2);
const sp = rand(70, huge ? 430 : 320);
const up = Math.random() < .62;
particles.push({
kind: "heart",
x: x + rand(-8, 8),
y: y + rand(-6, 6),
vx: Math.cos(a) * sp,
vy: Math.sin(a) * sp * .72 + (up ? rand(-170, -35) : 35),
gravity: up ? -18 : 78,
size: rand(huge ? 7 : 5, huge ? 16 : 13),
rot: rand(-.5, .5),
rv: rand(-4, 4),
life: rand(1.5, huge ? 3.4 : 2.7),
max: 3.4,
delay: Math.random() * .22,
color: pick(heartColors),
drag: rand(.55, 1.25)
});
}
for (let i = 0; i < (huge ? 22 : 9); i++) {
const a = rand(0, Math.PI * 2);
const sp = rand(100, 360);
particles.push({
kind: "spark",
x, y,
vx: Math.cos(a) * sp,
vy: Math.sin(a) * sp,
gravity: 110,
size: rand(1.5, 3.8),
life: rand(.35, .8),
max: .8,
delay: 0,
color: pick(["#fff", "#ffd6e8", "#ffefad"]),
drag: .6
});
}
rings.push({ x, y, r: 5, life: .55, max: .55, color: pick(["#ff4f91", "#ff85bd", "#b366ff"]) });
}
function spawnIntroHearts() {
for (let i = 0; i < 34; i++) {
particles.push({
kind: "heart",
x: rand(W * .08, W * .92),
y: rand(playTop + 30, H * .72),
vx: rand(-18, 18),
vy: rand(-42, -8),
gravity: -5,
size: rand(4, 10),
rot: rand(-.4, .4),
rv: rand(-1.3, 1.3),
life: rand(2.2, 4.5),
max: 4.5,
delay: rand(0, 1.5),
color: pick(heartColors),
drag: .15
});
}
}
function updatePaddle(dt) {
let target = paddle.x + paddle.w / 2;
if (mode === "demo") {
let predicted = ball.x;
if (ball.vy > 0 && ball.y < paddle.y) {
const t = (paddle.y - ball.y) / ball.vy;
predicted = ball.x + ball.vx * t;
}
predicted += Math.sin(elapsed * 2.2) * 13;
target = clamp(predicted, left + paddle.w / 2, right - paddle.w / 2);
const d = target - (paddle.x + paddle.w / 2);
paddle.x += clamp(d, -560 * dt, 560 * dt);
} else {
if (keys.ArrowLeft || keys.KeyA) {
paddle.x -= paddle.speed * dt;
targetX = null;
} else if (keys.ArrowRight || keys.KeyD) {
paddle.x += paddle.speed * dt;
targetX = null;
} else if (targetX !== null) {
const d = targetX - (paddle.x + paddle.w / 2);
paddle.x += clamp(d, -paddle.speed * dt, paddle.speed * dt);
}
}
paddle.x = clamp(paddle.x, left, right - paddle.w);
paddleFlash = Math.max(0, paddleFlash - dt * 4);
}
function damageBrick(b, index) {
b.hp--;
b.flash = 1;
if (b.hp <= 0) {
bricks.splice(index, 1);
combo++;
comboTimer = 2.3;
const gain = 100 + Math.min(combo, 10) * 25;
score += gain;
spawnBurst(b.x + b.w / 2, b.y + b.h / 2, 32, false);
shake = Math.min(9, shake + 3.8);
addFloater(b.x + b.w / 2, b.y, "+" + gain, "#ffe3a2", 17);
if (combo >= 3) {
addFloater(b.x + b.w / 2, b.y - 24, "SWEET x" + combo, "#ff9bd0", 21);
if (combo === 3 || combo % 5 === 0) showToast("COMBO x" + combo + " — EXTRA SWEET!");
}
updateHud();
if (bricks.length === 0) levelCleared();
} else {
spawnBurst(b.x + b.w / 2, b.y + b.h / 2, 8, false);
score += 15;
updateHud();
}
}
function levelCleared() {
levelState = "clearing";
levelTimer = 1.65;
ball.active = false;
score += 500 * level;
showToast("LEVEL " + level + " CLEARED! ♥");
addFloater(W / 2, playTop + 90, "LOVE OVERLOAD!", "#ff9bd0", 34);
spawnBurst(W / 2, playTop + 100, 100, true);
updateHud();
}
function loseBall() {
if (mode === "demo") {
resetBall(.65);
return;
}
lives--;
combo = 0;
updateHud();
spawnBurst(ball.x, H - 40, 24, false);
if (lives <= 0) {
lives = 3;
score = 0;
level = 1;
showToast("FRESH BURGERS — TRY AGAIN!");
buildLevel(1);
} else {
showToast(lives === 2 ? "ONE BITE LEFT!" : "REORDER THE BURGER!");
resetBall(.9);
}
}
function updateBall(dt) {
if (!ball.active) {
if (ball.delay > 0) {
ball.delay -= dt;
ball.x = paddle.x + paddle.w / 2;
ball.y = paddle.y - ball.r - 2;
if (ball.delay <= 0) {
ball.active = true;
const a = rand(-.42, .42);
ball.vx = Math.sin(a) * ball.speed;
ball.vy = -Math.cos(a) * ball.speed;
}
}
return;
}
const steps = Math.max(1, Math.ceil(ball.speed * dt / 5));
const sdt = dt / steps;
for (let s = 0; s < steps; s++) {
ball.x += ball.vx * sdt;
ball.y += ball.vy * sdt;
if (ball.y - ball.r < playTop) {
ball.y = playTop + ball.r;
ball.vy = Math.abs(ball.vy);
spawnBurst(ball.x, playTop, 5, false);
}
if (ball.x - ball.r < left + 4) {
ball.x = left + 4 + ball.r;
ball.vx = Math.abs(ball.vx);
}
if (ball.x + ball.r > right - 4) {
ball.x = right - 4 - ball.r;
ball.vx = -Math.abs(ball.vx);
}
if (ball.vy > 0 &&
ball.y + ball.r >= paddle.y &&
ball.y - ball.r <= paddle.y + paddle.h &&
ball.x + ball.r >= paddle.x &&
ball.x - ball.r <= paddle.x + paddle.w) {
ball.y = paddle.y - ball.r;
const off = clamp((ball.x - (paddle.x + paddle.w / 2)) / (paddle.w / 2), -1, 1);
const angle = off * 1.05;
ball.speed = Math.min(ball.speed + 1.4, 690);
ball.vx = Math.sin(angle) * ball.speed;
ball.vy = -Math.cos(angle) * ball.speed;
combo = 0;
paddleFlash = 1;
spawnBurst(ball.x, paddle.y, 5, false);
}
for (let i = bricks.length - 1; i >= 0; i--) {
const b = bricks[i];
if (ball.x + ball.r > b.x && ball.x - ball.r < b.x + b.w &&
ball.y + ball.r > b.y && ball.y - ball.r < b.y + b.h) {
const overlapX = Math.min(ball.x + ball.r - b.x, b.x + b.w - (ball.x - ball.r));
const overlapY = Math.min(ball.y + ball.r - b.y, b.y + b.h - (ball.y - ball.r));
if (overlapX < overlapY) {
ball.vx = ball.x < b.x + b.w / 2 ? -Math.abs(ball.vx) : Math.abs(ball.vx);
} else {
ball.vy = ball.y < b.y + b.h / 2 ? -Math.abs(ball.vy) : Math.abs(ball.vy);
}
damageBrick(b, i);
break;
}
}
if (ball.y - ball.r > H + 20) {
loseBall();
break;
}
}
if (ball.active) {
trailTimer -= dt;
if (trailTimer <= 0) {
trailTimer = .055;
trail.push({ x: ball.x, y: ball.y, life: .38, max: .38 });
if (Math.random() < .55) {
particles.push({
kind: "heart", x: ball.x, y: ball.y,
vx: rand(-18, 18) - ball.vx * .03,
vy: rand(-30, 10) - ball.vy * .02,
gravity: -6, size: rand(3, 6), rot: rand(-.4, .4), rv: rand(-2, 2),
life: .8, max: .8, delay: 0, color: pick(heartColors), drag: .5
});
}
}
}
}
function updateParticles(dt) {
for (let i = particles.length - 1; i >= 0; i--) {
const p = particles[i];
if (p.delay > 0) { p.delay -= dt; continue; }
p.life -= dt;
if (p.life <= 0) { particles.splice(i, 1); continue; }
p.vy += p.gravity * dt;
p.vx -= p.vx * p.drag * dt;
p.x += p.vx * dt;
p.y += p.vy * dt;
p.rot += p.rv * dt;
}
for (let i = rings.length - 1; i >= 0; i--) {
rings[i].life -= dt;
if (rings[i].life <= 0) rings.splice(i, 1);
}
for (let i = floaters.length - 1; i >= 0; i--) {
floaters[i].life -= dt;
floaters[i].y -= 34 * dt;
if (floaters[i].life <= 0) floaters.splice(i, 1);
}
for (let i = trail.length - 1; i >= 0; i--) {
trail[i].life -= dt;
if (trail[i].life <= 0) trail.splice(i, 1);
}
for (const b of bricks) b.flash = Math.max(0, b.flash - dt * 4.5);
if (particles.length > 1500) particles.splice(0, particles.length - 1500);
}
function update(dt) {
elapsed += dt;
shake = Math.max(0, shake - dt * 18);
comboTimer -= dt;
if (comboTimer <= 0) combo = 0;
updatePaddle(dt);
updateBall(dt);
updateParticles(dt);
if (levelState === "clearing") {
levelTimer -= dt;
if (levelTimer <= 0) buildLevel(level + 1);
}
for (const h of ambient) {
h.y += h.vy * dt;
h.x += Math.sin(elapsed * h.s + h.p) * 10 * dt;
h.rot += h.rv * dt;
if (h.y < -30) { h.y = H + 30; h.x = rand(0, W); }
}
}
function drawHeart(x, y, size, rot, color, alpha) {
ctx.save();
ctx.translate(x, y);
ctx.rotate(rot);
ctx.globalAlpha = alpha;
ctx.fillStyle = color;
ctx.beginPath();
ctx.moveTo(0, size * .36);
ctx.bezierCurveTo(-size * .62, -size * .08, -size * .47, -size * .66, 0, -size * .3);
ctx.bezierCurveTo(size * .47, -size * .66, size * .62, -size * .08, 0, size * .36);
ctx.fill();
ctx.restore();
}
function drawBackground() {
const g = ctx.createLinearGradient(0, 0, W, H);
g.addColorStop(0, "#170823");
g.addColorStop(.48, "#0b0719");
g.addColorStop(1, "#090518");
ctx.fillStyle = g;
ctx.fillRect(0, 0, W, H);
const r1 = ctx.createRadialGradient(W * .18, H * .18, 0, W * .18, H * .18, Math.max(W, H) * .55);
r1.addColorStop(0, "rgba(255,45,130,.12)");
r1.addColorStop(1, "rgba(255,45,130,0)");
ctx.fillStyle = r1;
ctx.fillRect(0, 0, W, H);
const r2 = ctx.createRadialGradient(W * .84, H * .7, 0, W * .84, H * .7, Math.max(W, H) * .5);
r2.addColorStop(0, "rgba(115,65,255,.12)");
r2.addColorStop(1, "rgba(115,65,255,0)");
ctx.fillStyle = r2;
ctx.fillRect(0, 0, W, H);
ctx.save();
ctx.strokeStyle = "rgba(255,255,255,.038)";
ctx.lineWidth = 1;
const gap = 46;
for (let x = 0; x < W; x += gap) {
ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, H); ctx.stroke();
}
for (let y = 0; y < H; y += gap) {
ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(W, y); ctx.stroke();
}
ctx.restore();
for (const h of ambient) {
drawHeart(h.x, h.y, h.size, h.rot, h.color, h.alpha);
}
}
function drawFrame() {
ctx.save();
ctx.strokeStyle = "rgba(255,255,255,.075)";
ctx.lineWidth = 1;
rr(left - 5, playTop - 7, right - left + 10, H - playTop + 3, 18);
ctx.stroke();
const glow = ctx.createLinearGradient(0, playTop, 0, H);
glow.addColorStop(0, "rgba(255,90,160,.4)");
glow.addColorStop(.35, "rgba(140,80,255,.12)");
glow.addColorStop(1, "rgba(70,220,255,.22)");
ctx.strokeStyle = glow;
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(left - 4, playTop);
ctx.lineTo(left - 4, H - 8);
ctx.moveTo(right + 4, playTop);
ctx.lineTo(right + 4, H - 8);
ctx.stroke();
ctx.restore();
}
function drawBricks() {
for (const b of bricks) {
const g = ctx.createLinearGradient(b.x, b.y, b.x, b.y + b.h);
g.addColorStop(0, b.flash > 0 ? "#ffffff" : b.color);
g.addColorStop(1, b.color2);
ctx.save();
ctx.shadowColor = b.color;
ctx.shadowBlur = b.flash > 0 ? 20 : 8;
rr(b.x, b.y, b.w, b.h, Math.min(6, b.h * .28));
ctx.fillStyle = g;
ctx.fill();
ctx.shadowBlur = 0;
ctx.strokeStyle = "rgba(255,255,255,.25)";
ctx.lineWidth = 1;
ctx.stroke();
ctx.fillStyle = "rgba(255,255,255,.25)";
rr(b.x + 3, b.y + 2.5, b.w - 6, Math.max(2, b.h * .18), 2);
ctx.fill();
if (b.w > 55) {
ctx.globalAlpha = .62;
ctx.fillStyle = "#fff";
ctx.font = "900 9px Trebuchet MS, sans-serif";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText("♥", b.x + b.w * .22, b.y + b.h * .6);
if (b.maxHp > 1) {
ctx.font = "900 8px Trebuchet MS, sans-serif";
ctx.fillText("♥".repeat(Math.min(3, b.hp)), b.x + b.w * .77, b.y + b.h * .58);
}
}
ctx.restore();
}
}
function drawBall() {
for (const t of trail) {
ctx.globalAlpha = (t.life / t.max) * .48;
ctx.fillStyle = "#ff76b7";
ctx.beginPath();
ctx.arc(t.x, t.y, ball.r * (t.life / t.max) * .85, 0, Math.PI * 2);
ctx.fill();
}
ctx.globalAlpha = 1;
if (!ball.active && ball.delay > 0) return;
const g = ctx.createRadialGradient(ball.x - 2, ball.y - 3, 1, ball.x, ball.y, ball.r + 3);
g.addColorStop(0, "#ffffff");
g.addColorStop(.35, "#ffe4f1");
g.addColorStop(.72, "#ff5b9f");
g.addColorStop(1, "rgba(255,62,140,.15)");
ctx.save();
ctx.shadowColor = "#ff4b9a";
ctx.shadowBlur = 22;
ctx.fillStyle = g;
ctx.beginPath();
ctx.arc(ball.x, ball.y, ball.r + 1.5, 0, Math.PI * 2);
ctx.fill();
ctx.shadowBlur = 0;
ctx.fillStyle = "rgba(255,255,255,.85)";
ctx.beginPath();
ctx.arc(ball.x - 2.5, ball.y - 3, 1.8, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
function drawPaddle() {
const x = paddle.x, y = paddle.y, w = paddle.w, h = paddle.h;
ctx.save();
ctx.shadowColor = "rgba(255,159,73,.45)";
ctx.shadowBlur = 14 + paddleFlash * 18;
ctx.fillStyle = "rgba(0,0,0,.38)";
ctx.beginPath();
ctx.ellipse(x + w / 2, y + h + 5, w * .44, 5, 0, 0, Math.PI * 2);
ctx.fill();
ctx.shadowBlur = 0;
// bottom bun
let g = ctx.createLinearGradient(0, y + h * .58, 0, y + h);
g.addColorStop(0, "#f0a04c");
g.addColorStop(1, "#b95325");
rr(x + 2, y + h * .60, w - 4, h * .40, h * .24);
ctx.fillStyle = g;
ctx.fill();
ctx.strokeStyle = "rgba(87,35,20,.65)";
ctx.lineWidth = 1.3;
ctx.stroke();
// patty
g = ctx.createLinearGradient(0, y + h * .26, 0, y + h * .70);
g.addColorStop(0, "#86412f");
g.addColorStop(.55, "#5e2921");
g.addColorStop(1, "#3d1b19");
rr(x + 2, y + h * .29, w - 4, h * .31, h * .16);
ctx.fillStyle = g;
ctx.fill();
ctx.strokeStyle = "rgba(35,13,13,.7)";
ctx.stroke();
// cheese
const cy = y + h * .30;
ctx.beginPath();
ctx.moveTo(x + 3, cy);
ctx.lineTo(x + w * .25, cy - 3);
ctx.lineTo(x + w * .48, cy + 1);
ctx.lineTo(x + w * .72, cy - 3);
ctx.lineTo(x + w - 3, cy);
ctx.lineTo(x + w * .78, cy + 3);
ctx.lineTo(x + w * .72, cy + h * .17);
ctx.lineTo(x + w * .63, cy + 3);
ctx.lineTo(x + w * .51, cy + h * .13);
ctx.lineTo(x + w * .41, cy + 3);
ctx.lineTo(x + w * .29, cy + h * .16);
ctx.lineTo(x + w * .22, cy + 2);
ctx.closePath();
ctx.fillStyle = "#ffd44d";
ctx.fill();
ctx.strokeStyle = "rgba(173,99,12,.55)";
ctx.lineWidth = 1;
ctx.stroke();
// lettuce
ctx.beginPath();
ctx.moveTo(x + 1, y + h * .26);
const lw = w - 2;
ctx.quadraticCurveTo(x + lw * .12, y + h * .16, x + lw * .24, y + h * .25);
ctx.quadraticCurveTo(x + lw * .37, y + h * .15, x + lw * .5, y + h * .24);
ctx.quadraticCurveTo(x + lw * .64, y + h * .15, x + lw * .77, y + h * .25);
ctx.quadraticCurveTo(x + lw * .89, y + h * .17, x + w - 1, y + h * .25);
ctx.quadraticCurveTo(x + w - 5, y + h * .39, x + w * .9, y + h * .30);
ctx.quadraticCurveTo(x + lw * .74, y + h * .42, x + lw * .62, y + h * .31);
ctx.quadraticCurveTo(x + lw * .48, y + h * .42, x + lw * .36, y + h * .31);
ctx.quadraticCurveTo(x + lw * .2, y + h * .41, x + lw * .08, y + h * .30);
ctx.closePath();
ctx.fillStyle = "#62d34d";
ctx.fill();
ctx.strokeStyle = "rgba(28,92,35,.6)";
ctx.lineWidth = 1;
ctx.stroke();
// top bun
g = ctx.createLinearGradient(0, y, 0, y + h * .36);
g.addColorStop(0, "#ffe49a");
g.addColorStop(.6, "#f4ac52");
g.addColorStop(1, "#d67a30");
rr(x, y, w, h * .35, h * .26);
ctx.fillStyle = g;
ctx.fill();
ctx.strokeStyle = "rgba(113,55,22,.62)";
ctx.lineWidth = 1.3;
ctx.stroke();
ctx.fillStyle = "rgba(255,250,218,.8)";
const seeds = [[.16,.42],[.32,.25],[.52,.46],[.69,.28],[.84,.44],[.43,.70],[.61,.67]];
for (const s of seeds) {
ctx.save();
ctx.translate(x + w * s[0], y + h * s[1]);
ctx.rotate(-.35);
ctx.beginPath();
ctx.ellipse(0, 0, Math.max(1.5, w * .014), Math.max(.7, h * .035), 0, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
ctx.restore();
}
function drawParticles() {
for (const r of rings) {
const t = r.life / r.max;
ctx.save();
ctx.globalAlpha = t * .8;
ctx.strokeStyle = r.color;
ctx.lineWidth = 2 + t * 3;
ctx.beginPath();
ctx.arc(r.x, r.y, r.r + (1 - t) * 70, 0, Math.PI * 2);
ctx.stroke();
ctx.restore();
}
for (const p of particles) {
if (p.delay > 0) continue;
const alpha = clamp(p.life / Math.min(p.max, .8), 0, 1);
if (p.kind === "heart") {
drawHeart(p.x, p.y, p.size, p.rot, p.color, alpha * .95);
} else {
ctx.globalAlpha = alpha;
ctx.fillStyle = p.color;
ctx.beginPath();
ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2);
ctx.fill();
ctx.globalAlpha = 1;
}
}
}
function drawFloaters() {
for (const f of floaters) {
const alpha = clamp(f.life / f.max, 0, 1);
ctx.save();
ctx.globalAlpha = alpha;
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.font = "900 " + f.size + "px Trebuchet MS, sans-serif";
ctx.lineWidth = 4;
ctx.strokeStyle = "rgba(10,4,20,.75)";
ctx.strokeText(f.text, f.x, f.y);
ctx.fillStyle = f.color;
ctx.fillText(f.text, f.x, f.y);
ctx.restore();
}
}
function render() {
ctx.save();
if (shake > .1) {
ctx.translate(rand(-shake, shake), rand(-shake, shake));
}
drawBackground();
drawFrame();
drawBricks();
drawBall();
drawPaddle();
drawParticles();
drawFloaters();
if (levelState === "clearing") {
const a = clamp(levelTimer / 1.65, 0, 1);
ctx.save();
ctx.globalAlpha = a;
ctx.textAlign = "center";
ctx.font = "900 " + Math.min(48, W * .075) + "px Trebuchet MS, sans-serif";
ctx.fillStyle = "#fff";
ctx.shadowColor = "#ff4b9a";
ctx.shadowBlur = 24;
ctx.fillText("LOVE OVERLOAD!", W / 2, H * .48);
ctx.restore();
}
ctx.restore();
}
function loop(t) {
const dt = Math.min(.033, (t - loop.last) / 1000 || .016);
loop.last = t;
update(dt);
render();
requestAnimationFrame(loop);
}
loop.last = performance.now();
window.addEventListener("resize", resize);
window.addEventListener("pointermove", e => {
if (mode === "player") targetX = e.clientX;
});
window.addEventListener("pointerdown", e => {
if (mode === "demo") {
targetX = e.clientX;
startPlayer();
}
});
window.addEventListener("touchmove", e => {
if (mode === "player" && e.touches.length) targetX = e.touches[0].clientX;
}, { passive: true });
window.addEventListener("keydown", e => {
if (["ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown", "Space"].includes(e.code)) e.preventDefault();
keys[e.code] = true;
if (mode === "demo") startPlayer();
});
window.addEventListener("keyup", e => { keys[e.code] = false; });
overlay.addEventListener("pointerdown", startPlayer);
for (let i = 0; i < 34; i++) {
ambient.push({
x: rand(0, W), y: rand(0, H),
size: rand(5, 13), rot: rand(-.5, .5), rv: rand(-.2, .2),
vy: rand(-14, -4), s: rand(.4, 1.4), p: rand(0, 7),
color: pick(heartColors), alpha: rand(.045, .13)
});
}
ready = true;
resize();
buildLevel(1);
spawnIntroHearts();
updateHud();
requestAnimationFrame(loop);
})();
</script>
</body>
</html>Reasoning trace (7,204 tokens)
We need produce one HTML block, self-contained breakout with hamburger paddle, bricks explode into tons of little hearts. Must be attractive at 1280x800 and responsive to 375. Need first paint visible, demo mode with "Click or press a key to start" overlay and AI plays behind. Use canvas maybe DOM? Canvas best. Need no external assets. Draw hamburger paddle, bricks, particles hearts. Need rich polish.
We should output complete HTML. Need obey exactly one fenced block. Let's design.
Canvas game full screen, gradient background, glowing neon arcade aesthetic, "HEARTBREAKER" title? Breakout "LOVE BUN" maybe. Bricks break into hearts, paddle burger. HUD score, level, hearts maybe lives. Game starts in attract mode AI plays; overlay center "CLICK OR PRESS A KEY TO START", with animated pulsing. Underlying game already moving.
Canvas resizing with devicePixelRatio, logical coordinates scale to fit? Use logical W=960 H=700? Better responsive: canvas fills viewport; use CSS pixel coordinate W=window.innerWidth, H=window.innerHeight. Physics in design units scale factor based on min(W/960, H/720)? But at 375 need adapt. Could set design width 960 and virtual height = 960*H/W capped? Simpler use actual CSS pixels and adjust brick rows/cols based on width. Use canvas width = W*dpr and ctx scale dpr.
At 1280x800 nice. Background dark gradient with radial glows, subtle grid, floating hearts.
Paddle: hamburger drawn with bun top sesame, lettuce, cheese, patty, bottom bun. width ~120 at desktop, ~86 mobile. Gradient.
Bricks: rounded rectangles with candy gradients, different rows, some "LOVE" labels. Each brick has hp? Some bricks multi-hit showing crack. On break spawn 18-28 heart particles with heart shapes rotated, varied colors, plus sparkles. Hearts also rise/ float with gravity? "tons of little hearts" - burst outward plus some float up. Particle count 20+ per brick with trail. Cap particles maybe 800.
Ball: glowing sphere with trailing particles, maybe pink.
Also destruction can trigger chain? Add combo.
Gameplay: paddle AI tracks ball in demo. Player controls mouse/touch/arrow keys after start. lives 3, levels maybe when all bricks cleared advance with heart rain. Keep AI auto resume if idle? Attract mode.
Start overlay: "CLICK OR PRESS ANY KEY TO START" button-like. Must be visible and clear. After click overlay fades and starts player mode; but demo AI plays continuously. Requirement says games start in self-playing demo mode behind message. We can have overlay that, upon click, dismisses and player takes over. Also after 3 sec? Keep visible until interaction. That satisfies.
Need no console errors.
Let's think UI layout. Full canvas with top HUD drawn in DOM or canvas. Use DOM for crisp text and buttons? Use HTML overlay HUD with score, level, lives. Canvas game. Background CSS + canvas. Header top left logo "HEART BREAKER" and subtitle; top right pills. On mobile stack.
We can build DOM overlay:
- .hud top: brand with burger icon (CSS maybe simple emoji 🍔? allowed no external, emoji may render). Better draw hamburger in canvas.
- stats: SCORE, LEVEL, LIVES hearts drawn as svg? inline SVG hearts in DOM.
- center message overlay card with big "HEARTBREAKER", "Break bricks. Make it hearts.", button "CLICK OR PRESS ANY KEY TO START". Actually overlay should not obscure too much; place center but translucent, with game visible behind. Semi-transparent.
- Footer hint.
But user asked hamburger paddle. Need make visually obvious.
Implement canvas coordinates. Let W,H. Bounds: playfield left 24 right W-24 top 100? HUD outside canvas via DOM. Canvas covers all but header overlays? We can use canvas full viewport and top margin 86. At 375x maybe HUD top 70. Let's have canvas position absolute inset 0. play area top = 86 (desktop) or 74 mobile, bottom = H-36. ball reset.
Rounded corner frame? Draw subtle lane lines.
Bricks layout: columns depending W: W<600 ? 7 : 10; rows 5 desktop, 6 mobile maybe. Brick area width = W - 2*margin; margin = W<600? 12: 36. Brick gap 8 desktop, 5 mobile. brickH = clamp((rows?), 22-30). Top = playTop+24. Brick rows count 5.
Bricks have hp 1-3 depending row/color. To ensure all break and hearts. Each brick: x,y,w,h,hp,maxHp,color palette, pulse random, glow.
Physics: ball x,y,vx,vy,r=8. Use dt in seconds capped .033. paddle speed. For collision with rectangle: calculate closest point, if inside reverse. But paddle moving and ball fast. Use swept? Standard with substeps: update in small steps based on speed, 4 substeps per frame. Or use dt and reflect. Implement collision robust by checking overlap, compare distances.
Ball trails: array of positions; particles.
Paddle AI: predict ball x when descending? simply target ball.x + random offset limited. In demo AI follows with max speed. Player mouse x target.
Paddle collision: if ball descending and bottom near paddle top and x within paddle extents + ball radius, set y, vy negative based on hit offset (angle between -60 and 60 degrees). Add particles hearts small.
Brick collision: determine side by previous position maybe overlap. Reverse appropriate axis based on minimum overlap (left/right/top/bottom). Apply damage; when hp 0 remove brick and burst hearts.
Combo increases on consecutive brick hits without paddle touch. Score. Floating text "SWEET! +100", combo.
Level clear: when no bricks, after delay maybe level++ and rebuild with a heart explosion wave. Keep game.
Lives: if ball falls, decrement; reset with "Ouch!" and hearts burst? Lives 3. Demo mode should keep going; on loss reset.
Particles: object {x,y,vx,vy,life,maxLife,size,rot,rv,color,kind:'heart'|'spark'|'ring'}. Draw heart path. Tons: 28 per brick plus 8 smaller delayed? Use 24-32 hearts with initial random velocity and gravity slight negative then drag; lifetime 1.4-2.6s. To make tons visible, hearts float up (vy - random 120..300), with gravity -? Actually float up then slow. Use vy negative and gravity -30. At end they fade. Also spawn ring shockwave.
Cap to 1200.
Additional ambient hearts floating in background (canvas, low alpha) always visible. 40.
Background: dark navy/purple gradient, radial pink glow, grid lines, stars. Canvas draw.
DOM styles: font family system (no external fonts). Could use @font-face no. Use "Trebuchet MS", Arial Rounded. Gradient text.
HUD update via textContent every frame maybe fine, but only when changed.
Top HUD pointer-events none except maybe button.
Overlay in center. Card with border, blur backdrop. Big title. Pulsing "CLICK OR PRESS A KEY TO START". On click start player. Keydown start.
Need start message but the game behind actively playing. AI mode maybe paddle tracks and clears bricks, but overlay might be in the way. AI starts immediately on load, no interaction. The ball should move. Ball could clear bricks eventually under 3 seconds? That's okay. Maybe reset level in demo? We want initial screenshot with bricks and action. Ball starts. Overlay centered could cover. Place overlay at center with card width 460, translucent. Game visible behind.
Better overlay top-ish center at 50%. Fine.
Could also include "DEMO" badge pulsing in corner under overlay.
Interactions: mouse move sets playerX, pointerdown start. keys: arrows/A/D, space start. On mobile touch move.
Accessibility: aria labels.
Need ensure no external requests.
Let's craft code carefully.
HTML:
<html><head>...style...</head><body>
<div class="ambient"></div>
<canvas id="game"></canvas>
<header class="hud">...
stats
</header>
<div class="hint">Move with mouse · ← → keys · Tap on mobile</div>
<div id="start" class="start-card"><span class="demo-tag">SELF-PLAYING DEMO</span><h1>HEART<span>BREAKER</span></h1><p>Smash every brick into a burst of little hearts.</p><div class="cta"><span class="mouse-icon">...</span>CLICK OR PRESS A KEY TO START</div><small>Your paddle is a hamburger. Obviously.</small></div>
<div id="toast" class="toast"></div>
<script>...</script>
</body></html>
Canvas z-index 1, hud 5, start 6.
The draw of paddle must be hamburger. Let's define drawPaddle(x,y,w,h): bun top rounded rect from x to x+w, height h*.34, fill gradient gold, bottom stroke. Top highlight. Sesame seeds ellipse positions. lettuce wavy green shape between top and patty. cheese yellow polygon with drips. patty dark brown rounded rect. bottom bun. Outline dark. Maybe scale for 120x28 - details small. h=30 desktop, 26 mobile. Make 3 layers.
Order bottom bun, patty, cheese, lettuce, top bun. Draw top bun rounded top with roundRect. Use path.
Also little face? no.
Ball draw radial gradient and sparkle.
Brick draw: roundRect fill linear gradient, top highlight, inner border. hp cracks. Some bricks have white mini heart symbol.
Bricks arrangement color by row: pink, red, orange, purple, cyan.
Potential issue ctx.roundRect supported modern chrome yes. But could implement roundRect helper for compatibility.
Collision algorithm:
function updateBall(dt):
ball.x += vx*dt; ball.y += vy*dt.
check walls top playTop, sides margin+8. If top, y=..., vy=abs.
paddle.
bricks: iterate reverse. if overlap:
compute overlap left = ball.x+ r - b.x etc. min.
if min left/right -> vx sign etc. else vy.
b.hp--; if <=0 remove; spawn. ball may collide multiple in same frame; okay.
Score.
Ball speed gradually increases each level/hit slightly. maintain speed min 620.
Paddle: baseY = H - 58 maybe playBottom-24? paddle y = H - 54 desktop, H-42 mobile. In playfield bottom H-30. use paddleY = H - 52 (desktop), H-40 mobile. When H short? min.
Play bounds: left = 18, right = W-18, top = HUD bottom maybe 96 desktop / 84 mobile; bottom = H-22.
Level bricks top = top+16.
Canvas resizing: dpr min(2, devicePixelRatio). canvas.width=W*dpr etc; ctx.setTransform(dpr,0,0,dpr,0,0).
Use resize observer window.
We should ensure no horizontal scroll: body overflow hidden.
AI paddle: target = ball.x + clamp(ball.vx*0.08?) Actually if ball descending, aim at projected intercept: t = (paddleY - ball.y)/ball.vy if vy>0; targetX = ball.x + ball.vx*t. else target ball.x. Clamp. AI speed 520. In demo.
Start state: mode='demo'. Upon interaction mode='player'; overlay hidden class. If mouse, playerX = e.clientX. If key arrows, move.
Player paddle: keyboard left/right and mouse target. Use pointer target; keyboard sets target null and moves x.
When start via key, startGame sets mode player, overlay fade, show toast "YOU'RE COOKING!".
If player idles, maybe stays; demo mode after 8 sec inactivity? Not required but nice: if player mode and no input for 6 sec, switch to demo? Maybe could annoy. Could have "AUTO-PLAY" small badge if demo. Keep.
Lives icons update hearts. Use inline SVG in spans? We can create via JS innerHTML? fine. Better HTML with 3 <span class=life>♥</span>, update opacity.
HUD score padded 6 digits.
Level.
Combo popups: array of floatingTexts.
Level clear: if bricks length 0: levelPending=1.2s, then next level. During pending maybe draw "LEVEL CLEAR" big; after, buildLevel(level+1), ball reset to paddle, release after delay. Implement levelState='clearing', levelTimer. In clearing, ball maybe continues? freeze? We'll set ball attached to paddle? When all bricks gone, spawn wave hearts and set state clearing; paddle still moves but ball removed? Keep ball slowly bounce? Simplest set ball.active=false; draw ball maybe on paddle. At timer <=0 build next level and launch.
Lose ball: lives--; if 0 then lives=3 score maybe reset? Game over overlay? Requirement demo should continue. Perhaps after lives zero, show toast "NEW GAME — DEMO MODE" and rebuild; but player might be annoyed. Use 5 lives? Set 3. On zero, reset level and lives 3, mode demo, show "OUT OF BURGERS — DEMO RESTART". Keep game running. No blocking modal.
Also in demo mode, lives don't deplete? Better demo mode: if lose, just reset without losing life; keeps endless demo. Player mode decrement. When demo cleared level, rebuild next.
Bricks count 10*5=50 at desktop, mobile 7*6=42.
Heart burst visible. On each brick break 26 hearts. 50 bricks => many but cap. Lifetime 2 sec, might be up to 500. Good.
Rendering hearts shape:
function heartPath(size): move 0, .3 size; bezier. Better centered.
ctx.save translate rotate; scale; beginPath moveTo(0,0.35); bezierCurveTo(-0.6,-0.05,-0.35,-0.55,0,-0.25)... Let's define normalized around size:
moveTo(0, size*0.35)
bezierCurveTo(-size*0.6, -size*0.1, -size*0.45, -size*0.65, 0, -size*0.3)
bezierCurveTo(size*0.45, -size*0.65, size*0.6, -size*0.1, 0, size*0.35)
fill.
Ambient hearts similarly.
Draw particles with globalAlpha life/max, shadows maybe expensive but okay 600. Use shadow only for spark? Hearts can have shadowBlur 8 perhaps expensive but fine at 1280. Maybe reduce.
Need "tons of little hearts" obvious: 30 per brick, plus trail of smaller hearts from ball. Maybe spawn 34 hearts, varied sizes 5-13. Also 6 sparkles. On each brick break. Use bigger bursts at combo.
Ball trail: store {x,y,life}. Draw gradient circles.
Also brick break flash: shockwave rings.
Toast DOM for combo maybe easier with floating canvas.
Floating texts drawn.
Screen shake on brick break: shake=6; update decays. Draw canvas with ctx translate random. But HUD unaffected. Good.
Need visual: subtle vignette CSS.
Let's write code with all.
Potential problem: overlay click event must start. We'll attach to overlay and window pointerdown? If window pointerdown triggers and then paddle target. Use:
window.addEventListener('pointermove', e=> { if(mode==='player') targetX=e.clientX;});
startOverlay.addEventListener('pointerdown', startPlayer);
window.addEventListener('keydown', e => { if(mode==='demo'){startPlayer();} ...});
Avoid duplicate calls with if.
Also window pointerdown anywhere starts: could set target and start. But clicking overlay also. Add window pointerdown start once.
StartPlayer sets mode='player'; startOverlay.classList.add('hidden'); player target from event; toast.
Game loop always running.
Intro maybe toast demo.
Let's set overlay pointer-events auto.
CSS responsive: .hud padding 18px 24px; brand; stats flex gap. On 375 brand title 18px; stats small. .hint bottom 8px maybe. start-card width min(92vw,520px). On small, padding 22, h1 34.
Canvas playTop must avoid header; set top = W<600 ? 82 : 96. Header height maybe ~70. Fine.
Let's ensure hint doesn't overlap paddle at bottom. Hint at bottom center, canvas paddle at H-48, overlap. Put hint in HUD right? Could omit bottom hint or place top under? We'll put hint in .hud .controls small under stats? Simpler hide on mobile. Place bottom-left small, but paddle centered may overlap. Use bottom 8, centered; paddle at H-46. Could overlap. Let's instead put controls inside header next to stats, but crowded. Use .controls under brand, small.
HUD structure:
<div class=brand><div class=logo-burger CSS> maybe draw CSS burger icon</div><div><b>HEARTBREAKER</b><small>BURGER BREAKOUT</small></div></div>
<div class=stats>
<div class=stat><label>SCORE</label><span id=score>000000</span></div>
...
<div class=stat lives>...
</div>
Header pointer-events none.
Burger logo CSS: three stacked rounded bars colored.
On 375, brand text + stats 3 items may not fit. media: brand small; stats gap 8; labels hidden maybe. Score maybe 6 digits small. lives hearts. Header flex-wrap? At 375, brand takes ~130, stats 180 -> 310 fits. Use font sizes.
Let's compute top=84 mobile. Header padding 14. brand height 44. stats height 44. top 84 gives clearance.
Drawing background uses W,H.
Ambient background via canvas gradient:
const bg=ctx.createLinearGradient(0,0,W,H) #12071f -> #070612.
radial glows pink at 20% 25%, purple at 80% 60%.
Grid: lines every 44, alpha .05, plus perspective? simple.
Also draw side rail neon arcs.
Draw top "DEMO PLAY" badge? DOM overlay has tag.
Maybe under overlay game visible. Overlay card dark translucent with border. Good.
Let's write final code. Need maybe avoid syntax errors. I'll be thorough.
Implementation details:
const canvas=... ctx.
let W=innerWidth,H=innerHeight,DPR;
function resize(){...; layout();}
layout computes paddle dims and if bricks empty build? On resize rebuild bricks? Keep positions scale? easiest rebuildLevel(current level) but that resets game. Accept on resize. But resize fires initial before init. We'll init after resize. Use let initialized=false; resize calls buildBricks if bricks.length===0 && initialized? Hmm.
Set variables then resize(); buildLevel(1); requestAnimationFrame.
On resize after init, rebuild bricks preserving level maybe.
Bricks count and top:
function buildLevel(n){
cols = W<600?7: W<900?9:10;
rows = W<600?6:5;
margin = W<600?12:26;
gap = W<600?5:8;
totalW = W-margin*2;
bw = (totalW - gap*(cols-1))/cols;
bh = W<600?21:28;
top = playTop+20;
bricks=[]
for r rows: hp = r<2?1: r<4?2:3 maybe too many hits. Use hp 1 mostly, rows 2-3 have hp 2, bottom row hp3. This means 50 bricks many hits. Good.
colors per row.
x=margin+c*(bw+gap), y=top+r*(bh+gap)
b.seed random.
}
Ball speed = 420 desktop? At W 1280, 470. mobile 380. Use baseSpeed = clamp(W*0.42,340,480). vy initial base*0.75.
Ball reset: x=paddle.x+paddle.w/2; y=paddle.y-12; vx = random ±120; vy=-base. Launch immediately after 0.8s? In demo start moving. On level build, ball on paddle and a timer launchDelay=.7. update if launchDelay>0 decrease and stick to paddle. For demo initial, maybe start launch after 1 sec so screenshot shows ball. Fine 3 sec screenshot after, ball moving.
Player paddle movement in update: targetX - paddle.w/2, speed 900.
Collision with bricks when ball active.
Substeps: total dt; steps = ceil(speed*dt/6); update per sub dt/steps. But paddle collision and brick handling inside substeps; particle spawns multiple times? A brick breaks once removed. okay.
For each substep:
ball.x+=vx*sdt etc
walls
paddle collision
bricks loop.
Paddle y constant. paddle collision check:
if ball.vy>0 && ball.y+r >= paddle.y && ball.y-r <= paddle.y+paddle.h && ball.x+r>=paddle.x && ball.x-r<=paddle.x+paddle.w:
set ball.y=paddle.y-r; offset=(ball.x-(paddle.x+w/2))/(w/2); angle=offset*1.05 (max 60deg). speed maintain; vx=sin(angle)*speed; vy=-cos(angle)*speed. This only works angle mapping where offset 1 -> vy small? cos 1.05 rad =0.5 so vy=-0.5speed, vx=.87. Fine.
combo=0; spark hearts 4; paddleFlash.
Wall top: ball.y <= top+r => y=top+r; vy=abs(vy). Side x <= left+r etc.
Bottom: if ball.y-r > H+20 => lose.
Brick collision overlap:
if circle intersects rect roughly: ball.x+r>b.x && ball.x-r<b.x+b.w && ball.y+r>b.y && ball.y-r<b.y+b.h.
Compute overlapX = min(ball.x+r - b.x, b.x+b.w-(ball.x-r)); overlapY similarly. If overlapX < overlapY then horizontal else vertical. reverse. Damage.
Need avoid immediate repeated collision after reversing? substeps small. Could also mark b.cool=0.05. Use cooldown map? Set brick.hitFlash. After collision we reverse so next step out.
Remove brick via splice. spawn burst. score = 100*(1+combo/5 floor) etc.
Combo increments each brick; combo timer maybe.
Brick hp > 0 keep, flash.
Level clear detection after brick removal: if bricks.length===0 and levelState==='play' -> levelState='clearing', levelTimer=1.4; big burst across center (80 hearts). ball.active=false. score bonus. show toast maybe.
In update clearing timer decrement; when <=0 level++; build; ball reset with launch delay; state play.
Demo mode loss: reset ball no lives. Player loss: lives--; if <=0 reset lives=3, level=1, score=0? show toast; mode='demo'? If we switch to demo, start overlay remains hidden and demo tag? We can switch to demo and show toast "DEMO BACK ON". But requirement initial demo only. fine. Maybe keep player mode but reset. Let's keep mode player, reset lives to 3 and level 1, show toast "FRESH BURGER! TRY AGAIN". But then endless attempts. Fine.
Score display.
Toast function set text, class show, timeout clear. Use setTimeout allowed (not banned). yes.
Floating texts array {x,y,text,life,color,size}. update y -= 30dt; draw.
Shake: shake = Math.max(0,shake-dt*20); in render ctx.save translate random.
Rendering order: bg, ambient hearts, frame, bricks, particles maybe behind ball? particles on top of bricks. Draw ball trail, ball, paddle, particles, texts, HUD? DOM HUD.
Background ambient hearts array generated 40; update positions slowly, wrap. Use deterministic.
DrawFrame: rounded rect stroke around play area with gradient? play area top to bottom. Maybe subtle left/right neon lines.
Brick text: for wide bricks desktop show "♥" or mini labels? Draw tiny heart on each brick maybe too repetitive. Instead draw 2 small gloss and for hp>1 small dots indicating remaining. Draw cracks lines. Some row bricks have text "LOVE" if bw>60. Use ctx font 10 bold.
Colors:
row palette arrays hex.
Fill gradient lighten top.
Glow for ball.
Particles: hearts and sparks and rings. ring separate array {x,y,r,life,max,color}. draw expanding circle stroke.
Score popups.
DOM HUD update:
scoreEl.textContent = String(score).padStart(6,'0')
levelEl...
life icons: spans class lost based index>=lives.
Combo displayed maybe a combo pill in HUD? Add stat COMBO maybe too much. Could add small floating.
Let's include "x3 SWEET COMBO" popup.
On initial overlay message clear.
Need careful with key handling prevents default for arrows/space.
Audio? Could add WebAudio simple bleeps, but autoplay restricted; after interaction can. Not necessary. Could add mute button but no. Keep visual.
"tons of little hearts" ensure particle heart count visible. On initial screenshot maybe some brick already broken? Since demo starts and 3 seconds, ball may hit brick and burst. likely. But to guarantee visible hearts at first paint, we can pre-spawn some ambient/celebratory hearts at load (e.g., 24 gentle hearts near top) so screenshot shows hearts. Also AI ball may break. Do initial burst at center top? Spawn 30 hearts around center with delay. Let's spawn "intro hearts" from random positions across upper area, drifting. That ensures.
Maybe on load spawn 20 hearts with life 3 around y 120-400, slow upward. Good.
Let's code.
Potential use of ctx.filter blur? supported but could be slow. Avoid.
CSS background radial overlays via body::before.
Let's produce final HTML code block only.
I should double-check any use of localStorage etc: none. alert none.
The code must run zero errors. Avoid relying on ctx.roundRect? Modern Chrome supports. But I'll implement rr function.
Let's write.
Detailed CSS:
* box-sizing
html,body margin 0 width 100% height 100% overflow hidden background #080612 color white font-family...
canvas position fixed inset 0 width 100% height 100% touch-action:none
body::after vignette pointer-events none z-index 2.
.hud position fixed top 0 left 0 right 0 display flex justify-content space-between align-items flex-start padding 18px 24px z-index 10 pointer-events none.
.brand display flex gap 11.
.logo 40x40.
.brand b letter spacing 1.5 gradient text.
.stats display flex gap 10.
.stat background rgba border radius 14 padding 8 13 min-width.
.life heart red.
.start-overlay position fixed inset 0 display grid place-items center z-index 20 background radial gradient transparent...
.start-card width min(520px, calc(100vw - 30px)); text-align center; padding 34px; border-radius 28px; background linear; border 1px solid rgba; box-shadow; backdrop-filter blur(10px); animation float.
.demo-tag inline-block...
h1 font-size 58; gradient; span block? maybe title two lines. "HEART" then "BREAKER". h1 line-height .88.
cta margin-top 22; border; animation pulse; text.
Toasts z-index 30.
Hidden class opacity 0 pointer-events none transform scale .96 transition.
On mobile h1 40, card padding 22, overlay align center. Brand subtitle hidden.
Let the start card not too opaque so game visible: background rgba(12,6,25,.72), backdrop blur 6.
Animation.
Now JS.
Let's ensure layout bricks top under header. playTop = W<600 ? 88 : 104. Paddle y = H - (W<600?38:50). Paddle h= W<600?25:30. Bottom maybe H-20. Good.
Ball radius = W<600?7:8.
At H small (landscape phone 375x?) viewport width 375 height maybe 800. fine. Adapt if H<520: playTop=80, paddleY=H-38, brick top 96.
Score.
Let's write code fully.
Possible issue initial build called before bricks variable? define all.
Ordering in script:
const...
let ...
function definitions
resize();
buildLevel(1);
spawnIntroHearts();
updateHud();
requestAnimationFrame(loop);
resize sets paddle etc. If resize after initial, calls buildLevel? We can set a flag. resize():
W=innerWidth; H=innerHeight; DPR...
canvas width...
ctx.setTransform...
compute layout.
if (typeof level !== 'undefined' && bricks.length===0) no. We'll just rebuild bricks only on resize if mode... But resizing mid-game resets bricks; okay if we call buildLevel(level) but that resets ball? buildLevel sets levelState='play' and ball reset maybe. Let's have resize only recompute layout and scale brick coords? Simpler: don't rebuild. On resize, brick x positions could off but rare. We can rebuild if bricks.length>0: buildLevel(level, true) but ball might be lost. Fine. But buildLevel resets bricks and ball state. Accept. However resize triggers at initial after declarations. Let's implement resize calls buildLevel(currentLevel) only if ready flag. ready=true after functions; initial resize then buildLevel(1) manually. To avoid double, set ready false initially, resize, then buildLevel.
On later resize: preserve score, buildLevel(level), reset ball.
That's acceptable.
Need track last time performance.now. loop(t){dt=min(.033,(t-last)/1000); last=t; update(dt); render(); requestAnimationFrame(loop)}
When tab inactive dt capped.
Let's implement heart particles with 'delay' maybe. particle has delay, update skip until delay. For burst use delay random .15 to stagger, creating wave.
particle update:
p.delay -= dt; if >0 continue
p.life -= dt;
p.vy += p.gravity*dt (gravity negative ~ -20)
p.vx *= pow(0.6? friction) use p.vx -= p.vx*0.6*dt
p.x +=...
p.rot +=...
If life ends remove via reverse loop.
Burst hearts:
for i<30:
angle random; speed 80-330; vx=cos*speed; vy=sin*speed - 80; gravity=35? if coordinate y down, hearts should float up so vy negative and gravity maybe -12 (upward acceleration). Use gravity = -25 (pull upward) but then they accelerate upward. Want float: vy initial -40..-160, gravity=-10, drag. Some fly outward. Let's use mix: half burst outward with gravity 120 (fall) but hearts should linger; half float up. Better all: vx random -220..220, vy random -260..60, gravity = vy<0 ? -8 : 60. That means upward ones float. life 1.6-3.
sizes 5-13.
Colors pink/red/purple/gold.
Also spark particles circles.
Ring waves.
Ball trail hearts? Draw trail as fading circles and occasionally spawn tiny heart every 0.08s in demo? Could add ballTrailTimer, spawn small heart with random at ball pos, life .6, size 4, vx -vx*.05, vy -vy*.05. This adds little hearts continuously. Good.
Now demo AI. paddle target:
if ball.vy>0: t=(paddle.y-ball.y)/ball.vy; predicted=ball.x+ball.vx*t; else predicted=ball.x;
target = clamp(predicted + Math.sin(time*2)*12, left, right)
paddle x moves toward target at speed 560*dt.
In player mode: targetX tracked; keyboard.
When mode switches to player, paddle stays.
Combo resets on paddle hit.
Score floating.
Show toast when combo >=3 "COMBO x3 — EXTRA SWEET!".
Level clear big heart wave: spawnBurst(W/2, playTop+120, 70, true).
Ball trail: Use fading circles and occasionally spawn tiny heart every 0.08s.
Draw paddle burger:
function drawBurger(p):
w=p.w h=p.h x=p.x y=p.y.
shadow ellipse under.
Bottom bun rr(x,y+h*.62,w,h*.36,8) fill #d9792f. highlight.
Patty rr(x+2,y+h*.30,w-4,h*.30,6) fill #5b2a21; top #7a3b2d.
Cheese polygon y around .38: points and drips.
Lettuce: draw wavy path across y .28.
Top bun rr(x,y,w,h*.34, h*.45?) roundRect uses same radius; use path with rounded top corners maybe rr radius h*.3 then cover bottom? draw rr(x,y,w,h*.4, h*.28). Seeds ellipses.
outline stroke rgba dark.
Add shine.
At small h=25 still okay.
Logo CSS burger: .logo spans.
Okay let's write final.
One more: HUD score etc DOM. Use getElementById.
Toast function.
No errors from accessing elements.
Let's produce.