Hamburger breakoutOne-line
Kimi K3.1 Pro
by MoonshotAIreasoning defaultClean
Running live in a sandboxed frame. Click inside it before using the keyboard.
How it went
- Total time
- 7 min 54 s
- Cost
- $0.373
- Output tokens
- 42,480
- Spent thinking
- 23,360 tokens
- Speed
- 89.8 tok/s
- Console errors
- 0
- First token after
- 1.4 s
- First visible text after
- 4 min 23 s
- Prompt tokens
- 444
- Code size
- 55.2 KB
- Finish reason
- stop
- Served by
- Relace
- Model id
moonshotai/kimi-k3.1-pro- Reasoning sent
model default- Max output
- 943,718 tokens
- Generated
- Nov 12, 2026
What it was asked
The prompt
“i want a breakout game where the paddle is a hamburger and every brick you break explodes into tons of little hearts”
System prompt, sent with every prompt
You are generating a self-contained web demo. It will be rendered automatically in a sandboxed iframe and compared side by side with the output of other AI models given the exact same task. Output rules: - Reply with exactly ONE fenced code block containing the complete file: ```html (or ```svg when the task asks for an SVG). No text before or after the code block. - Everything must be inline: CSS in <style>, JavaScript in <script>. No external files, images, fonts or network requests, except the CDN URLs explicitly allowed by the task. - Do not use localStorage, sessionStorage, cookies, IndexedDB, alert(), confirm(), prompt() or window.open(): they are blocked in the sandbox and will throw. - The page must look right in a 1280×800 viewport and adapt gracefully down to 375px wide. No horizontal scrollbar. - Something meaningful must be visible on first paint: a screenshot is taken automatically about 3 seconds after load, without any interaction. - Everything that makes the demo interesting must be visible within the first seconds: nothing hidden behind a high score, a later level or a long play session. - Games must start in a self-playing demo mode (a simple AI plays) behind a "Click or press a key to start" message, so the game can be judged in action without playing. - The iframe only receives keyboard events after the user clicks it: if the demo uses the keyboard, display a clear "Click or press a key to start" message. - The code must run with zero errors in the console of the latest Chrome.
Code run-1.html · 2033 lines
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="theme-color" content="#130e1b">
<title>Burger Breakout</title>
<style>
:root {
--ink: #17111f;
--panel: #201827;
--panel-2: #281e31;
--line: rgba(255, 244, 223, 0.12);
--cream: #fff2d9;
--muted: #aa9bad;
--pink: #ff7195;
--pink-light: #ffb3c8;
--gold: #ffc86b;
--mint: #9de3b2;
}
* { box-sizing: border-box; }
html { color-scheme: dark; }
body {
margin: 0;
min-height: 100vh;
overflow-x: hidden;
color: var(--cream);
font-family: "Trebuchet MS", "Arial Rounded MT Bold", Arial, sans-serif;
background:
radial-gradient(circle at 10% -5%, rgba(255, 93, 139, 0.16), transparent 32rem),
radial-gradient(circle at 96% 90%, rgba(255, 194, 99, 0.10), transparent 30rem),
#130e1b;
}
body::before {
content: "";
position: fixed;
inset: 0;
pointer-events: none;
opacity: 0.22;
background-image:
linear-gradient(rgba(255,255,255,0.025) 1px, transparent 1px),
linear-gradient(90deg, rgba(255,255,255,0.025) 1px, transparent 1px);
background-size: 44px 44px;
}
button { font: inherit; }
.ambient-heart {
position: fixed;
z-index: 0;
color: rgba(255, 124, 159, 0.09);
font-size: 100px;
line-height: 1;
pointer-events: none;
filter: blur(1px);
animation: drift 8s ease-in-out infinite;
}
.ambient-heart.one { left: 2vw; top: 58%; transform: rotate(-18deg); }
.ambient-heart.two { right: 2vw; top: 13%; font-size: 72px; animation-delay: -3s; }
@keyframes drift {
0%, 100% { translate: 0 0; }
50% { translate: 0 -16px; }
}
.app {
position: relative;
z-index: 1;
width: min(1160px, calc(100% - 32px));
margin: 0 auto;
padding: 14px 0 10px;
}
.site-header {
min-height: 48px;
margin-bottom: 14px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 18px;
}
.brand {
min-width: 0;
display: flex;
align-items: center;
gap: 11px;
}
.brand-mark {
width: 45px;
height: 45px;
flex: 0 0 auto;
display: grid;
place-items: center;
border: 1px solid rgba(255,255,255,0.22);
border-radius: 14px;
background: linear-gradient(145deg, #ffcf77, #ff8b72);
box-shadow: 0 8px 25px rgba(255, 112, 126, 0.22), inset 0 1px rgba(255,255,255,0.4);
font-size: 25px;
transform: rotate(-4deg);
}
.brand-eyebrow {
margin-bottom: 2px;
color: #b7a7b9;
font-size: 8px;
font-weight: 800;
letter-spacing: 0.22em;
}
h1 {
margin: 0;
white-space: nowrap;
font-size: 24px;
line-height: 1;
letter-spacing: 0.035em;
font-weight: 900;
}
h1 span { color: var(--pink); }
.header-note {
display: flex;
align-items: center;
gap: 9px;
color: #9f91a5;
font-size: 9px;
font-weight: 800;
letter-spacing: 0.17em;
}
.header-note::before,
.header-note::after {
content: "";
width: 26px;
height: 1px;
background: rgba(255,255,255,0.14);
}
.header-note b { color: var(--pink); font-size: 14px; }
.header-actions { display: flex; gap: 7px; }
.icon-button {
height: 37px;
padding: 0 12px;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 7px;
border: 1px solid var(--line);
border-radius: 11px;
color: #d8cadc;
background: rgba(255,255,255,0.035);
cursor: pointer;
font-size: 10px;
font-weight: 800;
letter-spacing: 0.06em;
transition: background 160ms ease, border-color 160ms ease, transform 160ms ease;
}
.icon-button:hover {
background: rgba(255,255,255,0.08);
border-color: rgba(255,255,255,0.22);
transform: translateY(-1px);
}
.icon-button:active { transform: translateY(1px); }
.icon-button.muted { opacity: 0.55; }
.restart-button { width: 37px; padding: 0; font-size: 20px; }
.layout {
display: grid;
grid-template-columns: minmax(0, 1fr) 250px;
gap: 16px;
align-items: start;
}
.game-card {
min-width: 0;
overflow: hidden;
border: 1px solid rgba(255, 237, 220, 0.14);
border-radius: 21px;
background: var(--panel);
box-shadow: 0 24px 70px rgba(0,0,0,0.36), inset 0 1px rgba(255,255,255,0.035);
}
.hud {
height: 64px;
display: flex;
align-items: stretch;
padding: 0 5px;
background: linear-gradient(180deg, #251c2d, #211927);
}
.hud-block {
min-width: 0;
padding: 0 17px;
display: flex;
flex-direction: column;
justify-content: center;
border-left: 1px solid rgba(255,255,255,0.07);
}
.hud-block:first-child { border-left: 0; }
.hud-score { flex: 1.25; }
.hud-level { flex: 0.55; }
.hud-lives { flex: 0.9; }
.hud-combo { flex: 0.65; align-items: center; }
.hud-label {
margin-bottom: 4px;
color: #9e90a4;
font-size: 8px;
line-height: 1;
font-weight: 900;
letter-spacing: 0.19em;
}
.hud-value {
color: var(--cream);
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
font-size: 21px;
line-height: 1;
font-weight: 800;
letter-spacing: 0.06em;
font-variant-numeric: tabular-nums;
}
#lives {
display: flex;
gap: 5px;
color: var(--pink);
font-family: Arial, sans-serif;
font-size: 19px;
letter-spacing: 0;
}
#lives .off { color: #59475c; opacity: 0.55; }
.combo-badge {
min-width: 52px;
padding: 7px 8px;
border: 1px solid rgba(255,255,255,0.09);
border-radius: 10px;
color: #786a7e;
background: rgba(0,0,0,0.12);
text-align: center;
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
font-size: 15px;
font-weight: 900;
transition: 180ms ease;
}
.combo-badge.hot {
color: #2a1722;
border-color: #ffbbcc;
background: linear-gradient(145deg, #ffc1d1, #ff7195);
box-shadow: 0 0 22px rgba(255, 100, 146, 0.28);
animation: comboPulse 600ms ease-in-out infinite alternate;
}
@keyframes comboPulse { to { transform: scale(1.06) rotate(-2deg); } }
.stage {
position: relative;
width: 100%;
aspect-ratio: 8 / 5;
overflow: hidden;
border-top: 1px solid rgba(255,255,255,0.06);
border-bottom: 1px solid rgba(255,255,255,0.07);
background: #17111f;
touch-action: none;
outline: none;
}
.stage:focus-visible { box-shadow: inset 0 0 0 2px var(--pink); }
#gameCanvas {
position: absolute;
inset: 0;
display: block;
width: 100%;
height: 100%;
cursor: crosshair;
}
.demo-chip {
position: absolute;
z-index: 3;
top: 12px;
left: 12px;
display: flex;
align-items: center;
gap: 7px;
padding: 7px 10px;
border: 1px solid rgba(255,255,255,0.12);
border-radius: 999px;
color: #d6c8d9;
background: rgba(22, 15, 29, 0.68);
backdrop-filter: blur(10px);
font-size: 8px;
font-weight: 900;
letter-spacing: 0.16em;
pointer-events: none;
}
.live-dot {
width: 6px;
height: 6px;
flex: 0 0 auto;
border-radius: 50%;
background: var(--mint);
box-shadow: 0 0 0 4px rgba(157,227,178,0.10), 0 0 10px rgba(157,227,178,0.6);
animation: livePulse 1.3s ease-in-out infinite;
}
@keyframes livePulse { 50% { opacity: 0.45; } }
.start-prompt {
position: absolute;
z-index: 4;
left: 50%;
bottom: 66px;
width: max-content;
max-width: calc(100% - 26px);
padding: 11px 19px 12px;
transform: translateX(-50%);
border: 1px solid rgba(255, 209, 221, 0.30);
border-radius: 16px;
color: var(--cream);
background: rgba(28, 18, 35, 0.86);
box-shadow: 0 12px 40px rgba(0,0,0,0.34), inset 0 1px rgba(255,255,255,0.08);
backdrop-filter: blur(12px);
text-align: center;
pointer-events: none;
transition: opacity 200ms ease, visibility 200ms ease, transform 200ms ease;
}
.start-prompt.is-hidden {
opacity: 0;
visibility: hidden;
transform: translate(-50%, 8px);
}
.prompt-kicker {
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
margin-bottom: 4px;
color: var(--pink-light);
font-size: 8px;
font-weight: 900;
letter-spacing: 0.18em;
}
.start-prompt strong {
display: block;
font-size: 14px;
line-height: 1.2;
letter-spacing: 0.01em;
}
.start-prompt small {
display: block;
margin-top: 5px;
color: #ad9caf;
font-size: 9px;
}
.toast {
position: absolute;
z-index: 5;
top: 12px;
left: 50%;
min-width: 190px;
padding: 9px 18px;
transform: translate(-50%, -12px);
opacity: 0;
visibility: hidden;
border: 1px solid rgba(255,255,255,0.18);
border-radius: 12px;
color: #321d28;
background: linear-gradient(135deg, #fff0d4, #ffc4d2);
box-shadow: 0 10px 30px rgba(0,0,0,0.24);
text-align: center;
pointer-events: none;
transition: 180ms ease;
}
.toast.show {
opacity: 1;
visibility: visible;
transform: translate(-50%, 0);
}
.toast strong {
display: block;
font-size: 13px;
letter-spacing: 0.1em;
}
.toast span { display: block; margin-top: 2px; font-size: 9px; }
.game-over {
position: absolute;
z-index: 7;
inset: 0;
display: grid;
place-items: center;
padding: 20px;
opacity: 0;
visibility: hidden;
background: rgba(17, 11, 24, 0.70);
backdrop-filter: blur(5px);
transition: 200ms ease;
}
.game-over.show { opacity: 1; visibility: visible; }
.over-card {
width: min(310px, 100%);
padding: 25px;
border-radius: 20px;
color: #2e1c27;
background: #fff0d7;
box-shadow: 0 22px 60px rgba(0,0,0,0.4);
text-align: center;
transform: rotate(-1deg);
}
.over-card .tiny {
font-size: 8px;
font-weight: 900;
letter-spacing: 0.2em;
}
.over-card h2 { margin: 9px 0 6px; font-size: 30px; line-height: 1; }
.over-card p { margin: 0 0 17px; color: #80626b; font-size: 12px; }
.over-card button {
width: 100%;
padding: 12px;
border: 0;
border-radius: 12px;
color: white;
background: #ed6288;
cursor: pointer;
font-size: 12px;
font-weight: 900;
letter-spacing: 0.06em;
}
.game-status {
height: 38px;
padding: 0 15px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
color: #93859a;
background: #1d1624;
font-size: 9px;
letter-spacing: 0.035em;
}
.status-mode {
display: flex;
align-items: center;
gap: 7px;
color: #c7b8cb;
font-size: 8px;
font-weight: 900;
letter-spacing: 0.13em;
white-space: nowrap;
}
.status-mode::before {
content: "";
width: 5px;
height: 5px;
border-radius: 50%;
background: var(--pink);
box-shadow: 0 0 8px var(--pink);
}
.side { display: grid; gap: 14px; min-width: 0; }
.ticket {
position: relative;
overflow: hidden;
padding: 20px;
border-radius: 19px;
color: #34212b;
background:
radial-gradient(circle at 100% 0, rgba(255,117,148,0.18), transparent 130px),
#fff0d7;
box-shadow: 0 18px 45px rgba(0,0,0,0.20);
}
.ticket::after {
content: "♥";
position: absolute;
right: -8px;
top: 45px;
color: rgba(235, 96, 134, 0.08);
font-family: Arial, sans-serif;
font-size: 110px;
transform: rotate(12deg);
pointer-events: none;
}
.ticket-top {
position: relative;
z-index: 1;
display: flex;
justify-content: space-between;
color: #a07579;
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
font-size: 8px;
font-weight: 900;
letter-spacing: 0.16em;
}
.ticket-top b { color: #ed6288; font-size: 15px; line-height: 0.6; }
.ticket h2 {
position: relative;
z-index: 1;
margin: 13px 0 8px;
color: #33202a;
font-size: 38px;
line-height: 0.86;
letter-spacing: -0.045em;
font-weight: 900;
}
.ticket h2 span { color: #e95e83; }
.ticket-description {
position: relative;
z-index: 1;
margin: 0;
max-width: 185px;
color: #82666c;
font-size: 11px;
line-height: 1.5;
}
.ticket-rule {
margin: 17px 0 14px;
border-top: 1px dashed rgba(76,45,57,0.25);
}
.progress-heading {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 8px;
font-size: 8px;
font-weight: 900;
letter-spacing: 0.12em;
}
#orderCount { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; letter-spacing: 0; }
.meter {
height: 7px;
overflow: hidden;
border-radius: 99px;
background: rgba(78,45,58,0.12);
}
.meter-fill {
width: 0;
height: 100%;
border-radius: inherit;
background: linear-gradient(90deg, #ff9b82, #ed6288);
box-shadow: 0 0 12px rgba(237,98,136,0.30);
transition: width 220ms ease;
}
.ticket-stats {
margin-top: 15px;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
}
.ticket-stat + .ticket-stat { border-left: 1px solid rgba(76,45,57,0.15); padding-left: 12px; }
.ticket-stat span { display: block; color: #9a7c7f; font-size: 8px; font-weight: 900; letter-spacing: 0.1em; }
.ticket-stat strong { display: block; margin-top: 4px; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 17px; }
.controls-card {
padding: 18px;
border: 1px solid var(--line);
border-radius: 19px;
background: linear-gradient(145deg, rgba(43,32,52,0.96), rgba(30,22,38,0.96));
box-shadow: 0 16px 40px rgba(0,0,0,0.18);
}
.controls-title {
margin: 0 0 13px;
color: #a99aad;
font-size: 8px;
font-weight: 900;
letter-spacing: 0.19em;
}
.control-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
margin-bottom: 10px;
color: #d9cbdc;
font-size: 10px;
}
.keys { display: flex; align-items: center; gap: 4px; }
kbd {
min-width: 25px;
height: 25px;
padding: 0 6px;
display: inline-grid;
place-items: center;
border: 1px solid rgba(255,255,255,0.16);
border-bottom-width: 3px;
border-radius: 6px;
color: #f6e9f3;
background: rgba(255,255,255,0.055);
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
font-size: 10px;
font-weight: 800;
}
.play-button {
width: 100%;
margin-top: 6px;
padding: 12px 13px;
display: flex;
align-items: center;
justify-content: space-between;
border: 0;
border-radius: 11px;
color: #321d27;
background: linear-gradient(135deg, #ffd17c, #ff8a88);
box-shadow: 0 8px 22px rgba(255,123,133,0.17), inset 0 1px rgba(255,255,255,0.45);
cursor: pointer;
font-size: 11px;
font-weight: 900;
letter-spacing: 0.025em;
transition: transform 150ms ease, filter 150ms ease;
}
.play-button:hover { transform: translateY(-2px); filter: brightness(1.06); }
.play-button:active { transform: translateY(1px); }
.play-button b { font-size: 17px; }
.pro-tip {
margin: 12px 0 0;
color: #8e8096;
font-size: 9px;
line-height: 1.45;
}
.pro-tip b { color: #cdbed1; }
.footer {
min-height: 25px;
padding: 9px 3px 0;
display: flex;
justify-content: space-between;
color: #665b6d;
font-size: 7px;
font-weight: 900;
letter-spacing: 0.17em;
}
.footer b { color: #a65c75; }
@media (max-width: 980px) {
.header-note { display: none; }
}
@media (max-width: 860px) {
.layout { grid-template-columns: 1fr; }
.side { grid-template-columns: 1fr 1fr; }
.ticket, .controls-card { height: 100%; }
}
@media (max-width: 560px) {
.app { width: calc(100% - 20px); padding-top: 10px; }
.site-header { min-height: 42px; margin-bottom: 10px; gap: 8px; }
.brand { gap: 8px; }
.brand-mark { width: 38px; height: 38px; border-radius: 12px; font-size: 21px; }
.brand-eyebrow { display: none; }
h1 { font-size: 19px; }
.icon-button { width: 34px; height: 34px; padding: 0; }
.icon-button .button-text { display: none; }
.restart-button { width: 34px; }
.header-actions { gap: 5px; }
.layout { gap: 12px; }
.game-card { border-radius: 17px; }
.hud { height: 56px; padding: 0 2px; }
.hud-block { padding: 0 9px; }
.hud-label { font-size: 7px; }
.hud-value { font-size: 17px; }
#lives { gap: 3px; font-size: 16px; }
.combo-badge { min-width: 42px; padding: 6px; font-size: 12px; }
.stage { aspect-ratio: 4 / 5; }
.demo-chip { top: 9px; left: 9px; padding: 6px 8px; font-size: 7px; }
.start-prompt { bottom: 57px; padding: 10px 14px; }
.start-prompt strong { font-size: 13px; }
.start-prompt small { font-size: 8px; }
.prompt-kicker { font-size: 7px; }
.game-status { height: 36px; padding: 0 10px; font-size: 8px; }
.game-status .desktop-copy { display: none; }
.game-status::after { content: "Drag anywhere to move your burger"; }
.side { grid-template-columns: 1fr; gap: 12px; }
.ticket { padding: 18px; }
.ticket h2 { font-size: 34px; }
.ticket-description { max-width: 260px; }
.ambient-heart { display: none; }
.footer { font-size: 6px; }
}
@media (prefers-reduced-motion: reduce) {
.ambient-heart, .live-dot, .combo-badge.hot { animation: none; }
* { scroll-behavior: auto !important; }
}
</style>
</head>
<body>
<div class="ambient-heart one" aria-hidden="true">♥</div>
<div class="ambient-heart two" aria-hidden="true">♥</div>
<main class="app">
<header class="site-header">
<div class="brand">
<div class="brand-mark" aria-hidden="true">🍔</div>
<div>
<div class="brand-eyebrow">PATTY'S ARCADE PRESENTS</div>
<h1>BURGER <span>BREAKOUT</span></h1>
</div>
</div>
<div class="header-note">MADE WITH <b>♥</b> & MUSTARD</div>
<div class="header-actions">
<button class="icon-button" id="soundButton" type="button" aria-label="Toggle sound" aria-pressed="true">
<span id="soundIcon" aria-hidden="true">♪</span>
<span class="button-text" id="soundText">SOUND ON</span>
</button>
<button class="icon-button restart-button" id="restartButton" type="button" aria-label="Start a new game" title="New game">↻</button>
</div>
</header>
<section class="layout">
<div class="game-card">
<div class="hud" aria-label="Game statistics">
<div class="hud-block hud-score">
<span class="hud-label">SCORE</span>
<strong class="hud-value" id="score">000000</strong>
</div>
<div class="hud-block hud-level">
<span class="hud-label">LEVEL</span>
<strong class="hud-value" id="level">01</strong>
</div>
<div class="hud-block hud-lives">
<span class="hud-label">LIVES</span>
<strong class="hud-value" id="lives" aria-label="3 lives">♥ ♥ ♥</strong>
</div>
<div class="hud-block hud-combo">
<span class="hud-label">LOVE STREAK</span>
<strong class="combo-badge" id="comboBadge">×1</strong>
</div>
</div>
<div class="stage" id="stage" tabindex="0" aria-label="Burger Breakout game area. Move with arrow keys, A and D, mouse, or touch.">
<canvas id="gameCanvas" role="img" aria-label="A hamburger paddle bouncing a ball into colorful bricks that explode into hearts"></canvas>
<div class="demo-chip">
<span class="live-dot"></span>
<span id="modeLabel">AUTO DEMO</span>
</div>
<div class="start-prompt" id="startPrompt">
<div class="prompt-kicker">♥ THE AUTO CHEF IS PLAYING</div>
<strong>Click or press a key to start</strong>
<small>Arrow keys / A D / mouse / touch to move</small>
</div>
<div class="toast" id="toast" aria-live="polite">
<strong id="toastTitle">ORDER UP!</strong>
<span id="toastSub">Fresh bricks coming right up.</span>
</div>
<div class="game-over" id="gameOver">
<div class="over-card">
<div class="tiny">TICKET CLOSED</div>
<h2>ONE MORE<br>ORDER?</h2>
<p>You served up <strong id="finalScore">0</strong> points of pure love.</p>
<button id="againButton" type="button">FIRE IT UP ↗</button>
</div>
</div>
</div>
<div class="game-status">
<span class="status-mode" id="statusMode">AUTO CHEF ON SHIFT</span>
<span class="desktop-copy">Break bricks. Make hearts. Don't drop the ball.</span>
</div>
</div>
<aside class="side">
<section class="ticket">
<div class="ticket-top"><span>ORDER #001 • EXTRA LOVE</span><b>♥</b></div>
<h2>HEART-<br><span>STORM</span></h2>
<p class="ticket-description">Every brick bursts into a full-flavored flurry of tiny hearts. No crumbs. Just feelings.</p>
<div class="ticket-rule"></div>
<div class="progress-heading">
<span>ORDER PROGRESS</span>
<span id="orderCount">00 / 50</span>
</div>
<div class="meter"><div class="meter-fill" id="progressFill"></div></div>
<div class="ticket-stats">
<div class="ticket-stat">
<span>BRICKS LEFT</span>
<strong id="bricksLeft">50</strong>
</div>
<div class="ticket-stat">
<span>BEST RALLY</span>
<strong id="bestRally">0×</strong>
</div>
</div>
</section>
<section class="controls-card">
<h2 class="controls-title">HOW TO WORK THE GRILL</h2>
<div class="control-row">
<span>Move your burger</span>
<span class="keys"><kbd>←</kbd><kbd>→</kbd><kbd>A</kbd><kbd>D</kbd></span>
</div>
<div class="control-row">
<span>Mouse / touch</span>
<span class="keys"><kbd>DRAG</kbd></span>
</div>
<button class="play-button" id="playButton" type="button">
<span class="button-label">Take the paddle</span><b>↗</b>
</button>
<p class="pro-tip"><b>PRO TIP:</b> Catch the ball near the bun's edges to steer it. Chain bricks quickly for a bigger love streak.</p>
</section>
</aside>
</section>
<footer class="footer">
<span>FRESH OFF THE GRILL • ZERO BRICKS LEFT BEHIND</span>
<span>STAY GOLDEN <b>♥</b></span>
</footer>
</main>
<script>
(() => {
"use strict";
const $ = id => document.getElementById(id);
const canvas = $("gameCanvas");
const ctx = canvas.getContext("2d");
const stage = $("stage");
const scoreEl = $("score");
const levelEl = $("level");
const livesEl = $("lives");
const comboBadge = $("comboBadge");
const startPrompt = $("startPrompt");
const modeLabel = $("modeLabel");
const statusMode = $("statusMode");
const orderCount = $("orderCount");
const progressFill = $("progressFill");
const bricksLeftEl = $("bricksLeft");
const bestRallyEl = $("bestRally");
const playButton = $("playButton");
const playButtonLabel = playButton.querySelector(".button-label");
const gameOverEl = $("gameOver");
const toast = $("toast");
const toastTitle = $("toastTitle");
const toastSub = $("toastSub");
const clamp = (value, min, max) => Math.max(min, Math.min(max, value));
const rand = (min, max) => min + Math.random() * (max - min);
let W = 900;
let H = 560;
let dpr = 1;
let initialized = false;
let bgGradient = null;
let bgGlow = null;
let elapsed = 0;
let lastTime = performance.now();
let toastTimer = 0;
let state = "demo";
let score = 0;
let lives = 3;
let level = 1;
let combo = 0;
let bestCombo = 0;
let comboTimer = 0;
let broken = 0;
let totalBricks = 0;
let serveTimer = 0;
let between = false;
let betweenTimer = 0;
let shake = 0;
let bricks = [];
let hearts = [];
let sparks = [];
let waves = [];
let floaters = [];
let trails = [];
const keys = { left: false, right: false };
let pointerDown = false;
let usingPointer = false;
let pointerTarget = 0;
const paddle = {
x: 0, y: 0, w: 120, h: 40,
vx: 0, squash: 0
};
const ball = {
x: 0, y: 0, vx: 0, vy: 0,
r: 8, active: false
};
const palette = ["#ff6f91", "#ff9a7c", "#ffd166", "#91dcae", "#aa98f4", "#73c7e5"];
const heartColors = ["#ff5f8f", "#ff86aa", "#ffc0d2", "#fff0d6", "#ff9a85"];
const dots = Array.from({ length: 68 }, (_, i) => ({
x: ((i * 47 + 13) % 101) / 101,
y: ((i * 71 + 7) % 103) / 103,
size: 0.6 + (i % 3) * 0.35
}));
function paddleWidth() {
return clamp(W * 0.15, 82, 134);
}
function baseSpeed() {
return clamp(H * 0.78, 290, 450) + (level - 1) * 14;
}
function roundedRectPath(c, x, y, w, h, radius) {
const r = Math.min(radius, Math.abs(w) / 2, Math.abs(h) / 2);
c.beginPath();
c.moveTo(x + r, y);
c.lineTo(x + w - r, y);
c.quadraticCurveTo(x + w, y, x + w, y + r);
c.lineTo(x + w, y + h - r);
c.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
c.lineTo(x + r, y + h);
c.quadraticCurveTo(x, y + h, x, y + h - r);
c.lineTo(x, y + r);
c.quadraticCurveTo(x, y, x + r, y);
c.closePath();
}
function traceHeart(c) {
c.beginPath();
c.moveTo(0, 0.9);
c.bezierCurveTo(-0.25, 0.56, -1.25, 0.02, -1.25, -0.55);
c.bezierCurveTo(-1.25, -1.25, -0.35, -1.45, 0, -0.75);
c.bezierCurveTo(0.35, -1.45, 1.25, -1.25, 1.25, -0.55);
c.bezierCurveTo(1.25, 0.02, 0.25, 0.56, 0, 0.9);
c.closePath();
}
function drawHeart(x, y, size, rotation, color, alpha = 1, stroke = false) {
ctx.save();
ctx.translate(x, y);
ctx.rotate(rotation);
ctx.scale(size, size);
ctx.globalAlpha = alpha;
traceHeart(ctx);
if (stroke) {
ctx.strokeStyle = color;
ctx.lineWidth = 0.085;
ctx.stroke();
} else {
ctx.fillStyle = color;
ctx.fill();
}
ctx.restore();
}
function makeBackground() {
bgGradient = ctx.createLinearGradient(0, 0, W, H);
bgGradient.addColorStop(0, "#1e1528");
bgGradient.addColorStop(0.55, "#17111f");
bgGradient.addColorStop(1, "#211526");
bgGlow = ctx.createRadialGradient(W * 0.52, H * 0.18, 0, W * 0.52, H * 0.18, W * 0.64);
bgGlow.addColorStop(0, "rgba(147,54,93,0.22)");
bgGlow.addColorStop(0.5, "rgba(77,38,76,0.08)");
bgGlow.addColorStop(1, "rgba(20,13,27,0)");
}
function resizeCanvas() {
const rect = stage.getBoundingClientRect();
if (rect.width < 10 || rect.height < 10) return;
const newW = rect.width;
const newH = rect.height;
const newDpr = Math.min(2, window.devicePixelRatio || 1);
if (initialized && Math.abs(newW - W) < 0.5 && Math.abs(newH - H) < 0.5 && newDpr === dpr) return;
const oldW = W;
const oldH = H;
const hadLayout = initialized && oldW > 0 && oldH > 0;
const sx = newW / oldW;
const sy = newH / oldH;
const ss = Math.sqrt(sx * sy);
W = newW;
H = newH;
dpr = newDpr;
canvas.width = Math.max(1, Math.round(W * dpr));
canvas.height = Math.max(1, Math.round(H * dpr));
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
if (hadLayout) {
for (const b of bricks) {
b.x *= sx; b.y *= sy; b.w *= sx; b.h *= sy;
}
for (const p of hearts) {
p.x *= sx; p.y *= sy; p.vx *= sx; p.vy *= sy; p.size *= ss;
}
for (const p of sparks) {
p.x *= sx; p.y *= sy; p.vx *= sx; p.vy *= sy; p.size *= ss;
}
for (const w of waves) {
w.x *= sx; w.y *= sy; w.r *= ss;
}
for (const f of floaters) {
f.x *= sx; f.y *= sy;
}
for (const t of trails) {
t.x *= sx; t.y *= sy;
}
paddle.x *= sx;
pointerTarget *= sx;
ball.x *= sx;
ball.y *= sy;
ball.vx *= sx;
ball.vy *= sy;
}
paddle.w = paddleWidth();
paddle.h = clamp(W * 0.045, 32, 42);
paddle.y = H - Math.max(38, H * 0.07);
paddle.x = clamp(paddle.x, paddle.w / 2, W - paddle.w / 2);
ball.r = clamp(W * 0.009, 6, 8);
makeBackground();
initialized = true;
}
function buildBricks() {
bricks = [];
const compact = W < 480;
const cols = compact ? 7 : 10;
const rows = compact ? 6 : 5;
const gap = compact ? 5 : clamp(W * 0.008, 5, 8);
const margin = W * 0.055;
const top = compact ? 58 : clamp(H * 0.12, 62, 72);
const brickH = compact ? 20 : clamp(H * 0.041, 20, 25);
const brickW = (W - margin * 2 - gap * (cols - 1)) / cols;
for (let row = 0; row < rows; row++) {
for (let col = 0; col < cols; col++) {
bricks.push({
x: margin + col * (brickW + gap),
y: top + row * (brickH + gap),
w: brickW,
h: brickH,
row,
col,
color: palette[(row + level - 1) % palette.length],
alive: true,
shine: 0.12 + ((row * 7 + col * 3) % 5) * 0.025
});
}
}
totalBricks = bricks.length;
broken = 0;
}
function remainingBricks() {
let count = 0;
for (const b of bricks) if (b.alive) count++;
return count;
}
function updateHUD() {
scoreEl.textContent = String(score).padStart(6, "0");
levelEl.textContent = String(level).padStart(2, "0");
livesEl.innerHTML = [0, 1, 2].map(i => `<span class="${i < lives ? "" : "off"}">♥</span>`).join("");
livesEl.setAttribute("aria-label", `${lives} ${lives === 1 ? "life" : "lives"}`);
comboBadge.textContent = `×${Math.max(1, combo)}`;
comboBadge.classList.toggle("hot", combo > 1);
const left = remainingBricks();
bricksLeftEl.textContent = left;
orderCount.textContent = `${String(broken).padStart(2, "0")} / ${totalBricks}`;
progressFill.style.width = `${totalBricks ? clamp(broken / totalBricks * 100, 0, 100) : 0}%`;
bestRallyEl.textContent = `${bestCombo}×`;
}
function syncStateUI() {
const demo = state === "demo";
startPrompt.classList.toggle("is-hidden", !demo);
if (demo) {
modeLabel.textContent = "AUTO DEMO";
statusMode.textContent = "AUTO CHEF ON SHIFT";
playButtonLabel.textContent = "Take the paddle";
} else if (state === "playing") {
modeLabel.textContent = "YOUR SHIFT";
statusMode.textContent = "YOU'RE ON THE GRILL";
playButtonLabel.textContent = "Restart shift";
} else {
modeLabel.textContent = "SHIFT OVER";
statusMode.textContent = "TICKET CLOSED";
playButtonLabel.textContent = "Try again";
}
}
function showToast(title, subtitle, duration = 1500) {
clearTimeout(toastTimer);
toastTitle.textContent = title;
toastSub.textContent = subtitle;
toast.classList.add("show");
toastTimer = setTimeout(() => toast.classList.remove("show"), duration);
}
function resetGame(demo) {
state = demo ? "demo" : "playing";
score = 0;
lives = 3;
level = 1;
combo = 0;
bestCombo = 0;
comboTimer = 0;
between = false;
betweenTimer = 0;
serveTimer = demo ? 0.25 : 0.35;
shake = 0;
hearts = [];
sparks = [];
waves = [];
floaters = [];
trails = [];
keys.left = false;
keys.right = false;
usingPointer = false;
paddle.w = paddleWidth();
paddle.h = clamp(W * 0.045, 32, 42);
paddle.x = W / 2;
paddle.y = H - Math.max(38, H * 0.07);
paddle.vx = 0;
paddle.squash = 0;
pointerTarget = paddle.x;
ball.r = clamp(W * 0.009, 6, 8);
ball.x = paddle.x;
ball.y = paddle.y - ball.r - 3;
ball.vx = 0;
ball.vy = 0;
ball.active = false;
buildBricks();
clearTimeout(toastTimer);
toast.classList.remove("show");
gameOverEl.classList.remove("show");
updateHUD();
syncStateUI();
}
function startUserGame() {
ensureAudio();
resetGame(false);
tone(330, 0.09, "triangle", 0.025);
tone(495, 0.12, "triangle", 0.02, 0.07);
}
function launchBall() {
const speed = baseSpeed();
const fraction = state === "demo" ? Math.sin(elapsed * 1.7 + level) * 0.29 : rand(-0.27, 0.27);
ball.vx = speed * fraction;
ball.vy = -Math.sqrt(speed * speed - ball.vx * ball.vx);
ball.active = true;
spawnHearts(ball.x, ball.y + 8, "#ffb2c7", 7, 8, 0.45);
tone(440, 0.07, "sine", 0.018);
}
function spawnHearts(x, y, sourceColor, count = 30, spread = 8, power = 1) {
const scale = clamp(H / 560, 0.76, 1.12);
for (let i = 0; i < count; i++) {
const angle = rand(0, Math.PI * 2);
const speed = rand(65, 265) * scale * power;
const life = rand(0.95, 1.75) * (0.8 + power * 0.2);
hearts.push({
x: x + rand(-spread, spread),
y: y + rand(-4, 4),
vx: Math.cos(angle) * speed + rand(-25, 25) * power,
vy: Math.sin(angle) * speed - rand(30, 105) * scale * power,
gravity: rand(250, 400) * scale,
size: rand(3.0, 6.9) * (power < 0.7 ? 0.75 : 1),
rotation: rand(-0.8, 0.8),
vr: rand(-5, 5),
life,
maxLife: life,
color: Math.random() < 0.38 ? sourceColor : heartColors[(Math.random() * heartColors.length) | 0],
glow: Math.random() < 0.23,
phase: rand(0, Math.PI * 2)
});
}
if (hearts.length > 700) hearts.splice(0, hearts.length - 700);
}
function spawnSparks(x, y, color, count = 12) {
const scale = clamp(H / 560, 0.75, 1.1);
for (let i = 0; i < count; i++) {
const a = rand(0, Math.PI * 2);
const speed = rand(80, 290) * scale;
const life = rand(0.3, 0.7);
sparks.push({
x, y,
vx: Math.cos(a) * speed,
vy: Math.sin(a) * speed,
size: rand(1.2, 3.1),
rotation: a,
vr: rand(-8, 8),
life,
maxLife: life,
color
});
}
}
function breakBrick(brick) {
brick.alive = false;
broken++;
combo = comboTimer > 0 ? combo + 1 : 1;
comboTimer = 2.4;
bestCombo = Math.max(bestCombo, combo);
const points = 100 * level * Math.min(combo, 8);
score += points;
const x = brick.x + brick.w / 2;
const y = brick.y + brick.h / 2;
spawnHearts(x, y, brick.color, 30 + Math.min(combo, 6) * 2, brick.w * 0.34, 1);
spawnSparks(x, y, brick.color, 13);
waves.push({ x, y, r: 4, life: 0.46, maxLife: 0.46, color: brick.color });
floaters.push({
x, y: brick.y - 3,
text: `+${points}`,
color: "#fff3db",
life: 0.95,
maxLife: 0.95,
size: clamp(W * 0.016, 11, 15)
});
if (combo >= 3) {
floaters.push({
x, y: brick.y + 17,
text: `${combo}× LOVE STREAK`,
color: "#ff9dbb",
life: 1.1,
maxLife: 1.1,
size: clamp(W * 0.011, 8, 11)
});
}
shake = Math.min(9, 2.5 + combo * 0.35);
tone(330 + brick.row * 42 + Math.min(combo, 10) * 15, 0.075, "triangle", 0.023);
if (combo > 2 && combo % 3 === 0) tone(660 + combo * 10, 0.11, "sine", 0.015, 0.035);
updateHUD();
if (remainingBricks() === 0) completeLevel();
}
function completeLevel() {
between = true;
betweenTimer = 1.9;
ball.active = false;
serveTimer = 0;
showToast("ORDER UP!", `Level ${level} cleared with extra love.`, 1750);
for (let i = 0; i < 5; i++) {
spawnHearts(
W * (0.15 + i * 0.175),
H * (0.24 + (i % 2) * 0.06),
palette[i % palette.length],
18,
15,
0.9
);
}
tone(523, 0.18, "triangle", 0.025);
tone(659, 0.18, "triangle", 0.025, 0.12);
tone(784, 0.25, "triangle", 0.025, 0.24);
}
function nextLevel() {
level++;
combo = 0;
comboTimer = 0;
between = false;
buildBricks();
serveTimer = 0.65;
ball.active = false;
showToast(`LEVEL ${level}`, "Fresh bricks. Same hungry burger.", 1200);
updateHUD();
}
function loseBall() {
lives--;
combo = 0;
comboTimer = 0;
ball.active = false;
shake = 10;
spawnHearts(clamp(ball.x, 20, W - 20), H - 12, "#ff658d", 22, 20, 0.8);
tone(170, 0.3, "sawtooth", 0.025);
if (lives <= 0) {
if (state === "demo") {
score = 0;
lives = 3;
level = 1;
bestCombo = 0;
buildBricks();
serveTimer = 0.9;
showToast("BURNT THE BUNS", "The auto chef is trying again.", 1100);
} else {
state = "gameover";
serveTimer = 0;
$("finalScore").textContent = score.toLocaleString();
syncStateUI();
gameOverEl.classList.add("show");
}
} else {
serveTimer = 0.8;
}
updateHUD();
}
function predictedPaddleX() {
if (!ball.active || ball.vy <= 0) {
return W / 2 + Math.sin(elapsed * 0.9) * W * 0.09;
}
const time = (paddle.y - ball.y) / ball.vy;
if (time <= 0) return ball.x;
const min = ball.r;
const max = W - ball.r;
const span = Math.max(1, max - min);
let projected = ball.x + ball.vx * time - min;
projected %= span * 2;
if (projected < 0) projected += span * 2;
if (projected > span) projected = span * 2 - projected;
return min + projected + Math.sin(elapsed * 2.3) * Math.min(4, W * 0.006);
}
function updatePaddle(dt) {
const oldX = paddle.x;
const direction = (keys.right ? 1 : 0) - (keys.left ? 1 : 0);
if (state === "demo") {
const target = predictedPaddleX();
const maxMove = Math.max(390, W * 1.35) * dt;
paddle.x += clamp(target - paddle.x, -maxMove, maxMove);
} else if (direction !== 0) {
paddle.x += direction * Math.max(390, W * 0.86) * dt;
pointerTarget = paddle.x;
} else if (usingPointer) {
const difference = pointerTarget - paddle.x;
paddle.x += difference * Math.min(1, dt * 18);
}
paddle.x = clamp(paddle.x, paddle.w / 2, W - paddle.w / 2);
paddle.vx = (paddle.x - oldX) / Math.max(dt, 0.001);
}
function bouncePaddle() {
const hit = clamp((ball.x - paddle.x) / (paddle.w / 2), -1, 1);
const angle = hit * 1.07;
const speed = Math.min(Math.hypot(ball.vx, ball.vy) * 1.02, baseSpeed() + 85);
ball.vx = Math.sin(angle) * speed + paddle.vx * 0.025;
ball.vy = -Math.abs(Math.cos(angle) * speed);
ball.y = paddle.y - ball.r - 0.5;
paddle.squash = 1;
shake = Math.max(shake, 1.5);
spawnHearts(ball.x, paddle.y, "#ffc0d2", 6, paddle.w * 0.17, 0.48);
waves.push({ x: ball.x, y: paddle.y, r: 4, life: 0.25, maxLife: 0.25, color: "#ffd48a" });
tone(205 + Math.abs(hit) * 55, 0.065, "square", 0.014);
}
function updateBall(dt) {
trails.push({ x: ball.x, y: ball.y, life: 0.28, maxLife: 0.28 });
if (trails.length > 22) trails.shift();
const previousX = ball.x;
const previousY = ball.y;
ball.x += ball.vx * dt;
ball.y += ball.vy * dt;
if (ball.x - ball.r < 0) {
ball.x = ball.r;
ball.vx = Math.abs(ball.vx);
tone(250, 0.035, "sine", 0.01);
} else if (ball.x + ball.r > W) {
ball.x = W - ball.r;
ball.vx = -Math.abs(ball.vx);
tone(250, 0.035, "sine", 0.01);
}
if (ball.y - ball.r < 0) {
ball.y = ball.r;
ball.vy = Math.abs(ball.vy);
tone(270, 0.04, "sine", 0.012);
}
if (
ball.vy > 0 &&
ball.y + ball.r >= paddle.y &&
ball.y < paddle.y + paddle.h * 0.65 &&
ball.x + ball.r >= paddle.x - paddle.w / 2 &&
ball.x - ball.r <= paddle.x + paddle.w / 2
) {
bouncePaddle();
}
for (const b of bricks) {
if (!b.alive) continue;
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
) continue;
if (previousY + ball.r <= b.y + 2) {
ball.y = b.y - ball.r - 0.5;
ball.vy = -Math.abs(ball.vy);
} else if (previousY - ball.r >= b.y + b.h - 2) {
ball.y = b.y + b.h + ball.r + 0.5;
ball.vy = Math.abs(ball.vy);
} else if (previousX + ball.r <= b.x + 2) {
ball.x = b.x - ball.r - 0.5;
ball.vx = -Math.abs(ball.vx);
} else if (previousX - ball.r >= b.x + b.w - 2) {
ball.x = b.x + b.w + ball.r + 0.5;
ball.vx = Math.abs(ball.vx);
} else {
const distances = [
{ value: Math.abs(ball.x - (b.x - ball.r)), side: "left" },
{ value: Math.abs(ball.x - (b.x + b.w + ball.r)), side: "right" },
{ value: Math.abs(ball.y - (b.y - ball.r)), side: "top" },
{ value: Math.abs(ball.y - (b.y + b.h + ball.r)), side: "bottom" }
].sort((a, c) => a.value - c.value);
const side = distances[0].side;
if (side === "left") { ball.x = b.x - ball.r; ball.vx = -Math.abs(ball.vx); }
else if (side === "right") { ball.x = b.x + b.w + ball.r; ball.vx = Math.abs(ball.vx); }
else if (side === "top") { ball.y = b.y - ball.r; ball.vy = -Math.abs(ball.vy); }
else { ball.y = b.y + b.h + ball.r; ball.vy = Math.abs(ball.vy); }
}
breakBrick(b);
break;
}
if (ball.y - ball.r > H) loseBall();
}
function update(dt) {
updatePaddle(dt);
if (comboTimer > 0) {
comboTimer -= dt;
if (comboTimer <= 0) {
combo = 0;
updateHUD();
}
}
if (between) {
betweenTimer -= dt;
if (betweenTimer <= 0) nextLevel();
return;
}
if (!ball.active) {
if (serveTimer > 0) {
serveTimer -= dt;
ball.x = paddle.x;
ball.y = paddle.y - ball.r - 3;
if (serveTimer <= 0) launchBall();
}
return;
}
updateBall(dt);
}
function updateEffects(dt) {
shake = Math.max(0, shake - dt * 18);
paddle.squash = Math.max(0, paddle.squash - dt * 5.5);
for (let i = hearts.length - 1; i >= 0; i--) {
const p = hearts[i];
p.life -= dt;
if (p.life <= 0 || p.y > H + 45) {
hearts.splice(i, 1);
continue;
}
p.vy += p.gravity * dt;
p.vx *= Math.pow(0.988, dt * 60);
p.x += p.vx * dt + Math.sin(elapsed * 7 + p.phase) * 7 * dt;
p.y += p.vy * dt;
p.rotation += p.vr * dt;
}
for (let i = sparks.length - 1; i >= 0; i--) {
const p = sparks[i];
p.life -= dt;
if (p.life <= 0) {
sparks.splice(i, 1);
continue;
}
p.vy += 310 * dt;
p.x += p.vx * dt;
p.y += p.vy * dt;
p.rotation += p.vr * dt;
}
for (let i = waves.length - 1; i >= 0; i--) {
const w = waves[i];
w.life -= dt;
w.r += 210 * dt;
if (w.life <= 0) waves.splice(i, 1);
}
for (let i = floaters.length - 1; i >= 0; i--) {
const f = floaters[i];
f.life -= dt;
f.y -= 34 * dt;
if (f.life <= 0) floaters.splice(i, 1);
}
for (let i = trails.length - 1; i >= 0; i--) {
trails[i].life -= dt;
if (trails[i].life <= 0) trails.splice(i, 1);
}
}
function drawBackground() {
ctx.globalAlpha = 1;
ctx.fillStyle = bgGradient || "#17111f";
ctx.fillRect(0, 0, W, H);
ctx.fillStyle = bgGlow || "rgba(0,0,0,0)";
ctx.fillRect(0, 0, W, H);
ctx.save();
ctx.fillStyle = "#ffd8e3";
for (const d of dots) {
const alpha = 0.055 + (Math.sin(elapsed * 1.4 + d.x * 20) + 1) * 0.018;
ctx.globalAlpha = alpha;
ctx.fillRect(d.x * W, d.y * H, d.size, d.size);
}
ctx.restore();
drawHeart(W * 0.075, H * 0.58, Math.min(W, H) * 0.075, -0.2, "#ff94b2", 0.045, true);
drawHeart(W * 0.93, H * 0.42, Math.min(W, H) * 0.05, 0.25, "#ffd18a", 0.04, true);
const topY = W < 480 ? 27 : 31;
ctx.save();
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.font = `900 ${W < 480 ? 7 : 8}px "Trebuchet MS", sans-serif`;
ctx.fillStyle = "rgba(236,213,228,0.32)";
ctx.fillText("FRESHLY STACKED • EXTRA LOVE", W / 2, topY);
ctx.strokeStyle = "rgba(255,255,255,0.07)";
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(18, topY);
ctx.lineTo(Math.max(20, W / 2 - 112), topY);
ctx.moveTo(Math.min(W - 20, W / 2 + 112), topY);
ctx.lineTo(W - 18, topY);
ctx.stroke();
ctx.restore();
ctx.strokeStyle = "rgba(255,255,255,0.035)";
ctx.strokeRect(0.5, 0.5, W - 1, H - 1);
}
function drawBricks() {
for (const b of bricks) {
if (!b.alive) continue;
ctx.save();
ctx.shadowColor = b.color;
ctx.shadowBlur = 7;
roundedRectPath(ctx, b.x, b.y, b.w, b.h, Math.min(7, b.h * 0.28));
ctx.fillStyle = b.color;
ctx.fill();
ctx.shadowBlur = 0;
roundedRectPath(ctx, b.x + 2, b.y + 2, b.w - 4, Math.max(3, b.h * 0.34), 4);
ctx.fillStyle = `rgba(255,255,255,${b.shine})`;
ctx.fill();
roundedRectPath(ctx, b.x + 3, b.y + b.h - 4, b.w - 6, 2, 1);
ctx.fillStyle = "rgba(37,16,34,0.16)";
ctx.fill();
ctx.restore();
drawHeart(
b.x + b.w / 2,
b.y + b.h * 0.57,
clamp(b.h * 0.14, 2.5, 3.6),
0,
"#ffffff",
0.40
);
}
}
function drawWaves() {
ctx.save();
for (const w of waves) {
ctx.globalAlpha = clamp(w.life / w.maxLife, 0, 1) * 0.55;
ctx.strokeStyle = w.color;
ctx.lineWidth = 2;
ctx.beginPath();
ctx.arc(w.x, w.y, w.r, 0, Math.PI * 2);
ctx.stroke();
}
ctx.restore();
}
function drawTrails() {
ctx.save();
for (const t of trails) {
const alpha = clamp(t.life / t.maxLife, 0, 1);
ctx.globalAlpha = alpha * 0.18;
ctx.fillStyle = "#ff9fba";
ctx.beginPath();
ctx.arc(t.x, t.y, ball.r * (0.25 + alpha * 0.55), 0, Math.PI * 2);
ctx.fill();
}
ctx.restore();
}
function drawPaddle() {
const x = paddle.x;
const y = paddle.y;
const w = paddle.w;
const h = paddle.h;
ctx.save();
ctx.translate(x, y + h / 2);
ctx.scale(1 + paddle.squash * 0.075, 1 - paddle.squash * 0.14);
ctx.translate(-x, -(y + h / 2));
ctx.globalAlpha = 0.26;
ctx.fillStyle = "#000000";
ctx.beginPath();
ctx.ellipse(x, y + h + 3, w * 0.43, 5, 0, 0, Math.PI * 2);
ctx.fill();
ctx.globalAlpha = 1;
const bunGradient = ctx.createLinearGradient(0, y, 0, y + h);
bunGradient.addColorStop(0, "#ffe099");
bunGradient.addColorStop(0.55, "#f7b967");
bunGradient.addColorStop(1, "#d98749");
ctx.shadowColor = "rgba(255,179,94,0.45)";
ctx.shadowBlur = 13;
roundedRectPath(ctx, x - w * 0.45, y + h * 0.72, w * 0.9, h * 0.24, h * 0.12);
ctx.fillStyle = bunGradient;
ctx.fill();
roundedRectPath(ctx, x - w * 0.48, y + h * 0.56, w * 0.96, h * 0.22, h * 0.09);
ctx.fillStyle = "#63382f";
ctx.fill();
ctx.shadowBlur = 0;
roundedRectPath(ctx, x - w * 0.43, y + h * 0.58, w * 0.86, h * 0.045, 2);
ctx.fillStyle = "rgba(255,184,128,0.25)";
ctx.fill();
ctx.beginPath();
ctx.moveTo(x - w * 0.47, y + h * 0.47);
ctx.lineTo(x + w * 0.47, y + h * 0.47);
ctx.lineTo(x + w * 0.40, y + h * 0.60);
ctx.lineTo(x + w * 0.23, y + h * 0.56);
ctx.lineTo(x + w * 0.13, y + h * 0.68);
ctx.lineTo(x + w * 0.04, y + h * 0.56);
ctx.lineTo(x - w * 0.22, y + h * 0.58);
ctx.lineTo(x - w * 0.36, y + h * 0.63);
ctx.closePath();
ctx.fillStyle = "#ffd64f";
ctx.fill();
roundedRectPath(ctx, x - w * 0.46, y + h * 0.405, w * 0.92, h * 0.10, h * 0.045);
ctx.fillStyle = "#ef6e60";
ctx.fill();
ctx.beginPath();
ctx.moveTo(x - w * 0.49, y + h * 0.40);
for (let i = 0; i <= 10; i++) {
const px = x - w * 0.49 + (w * 0.98 * i / 10);
const py = y + h * (0.38 + (i % 2 ? 0.055 : 0));
ctx.lineTo(px, py);
}
ctx.lineTo(x + w * 0.45, y + h * 0.48);
ctx.lineTo(x - w * 0.45, y + h * 0.48);
ctx.closePath();
ctx.fillStyle = "#82cc76";
ctx.fill();
ctx.shadowColor = "rgba(255,190,104,0.42)";
ctx.shadowBlur = 12;
ctx.beginPath();
ctx.moveTo(x - w * 0.47, y + h * 0.36);
ctx.bezierCurveTo(x - w * 0.45, y + h * 0.03, x - w * 0.25, y, x, y);
ctx.bezierCurveTo(x + w * 0.25, y, x + w * 0.45, y + h * 0.03, x + w * 0.47, y + h * 0.36);
ctx.quadraticCurveTo(x, y + h * 0.43, x - w * 0.47, y + h * 0.36);
ctx.closePath();
ctx.fillStyle = bunGradient;
ctx.fill();
ctx.shadowBlur = 0;
ctx.beginPath();
ctx.moveTo(x - w * 0.35, y + h * 0.12);
ctx.quadraticCurveTo(x, y - h * 0.025, x + w * 0.29, y + h * 0.09);
ctx.strokeStyle = "rgba(255,246,197,0.34)";
ctx.lineWidth = Math.max(1, h * 0.045);
ctx.lineCap = "round";
ctx.stroke();
const seeds = [
[-0.24, 0.15, -0.45], [-0.08, 0.10, 0.30], [0.11, 0.14, -0.25],
[0.27, 0.20, 0.45], [-0.30, 0.26, 0.25], [0.00, 0.25, -0.40]
];
ctx.fillStyle = "#fff1bd";
for (const seed of seeds) {
ctx.save();
ctx.translate(x + seed[0] * w, y + seed[1] * h);
ctx.rotate(seed[2]);
ctx.beginPath();
ctx.ellipse(0, 0, Math.max(1.2, w * 0.012), Math.max(0.65, h * 0.022), 0, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
ctx.restore();
}
function drawBall() {
if (!ball.active && serveTimer <= 0) return;
const pulse = 1 + Math.sin(elapsed * 7) * 0.025;
const r = ball.r * pulse;
ctx.save();
if (!ball.active) {
ctx.globalAlpha = 0.32;
ctx.strokeStyle = "#ffafc4";
ctx.lineWidth = 1;
ctx.beginPath();
ctx.arc(ball.x, ball.y, r + 5 + Math.sin(elapsed * 6) * 2, 0, Math.PI * 2);
ctx.stroke();
ctx.globalAlpha = 1;
}
ctx.shadowColor = "#ff92b1";
ctx.shadowBlur = 18;
const gradient = ctx.createRadialGradient(
ball.x - r * 0.35, ball.y - r * 0.4, 1,
ball.x, ball.y, r
);
gradient.addColorStop(0, "#ffffff");
gradient.addColorStop(0.55, "#fff1df");
gradient.addColorStop(1, "#ffabc2");
ctx.fillStyle = gradient;
ctx.beginPath();
ctx.arc(ball.x, ball.y, r, 0, Math.PI * 2);
ctx.fill();
ctx.shadowBlur = 0;
ctx.restore();
drawHeart(ball.x, ball.y + 0.6, r * 0.34, 0, "#ec658b", 0.9);
}
function drawEffects() {
for (const p of hearts) {
const alpha = clamp(p.life / Math.min(0.38, p.maxLife), 0, 1);
const age = p.maxLife - p.life;
const scale = 1 + Math.sin(age * 12 + p.phase) * 0.055;
if (p.glow) {
drawHeart(p.x, p.y, p.size * 1.85 * scale, p.rotation, p.color, alpha * 0.10);
}
drawHeart(p.x, p.y, p.size * scale, p.rotation, p.color, alpha);
}
ctx.save();
for (const p of sparks) {
const alpha = clamp(p.life / p.maxLife, 0, 1);
ctx.save();
ctx.translate(p.x, p.y);
ctx.rotate(p.rotation);
ctx.globalAlpha = alpha;
ctx.fillStyle = p.color;
roundedRectPath(ctx, -p.size * 1.5, -p.size / 2, p.size * 3, p.size, p.size / 2);
ctx.fill();
ctx.restore();
}
ctx.restore();
ctx.save();
ctx.textAlign = "center";
ctx.textBaseline = "middle";
for (const f of floaters) {
const alpha = clamp(f.life / 0.25, 0, 1);
ctx.globalAlpha = alpha;
ctx.font = `900 ${f.size}px "Trebuchet MS", sans-serif`;
ctx.lineWidth = 3;
ctx.strokeStyle = "rgba(25,14,30,0.65)";
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.setTransform(dpr, 0, 0, dpr, 0, 0);
ctx.globalAlpha = 1;
ctx.globalCompositeOperation = "source-over";
drawBackground();
ctx.save();
if (shake > 0) {
ctx.translate(Math.cos(elapsed * 79) * shake, Math.sin(elapsed * 91) * shake * 0.65);
}
drawWaves();
drawBricks();
drawTrails();
drawPaddle();
drawBall();
drawEffects();
ctx.restore();
}
function frame(now) {
const dt = Math.min(0.033, Math.max(0, (now - lastTime) / 1000));
lastTime = now;
elapsed += dt;
if (state !== "gameover") update(dt);
updateEffects(dt);
render();
requestAnimationFrame(frame);
}
function pointerToPaddle(event) {
const rect = stage.getBoundingClientRect();
if (!rect.width) return;
pointerTarget = clamp(
(event.clientX - rect.left) / rect.width * W,
paddle.w / 2,
W - paddle.w / 2
);
}
stage.addEventListener("pointerdown", event => {
if (event.target instanceof Element && event.target.closest("button")) return;
if (state !== "playing") startUserGame();
pointerDown = true;
usingPointer = true;
pointerToPaddle(event);
stage.focus({ preventScroll: true });
});
stage.addEventListener("pointermove", event => {
if (event.pointerType === "mouse" || pointerDown) {
usingPointer = true;
pointerToPaddle(event);
}
});
window.addEventListener("pointerup", () => { pointerDown = false; });
window.addEventListener("pointercancel", () => { pointerDown = false; });
stage.addEventListener("contextmenu", event => event.preventDefault());
window.addEventListener("keydown", event => {
const key = event.key.toLowerCase();
const gameKey = ["arrowleft", "arrowright", "a", "d", " "].includes(key);
if (gameKey) event.preventDefault();
if (
event.target instanceof HTMLButtonElement &&
(key === " " || key === "enter")
) return;
if (event.ctrlKey || event.metaKey || event.altKey) return;
if (state !== "playing") startUserGame();
if (key === "arrowleft" || key === "a") {
keys.left = true;
usingPointer = false;
}
if (key === "arrowright" || key === "d") {
keys.right = true;
usingPointer = false;
}
});
window.addEventListener("keyup", event => {
const key = event.key.toLowerCase();
if (key === "arrowleft" || key === "a") keys.left = false;
if (key === "arrowright" || key === "d") keys.right = false;
});
window.addEventListener("blur", () => {
keys.left = false;
keys.right = false;
pointerDown = false;
});
document.addEventListener("visibilitychange", () => {
lastTime = performance.now();
});
playButton.addEventListener("click", startUserGame);
$("restartButton").addEventListener("click", startUserGame);
$("againButton").addEventListener("click", startUserGame);
let audioContext = null;
let soundOn = true;
function ensureAudio() {
if (!soundOn) return;
const AudioCtor = window.AudioContext || window.webkitAudioContext;
if (!AudioCtor) return;
if (!audioContext) audioContext = new AudioCtor();
if (audioContext.state === "suspended") {
const result = audioContext.resume();
if (result && result.catch) result.catch(() => {});
}
}
function tone(frequency, duration, type = "sine", volume = 0.02, delay = 0) {
if (!soundOn || !audioContext || state === "demo") return;
const now = audioContext.currentTime + delay;
const oscillator = audioContext.createOscillator();
const gain = audioContext.createGain();
oscillator.type = type;
oscillator.frequency.setValueAtTime(frequency, now);
gain.gain.setValueAtTime(0.0001, now);
gain.gain.exponentialRampToValueAtTime(Math.max(0.0002, volume), now + 0.008);
gain.gain.exponentialRampToValueAtTime(0.0001, now + duration);
oscillator.connect(gain);
gain.connect(audioContext.destination);
oscillator.start(now);
oscillator.stop(now + duration + 0.025);
}
$("soundButton").addEventListener("click", () => {
soundOn = !soundOn;
const button = $("soundButton");
button.classList.toggle("muted", !soundOn);
button.setAttribute("aria-pressed", String(soundOn));
$("soundText").textContent = soundOn ? "SOUND ON" : "SOUND OFF";
$("soundIcon").textContent = soundOn ? "♪" : "×";
if (soundOn) {
ensureAudio();
tone(520, 0.09, "sine", 0.025);
}
});
if ("ResizeObserver" in window) {
new ResizeObserver(resizeCanvas).observe(stage);
}
window.addEventListener("resize", resizeCanvas);
resizeCanvas();
resetGame(true);
requestAnimationFrame(frame);
})();
</script>
</body>
</html>Reasoning trace (23,360 tokens)
I need respond exactly one fenced html complete file. Should build polished self-contained Breakout game hamburger paddle hearts explosions, self-playing demo behind click/key message, visible action immediate. Should have no externals. Should design impressive within 1280x800 and 375 adapt. Should have zero errors. Should plan carefully mechanics, visuals, interactions, game states, responsive, canvas likely. Could create full viewport stylish page, canvas game area with CSS overlay HUD and start. Should game demo autonomous immediately, start overlay message not blocking much. On interaction user starts playing reset game. AI plays demo. Bricks explode tons hearts particles. Paddle rendered as hamburger with bun lettuce cheese patty etc perhaps canvas stylized. Should game juice sound perhaps WebAudio initiated after click, optional sound toggle. Should handle keyboard arrows A D pointer drag/touch, start click/key. Demo mode behind "Click or press a key to start". Auto restart in demo without user. Meaningful first 3 seconds. Could have initial demo AI hitting soon; particles visible quickly. Setup start ball moving toward bricks after some seconds. Make sure screenshot at 3 secs shows explosion and gameplay. Could even ambient floating hearts and initial heart bursts to ensure? Better game launch ball upwards with speed and bricks close enough, collision <1sec. Demo AI paddle tracks. Should have code robust.
Should evaluate aesthetics. Single HTML CSS responsive. Perhaps dark midnight/neon pastel, cream. Center game panel. Title "BURGER BREAKER" or "LOVE AT FIRST BITE" heart motif. Canvas dimensions logical 900x620 with responsive sizing fit viewport. HUD above, side info perhaps. At 1280x800 look right. Should not excessive text. Make game visually polished with canvas, CSS. Build responsive shell: background dark #100d1a with radial gradients, grid subtle, floating decorative CSS? Main container width min(1120px, calc(100vw - 32px)), display grid game stage + sidebar at desktop perhaps board 900x600 and sidebar 220; height fit 800. Or game area full central with top brand header, HUD. A side card "Order up!" controls etc. Main request game, best make large canvas ~900x600 with right sidebar 250. At 1280x800 header 70, main 620, footer. Canvas internal 900x600; CSS aspect ratio 3/2, width ~900; main side 240 => 1180 and height 600. At 1280 fits. At 375 use single column and hide sidebar or compact; canvas aspect ratio perhaps logical 900x620 (wide) would become height 250 too small; can still okay but mobile play small. Could adapt canvas internal to CSS aspect ratio? Better maintain ratio, mobile board width 343 height 500 perhaps portrait if change coordinate? Could use canvas fixed 900x600 ratio on mobile =229 high which not ideal. I can set wrapper aspect ratio 3/2 desktop and on mobile aspect-ratio: 4/5 and canvas scale fill with coordinate transformation? Could maintain logical width 900 height 600 and letterboxing? Use responsive world width fixed 900 height variable? More complexity but doable coordinate system uses W,H derived from canvas.clientWidth*dpr and world units based actual css pixels normalized. Game logic can use base dimensions set per resize, brick layout recalculated. But resizing should reposition etc. Simpler fixed internal logical W=900 H=600, CSS canvas width 100%, aspect ratio 3/2. On mobile viewport 375, board 343x229 plus touch area; usable but tiny. Could instead CSS mobile rotate? Not needed "adapt gracefully down to 375" means no horizontal and visible. Could redesign canvas logical W=600 H=700 portrait and desktop board dimensions? Use canvas dimensions computed and dynamic world. Perhaps use logical 800x600 all, CSS fit.
I can build board canvas width=960 height=640 and display with container width calc. At mobile set min-height: 480px; canvas stretched (aspect ratio distortion) not ideal but could use dynamic coordinate transform. Better robust dynamic world canvas based on element size in CSS pixels. Physics use units per second tied to dimensions. On resize update W,H canvas CSS and scale positions proportionally, reconstruct bricks on new game or rescale bricks. Draw based W,H. Rendering details responsive. This manageable.
HTML structure:
body with decorative blobs and noise perhaps no external. .app max-width 1200 margin auto padding 20.
header brand icon tiny burger in CSS or inline svg? I can draw using HTML spans? Use logo rounded square with emoji 🍔 but external font system emoji okay; use stylized emoji. Header title "LOVE / AT FIRST BITE" perhaps name "BURGER BREAKOUT" and kicker "a very lovely arcade".
Main .layout display grid columns minmax(0, 1fr) 250px gap16. Game card with .topbar HUD: score, lives represented heart symbols, level. Canvas container .stage position relative, aspect-ratio: 3/2; min? At desktop width ~900 height 600. Overlay .start-card centered bottom? Must not obstruct. "Click or press a key to start" pill at bottom center with mouse icon. Perhaps overlay entire stage transparent gradient but gameplay visible behind. Include "DEMO PLAYING" green dot top left and instruction chip bottom. On click user start.
Canvas itself draws board background, bricks, particles, paddle etc. DOM overlay for message and status.
Right panel: "TODAY'S SPECIAL" card perhaps challenge progress, controls key caps, ingredient legend; "HEART COMBO" combo meter. Should update stats in sidebar. Buttons Sound / Restart etc. Can keep attractive. At mobile right panel after canvas but height might exceed first screen; okay adapt. Better desktop side panel visible.
Header includes controls buttons sound and reset.
Footer minimal "Burgers. Bricks. Butterflies."
Colors: background #140f1c, panel #201827, border #3b2a43, coral #ff6b87, pink #ff82b2, golden #ffc966, mint #a9e6b1, cream #fff4df.
Canvas background deep #171220 with subtle grid/dots, glow.
Bricks shaped like various burger ingredients? Request only paddle hamburger. Bricks could be glossy candy-like tiles in colors coral, mustard, mint, lavender. Each brick heart emblem small centered? To reinforce. Every brick break tons of little hearts: particle class hearts with x y vx vy size rot life color gravity. 22-34 per brick plus sparks. Draw hearts via bezier or path. Make sure performant. Heart shape draw path normalized: moveTo(0, 0.3), bezier etc; scale. Use ctx.translate rotate scale(s,s); path moveTo(0, 2.5); bezierCurveTo(-6,-2, -6,-8,0? Should design). Standard: moveTo(0,3); bezierCurveTo(-7,-2,-5,-9,0,-5); bezierCurveTo(5,-9,7,-2,0,3) yields heart. Better local normalized around 0: moveTo(0, 0.85); bezier(-1.25,-0.05,-0.9,-1.25,0,-0.55); bezier(0.9,-1.25,1.25,-0.05,0,0.85). Scale size ~4. Add two lobes? This shape okay. Use path M0,0.8 C -1.4,-0.1 -1,-1.3 0,-0.65 C 1,-1.3 1.4,-0.1 0,0.8. Fill.
Can precompute heart Path2D? Path2D widely supported. Use function drawHeart each particle beginPath. 400 particles max okay.
Paddle burger: x,y,w,h logical dynamic. Draw shadow/glow, bottom bun rounded rect, patty dark brown, cheese yellow polygon dripping, lettuce wavy green, top bun arc with rounded shape, sesame seeds. Canvas drawn burger at w~124 h~45. Should have actual collision paddle surface. Paddle y=H-52, h=36. Ball radius 8. Burger dimensions w=130, h=42. Draw at x center.
Ball: glossy white-pink orb with tiny heart symbol? Could be heart-shaped ball? User says ball unspecified. Make glowing pearl with small highlight, trailing heart sparkles perhaps. Use circle pink cream with tiny shine. Trail circles. Game.
Bricks: 6 rows x 10 perhaps dynamic 8-10 based width. Bricks area top 80 to ~260. Each brick with rounded corners, top highlight, tiny heart stamp. Some multi-hit? Simplicity one hit each except special? All one hit to get lots explosions. Should have levels, score combos, lives. Start with 5 rows 9 =45 bricks, screenshot hearts quickly.
Physics:
const state = 'demo' initially. Demo AI active, demo score perhaps starts 0. Ball attached? I want immediate. initGame(true) sets score=0, lives=3, level=1, combo=0, ball launched upward angle random around -Math.PI/2 with vx=170 vy=-330 in units relative base? Use units fixed world width 900 height 600 independent canvas? Dynamic world using coordinates in actual CSS pixels would speed differ on mobile. Could set speed based on H ~0.65H/s. For desktop H=560, vy=-390, reach bricks in ~1 sec. On mobile H=500 (stage aspect 4/5) same. Good.
Canvas CSS desktop stage aspect-ratio 3/2 max-height calc(100vh - 180px) perhaps width auto? Layout constraints. App width 1180, side250, canvas width ~890 height 593. Header ~58, margins etc total 720. At 800 good. CSS .game-card min-width0; .stage aspect-ratio: 3/2; max-height? Since width determines. At 1280 app width 1216? header 50 + padding 18*2 + main 600 + footer30 =716. Fine. At 1280 board left width 910 if side 250 and gaps: app max 1180 -> left 900. height600. header60+main? app padding 18 top bottom, header 50 + margin16 + main600 =710 plus footer25=735. Good. At 800 fits. At 1024 width app~992 left~716 height477; fine. At 375 layout one column; stage width 343 and aspect ratio 4/5 =>429, header ~50, HUD ~50, total~560 plus side cards below. Great. Dynamic canvas W/H. Desktop canvas stage aspect-ratio: 3/2. On mobile 4/5. I can use ResizeObserver to set canvas dimensions to css px*DPR and set transform, update W,H=rect.width,height; rescale entities. Should stage overlays DOM sizing relative. Logic coordinates in css pixels. `resize()` get old W,H; canvas width = floor(rect.width*dpr), etc; ctx.setTransform(dpr,..). If old, scale all x/y/v? I can simply scale x, y positions, paddle w based clamp(W*0.15, 82,136), ball velocities based ratio perhaps leave speed? Better function scaleGame(sx,sy) for bricks x,y,w,h; particles etc. On initial resize before init W=0 then init. ResizeObserver may call after init. I need avoid rebuilding brick formation every tiny change. On resize scale. Ball radius = clamp(W*.009,6,8). Speeds could be rescaled with `Math.hypot`? Use direction and speed based min? Set ball speed = clamp(H*0.66,280,440) after resize retaining angle. Paddle speed W*1.1. All velocities in particles scale by sx sy. Fine.
Initialize after first resize via requestAnimationFrame? Code at end `resizeCanvas(); resetGame(true); requestAnimationFrame(loop);` Canvas rect available after DOM loaded script at end body. ResizeObserver calls later and scaling. Good.
Physics with dt capped .025 and perhaps fixed substeps to avoid tunneling. Ball speed 400 px/s, brick h ~26, dt 16 ms =6.4 no issue. Substeps 2 or dynamic based movement. Use dt min .033; update with steps = ceil(ball.speed*dt/(ball.r*0.8)) cap perhaps 6. Simpler update per frame with continuous enough. Should prevent multiple brick collisions same frame: after first break reflect vy and break. Collision find nearest? Loop bricks, if intersects and not broken. Compute previous ball pos to determine bounce side using overlap. Ball is circle, simple AABB expanded by r:
if ball.x+r>b.x etc. Determine prevX prevY and if prevY+r<=b.y then hit top vy=-abs(vy), else if prevY-r>=b.y+b.h bottom vy=abs, else if prevX+r<=b.x left etc; else vy*=-1. Update one and break. On collision with brick call breakBrick with hearts. Score increases base 100*level*combo multiplier perhaps combo increments within 2.5sec. DOM update. Add screen shake 4, flash ring.
Paddle collision if vy>0 and circle intersects top region and previous bottom<=paddle.y+? Set ball.y=paddle.y-ball.r; reflect based hit position. angle = hit*(Math.PI*0.38) ~68 deg; vx=sin(angle)*speed; vy=-cos(angle)*speed; ensure. Add paddle squash timer, small hearts (5) on bounce perhaps optional. Combo resets on paddle hit? Usually combo bricks consecutively per rally, could not reset. Keep combo decays 2.2 sec after last brick. Show floating text e.g. "+100", "x3 LOVE STREAK".
Walls: left/right bounce, top bounce with small spark. bottom if y-r>H: lives--; if lives>0 reset ball after 0.8s (serve). In demo never lose? AI robust. For demo auto catches by tracking predictive ball. If life zero in demo auto reset after 1.5 sec; user game over overlay card with "Order again" button perhaps. User game start via click resets. `mode` values 'demo','playing','gameover','win'. In demo overlay start pill remains. On any click or key except sound/reset button starts. I need avoid clicking sound triggers start due global handler? Add explicit canvas stage listener and global keydown. Stage click calls startUserGame; sound button stops propagation. Start message pointer events none except stage handles. Whole game card perhaps click stage only. User click sidebar Start Shift? Have .primary button "Take the paddle" calls start.
Demo self playing behind start message. `isDemo=true`, overlay .demo-badge visible and start hint. When start, hide start hint after transition, demo badge text "YOU'RE UP" perhaps. Start user resetGame(false). Keyboard keydown anywhere calls start if demo or gameover, then handle control. Prevent default arrows space. Pointer move on stage: if playing and pointer active? use mouse move always move paddle to x. On touch pointermove with pointerdown. Mouse pointer move allows play intuitively. `pointerdown` stage sets pointer active and start if demo. If start from pointerdown set paddle to pointer x after reset. `pointermove` if mode playing update target. Use pointer capture? Canvas stage touch-action:none. Keyboard track left right. AI paddle target = predicted landing x using ball trajectory perhaps simply ball.x plus vx*0.12 with smoothing. Enough to not lose. To guarantee demo action, use paddle.speed sufficiently. AI move at max W*0.95/s, track. Could compute direct smooth. Paddle.x += clamp(target-paddle.x, -aiSpeed*dt, aiSpeed*dt) plus anticipation. If ball.vy>0 target predict x at paddle y: t=(paddle.y-ball.y)/vy; x=ball.x+vx*t; reflect off walls using modulo. Add small imperfection sinusoidal 6 px. If vy<0 target W/2 + sin*W*.1. Demo virtually no miss. If ball gets stuck horizontal enforce min vy.
Bricks reset levels: `buildBricks()` cols = W<480?7:10; rows=W<480?6:5 perhaps mobile narrower and taller use 7x7 =49. margins = W*0.055, gap=clamp(W*.009,4,8), brickW=(W-2m-(cols-1)gap)/cols; brickH=clamp(H*.04,18,28); top=clamp(H*.12,64,90). On desktop H~580 top70, five rows to ~225. Mobile H~429 top64, rows6 to~220. Good.
Bricks colors by row palettes. Each perhaps variant heart icon and small line. Add subtle pattern. Draw rounded rectangle via ctx.roundRect? Latest Chrome yes but could helper roundedRect for compatibility. Use function pathRoundRect.
Background: canvas fill dark; radial gradient at top perhaps `createRadialGradient` each draw expensive but okay cache on resize. Add star/dot grid static positions generated via seeded random arrays and scale on resize? Could draw ~50 tiny dots computed deterministic row columns. Add curved glow shapes. Draw top label "FRESHLY STACKED • 100% LOVE" perhaps canvas small text but HUD enough. At top empty region behind bricks can show faint large heart outlines? Could draw decorative heart outlines in corners.
Particles: hearts array perhaps each {x,y,vx,vy,size,rot,vr,life,maxLife,color,gravity,drag,glow}. On brick break 28 hearts plus 10 spark rectangles. Heart velocity radial with upward bias; positions center random. `for i<count` angle=random 0..2π; speed=random 80..280 relative scale perhaps `H/600`; vx=cos(angle)*speed +...; vy=sin(angle)*speed -90; gravity=420. Life .8-1.5. At 60fps ~300 on screen. Draw alpha `Math.min(1,life/.25)` etc, scale pulsate. Add additive glow perhaps `ctx.shadowBlur=8` expensive if each. Could group by no shadows except 1 in 4. Use globalCompositeOperation='lighter' perhaps colors vibrant but hearts should solid. Draw normal with shadowBlur 0. Big heart initial chunks 6 px. ` Hearts in screenshot obvious.
Shockwaves array ring expands with alpha.
Floating score texts.
Ambient hearts occasionally from brick break yes.
Screen shake apply ctx.translate(shakeX,shakeY) for world. Should background shake? Could apply only objects. Clear base full.
Confetti perhaps CSS no.
HUD DOM: Score with tabular numbers, lives hearts drawn as small svg? Use inline characters `♥` with colors and empty. Level. Combo pill dynamic. Score updates on break. `livesEl.innerHTML` safe generated static hearts. Use text hearts repeated.
Sidebar:
.section-label "TODAY'S SPECIAL"
h2 "Catch feelings.<br>Break everything."
p "Your hamburger has one job: keep the love ball in play."
Divider.
.stat row "Bricks left" value `45`
"Best rally" value `0×`
Love meter .meter fill percent bricks broken perhaps.
Controls .key grid arrow keys and A D; "Move" and "Mouse / touch works too"
Button "Take the paddle" arrow
Perhaps small badge "AUTO DEMO" animated dot.
Footer header with logo and sound button. Sound actual optional. AudioContext only after interaction, create beep on brick/bounce. Initially muted? Button shows sound off; avoid autoplay error. On start enable sound? Better sound default true but audio context only after click, no errors. Button with icon and label. `soundOn=true`, clicking toggles and ensureAudio if enabling. On start ensureAudio; create oscillator/gain transient. No external. Sounds pleasant. Could set default `soundOn=false` and label "Sound off" to avoid unexpected. User can enable. Start game no audio needed. But game polish sound can default true after interaction. Accessibility `aria-label`.
Should have zero errors: AudioContext not defined? window.AudioContext || window.webkitAudioContext guard.
Avoid localStorage etc. Best score in memory only.
Start overlay meaningful: At bottom center `.serve-hint` translucent pill `◉ DEMO PLAYING` separate top left; center card perhaps "Click or press a key to start" with small "The burger is currently on autopilot" in overlay. Don't obscure center gameplay too much. Position bottom: 16px, paddle at bottom behind, might obscure paddle. Better center y60%? Could overlay card in center covering bricks? Should have "behind message". Place `startPrompt` position absolute left50 bottom18 transform; paddle y H-48, prompt height~50 bottom 18 would overlap burger. Place bottom 78px above paddle. Contains `span` "YOU'RE WATCHING THE DEMO" and strong "Click or press a key to start" small. It floats over gameplay, can hide after start. On mobile compact. Demo badge top left perhaps enough.
On gameover show center overlay `.modal` hidden normally with card and restart button; `stage` click restarts. Win all bricks => next level after 1.4sec automatically in playing, show announcement "ORDER UP! Level 2". Demo cycles indefinitely. Should have no hidden features. If no bricks, set levelClear=true and ball active? When last brick broken call `levelComplete`: mode stays playing but `between=true`, freeze ball? set ball.active=false, timer=1.8, show toast; after timer build level++ reset ball, etc. Demo continues.
At initial 3 sec: ball launches up and should break bricks around ~0.4? H 580, paddle y530, bricks top70-220. Launch at y510 vy -390 => to bottom bricks at 220 in 0.75sec, then bounce down. First break at .75, second after return ~2 sec. At screenshot3s should have some particles perhaps particles from second just happened. Many bricks break at 0.75,2.1 etc; particles lifetime max1.6, at3s perhaps from second around2.1 still. Good. Could initial ball launch from paddle toward near bottom bricks with vy -430, and collisions ~0.65. Demo AI hits ~2.1. Third ~3.5. Screenshot3 likely visible particles fading. Ambient emit hearts every brick. To guarantee at first paint something, page visible anyway. Could create opening tiny heart bursts behind overlay? Not necessary but user demands explosions. I can time first collision at 0.5, second 1.7, third2.9. Use speed H*.82 (475) -> distance300 /390 vertical =.77; roundtrip after bounce ~1.5; third ~3.0. At3 second third just explosion. Good. Ball total speed H*.78, angle near vertical, vertical ~0.9 speed. First ~0.75, second ~2.0, third~3.2. Screenshot3 sec sees trail and remaining particles perhaps.
Could start ball at H*.68 (closer bricks) rather than paddle? Demo should natural. Or initial countdown none. Could set `serveTimer=0.25`. Fine.
Demo message can show animated hearts CSS floating near title too.
Perhaps avoid overlay blocking ball; prompt pointer-events none.
Code architecture:
```
<!doctype html><html><head><meta...><style>...</style></head><body>
<div class="ambient ...">
...
<main class=app>
<header>...
<section class=layout>
<div class=game-shell>
<div class=hud>...
<div class=stage id=stage>
<canvas id=game></canvas>
<div class=demo-chip ...>
<div id=startPrompt>...
<div id=announce>
<div id=gameOver class=...>
</div>
<div class=bottom-strip> ...</div>
</div>
<aside>...
</section>
<footer>
</main>
<script>
(()=>{'use strict';
...
})();
</script>
</body></html>
```
Should have `startPrompt` visible on demo. On click `startGame()`. If mode demo reset false; if playing don't reset when clicked. Stage click pointerdown starts only if demo. If gameover reset.
Reset button always start new user game or demo? `resetBtn` text New game; clicking startUserGame.
Take paddle same.
Demo chip updates.
Keyboard if demo any key starts except perhaps Tab? Requirement any key, but don't intercept Tab? I can ignore modifiers and Tab for accessibility. On keydown `if (['ArrowLeft'...].includes) preventDefault`. If mode!playing and key not... start. Space when playing perhaps launch if serve? Ball auto launches no need. P toggles pause perhaps. Could add pause with P, but not necessary. Should have visibility no runaway when hidden? requestAnimationFrame stops. `document.visibilitychange` resets lastTime.
Pause button perhaps top right sound and restart. If add pause, complexity minimal. Not needed.
Game update:
Variables:
```
let W=900,H=600,dpr=1;
let bricks=[], particles=[], waves=[], floaters=[], trails=[];
let ball={x:..., y..., vx,vy,r:7,speed:... active:true};
let paddle={x:..., y..., w...,h...,targetX..., vx:0, squash:0};
let state='demo', score=0, lives=3, level=1, combo=0, bestCombo=0, comboTimer=0, broken=0,totalBricks=0;
let between=false, betweenTimer=0, serveTimer=0, gameoverTimer=0;
```
Ball always active unless serveTimer >0. `serveBall()` sets position above paddle and random angle in demo based sin. On reset `serveTimer=.35`, `ball.active=false`, then update decreases and calls launch. Draw attached ball if inactive at paddle.x,paddle.y-ball.r-5 and perhaps glow.
`launchBall()`:
speed = clamp(H*.76,300,460);
angle = (-0.22? etc) ball.vx=speed*(demo?0.25: random[-.28,.28]); vy=-sqrt(speed²-vx²); active=true; addServeHearts perhaps.
During update if !active due serve, position follows paddle and serveTimer; return physics but update particles.
If between true active false, timer update then nextLevel.
If gameover no physics but particles update; demo autostart resets.
State `gameover` update demo? I can call endGame; if state demo set `demoRestartTimer=1.5`, overlay remains start. Update if gameover? Could keep mode='demo' but gameOver true. Simpler no actual game over in demo likely.
Life loss: hearts explosion at bottom perhaps broken heart particles red, shake. state playing and lives <=0 => state='gameover', show gameOver overlay and startPrompt hidden; demoChip "ORDER COLD". `gameOver` visible with score and button. If demo lives<=0 set `resetGame(true)` after 1.5 via `demoResetTimer`; don't show gameOver overlay perhaps toast "Burnt the buns — restarting demo". Keep state demo with lives reset? `loseBall`: if lives<=0 and demo => lives=3; buildBricks perhaps reset? Could reset after brief but complexity. AI no miss.
`startUserGame`: state='playing'; score0 lives3 level1 best... build; hide prompt/gameover; demo chip text "YOUR SHIFT"; update.
`resetGame(demo)`: state=demo?'demo':'playing'; clear arrays, build bricks, paddle initial; launch after .2; update HUD.
`buildBricks`: random? deterministic pattern rows. all alive true. Each brick hue palette by row; some `accent`. To make beautiful use patterns each brick has small diagonal shine and heart shape. `row` values.
`level` increase colors rotated.
`breakBrick(b)`: b.alive=false; broken++; combo = comboTimer>0?combo+1:1; comboTimer=2.4; best; gained=100*level*Math.min(combo,8); score += gained; heartBurst center b color 26 + combo extras max40; wave; floater `+${gained}` plus if combo>=3 second floater `${combo}× LOVE STREAK`; shake = min(8, 2+combo*.25); updateHUD; audio.
`comboTimer` decreases, if <=0 combo=0 update combo UI perhaps DOM update every frame? update only when expires.
HUD `scoreEl.textContent=String(score).padStart(6,'0')` mobile. `levelEl` pad2. `bricksEl`.
Progress = broken/total initial across level? if rebuild resets broken=0. Love meter. Sidebar caption.
Lives hearts three with `.lost`.
Should have `updateHUD` generates:
```
livesEl.innerHTML = [0,1,2].map(i=>`<span class="${i>=lives?'off':''}">♥</span>`).join('');
```
No security issue static.
Combo chip visible when combo>1; `comboEl.textContent = combo+'×'; comboWrap.classList.toggle('hot',combo>1)`.
Use DOM ids.
Physics with dynamic resizing:
```
function resize() {
const rect=stage.getBoundingClientRect();
const nw=Math.max(1,rect.width), nh=...
old...
canvas.width=Math.round(nw*dpr); ...
W=nw; H=nh;
if(oldW && bricks.length) {sx... each...}
paddle.w=paddleWidth(); paddle.h=...; paddle.y=H-Math.max(38,H*.075); paddle.x*=sx;...
ball.x*=sx...
makeBackground perhaps.
}
```
Since canvas is absolute inset 0 width100% height100%. Stage aspect ratio ensures rect. `getBoundingClientRect` includes no border? Stage border? canvas width fill content box. Use stage.clientWidth/clientHeight avoids border. Stage border on game-shell not stage. Fine.
Scaling on first init if paddle undefined? Initialize object with x0 etc then check `initialized`.
`ResizeObserver(resizeCanvas).observe(stage)` plus window resize fallback. If callback after reset with same dims no issue. `sx=nw/oldW`.
On resize particles scale etc; brick build layout remains scaled gap perhaps slightly off but okay; fully rebuild? Would resurrect bricks undesirable. Scale bricks enough.
Use DPR min(devicePixelRatio,2).
World draw:
```
ctx.setTransform(dpr,0,0,dpr,0,0);
drawBackground();
ctx.save();
if(shake>0) ctx.translate(...);
drawWaves behind?
drawTrails();
drawBricks();
drawParticles perhaps behind paddle? hearts should above all.
drawPaddle();
drawBall();
drawParticles();
drawFloaters();
ctx.restore();
```
Background gradient created each draw based W,H; can cache `bgGradient` on resize since ctx gradient coordinates okay. `bgGradient = ctx.createLinearGradient(0,0,W,H)` after setTransform. Canvas width reset resets context state. Draw:
fill bg #17121f
radial gradient top center with #47213b66 to transparent
dots grid: for y=20;y<H;y+=28 for x... perhaps ~700 dots desktop, okay. Use 40 small diamonds not all. CSS background pattern could be on stage and canvas transparent? Canvas fill. ~500 fillRect okay.
Arc side lines.
Draw top small "LOVE LANE" at y28 perhaps bricks top70, include decorative side lines and `LEVEL ${level}` but HUD. Could draw center faint text `MAKE A MESS. MAKE IT CUTE.` y36 with letter spacing not supported? `ctx.letterSpacing` newer not needed. Use font `600 9px system-ui`, color.
Walls glow on bounce? simple.
Bricks draw each with `ctx.shadowColor=b.color; shadowBlur=8` 45 bricks okay. Rounded rect fill linear gradient per brick expensive create each; can solid fill + top strip. Use fill color; overlay gradient global? `ctx.fillStyle=b.color;` then `ctx.globalAlpha .18 fillRect`; top highlight line. `drawHeart` center small with rgba(255,255,255,.35). Use cracks? no.
Heart stamp on every brick perhaps user sees hearts before explosion. Small 4 px.
Special burger-related brick patterns? Could be "bun", "tomato", "lettuce", "cheese" rows with ingredient palette. Bricks in rows: top pink, second orange, third yellow, fourth green, fifth purple. Each row label not needed. Colors array: '#ff6f91', '#ff9e7a', '#ffd166', '#8ee0b0', '#a997ff', '#70c7e8'. Dark enough text? white hearts.
Could add tiny highlight circles.
Paddle detailed:
Coordinates center x, y top, w, h ~42. bottom bun at y+h*.67 height h*.28 rounded; patty at y+h*.52 h*.21 dark #61362c with highlights; cheese at y+h*.43 polygon with drips; lettuce y+h*.34 wavy line filled green; top bun from y to y+h*.4, shape rounded dome via bezier:
```
moveTo(x-w/2+4,y+h*.36)
bezier(x-w/2+5,y+3,x-w*.25,y, x,y)
bezier(x+w*.25,y,x+w/2-5,y+3,x+w/2-4,y+h*.36)
quadratic... close
```
fill gradient #ffd985 to #eaa45e. sesame seeds ellipses rotated. bottom bun gradient.
`paddle.squash` modifies scale y 1 - .15*... and x1+.08; draw around center.
paddle collision surface should y+? Top bun dome at y, so ball bounces at y. okay.
Ball draw trail: maintain array each update step? Push per frame when active; trail objects x,y,life; update life; draw circles fading pink. Ball gradient radial each frame, outer glow `shadowBlur=18`.
Perhaps ball itself small heart? Request hearts explosions, ball could be a glossy heart, but collisions circular. A white ball with small pink heart icon stamped center looks cute. Draw circle cream and tiny heart pink size3.5. Makes game coherent. Trail circles.
Hearts particles on paddle bounce small 5 pink.
Sound: function `tone(freq,duration,type='sine',volume=.03)` uses audioCtx currentTime. On brick tone freq=320+combo*20 etc plus secondary. But if lots break no issues one per. Audio default false? Could create satisfying. Set sound=true and start from user interaction, no auto audio. In demo audio context null so `tone` returns. On start `ensureAudio`, so user gets audio. Sound button label "Sound on" initially though no sound in demo until interaction; okay. Use aria-pressed true.
No prompt etc.
Accessibility: buttons, canvas aria-label, role img? Canvas interactive with tabindex=0 perhaps stage `tabindex=0` to receive keys after click. Global keydown works after iframe click. Add stage focus outline none perhaps. `canvas aria-label="Breakout game..."`.
`startPrompt` includes kbd "← →" etc.
Mobile control: touch drag anywhere on stage; also add two on-screen large left/right buttons below stage only on narrow perhaps `.mobile-controls` with pointer events. But pointer drag sufficient and hint says drag. To ensure mobile play, onscreen controls nice. Bottom strip desktop displays controls. I can include two .nudge buttons at left/right? Might clutter. Use touch stage. On mobile the prompt says "Tap to start • drag to move" via CSS text perhaps adapt JS? Not needed. Main instruction "Click or press a key to start" required exact, include.
Game title perhaps "PATTY<br><span>& LOVE</span>"? Request intuitive. "BURGER BREAKOUT" strong.
Aesthetic composition:
Header logo .brand-mark CSS burger mini using layers not emoji to avoid inconsistent. Could inline SVG logo? Allowed inline all. Use `<div class=logo>🍔</div>` emoji charming. But canvas hamburger custom. Brand text uppercase with wide tracking. Header right little `MADE WITH <span>♥</span> & MUSTARD`.
Sidebar order ticket style cream? Contrast: an off-white card "SHIFT NOTES" with dark text might stand out. Could make side panel dark with one coral gradient card. Main card #1e1727. Sidebar `.order-card` background #fff3dd color #2a1a25; transform rotate(1deg) perhaps. Header "ORDER #001" dashed border and huge "LOVE<br>EXTRA" etc. But all panels dark more game focused. Side top card gradient pink/orange with title "A recipe for destruction." nice. Right panel can be a receipt styled cream with controls below. User only game, design novel.
Perhaps entire game looks retro diner with neon. Background checkered subtly. Header "PATTY'S" neon sign, game title "BURGER BREAKOUT". Colors.
I'll settle polished arcade diner:
body dark plum. App header logo a tiny burger drawn with CSS? Use emoji in glowing tile. Brand:
small eyebrow "PATTY'S ARCADE PRESENTS"
h1 "BURGER <span>BREAKOUT</span>"
right `♥ 100% GOOD VIBES`.
HUD integrated at top game card: Score / Level / Lives and combo.
Stage canvas.
Below .statusbar `● AUTO DEMO` and `"Break bricks. Make hearts. Don't drop the ball."`.
Right `ticket` cream:
header "TODAY'S SPECIAL"
h2 "Heartstorm"
desc "Every brick bursts into a full-flavored flurry of hearts."
divider dashed
progress "ORDER PROGRESS" `0/45`
meter
stat rows
controls dark lower card with keys and take paddle button.
Could fit 600 height. ticket ~330, controls~200. Good.
At mobile aside display grid 2 columns? width343, could hide long desc or arrange. Main first screen game. .layout at max800 grid one column; aside grid two columns at min520; at max520 one col but ticket compact. Avoid page huge not issue.
Footer optional small.
Should game state announcements visually. `#toast` top center under demo chip, opacity via class. Show text set with timeout? Use DOM transition; timeouts okay. I should not use network etc.
`announce(text, sub)` set `toast.innerHTML` static generated from known strings; class show; clearTimeout. Could use textContent and child spans.
Gameover card:
```
<div class=game-over id=gameOver>
<div>Ticket closed</div><h2>One more<br>order?</h2><p>Score ...</p><button id=again>Fire it up</button>
</div>
```
Only hidden default. On gameover stage no demo prompt; restart button listener stopPropagation start. Start message initial stays while demo.
Potential issue clicks on overlay start prompt pointer-events none, stage pointerdown. Good.
AI and player:
`keys={left:false,right:false}`.
`updatePaddle(dt)`:
if demo AI as above.
else:
keyboard if nonzero target = paddle.x + dir*... use direct velocity; pointerTarget perhaps if pointer recently. I can track `controlMode` 'pointer' when pointer move, 'keys' on key. For keys:
```
let dir=(right?1:0)-(left?1:0)
if dir: paddle.x += dir*speed*dt; paddle.target=paddle.x
else if pointerActive? paddle.x += (target-paddle.x)*min(1,dt*18)
```
Pointer `mousemove` always set `pointerTarget`, `usingPointer=true`; if no pointer movement and keydown sets false.
Touch pointerdown sets target. `stage.setPointerCapture` perhaps if touch; no need. `pointermove`: if event.pointerType==='mouse'||pointerDown update; convert clientX to W. On desktop move even not pressed. Start on pointerdown resets before coordinates? Handler:
```
function handlePointerDown(e){
if(state!=='playing') startUserGame();
pointerDown=true; usingPointer=true; setTarget(e);
}
```
If clicking Restart inside stage stopPropagation.
Start on mouse movement? no only click/key.
On pointerup pointerDown false.
No external error from setPointerCapture avoid.
Keyboard keys get stuck if window blur reset.
Demo physics `ball.speed` etc. On every paddle bounce speed = min(speed*1.015, H*.95). Mobile H 429 speed~330. Should speed in resize recalc. `currentSpeed()` base clamp(H*.78,290,450) + (level-1)*18. Use set vector on bounce retaining normalized and cap. On brick perhaps speed slight. `ball.vx/vy` initial.
When bounce walls use sound if audio.
Brick collision tunneling if dt frame cap .033 speed450 step15, brick height20, radius7 okay. But at low FPS could skip if movement >20? dt cap .033 means15 < h+2r34 so fine. Paddle collision similar.
World bounds: Paddle y H - max(40,H*.075) e.g. H600 y555? Burger h40, bottom595. good. Ball loss y>H+r. HUD outside canvas no issue.
Bricks layout top 60, last bottom ~250. Score float text visible.
Collision with bricks after top wall etc. Ball can hit multiple only first, break. When `b.alive=false`, loop break. Should determine direction correctly:
```
const overlapLeft = ball.x + r - b.x;
...
const minOverlap = Math.min(...)
if min===overlapTop? etc
```
Simpler previous coords:
```
if (prevY + r <= b.y) vy=-abs;
else if(prevY -r >= b.y+b.h) vy=abs;
else if(prevX+r<=b.x) vx=-abs;
else if(prevX-r>=b.x+b.w) vx=abs;
else {
const left=Math.abs(ball.x-(b.x-r))... choose
}
```
Ball movement updated before collision. After bounce reposition slightly to corresponding boundary to prevent sticking:
top: ball.y=b.y-r-0.5; bottom: b.y+b.h+r+.5; left x=b.x-r; right...
When collision from inside corner choose min penetration.
Loop `for (const b of bricks)` with alive and check, break after handle. After break all? `bricksLeft===0` between.
Speed and collision score.
On paddle hit ensure x within expanded and vy>0. Paddle x is center.
`prevY+ball.r <= paddle.y+8` to avoid hitting from below. If ball below top but intersects side, perhaps bounce anyway if y<paddle.y+20. Standard.
AI target reflect:
```
function predictX() {
let t=(paddle.y-ball.y)/ball.vy;
let x=ball.x+ball.vx*t;
const min=ball.r,max=W-ball.r,span=max-min;
let z=(x-min)%(2*span); if(z<0)z+=2*span;
if(z>span)z=2*span-z;
return min+z;
}
```
if t<0 target center. Add `Math.sin(time*2.1)*Math.min(4,W*.006)`.
AI speed max W*1.25 perhaps catch all. Track with exponential `paddle.x += clamp(diff,-max,max)`. Paddle no inertia.
Player pointer smoothing, direct clamp.
Level complete:
```
function finishLevel() {
between=true; ball.active=false; betweenTimer=1.8;
announce('ORDER UP!', `Level ${level} cleared...`);
for 5 bursts around ...
}
update if between:
betweenTimer-=dt;
if<=0 {level++; lives=Math.min(3,lives+1?) ; buildBricks(); combo=0; broken=0; serveTimer=.6; announce...}
```
But ball.active false and serveTimer 0; drawBall code if !active && !between? no. Fine.
Hearts from last brick plus extra celebratory every .? call `celebrate` spawn 80 across perhaps tons. No hidden.
Demo start after all bricks automatically same.
Gameover update particles still, no paddle? Draw yes.
Demo prompt initial animation pulsing but reduced motion respect. CSS `@media (prefers-reduced-motion: reduce)` turn animations off; particle still game motion necessary no issue.
HeartBurst count "tons" 30 per brick plus mini particles. Could use 34 plus combo. At 45 bricks max particles perhaps cap 700; remove old when >700. `spawnHearts` colors [b.color, '#ff5d8f','#ffb3c8','#fff1dc'] 34; each life1.1-1.9. `particles` update gravity etc. They fall, fade. Some heart confetti flutter with `p.vx += Math.sin(...)*...` perhaps.
`particles.push({... spin... phase...})`.
Draw hearts with alpha and perhaps white stroke for larger. Use `ctx.globalAlpha` reset. If using shadowBlur set after each? draw no shadows. `ctx.fillStyle`; rotate.
Sparks as `sparkles` same array kind:'spark', draw small circles or lines. Every break 12 tiny squares. User sees mainly hearts.
Floating text with life .9, y rises.
Shockwaves 0.4sec.
Initial heart bursts perhaps at `resetGame` create 12 ambient around paddle? Better no artificial brick explosion without break. Could `launchBall` spawn 8 small hearts from ball as garnish. Fine.
Performance DPR2 canvas ~1800x1200 60fps, 45 bricks + 400 particles okay. Shadow blurs bricks expensive if 45 each but okay. Use no shadow each brick except global once? Canvas shadowBlur each. Fine.
Make sure first screenshot no font loading external; system fonts. Could use `font-family: Inter, ui-sans-serif...` Inter not loaded falls back. Better `'Trebuchet MS'` for display? System. Use `"Arial Rounded MT Bold", "Trebuchet MS", sans-serif` to get playful. Monospace `ui-monospace`.
No CDN needed.
I should include meta viewport and color-scheme. No scroll horizontal. Body min-height:100%; overflow-x:hidden. At desktop body perhaps overflow hidden? Keep vertical if needed. App width etc.
CSS precise:
```
:root { ... }
* {box-sizing:border-box}
body {margin:0; min-height:100vh; background...; color...;}
body::before {content:""; position:fixed; inset:0; background-image:radial-gradient(...); pointer-events:none;}
.orb ...
.app{width:min(1180px,calc(100% - 32px)); margin:0 auto; padding:18px 0 14px; position:relative;}
.topbar-main{height...}
.brand...
.layout{display:grid;grid-template-columns:minmax(0,1fr) 250px;gap:16px;align-items:start;}
.game-card{background...; border...; border-radius:22px; overflow:hidden; box-shadow...}
.hud{height:70px; display:grid;grid-template-columns:1.2fr .7fr 1fr auto;...}
.hud-item borders ...
.stage{position:relative;width:100%;aspect-ratio:3/2;max-height:calc(100vh - 200px);}
```
Max-height with aspect ratio and width can distort because canvas absolute fills actual max-height causing ratio changed when capped; W/H dynamic handles no issue. But CSS aspect-ratio with max-height may reduce height without width? Element width 100%, height derived then max-height clamps -> wider ratio. At 800 height calc600, left900 ratio1.5 exactly. At shorter okay dynamic. At mobile don't max-height perhaps `max-height:none`.
Could avoid max-height to ensure no strange. At 1280 board left ~896, h597. top header ~55+16, hud64 + status40 + app vertical 32 =>804? Calculate app padding top16 bottom12, header perhaps48 margin16 =>64, main game card hud64+stage597+status40=701, total 16+48+14+701+12=791. At 1280x800 barely fits. Side aside same. Fine. Header title h1 24 height29 plus eyebrow9+4 =>42. logo42. header48. margin14. stage left: app width min1180, grid side250+gap16 =>914 left? 1180-266=914. stage609.3. Total16+48+14+64+609+39+12=802. Should reduce max app width to1140 -> left874 stage583; total776. At 1280 width calc-32=1248, max1140. Great. Canvas 874x583. right250. total ~776. Use app max-width:1140. Good.
At viewport 1280 looks central with margins70. Could use 1160 -> left894 h596 total789. Fine. max1160. At 800 exactly. Header perhaps 48, top14, etc. Use stage aspect-ratio 1.55 instead of1.5 -> 577, total770. Bricks fine. Logical dynamic. `aspect-ratio: 8/5` =1.6, left894 h559; total~745. Great. Canvas wider 894x559. Classic Breakout. Use 3/2? 8/5 improves. At mobile 4/5.
Game card status 38. HUD64. Header 48 +14 + stage559 +102 + padding26 =749. Good.
Side panel height perhaps597 fits.
Main `stage` aspect-ratio: 8/5.
At width900 media layout side220, left perhaps... At max900 switch aside below? Breakpoint900. At viewport 1024 app992, left726 (side250) stage454, overall fine. At 900 app868, left602 stage376; perhaps too small but switch at920 to single gives stage543 if aspect8/5; good. Breakpoint900? Use 860. At 768 one column width736 stage460; fine. Right aside grid two cards. At mobile max560 stage aspect4/5 height~429. Total first screen header50+hud58+stage429+status36+padding=600; right below. good.
HUD on mobile score 6 digits width; grid columns 1.2fr .6fr 1fr, combo absolute? Use four cells score level lives and combo. At width343, each~80, labels small. Score font20. okay. Combo perhaps hide label. CSS `.hud-combo` display etc.
Desktop side 250. Scoreboard.
`header .header-note` hide under600.
Buttons icon accessible.
Ticket:
```
<aside class=side>
<section class=ticket>
<div class=ticket-top><span>ORDER #001</span><span>♥</span></div>
<h2>HEART-<br>STORM</h2>
<p>... </p>
<div class=ticket-rule></div>
...
</section>
<section class=controls-card>
...
<button id=playButton>Take the paddle <span>↗</span></button>
...
</section>
</aside>
```
Cream ticket with serrated bottom via radial gradient? Could use clip-path polygon zigzag? Perhaps border-radius18 and ::after small perforated semicircles not necessary. Use dashed lines and slight rotate? Avoid playful tilt causing horizontal overflow; no.
Ticket h2 font40 line. Right panel 250 wide. Colors.
Controls card dark border.
Button coral, hover.
Keyboard keys caps. `span.key` etc.
Game start prompt styling dark translucent with backdrop-filter perhaps allowed. `backdrop-filter: blur(12px)`.
```
.start-prompt {position:absolute;left:50%;bottom:70px;transform...;background:rgba(23,15,30,.82);border...;padding:10px 18px;border-radius:16px;text-align:center;box-shadow;pointer-events:none;transition}
.start-prompt .eyebrow ...
.start-prompt strong {font-size14}
.start-prompt .play-dot {...}
```
At mobile bottom68 and width max calc100%-24.
Demo chip top12 left12, font9 uppercase. Toast top12 center.
Combo float chip in HUD pulses.
`gameOver` full stage background rgba and display:grid; hidden with `.hidden` perhaps `opacity:0;visibility:hidden;pointer-events:none`. Card. Button pointer events auto when visible.
No `alert`.
Footer below main small perhaps not needed. Could add `<div class=footnote><span>NO BRICKS WERE HARMED...` humor but bricks harmed. "FRESH OFF THE GRILL • MADE WITH ♥" small. Main already enough. Keep footer only desktop, mobile below aside.
Need update playButton text while playing "Restart shift ↻" but clicking resets. On gameover "Try again ↗". Demo "Take the paddle ↗".
`newGameBtn` header icon ↻ title Restart.
Sound button uses Unicode speaker may render. Inline SVG icons more polished:
```
<svg viewBox...><path...>
```
But changing label on/off requires paths. Use text `SOUND ON` with small equalizer CSS? Simple button `<span id=soundIcon>♪</span><span id=soundText>Sound on</span>`.
Header restart `↻` symbol.
Ensure first click sound button doesn't start game because listener only stage, global keydown perhaps not click. Good.
Global click not used. "Click ... to start" refers click game stage; take paddle works.
Potential user click game at demo triggers start and pointer sets target. start resets `score`, overlay hidden. Good.
Keyboard press sound? any key start, including mouse? Fine.
Demo mode prompt visible always. Demo score etc changes. On first load `updateHUD`.
AI gets score and hearts. When demo starts new level prompt remains. On any key including arrow start and then `keys` set; keydown order:
```
if relevant preventDefault
if state!=='playing' {startUserGame();}
if ArrowLeft keys.left=true ...
if key p ...
```
Gameover arrow restart okay.
If pressing sound button space triggers click and global keydown start perhaps annoying; ignore if e.target.closest('button') for space? I can:
```
if ((e.code==='Space'||e.code==='Enter') && e.target instanceof HTMLButtonElement) return;
```
But requirement any key start perhaps no need. Accessibility. Use if target button return.
`e.repeat` no reset if state playing.
Reset keys on start? if keydown start and then set key after reset. `resetGame` sets keys false? Don't reset in start, or order set after. Fine.
Pause not included.
Game loop:
```
let last=performance.now(), elapsed=0;
function frame(now){
const dt=Math.min((now-last)/1000,.033); last=now; elapsed+=dt;
if(state!=='gameover') update(dt);
else updateEffects(dt);
render();
requestAnimationFrame(frame);
}
```
When state demo/playing and between etc.
`updateEffects` always particles and floaters, trails.
`update(dt)`:
- paddle.squash decays
- shake decays
- if combo active decrement only playing/demo not between perhaps.
- updatePaddle
- if between update timer and no ball
- else if serveTimer >0 decrement, position ball to paddle and if <=0 launch
- else if ball.active updateBall
- updateEffects
- update demo restart if...
Could updateEffects once. For gameover updateEffects and paddle squash perhaps.
`updateBall(dt)` trail push before move perhaps each frame. `trail.unshift?` push and filter.
Store prev each step. Use one step but dt. `ball.x += vx*dt`.
Wall bounce:
left if x-r<0 set x=r vx=abs
right...
top if y-r<0 set y=r vy=abs; tiny particles.
Paddle after bricks? Paddle near bottom no overlap bricks. Do paddle before bricks perhaps either.
Bottom if y-r>H -> lose.
Trail push `{x,y,life:.28,max:.28,r}` and cap20. Update in effects each frame, no double.
If ball speed after wall etc not changed.
`speed` property update after paddle:
```
let speed=Math.min(Math.hypot(vx,vy)*1.015,maxSpeed);
...
```
After brick no need.
On paddle:
`const english = (ball.x-paddle.x)/(paddle.w/2); const angle=english*1.05; speed...; vx=Math.sin(angle)*speed + paddleVelocity*0.08 perhaps can exceed; vy=-Math.abs(Math.cos(angle)*speed); normalize afterwards.` Ensure angle limited 1.08 rad. Add paddle velocity may result angle too horizontal; skip or tiny.
`paddle.prevX` compute velocity in updatePaddle.
AI max direct.
`minimum vertical`: after brick bounce if abs(vy)<speed*.35 set vy sign*.35 speed and adjust vx preserving. Initial vx angle max. Paddle angle max1.08 => cos.47, good.
Brick side collision can leave vy same fine.
When resize ball speed recalibrate:
```
if(oldW && ball){ ball.x*=sx;...; let angle...; let sp=clamp(Math.hypot(vx*sx,vy*sy),...); vx*=sx;vy*=sy; }
```
Could weird but rare.
Particles update:
```
p.life-=dt; p.vy+=p.gravity*dt; p.vx*=Math.pow(.985,dt*60); p.x+=...; p.rot+=...;
```
Remove by reverse loops. Bounce hearts off floor? Cute: if p.y>H+20 remove, no bounce. Some settle no.
Max particles if >800 splice(0,len-800). On every burst 40 and collisions every1.5, no huge.
`waves` ring update radius +=250dt.
Floaters.
Render background each frame create gradients? Cache.
`makeBackground` after resize:
```
bgGradient=ctx.createLinearGradient...
glowGradient=ctx.createRadialGradient(W*.5,H*.25,0,...)
```
Setting canvas width resets. Then resize set ctx.setTransform.
Draw uses current transform set each frame. Cache gradient remains valid after setting canvas width yes created after.
`render`:
```
ctx.setTransform(dpr,0,0,dpr,0,0);
ctx.globalAlpha=1;...
ctx.fillStyle=bgGradient; fillRect...
ctx.fillStyle=glowGradient...
draw dots loops perhaps `backgroundDots` computed in resize as normalized random deterministic. Use fixed deterministic formula:
for(let i=0;i<55;i++){let x=((i*83)%997)/997*W; let y=...; ...}
```
Avoid Math.random each render flicker. Create dots once array normalized at script start. Twinkle based elapsed subtle.
grid lines horizontal every44 with alpha .02 perhaps.
Large faint heart outlines at sides using drawHeart with stroke. `drawHeartPath(x,y,size,rot)` function includes begin path. For stroke background.
Top phrase center `FRESHLY STACKED • EXTRA LOVE` at y30 but bricks top min62; safe. On mobile bricks top55 and phrase at22.
Waves behind bricks perhaps expanding ring color white.
`drawBricks` only alive. Use scale bump on collision? bricks removed instantly. Add b.wobble? not needed.
`drawHearts` after paddle/ball means particles cover.
`drawPaddle` under particles but above bricks.
`drawFloaters` last.
Gameover overlay DOM hides canvas partly but okay. Demo no gameover.
Sounds on interactions:
`ensureAudio()` catches? Creating AudioContext shouldn't throw normally. `audioCtx.resume()` returns promise; calling .catch(()=>{}) to avoid unhandled.
`tone` if soundOn && audioCtx && state==='playing' perhaps only user game, so demo silent even after audio started if return to demo never. Fine.
On start soundOn true -> audio starts. `tone(220,.08,'triangle',.04)` perhaps.
Tone:
```
const o=ctx.createOscillator(),g=ctx.createGain();
o.type=type;o.frequency.setValueAtTime(freq,t); ...
g.gain.setValueAtTime(0.0001,t);
g.gain.exponentialRampToValueAtTime(volume,t+.01);
g.gain.exponentialRampToValueAtTime(.0001,t+duration);
o.connect(g).connect(ctx.destination);
o.start(t);o.stop(t+duration+.02);
```
No leak.
Button sound toggles no audio if turned off; if on ensure and tone.
`audioCtx` unsupported sound button still toggle but no errors.
Use state? `tone` function parameter; no issue.
Collision audio frequency based row.
Hearts for life representation use ♥ text, okay.
Progress "0 / 45" etc. total bricks. Ticket static special no dynamic label? `progressLabel`.
Best rally bestCombo.
Ball served status status text perhaps:
`statusText` "AUTO CHEF IS PLAYING" demo and "YOUR SHIFT • BREAK 'EM ALL" playing. Start chip also. Update.
Perhaps not overdo DOM.
Game logic test mentally at initial:
script executes resizeCanvas with W=rect width 0? Canvas stage rect aspect ratio known; yes. `canvas.width` etc; `initialized` false; paddle initial default values. Then resetGame(true) sets paddle.x=W/2 etc buildBricks. state demo. loop. startPrompt visible CSS. AI update with ball.active=false serveTimer=.25: updatePaddle sees ball.vy perhaps initial -... or 0? reset sets vx0 vy0 activefalse. updatePaddle if demo `if(ball.active&&vy>0)` else center+sin. serve update ball.x=paddle.x etc. After .25 launch. Ball up. Bricks top64 etc. Collision.
When no key, demo.
At 3 sec multiple breaks. Fine.
Start click `startUserGame()` resets arrays removing existing particles, then ball launch .15; user sees. okay.
Click start calls ensureAudio -> Chrome allowed pointerdown. I call in pointerdown, yes.
Play button click ensureAudio in click allowed.
Keydown start ensureAudio allowed.
Screen resize while state pre-init? ResizeObserver callback may run right after reset, old W=same, scaling sx1 no issue. Canvas resetting width clears but render next.
`resizeCanvas` if rect width/height 0 (page hidden) return to avoid division.
DPR perhaps changes only resize.
Bricks scale using `b.x*=sx` etc, but color stays. `b.row` etc.
Paddle w set based new W not scale.
Particles scale r etc average scale.
Background dots normalized.
CSS responsive no horizontal scrollbar. `min-width:0` on grid. body decorations fixed with transform could cause overflow? .orb position fixed border-radius and width, negative left might contribute? fixed usually no scroll but set overflow-x:hidden. okay.
Ticket h2 word Heartstorm no break? font36, width200 ~190, okay. Use `<h2>HEART-<br>STORM</h2>`.
Mobile brand eyebrow perhaps h1 fits 343: text 25 "BURGER BREAKOUT" 16 chars ~245, logo42 + gap10=297, header buttons ~80 -> total377 too wide. Should header responsive: header left brand and actions. h1 21, text ~200 + logo42=252, actions sound only icon+restart 38*2=76 total338+ gaps fits 343 exactly perhaps. Sound text hidden under600. At 375 app343, header width343; brand logo38 + gap9 + text (eyebrow + h1 19) h1 perhaps 179 =>226; actions 72 +gap =307 good. Header note hidden. h1 uses `BURGER <span>BREAKOUT</span>` perhaps no wrap white-space:nowrap. At 375 19px ~190. Good.
At mobile .brand-eyebrow display none perhaps. h1 font20. Logo38. actions button34. Fit.
Desktop header note in middle? Header `.header-inner` flex justify-between, brand left, header-note next, actions right. max. At under900 header-note hidden.
App width calc100%-24 mobile. Use 32 default then media20? At375 width calc100%-24=351. Nice. `app width:min(1160px,calc(100% - 32px))`; media max560 width calc100%-20 =355. Stage355x444.
HUD padding 0 12.
Mobile prompt strong13, width.
Aside mobile perhaps ticket + controls stacked leading long but okay. Could at max860 aside grid two equal columns and ticket h2 smaller. At max560 one column? This makes long page. Could keep 2 columns at375 each~172 too narrow; stack. Hide ticket description? I can .ticket {padding18}, h2 32. Fine.
Need perhaps explicit touch controls: Stage pointer with `touch-action:none` blocks scrolling if touch on game, intended. Outside page can scroll. On mobile 444 high, drag works.
At mobile startPrompt exact "Click or press a key to start" still. Add bottom status `Drag to move` CSS perhaps.
No explicit high score persistence because prohibited storage. Best rally session memory.
No external requests.
Potential use `innerHTML` with icons safe. Fine.
No errors if canvas ctx null? Browser supports; no need.
Path2D not use. Canvas roundRect helper:
```
function roundedRectPath(c,x,y,w,h,r){
r=Math.min(r,w/2,h/2); c.beginPath(); c.moveTo...
}
```
Heart path:
```
function heartPath(c,x,y,s,rot=0){
c.save();c.translate...;c.rotate...;c.scale(s,s);
c.beginPath();
c.moveTo(0,.85);
c.bezierCurveTo(-1.35,.05,-1.02,-1.12,-.35,-1.03);
c.bezierCurveTo(-.12,-1,0,-.78,0,-.62);
c.bezierCurveTo(0,-.78,.12,-1,.35,-1.03);
c.bezierCurveTo(1.02,-1.12,1.35,.05,0,.85);
c.closePath();
...
```
This makes central notch at -.62 and lobes. Standard shape better:
M0,0.9
C -1.6,-0.1 -1.0,-1.5 0,-0.75
C 1.0,-1.5 1.6,-0.1 0,0.9
But at notch both curves meet 0,-.75, lobes extremes around etc. Looks heart. Use:
M0,0.9
C-0.25,0.55 -1.25,0.0 -1.25,-0.55
C-1.25,-1.25 -0.35,-1.45 0,-0.75
C0.35,-1.45 1.25,-1.25 1.25,-0.55
C1.25,0 0.25,0.55 0,0.9
Good. `s` corresponds halfwidth1.25s, height1.65s. Particle size3-7 => width8-17. Tons visible.
Brick icon size3.2 width8.
Background huge outline size35.
Paddle tiny sesame seeds.
`drawHeart(c,...)` can fill or stroke:
```
function traceHeart(ctx,s) {...}
function paintHeart(x,y,s,rot,color,alpha,stroke=false) {save... trace... fill...restore}
```
In particles calling save per particle (300) okay. Optimize path transform manual? fine.
`drawHeartShape` does ctx.beginPath etc after transforms.
Particles drawn with `globalCompositeOperation='lighter'` perhaps lighter makes white hearts too pale. Use source-over. Shadow for only larger:
```
if(p.size>5){ctx.shadowColor=color;ctx.shadowBlur=6;} else ...
```
save/restore includes shadow. Could be expensive. Set no shadow and draw a low-alpha larger heart behind every 4th. Fine:
```
if(p.glow){ctx.globalAlpha=alpha*.16; paint size*1.9}
...
```
Doubles some.
No shadows all bricks? Game aesthetic via gradients.
Paddle draw top bun gradient each frame `createLinearGradient` 2 okay.
`ctx.ellipse` sesame.
Burger w on mobile clamp(W*.17,82,132) at W355 -> max(82,60)=82. Bricks7 width~40, ball6.5; good. Desktop W894 .15=134. Good.
Paddle y H - Math.max(38,H*.07): H559 -39=520, h40, bottom560 exactly if y+h=560. Burger bottom perhaps h40 and y520 ->560, touching bottom. Ball loss belowH. Fine. At mobile H444 y=H-38=406, h36 =>442. good. Prompt bottom 60 means around y326-384, above paddle.
Paddle h `clamp(W*.045,32,42)` desktop40 mobile32.
Bottom bun perhaps at y+h*.73 h*.23, bottom .96h leaving. Great.
Serve ball at paddle.y - r - 3.
Hearts bounce from paddle originate y.
Lives loss in user state:
```
function loseBall(){
lives--; updateHUD(); shake=10; spawnHearts(ball.x,H-20,'#ff6b...',18);
if(lives<=0){state='gameover';ball.active=false; gameOver...;}
else {serveTimer=.8;ball.active=false;combo=0; ...}
}
```
The spawn hearts on loss perhaps implies broken heart? All little hearts okay.
Demo if miss and lives zero: `lives=3; level=1; buildBricks(); score=0?` Could call resetGame(true) immediately after perhaps hearts lost; use `demoResetTimer=1`. Simpler in loseBall:
```
if(lives<=0){
if(state==='demo'){score=0;level=1;lives=3;buildBricks();broken=0;serveTimer=1;...}
else ...
}
```
No overlay, demo continues. Good.
Ball active false on life loss. `buildBricks` resets arrays? no only bricks and totals. updateHUD.
User gameover best etc. gameOverScore text.
`startPrompt` hidden via `.is-hidden`.
Demo prompt `classList.toggle('hidden',state!=='demo')`.
`setStateUI` in reset and start/end.
`demoChip` should say demo initially. `statusText`.
`playButton` etc.
`demoChip` inside stage at top left, and start prompt eyebrow repeats demo. Fine.
Announcement toast perhaps hidden initially. `toast` `aria-live=polite`.
GameOver overlay hidden by class. Buttons.
`announce` uses timeout; on reset clear timeout and hide to avoid stale. `toastTimer`.
`showToast(title,subtitle)`.
After last brick `between=true; showToast("ORDER UP!",...); for i 0..4 spawnHearts(random positions...)`. But spawnHearts expects color. 5*18 =90 plus 35. Festive.
After between timer nextLevel showToast(`LEVEL ${level}`, "Fresh bricks. Same hungry burger.");` timeout 1.2.
No long playing required all visible.
Colors hearts source: break color + pink + cream. Some hearts color brick green/yellow means rainbow hearts. Nice.
Brick burst count `30 + Math.min(combo,6)*2` =32 first. "tons". 12 sparks. Good.
Add score animations CSS when update? `scoreEl.classList.remove('bump'); void scoreEl.offsetWidth;` causes reflow every break only okay. Could avoid.
`comboBadge.classList.toggle('show',combo>1)`.
`loveFill.style.width=(broken/total*100)+'%'`.
Ticket meter on cream track pink.
Control arrow key icons. CSS keys.
Visual first paint before JS? HTML game stage canvas blank until loop but script immediate. Header/panels visible. Demo starts.
Do I need a high score? no.
Could add reduced particle when prefer reduced? user explicitly tons hearts, no.
No long explanations in response, exactly code block.
Need write full code perhaps ~1000 lines? Desired code robust around700 lines. I have token budget. Ensure no mistakes.
I'll detail CSS.
Body background radial:
```
background:
radial-gradient(circle at 12% 0%,rgba(255,103,141,.12),transparent 30rem),
radial-gradient(circle at 95% 80%,rgba(255,193,99,.08),transparent 28rem),
#110d18;
```
Pseudo checker:
```
body::before { background-image: linear-gradient(rgba...1px,transparent1px),...; background-size:48px 48px; mask-image:linear-gradient(...); opacity:.12}
```
Mask perhaps okay. Could use no mask. Fixed pointer.
Ambient .glow-orb blurred.
Buttons reset styles.
App z-index.
Header:
`.brand-mark` 44 square border radius14 background linear #ffca78 #ff8d6b; box-shadow; display:grid;font-size25; transform rotate(-4deg)`.
Emoji might vary but okay. Use CSS mini burger to custom:
```
<div class=brand-mark aria-hidden=true><span>🍔</span></div>
```
fine.
h1 letter spacing .06em; span color #ff809d.
Header note flex with small line and heart.
`.icon-button` etc.
Game HUD `display:flex` easier:
score flex1.3, level flex.7, lives flex1, combo min80. Each with border-left. At mobile fit.
`.hud-block{padding:0 18px}` score no border. `.hud-label` font9.
`#score` font22 mono.
Hearts spacing4.
combo pill border background.
Stage border-top/bottom perhaps.
`canvas{display:block;width:100%;height:100%;position:absolute;inset:0;cursor:crosshair}`.
When playing cursor none? Don't hide pointer, use grab.
`.stage:active{cursor:grabbing}`.
Game status bottom flex.
Demo chip etc.
`#startPrompt` z4.
Prompt kbd tiny.
`.prompt-icon` circle play? use small arrow `↗`.
No button because pointer-events none.
Gameover z6.
Toast z5 pointer none.
Ticket and control card.
Responsive:
```
@media(max-width:900px){
.layout{grid-template-columns:1fr}
.side{display:grid;grid-template-columns:1fr 1fr}
.ticket,.controls{...}
.stage{aspect-ratio:8/5}
.header-note{display:none}
}
@media(max-width:560px){
.app{width:calc(100% - 20px);padding-top:10px}
.site-header{margin-bottom:10px}
.brand-mark{width:38;height:38}
...
.layout{gap12}
.game-card{border-radius:17px}
.stage{aspect-ratio:4/5}
.hud{height:56px}
.hud-block{padding:0 9px}
.hud-label{font-size:8px}
.hud-value{font-size:18}
.combo... perhaps position absolute? all fit.
.side{grid-template-columns:1fr}
.ticket ... perhaps display grid?
.game-status{font-size:9;padding0 10}
.desktop-only{display:none}
.start-prompt{bottom:58px;...}
}
```
At width355, HUD score block flex1.25 (90) level.55(50) lives1(75) combo perhaps68 + paddings included -> total283 okay. Combo badge 48. Lives 3 hearts ~45. fine.
`#score` 6 digits at18 monospace width65 + padding18=83. level width perhaps50. lives 60. combo60. total253. good.
Brand + actions fits.
Mobile side ticket perhaps not in first 800? main total header48+10+hud56+stage444+status36=594 + top10=604, ticket below starts616 and visible part to800. Nice.
At desktop app total around? fine.
JS constants helpers. Need avoid name collisions `levelEl` etc.
IDs unique.
`const $=id=>document.getElementById(id);`.
`const canvas=$('gameCanvas'),ctx=...`.
`stage=$('stage')`.
Variables declaration before resize. Define `let initialized=false`.
```
const paddle={x:0,y:0,w:120,h:40,target:0,vx:0,squash:0};
const ball={...};
```
`resizeCanvas` uses particles arrays declared before invocation. Function declarations hoisted but arrays must initialized before call at bottom yes.
Random helper `rand(a,b)`. clamp.
`buildBricks` uses `const compact=W<480`. top=compact?56:64 perhaps. Rows compact6 else5. BrickH=compact?22:clamp(H*.041,20,26). Mobile H444 h20, 6 rows+gap7 total157 top64 bottom221. Good.
Desktop cols10, rows5, margin W*.055=49, brick w(894-98-9*7=733)/10=73.3, h23. total5*23+4*8=147 top68 bottom215. Nice. Ball first from y~475 to bottom215 dist260 / (speed420*.96)=.65.
Colors by `(row+level-1)%palette.length`.
Variants: every brick `seed:Math.random` but draw not random flicker.
Could set brick special every 9? All same hits. Add subtle `b.notch = random` used icon offset.
`totalBricks=bricks.length; broken=0`.
`resetGame` level1 then build, state; paddle positions. `ball.speed=baseSpeed`.
`serveTimer=.3`.
`score=0 etc`.
`clearTimeout(toastTimer)` toast hide.
`syncUI`.
No resetting particles at next level? Build only. reset clears.
`serveTimer` decreases while between? between branch excludes.
At reset ball values; `ball.trail?` global trails clear.
`playButton.innerHTML` can include span. Could static with text nodes via `innerHTML`. Safe. Use `playButton.querySelector('.button-label').textContent`.
HTML button has `<span class=button-label>Take the paddle</span><span>↗</span>`. Update label only.
`playButton` clicking if playing resets game (with label "Restart shift") good.
`newGameBtn` same.
`syncUI`:
```
const demo=state==='demo';
startPrompt.classList.toggle('is-hidden',!demo);
demoChip ...
status...
play label...
```
When state gameover startPrompt hidden.
`livesEl` perhaps sync every break rebuilding hearts unnecessarily but okay. Could update.
`syncHUD` separate.
`comboBadge.classList.toggle('visible',combo>1);`
Best.
`gameOverScore`.
`finalBricks` perhaps.
`updateHUD` calls bricksLeft count each break O45 fine.
`progressLabel`.
`progressFill`.
`rallyValue=bestCombo+'×'`.
`orderCount`.
`levelEl=String(level).padStart(2,'0')`.
`scoreEl=score.toString().padStart(6,'0')`.
Combo if 0 shows "×1" perhaps badge `×${Math.max(1,combo)}` and class active only >1. CSS opacity. In HUD visible muted `×1`.
Start reset best0.
Input:
```
window.addEventListener('keydown', e=>{
const k=e.key.toLowerCase();
if([...].includes(k))e.preventDefault();
if(e.target && e.target.tagName==='BUTTON' && (k===' '||k==='enter'))return;
if(state!=='playing')startUserGame();
if(k==='arrowleft'||k==='a'){keys.left=true;usingPointer=false;}
...
});
```
If gameover and press unrelated key start, fine. If state demo and Tab, should not start? requirement press key yes, but tab focus navigation could reset, not big. Could only ignore meta/ctrl.
Use `e.key` space `' '` list.
`keyup` update.
`blur` reset.
Pointer:
```
function pointPaddle(e){const r=stage.getBoundingClientRect();pointerTarget=clamp((e.clientX-r.left)/r.width*W,paddle.w/2,W-paddle.w/2);}
stage.addEventListener('pointerdown',e=>{
if(e.target.closest('button')) return;
if(state!=='playing')startUserGame();
pointerDown=true;usingPointer=true;setPointer(e);
});
stage.addEventListener('pointermove',e=>{
if(e.pointerType==='mouse'||pointerDown){usingPointer=true;setPointer(e);}
});
window pointerup false.
```
`e.target.closest` target canvas/overlay DIV supports Element.closest yes; stage listener target might text node? Event target Element for pointer. okay. Use `e.target instanceof Element`.
Click again button stops pointerdown via closest button.
Stage overlay gameover background target div, pointerdown will start user game if clicking anywhere. good.
No click event duplicate except play button outside.
`stage.addEventListener('contextmenu',e=>e.preventDefault())` optional.
Touch `pointermove` after pointerdown start and reset then set.
`startUserGame` `ensureAudio` every restart perhaps resumes; fine.
`resetBtn` click startUserGame.
Sound `toggle` not reset.
`againBtn` click startUserGame; event pointerdown stage closest button returns but click handler executes start. good.
Demo AI update if `state==='demo'`; if gameover user keys no update.
Player update pointer smoothing even not pointerDown and usingPointer true after mouse enters.
Keyboard movement:
```
let old=paddle.x;
if(state==='demo'){...} else {
if(dir!==0){paddle.x +=...; pointerTarget=paddle.x;}
else if(usingPointer){paddle.x += (pointerTarget-paddle.x)*Math.min(1,dt*20);}
}
paddle.vx=(paddle.x-old)/Math.max(dt,.001)
```
If usingPointer false and no keys paddle stays.
AI speed `Math.max(360,W*1.35)` at mobile479, desktop1207 huge enough. Good. Prediction sometimes target switching.
Paddle clamp.
Update effects before/after physics? after.
Combo decays:
```
if(comboTimer>0){... if reaches 0 {combo=0;updateHUD();}}
```
This rebuilds lives once.
Shake decays `Math.max(0,shake-dt*20)`.
Paddle squash decay.
`toast` managed CSS timeout independent.
UpdateBall trail cap:
```
trail.push(...);if(length>22)shift
```
Trail update life in effects after push, fine.
Wall sound tones every wall perhaps too many. use tone vol.015.
`spawnMini` hearts at wall? Don't spawn hearts every wall, small waves only.
`spawnHearts(x,y,color,count=...)` choose palette:
```
const colors=[color,'#ff6f9f','#ffd1dd','#fff0cf','#ff9a8a'];
```
At brick each heart angle uniform and `speed=rand(70,260)*scale` where scale=clamp(H/560,.75,1.1). Mobile distances. Gravity=rand(260,420)*scale.
Life=rand(.9,1.65).
`p.x=x+rand(-b.w/2...)? spawnHearts accepts spread perhaps.` Set `spread` parameter default8. Break call spread=b.w*.35. Function x+rand(-spread,spread), y+rand(-5,5).
Velocity angle random but upward bias `-rand(20,90)*scale`; if angle downward, gravity. Explosion omnidirectional.
Size rand(2.8,6.8)*clamp(W/900,.8,1) but mobile sizes should still visible 4-8. Use no scale or min.
`rotation=rand(-.6,.6)` and vr.
`glow=Math.random()<.25`.
Sparks separate objects in `sparks`.
Can unify effects array `particles` with kind hearts/spark. Draw branches. `spawnHearts` only hearts; break adds sparks via `sparks`.
Update both.
Cap hearts 650; if exceed splice.
`spawnHearts` on paddle 7, ball launch6, level extra. Fine.
`spawnSparks` count12 small rectangles.
Wave color.
Floater with main and optional combo separate:
```
floaters.push({x:b.x+b.w/2,y:b.y,text:`+${points}`,color:'#fff4dc',life:1,max:1,size:14});
if combo>=3 ... y+18 text...
```
Render font bold size etc, alpha. On mobile font12.
Shake transform random each render `Math.sin(elapsed*73)*shake` deterministic smoother; x cos, y sin.
`shake` decays.
Particles fade:
`const alpha=Math.min(1,p.life/.25)*Math.min(1,(p.max-p.life)/.08)` gives spawn fade in perhaps not needed. `alpha=Math.min(1,p.life/.35)`.
Scale pop `1+Math.sin((max-life)*12)*.05`.
Render in save global alpha per. `paintHeart` itself save, but globalAlpha set outside preserved? It calls save inherits alpha then restore to alpha still, then outer resets. Fine.
Use `paintHeart(c,x,y,s,rot,color,alpha)` function sets own globalAlpha to simplify.
For glow, call twice.
Draw background hearts:
```
paintHeart(...,null?)` needs fill only. Make `traceHeart` and direct:
ctx.save... traceHeart... ctx.strokeStyle...
```
Define `drawHeart(x,y,size,rotation,color,alpha=1,stroke=false)`.
If stroke true `ctx.strokeStyle=color;ctx.lineWidth=1/size?` Transform scales line width, need set lineWidth=0.12 for normalized; for fill no. For particles stroke false. For background set lineWidth=.05? Since scaling 36 gives1.8. Set property parameter.
Simpler background manual no use drawHeart? Use drawHeart with stroke and inside lineWidth=.08. Brick fill.
`drawHeart`:
```
ctx.save();translate rotate scale; traceHeart;ctx.globalAlpha=alpha;
if(stroke){ctx.strokeStyle=color;ctx.lineWidth=.09;ctx.stroke();} else fill...
ctx.restore();
```
Works.
Draw brick highlight: RoundedRect with `ctx.fillStyle=b.color;ctx.shadowColor=b.color;ctx.shadowBlur=6;fill;shadowBlur=0;`
gradient overlay:
```
const grad=ctx.createLinearGradient(0,b.y,0,b.y+b.h);
grad.addColorStop(0,'rgba(255,255,255,.22)');...
fill roundRect grad
```
45 gradients per frame perhaps okay but can just top line. Use `ctx.fillStyle='rgba(255,255,255,.18)'; roundedRectPath(...,b.h*.42); fill` small strip x+3 y+2 w-6 h*.35. Then small line bottom dark.
Heart icon. To add dimension `ctx.fillStyle='rgba(20,10,25,.18)'` bottom 3.
No text on bricks.
Background grid dot and streaks. Render only when screen dimensions.
`ctx.fillStyle=bgGradient`; bgGradient can be null if no resize? resize before loop.
`bgGlow` radial.
Dot fill based alpha and elapsed sin perhaps expensive style each; group one alpha. Use loop with fillRect and globalAlpha .12; some hearts.
Add frame border inside canvas `strokeStyle rgba...; strokeRect(.5,.5,W-1,H-1)` perhaps stage.
Ball:
Trail draw before bricks? Usually behind ball but can overlay background. Draw after bricks before paddle. Trail can cross bricks, okay.
`drawBall` if !ball.active:
- if serveTimer>0 draw at ball position with pulsing ring.
- if between or gameover skip.
Ball launch follows.
`if(!ball.active && serveTimer<=0)return`.
Radial gradient:
```
let g=ctx.createRadialGradient(x-r*.3,y-r*.4,1,x,y,r)
...
```
circle fill; shadow.
heart stamp size r*.32 at y+1 perhaps.
Trail circles white pink.
Paddle after trail and ball? Ball should topmost. Order paddle then ball then particles.
Draw serve indicator perhaps small dotted line? Not needed.
HUD update with high score no.
`formatScore` >999999 pad just grows perhaps fine. score max moderate.
`combo` points can balloon but okay.
Screen first message always visible in demo. It might be covered by toast? Toast top separate.
Game over CSS:
```
.game-over{opacity:0;visibility:hidden;...}
.game-over.show{...}
```
Full backdrop. `.over-card` cream color. Button.
When show, demo chip? remains "SHIFT OVER".
`syncUI` controls.
Sound on initial true but no demo sound. Header label "Sound on"; user may expect demo sound but autoplay not allowed. Good.
Could default false to avoid audio surprising, but game sound desired. Usually okay.
`ensureAudio` context if state suspended resume. Promise catch.
`toggle sound` uses soundOn; update `soundIcon` `♪` vs `×`? Text. aria-pressed.
`Sound off` when false, icon '♪' with opacity perhaps class.
`button.classList.toggle('muted')`.
When true tone.
No local storage.
Could include inline SVG favicon? Network no. Not needed.
Need ensure actual code not exceed output constraints. I can now compose the response. But before responding, scrutinize possible edge cases and quality.
At 1280 screenshot stage height 559; brick font etc. Start prompt at bottom70 and paddle at520, prompt box bottom H-70=489 top~435, ball and paddle below, no overlap. Ball descending might go behind prompt but transparent. Good.
Right ticket height: padding20, top line12, margin14 h2 76, desc40, divider, progress60, stats48 =300. controls padding18 title12, controls rows70, button44, hint20 =190 total +gap14 =504, game card total64+559+38=661. Sidebar shorter 504, okay bottom whitespace. Could add tip note in controls "PRO TIP: Catch with the bun's edges to steer." fill. Game card height661, app header etc total749. Side ~550. fine.
Actually game card 64+559+38=661, app total14+48+14+661+12=749. Great.
At 1280x800 centered vertical top14 not center but okay.
Desktop app max1160 -> left894, stage559.
At 375 mobile app355, stage443.75, game total56+444+36=536, header48+10+top10=604. ticket starts616. Good.
Canvas mobile W355 H444, top56, bricks 7 cols, margin19.5 gap4.5, brickW=(355-39-27)/7=41.3, h20, 6 rows +5*6=150, bottom206. paddle y406. Ball first distance from368 to226=142 / speed~300=.48, second every~.9. Many explosions at3 sec. Nice.
Ball speed clamp(H*.78=346,290,450), vertical ~330. first .43. Desktop speed436, dist~260 vertical .63. Good.
Paddle AI predict with `ball.active` false then center. At launch vx random .24 speed, near side. Fine.
Initial demo first break within1 sec. Particle life1-1.7, repeated breaks every~1.2 so always hearts.
Brick collision after bounce from bottom brick sets vy positive. It might overlap adjacent bricks horizontally next frames? Reposition y b.y+b.h+r+.5 ensures no. Good.
Top wall at y<r. Bricks top64 no collision wall while brick pattern.
Scoring combo every brick within2.4 sec, so combo grows across rally even paddle, likely 3+. Best visible.
When comboTimer expires updateHUD resetting meter? broken unchanged.
No timer display.
Level clear check `bricks.every(!alive)` after break, called. Set between; ball.active false. UpdateBall returns after break? Need code after collision check bottom? I'll structure:
```
... brick loop ... if(noBricks){completeLevel();}
```
Then after loop if state between return perhaps. `completeLevel` sets between and ball.activefalse, updateBall then ends. Fine.
Effects hearts.
`update` between branch prevents serve and ball.
`nextLevel` `buildBricks` resets broken; lives perhaps same; `serveTimer=.55`; `ball.active=false`; betweenfalse; ball position gets in next update serve. level increment before build.
Paddle stays.
`syncHUD`.
Demo resets after losing all lives immediate rebuild. Fine.
Gameover `state='gameover'` stops `update` physics but frame should still call updateEffects. Frame:
```
if(state==='gameover'){updateEffects(dt);...}else update(dt)
```
Shake perhaps decays only update; gameover shake frozen value, render random oscillation forever. Need update common effects and decay. Could call `updateEffects` always in frame, and update game no effects. Structure:
```
function update(dt){
updatePaddle...
updateTimers...
}
function frame {
if(state!=='gameover') update(dt);
else {paddle.squash...}
updateEffects(dt);
shake=Math.max...
render
}
```
Combo etc only update. Easier. updateEffects includes shake decay and paddle squash? yes.
`updateEffects` update trails, hearts, waves, sparks, floaters; `shake = max(0, shake-dt*18); paddle.squash=max...`.
`update` no effects.
Gameover particles continue.
`elapsed` always increases for background twinkle and prompt CSS independent.
`last=now` visibility.
`loseBall` called updateBall then state gameover; update continues? `update` returns after updateBall then nothing. fine.
`serveTimer` ball position set. On life loss ball y belowH then inactive; next update serve branch positions on paddle immediately. If gameover no update so ball inactive at below, draw skip serveTimer0. fine.
Demo life reset calls build and serve .8; updateHUD.
Paddle particles when life lose might render at H-10 and fall off quickly but visible.
`syncUI` start prompt uses class; gameOver overlay class only end/start.
`resetGame` hides gameover. `startUserGame` `resetGame(false)`.
No separate resume.
`playButton` labels.
`statusText` changes.
`demoChip` initial CSS dot class perhaps `demo-chip.live`. On gameover set dot red via class? Keep.
Use `demoChip.innerHTML` with dot each sync? Better HTML `<span class=live-dot></span><span id=modeLabel>AUTO DEMO</span>` update text. `modeLabel`.
`statusDot` similar.
No untrusted text.
Sound toggle event doesn't propagate to stage outside. okay.
CSS `.is-hidden{opacity:0;visibility:hidden}` generic might affect other? only start. `.start-prompt.is-hidden`.
`toast.show`.
`game-over.show`.
Could use `window.ResizeObserver` guard:
```
if('ResizeObserver' in window) new ResizeObserver(resizeCanvas).observe(stage);
window.addEventListener('resize',resizeCanvas);
```
No error. `resizeCanvas` invoked event object but ignores args.
`dpr=Math.min(2,window.devicePixelRatio||1)`.
If same dimensions but dpr same return? If return first then no bg reset. At observer same no need. Implement:
```
if(Math.abs(nw-W)<.5 && ... && dpr===newDpr && initialized)return;
```
But `W` initialized0. Fine.
Rescale only if initialized. Set `initialized=true` at end first resize before reset? If first resize sets paddle x0 etc and then reset; subsequent okay. `resizeCanvas`:
```
const hadLayout=initialized && oldW>0...
...
if(hadLayout){...} else {paddle...}
initialized=true;
```
At first reset sets. Fine.
If `buildBricks` not yet and bricks empty, loops no.
Scale ball velocities only hadLayout; if ball active etc.
Scale particle size by sqrt sx sy.
`makeBackground` after canvas set transform.
When dimensions same but observer dpr same return prevents extra clear. Good.
Window resize invokes if same no effect.
Use `createRadialGradient` radii >0 W H positive. Return if <10.
`bgGradient` let.
Physics speed on resize:
```
ball.x*=sx;ball.y*=sy;ball.vx*=sx;ball.vy*=sy;ball.r=...
```
paddle y set after scaling y? Set y always computed; don't scale y then override. paddle.x*=sx, target*=sx.
Particles positions scale.
Bricks all scale including dead.
Floaters etc scale.
trail.
No need arrays waves? scale x y radius.
Sparks etc.
W/H assigned before gradient and scaling? old saved, set W=nw H=nh then scale with old. base.
`paddle.w=paddleWidth()` function uses W.
`paddle.h=clamp(W*.045,32,42)`.
`paddle.y=H-Math.max(38,H*.07)`.
Ball r clamp(W*.009,6,8). On desktop8, mobile6.
Particles heart sizes scale on resize then perhaps too, okay.
`baseSpeed()` uses H.
On resize velocities scaled based sx/sy; if mobile orientation shift drastically speed perhaps but okay.
`paddle.target` not used, pointerTarget scale.
PointerTarget initialized W/2 in reset. resize if pointerTarget numbers.
`resetGame` keys left? I may keep key state; for click stale keys if keyup missed window blur clears. Fine. Set `keys.left=keys.right=false` in reset could cause key start not move until repeat. Better startUserGame resets then keydown set. For click reset desirable. In reset set false, keydown calls start then sets. Play button key activation Space global return then button click reset keys. fine.
`usingPointer=false` reset, pointerTarget center.
After pointerdown start then set true/target.
`resetGame` called automatically demo at initial.
Best score no.
Ball audio tone at brick on combo. If sound off no. `tone` call on wall only user audio context exists. In demo before interaction audioCtx null.
When user returns demo? never except reload.
Gameover audio descending tones perhaps two setTimeout? Avoid timers sound. `tone(150,.35,'sawtooth',.03)`.
Level complete sound three tones with setTimeout perhaps okay but if restart sounds later. Use one `tone(660,.18)` and 880 delayed via oscillator start future? `tone(freq,duration,type,volume,delay=0)` supports. call delay. Add optional.
`tone` `const t=audioCtx.currentTime+delay`.
No errors exponential ramp volume must >0; volume .03. Good.
`audioCtx.resume().catch` if returns undefined? Standard Promise. `const p=audioCtx.resume(); if(p&&p.catch)p.catch(...)`.
Tone if `audioCtx.state==='suspended'` returns? ensure called start. No issue.
Final response code no explanations. Need perhaps include heart symbols in HTML allowed UTF-8 meta.
I'll simulate code possible undefined `gameOver` variable conflict with id global? Use `gameOverEl`.
DOM loaded script at end.
`startPrompt` class initial not hidden.
`resetGame(true)` calls syncUI sets.
`requestAnimationFrame(frame)`.
`lastTime=performance.now()` before request. dt small.
`resizeCanvas` and `resetGame` then ResizeObserver. Good.
Accessibility user click iframe first keyboard: start prompt exact. Stage `tabindex=0`, keyboard events attached window. After pointerdown call `stage.focus({preventScroll:true})` perhaps key works. On pointerdown focusing stage ensures. Use try? `stage.focus({preventScroll:true})` supported. It might cause stage outline CSS. Add `:focus-visible` outline. On pointerdown, focus visible typically not.
Click play button outside stage, keyboard remains button but window receives. Fine.
`stage.setPointerCapture` omitted means touch drag outside stops but window pointerup, pointermove stage only; okay. Could set `canvas.setPointerCapture(e.pointerId)` but target may overlay and capture supports Element. Not needed.
`pointermove` desktop hover always sets usingPointer only when state playing? Handler currently even demo could change target but AI ignores. Upon click reset sets false then set true. okay.
On mobile scroll touching stage disabled intentional.
Prevent arrow keys page scroll always, only if not button.
Game state any key starts even sound keys perhaps.
Right controls labels "← → / A D", "MOUSE / TOUCH".
Play button "Take the paddle".
StartPrompt `Click or press a key to start` plus tiny `Demo is playing behind this ticket`.
Ticket progress label updates.
Draw burger shape needs visually identifiable:
- top bun domed with sesame.
- lettuce wavy green under bun.
- cheese square rotated? Draw yellow polygon across width at y+.45h with drips.
- tomato red thin line perhaps include.
- patty dark brown rounded.
- bottom bun orange.
I can layer:
bottom bun first (behind lower)
patty at y+.58h
cheese at y+.46h
tomato at y+.42h perhaps red line
lettuce y+.35h
top bun dome y to .42h.
Since drawing order bottom to top means top bun overlays lettuce top. Fine.
Actual burger appears height40 with each layer 4-10. Add glow behind `ctx.shadowBlur`.
Use `roundedRectPath`.
Top bun path fill gradient. Bun sesame ~7 seeds computed fixed ratios. `for (const [sx,sy,rot] of seeds)` static.
`paddle.squash`: `squash=Math.sin(paddle.squash*Math.PI)?` I set squash=1 on bounce and decays to0; draw scaleY=1-paddle.squash*.14, scaleX=1+paddle.squash*.08. At set 1 then decays, immediate squash then stretch back. Fine.
ctx translate(paddle.x,paddle.y+paddle.h/2); scale; translate(-paddle.x, -(paddle.y+paddle.h/2)); drawing coords. Shadow translated okay.
Collision paddle y unchanged.
Paddle glow `ctx.shadowColor='#ffb65d';shadowBlur=14` top bun and bottom; reset.
Brick burst originates center. Nice.
Floating text `ctx.textAlign='center';ctx.lineWidth=3;ctx.strokeStyle='rgba(...)'` stroke then fill for readability. Draw combo second with pink.
Hearts perhaps hide text but okay.
Background phrase perhaps text width mobile >W? "FRESHLY STACKED • EXTRA LOVE" at font9 ~170 fits355. Fine.
Top decorative lines start W*.12 to center-110 etc perhaps if negative mobile? Use line from18 to W/2-105 =>72 positive; right. Good.
On desktop broad.
Draw borders canvas perhaps no.
`ctx.globalAlpha=1` after each.
Effects `sparks` kind draws line with rotate. Update rot.
Wave draw ring `ctx.arc`.
`waves` initial r4, max life .45, update r+=190dt. draw alpha life/max.
No context leakage.
Dynamic dots fixed arrays:
```
const dots=Array.from({length:65},(_,i)=>({x:((i*47)%101)/101,y:... ,s:...}));
```
Multiplication modulo deterministic. y all across. Fill some hearts? dots enough.
CSS body decorative mini hearts fixed `.float-heart` perhaps adds atmosphere. Could include two spans with ♥ at sides animated float. But game already particles. Add `.ambient-heart.one` etc; at mobile hidden. Nice but not necessary.
Perhaps HTML `<div class="ambient a">♥</div>...` with blur? Avoid.
Footer microcopy visible after main perhaps not at800 if main749 + footer24=773 fits. Add `<footer class=footer><span>PATTY'S ARCADE • EST. 2025</span><span>STAY GOLDEN. <b>♥</b></span></footer>`. At mobile below content, okay. Does not impact layout side? Main height max game card661, footer24. Good.
App padding bottom10.
No actual asset. Good.
I'll consider security restrictions no localStorage etc. None. No alert. No window.open. No external.
No fonts requests.
No errors latest Chrome.
One potential bug: `ResizeObserver` callback calls `resizeCanvas` with entries args; function no params ignores. Fine.
`ctx.setTransform(dpr...` after resizing and in render. Gradients created in current transformed coordinates? Canvas gradients are in current user space at creation according spec coordinates transformed? Actually gradient coordinates global current transform at creation? Canvas gradients not relative to shapes and use current transformation at creation? Created with transform dpr, coordinates W,H CSS -> device size. At render same transform, consistent. Standard. Fine.
On canvas resized and ctx set transform then create gradients. Good.
Canvas CSS stage rect includes hidden? no.
Score HUD `String(score).padStart(6,'0')` perhaps score=0 show000000.
Header stats.
`progressFill.style.width` percent clamp.
`orderCount.textContent = `${String(broken).padStart(2,'0')} / ${totalBricks}`;`
`bricksLeft` label.
BestCombo value 0× initial.
`updateHUD` should set `comboValue` even combo 0.
`comboBadge.classList.toggle('hot',combo>1)`.
Combo badge initially `×1`.
CSS hot animation pulse; reduced motion disable.
When comboTimer expires updateHUD; yes.
Game UI state `state` variable could conflict with window.state no.
`level` global perhaps window.level no module scoped IIFE.
`showToast` clear timer. On reset hide.
`toastTitle`, `toastSub`.
`setTimeout` allowed.
`startUserGame` hide toast via reset.
Gameover hides toast perhaps `clearTimeout`.
`endGame` show gameOver and `syncUI`.
`syncUI` does not hide gameOver unless reset separately. End adds show after sync perhaps.
```
state='gameover'; syncUI(); gameOverEl.classList.add('show');
```
start reset removes.
Demo no gameover.
Life lost updateHUD before state perhaps.
`livesEl` heart off class CSS opacity.
`livesEl` aria-label update lives.
`scoreEl` bump no.
`announce level clear` and spawn extra hearts while between. Great.
Potential collision `remainingBricks()` uses `bricks.reduce` many calls only HUD.
On break after `updateHUD` before completeLevel `totalBricks` etc.
`completeLevel` calls `showToast`, no updateHUD necessary.
`nextLevel` level++ and build; sync HUD.
`buildBricks` sets broken0 but bestCombo remains session. Reset best resets.
`combo=0` next.
Difficulty user starts level1 same.
AI movement during between target center because ball.active false. fine.
Serve timer when between false and !active. When reset `ball.vx=0,vy=0`. launch angle picks based elapsed:
```
const direction = state==='demo' ? Math.sin(elapsed*1.7)*.32 : rand(-.28,.28)
ball.vx=baseSpeed()*direction; ball.vy=-sqrt(...)
```
direction is fraction speed not angle, vertical fine.
On next level speed adds level.
`ball.speed` property not needed, use current vector.
At paddle:
```
const speed=Math.min(baseSpeed()+(level-1)*12+30, Math.hypot(...)*1.018)
```
Could baseSpeed includes level? define `baseSpeed(){return clamp(H*.78,290,450)+(level-1)*14;}` cap? On level high >500 okay.
`const speed=Math.min(Math.hypot(...)*1.025,baseSpeed()+85);` On first paddle hit initial base=436, speed445, cap521. Good.
On resize scale might.
Ball initial launch `const speed=baseSpeed()`.
When brick side collisions no speed.
Minimum `Math.abs(ball.vy)<speed*.28` perhaps side angles only from paddle min .47, initial .97. no.
If collision overlap else flips vy could launch upward etc normal.
Paddle collision check:
```
if(ball.vy>0 && ball.y+ball.r>=paddle.y && ball.y-ball.r<paddle.y+paddle.h*.65 && ball.x... && prevY+ball.r<=paddle.y+10)
```
If prevY+ r =paddle.y+12 due high speed, condition fails and ball lost despite crossing because dt movement max15, threshold10; better just `prevY <= paddle.y + paddle.h*.35` or no prev. Use if ball.y < paddle.y+paddle.h*.55 and vy>0. Set. This catches.
Ball x expanded by r*.6. Standard.
At paddle burger curved edges, hit position.
After set y paddle.y-r-.5.
`paddle.squash=1`.
`spawnHearts(ball.x,paddle.y, '#ffc7d6', 6, paddle.w*.25)` small but same sizes/life perhaps lots but okay. Create `spawnHearts` options count,spread,power? Default brick power. On paddle 6 hearts with speeds up to260 huge; cute.
Wall bounce no particles.
Bottom life loss burst 22.
Heart spawn on launch 8 might be interpreted explosion but okay.
`spawnHearts` optional `scale=1` to make paddle hearts smaller:
```
function spawnHearts(x,y,color,count=30,spread=8,power=1)
...
speed rand*power, size rand*(power<1?.75:1), life...
```
Break `power=1`, launch `.45`, paddle `.5`, level `.9`.
At level celebration call20.
`gravity` scaled power? If .5, gravity same scale gives float; fine.
Always 30 first.
Sparks not on paddle.
This gives continual hearts but bricks most dramatic.
First paint at time0 before ball break still background and bricks, burger. Great.
Typography no canvas external fonts. Use `bold ${size}px "Trebuchet MS",sans-serif`.
Perhaps need HTML escaping in code fence no issue.
Respond exactly one code block no commentary.
I'll draft the response thoroughly.