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
- 6 min 12 s
- Cost
- $0.398
- Output tokens
- 44,112
- Spent thinking
- 25,628 tokens
- Speed
- 118.7 tok/s
- Console errors
- 0
- First token after
- 417 ms
- First visible text after
- 4 min 41 s
- Prompt tokens
- 593
- Code size
- 53.5 KB
- Finish reason
- stop
- Served by
- InferenceNet
- 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
Build a polished Breakout (brick breaker) game in a single HTML file using vanilla JavaScript and a <canvas>. Requirements: - A paddle controlled with the mouse and with the left/right arrow keys. - The bounce angle off the paddle depends on where the ball hits it (center = straight up, edges = sharp angle). - One level with at least 6 rows of bricks, mixing normal bricks and tougher bricks that need 2 hits and visibly crack after the first one. - Some bricks visibly carry a power-up icon; when broken they drop it, and catching it with the paddle gives either a wider paddle (10 seconds) or multi-ball (3 balls). - The ball must never pass through bricks or the paddle. - Particles when a brick breaks. - 3 lives, score displayed. Game over and victory screens with a restart option.
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 · 2002 lines
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Neon Breakout</title>
<style>
:root {
color-scheme: dark;
--bg: #050812;
--panel: rgba(12, 20, 39, 0.82);
--line: rgba(139, 184, 255, 0.18);
--text: #eef7ff;
--muted: #8294b5;
--cyan: #68e8ff;
--violet: #b99aff;
}
* { box-sizing: border-box; }
html, body {
margin: 0;
min-height: 100%;
overflow-x: hidden;
}
body {
min-height: 100dvh;
display: grid;
place-items: center;
padding: 10px 14px;
color: var(--text);
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
background:
radial-gradient(circle at 16% -10%, rgba(43, 71, 146, 0.42), transparent 42%),
radial-gradient(circle at 92% 108%, rgba(103, 43, 134, 0.28), transparent 43%),
#050812;
user-select: none;
}
body::before {
content: "";
position: fixed;
inset: 0;
pointer-events: none;
opacity: 0.16;
background-image:
linear-gradient(rgba(119, 160, 226, 0.08) 1px, transparent 1px),
linear-gradient(90deg, rgba(119, 160, 226, 0.08) 1px, transparent 1px);
background-size: 42px 42px;
mask-image: linear-gradient(to bottom, black, transparent 80%);
}
button { font: inherit; }
[hidden] { display: none !important; }
.app {
position: relative;
width: min(960px, calc(100vw - 28px), calc((100dvh - 170px) * 1.6));
display: flex;
flex-direction: column;
gap: 8px;
}
.topbar {
min-height: 42px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
}
.brand {
display: flex;
align-items: center;
gap: 11px;
min-width: 0;
}
.logo {
position: relative;
width: 38px;
height: 38px;
flex: 0 0 auto;
border: 1px solid rgba(113, 226, 255, 0.28);
border-radius: 11px;
background: linear-gradient(145deg, rgba(73, 190, 255, 0.15), rgba(109, 74, 189, 0.12));
box-shadow: inset 0 0 16px rgba(77, 212, 255, 0.08), 0 0 22px rgba(62, 180, 255, 0.08);
}
.logo i {
position: absolute;
width: 8px;
height: 5px;
top: 9px;
border-radius: 2px;
background: #70e9ff;
box-shadow: 0 0 7px rgba(104, 232, 255, 0.7);
}
.logo i:nth-child(1) { left: 7px; }
.logo i:nth-child(2) { left: 16px; background: #a8a2ff; }
.logo i:nth-child(3) { left: 25px; background: #ef9fdf; }
.logo b {
position: absolute;
width: 19px;
height: 4px;
left: 9px;
bottom: 8px;
border-radius: 5px;
background: #e9fbff;
box-shadow: 0 0 8px #68e8ff;
}
.logo b::before {
content: "";
position: absolute;
width: 5px;
height: 5px;
right: -2px;
top: -11px;
border-radius: 50%;
background: white;
box-shadow: 0 0 7px #68e8ff;
}
h1 {
margin: 0;
font-size: 20px;
line-height: 1.05;
letter-spacing: 0.16em;
font-weight: 850;
}
h1 span { color: var(--cyan); }
.brand p {
margin: 4px 0 0;
color: var(--muted);
font-size: 9px;
line-height: 1;
letter-spacing: 0.19em;
font-weight: 700;
}
.brand p strong {
color: var(--cyan);
margin-left: 7px;
font-weight: 800;
}
.sound-button {
height: 32px;
display: inline-flex;
align-items: center;
gap: 7px;
padding: 0 11px;
border: 1px solid var(--line);
border-radius: 9px;
color: #a9bad6;
background: rgba(11, 18, 34, 0.66);
cursor: pointer;
font-size: 9px;
font-weight: 750;
letter-spacing: 0.12em;
transition: border-color 150ms, color 150ms, background 150ms;
}
.sound-button:hover {
color: white;
border-color: rgba(104, 232, 255, 0.42);
background: rgba(21, 34, 58, 0.8);
}
.sound-button:focus-visible,
.restart-button:focus-visible {
outline: 2px solid var(--cyan);
outline-offset: 3px;
}
.sound-glyph {
color: var(--cyan);
font-size: 15px;
line-height: 1;
}
.sound-button.muted .sound-glyph { color: #72809a; }
.hud {
height: 56px;
display: grid;
grid-template-columns: 1fr auto 1fr;
align-items: center;
padding: 0 18px;
border: 1px solid var(--line);
border-radius: 13px;
background: linear-gradient(180deg, rgba(18, 29, 53, 0.84), rgba(9, 16, 31, 0.84));
box-shadow: inset 0 1px rgba(255, 255, 255, 0.025), 0 10px 30px rgba(0, 0, 0, 0.14);
backdrop-filter: blur(10px);
}
.hud-block {
display: flex;
flex-direction: column;
gap: 2px;
}
.hud-label {
color: #7083a5;
font-size: 8px;
font-weight: 800;
letter-spacing: 0.2em;
}
#score {
font-size: 21px;
line-height: 1.1;
letter-spacing: 0.08em;
font-variant-numeric: tabular-nums;
text-shadow: 0 0 16px rgba(104, 232, 255, 0.2);
}
.sector {
min-width: 140px;
text-align: center;
padding: 0 24px;
border-left: 1px solid rgba(135, 172, 229, 0.12);
border-right: 1px solid rgba(135, 172, 229, 0.12);
}
.sector span {
display: block;
color: #c5d5ed;
font-size: 10px;
font-weight: 800;
letter-spacing: 0.19em;
}
.sector small {
display: block;
margin-top: 3px;
color: var(--cyan);
font-size: 8px;
font-weight: 700;
letter-spacing: 0.15em;
}
.lives-block {
align-items: flex-end;
}
#lives {
height: 20px;
display: flex;
align-items: center;
gap: 7px;
}
.life {
width: 11px;
height: 11px;
border-radius: 50%;
background: radial-gradient(circle at 35% 30%, white 0 18%, #8cf0ff 28%, #3aa8df 72%);
box-shadow: 0 0 9px rgba(104, 232, 255, 0.68);
transition: opacity 180ms, filter 180ms;
}
.life.lost {
opacity: 0.18;
filter: grayscale(1);
box-shadow: none;
}
.canvas-wrap {
position: relative;
width: 100%;
aspect-ratio: 8 / 5;
overflow: hidden;
border: 1px solid rgba(124, 175, 239, 0.25);
border-radius: 16px;
background: #071020;
box-shadow:
0 22px 60px rgba(0, 0, 0, 0.42),
0 0 0 1px rgba(0, 0, 0, 0.35),
inset 0 0 35px rgba(52, 132, 207, 0.06);
touch-action: none;
cursor: crosshair;
isolation: isolate;
}
.canvas-wrap::after {
content: "";
position: absolute;
inset: 0;
z-index: 2;
pointer-events: none;
opacity: 0.13;
background: repeating-linear-gradient(to bottom, transparent 0, transparent 3px, rgba(116, 177, 235, 0.08) 4px);
mix-blend-mode: screen;
}
canvas {
display: block;
width: 100%;
height: 100%;
outline: none;
}
.start-overlay,
.pause-overlay,
.end-overlay {
position: absolute;
inset: 0;
z-index: 4;
}
.start-overlay {
display: flex;
align-items: flex-end;
justify-content: center;
padding: 0 16px 66px;
pointer-events: none;
background: linear-gradient(to top, rgba(4, 9, 20, 0.24), transparent 46%);
}
.start-card {
max-width: 100%;
padding: 15px 25px 14px;
text-align: center;
border: 1px solid rgba(128, 218, 255, 0.28);
border-radius: 14px;
background: linear-gradient(145deg, rgba(12, 24, 45, 0.88), rgba(12, 16, 35, 0.82));
box-shadow: 0 14px 40px rgba(0, 0, 0, 0.36), inset 0 1px rgba(255, 255, 255, 0.05), 0 0 28px rgba(72, 193, 255, 0.06);
backdrop-filter: blur(9px);
}
.ai-label {
display: flex;
align-items: center;
justify-content: center;
gap: 7px;
margin-bottom: 7px;
color: #8ea4c7;
font-size: 8px;
font-weight: 800;
letter-spacing: 0.22em;
}
.ai-label i {
width: 6px;
height: 6px;
border-radius: 50%;
background: #65edda;
box-shadow: 0 0 9px #65edda;
animation: pulse 1.5s ease-in-out infinite;
}
.start-title {
color: #f3fbff;
font-size: 15px;
font-weight: 850;
letter-spacing: 0.11em;
text-shadow: 0 0 18px rgba(104, 232, 255, 0.25);
}
.start-sub {
margin-top: 7px;
color: #8d9fbd;
font-size: 10px;
letter-spacing: 0.035em;
}
.start-sub b { color: #c4f7ff; font-weight: 800; }
@keyframes pulse {
0%, 100% { opacity: 0.45; transform: scale(0.8); }
50% { opacity: 1; transform: scale(1.12); }
}
.pause-overlay {
display: grid;
place-items: center;
pointer-events: none;
background: rgba(3, 7, 16, 0.52);
backdrop-filter: blur(3px);
}
.pause-card {
padding: 18px 30px;
text-align: center;
border: 1px solid var(--line);
border-radius: 13px;
background: rgba(10, 18, 35, 0.9);
}
.pause-card strong {
display: block;
font-size: 23px;
letter-spacing: 0.2em;
}
.pause-card span {
display: block;
margin-top: 6px;
color: var(--muted);
font-size: 10px;
}
.end-overlay {
display: grid;
place-items: center;
padding: 16px;
background: radial-gradient(circle, rgba(12, 21, 41, 0.64), rgba(3, 6, 14, 0.91));
backdrop-filter: blur(6px);
}
.end-panel {
width: min(420px, 100%);
padding: 27px;
text-align: center;
border: 1px solid rgba(255, 114, 145, 0.3);
border-radius: 18px;
background: linear-gradient(145deg, rgba(19, 28, 51, 0.96), rgba(9, 14, 28, 0.96));
box-shadow: 0 24px 80px rgba(0, 0, 0, 0.55), 0 0 40px rgba(255, 86, 128, 0.07);
}
.end-overlay.victory .end-panel {
border-color: rgba(104, 232, 255, 0.42);
box-shadow: 0 24px 80px rgba(0, 0, 0, 0.55), 0 0 46px rgba(104, 232, 255, 0.1);
}
.end-kicker {
color: #ff91ab;
font-size: 9px;
font-weight: 850;
letter-spacing: 0.23em;
}
.victory .end-kicker { color: var(--cyan); }
.end-panel h2 {
margin: 8px 0 6px;
font-size: 31px;
letter-spacing: 0.08em;
line-height: 1.1;
}
.end-message {
margin: 0;
color: #91a2be;
font-size: 12px;
}
.end-stats {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1px;
margin: 22px 0;
overflow: hidden;
border: 1px solid rgba(134, 170, 224, 0.13);
border-radius: 11px;
background: rgba(134, 170, 224, 0.13);
}
.end-stat {
padding: 13px 8px;
background: rgba(7, 13, 27, 0.88);
}
.end-stat span {
display: block;
color: #7183a2;
font-size: 8px;
font-weight: 800;
letter-spacing: 0.15em;
}
.end-stat strong {
display: block;
margin-top: 4px;
color: #f4f9ff;
font-size: 20px;
font-variant-numeric: tabular-nums;
}
.restart-button {
min-width: 180px;
padding: 12px 23px;
border: 0;
border-radius: 10px;
color: #04111a;
background: linear-gradient(100deg, #7bf1ff, #a6b5ff);
box-shadow: 0 8px 24px rgba(89, 211, 255, 0.2);
cursor: pointer;
font-size: 11px;
font-weight: 900;
letter-spacing: 0.13em;
transition: transform 140ms, filter 140ms;
}
.restart-button:hover { transform: translateY(-1px); filter: brightness(1.08); }
.restart-button:active { transform: translateY(1px); }
.restart-hint {
margin: 11px 0 0;
color: #647593;
font-size: 9px;
}
.footer {
min-height: 26px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 14px;
color: #6f809e;
font-size: 8px;
font-weight: 700;
letter-spacing: 0.1em;
}
.power-legend,
.controls {
display: flex;
align-items: center;
gap: 15px;
white-space: nowrap;
}
.power-legend span,
.controls span {
display: inline-flex;
align-items: center;
gap: 5px;
}
.power-legend i {
display: inline-grid;
place-items: center;
min-width: 18px;
height: 16px;
padding: 0 3px;
border: 1px solid rgba(104, 232, 255, 0.25);
border-radius: 5px;
color: var(--cyan);
background: rgba(104, 232, 255, 0.07);
font-style: normal;
font-size: 10px;
letter-spacing: 0;
}
.power-legend span:nth-child(2) i {
color: var(--violet);
border-color: rgba(185, 154, 255, 0.26);
background: rgba(185, 154, 255, 0.07);
}
@media (max-width: 600px) {
body { padding: 8px 10px; }
.app { width: min(calc(100vw - 20px), calc((100dvh - 150px) * 1.6)); gap: 6px; }
.topbar { min-height: 36px; }
.logo { width: 32px; height: 32px; transform: scale(0.86); transform-origin: left center; margin-right: -4px; }
h1 { font-size: 16px; }
.brand p { font-size: 7px; margin-top: 3px; }
.sound-button { height: 28px; padding: 0 8px; }
#soundText { display: none; }
.hud { height: 47px; padding: 0 11px; border-radius: 11px; }
#score { font-size: 16px; }
.hud-label { font-size: 7px; }
.sector { min-width: 100px; padding: 0 12px; }
.sector span { font-size: 8px; }
.sector small { font-size: 7px; }
.life { width: 9px; height: 9px; }
#lives { gap: 5px; }
.canvas-wrap { border-radius: 11px; }
.start-overlay { padding: 0 9px 28px; }
.start-card { padding: 9px 12px; border-radius: 10px; }
.ai-label { font-size: 6px; margin-bottom: 4px; }
.start-title { font-size: 10px; letter-spacing: 0.07em; }
.start-sub { display: none; }
.end-panel { padding: 16px; border-radius: 13px; }
.end-panel h2 { font-size: 21px; margin: 5px 0; }
.end-message { font-size: 9px; }
.end-stats { margin: 12px 0; }
.end-stat { padding: 8px 5px; }
.end-stat strong { font-size: 15px; }
.restart-button { min-width: 150px; padding: 9px 15px; font-size: 9px; }
.restart-hint { margin-top: 7px; font-size: 7px; }
.footer { justify-content: center; font-size: 6px; min-height: 20px; }
.power-legend, .controls { gap: 8px; }
.controls { display: none; }
.power-legend i { height: 13px; min-width: 15px; font-size: 8px; }
}
</style>
</head>
<body>
<div class="app">
<header class="topbar">
<div class="brand">
<div class="logo" aria-hidden="true"><i></i><i></i><i></i><b></b></div>
<div>
<h1>BREAKOUT<span>//</span>NEON</h1>
<p>SECTOR 01 <strong id="modeLabel">AI DEMO</strong></p>
</div>
</div>
<button class="sound-button" id="soundButton" type="button" aria-label="Toggle sound" aria-pressed="true">
<span class="sound-glyph" id="soundGlyph">♪</span>
<span id="soundText">SOUND ON</span>
</button>
</header>
<section class="hud" aria-label="Game status">
<div class="hud-block">
<span class="hud-label">SCORE</span>
<strong id="score">000000</strong>
</div>
<div class="sector">
<span>NEON GRID</span>
<small id="remaining">60 BRICKS</small>
</div>
<div class="hud-block lives-block">
<span class="hud-label">LIVES</span>
<div id="lives" role="img" aria-label="3 lives"></div>
</div>
</section>
<div class="canvas-wrap" id="canvasWrap">
<canvas id="game" width="960" height="600" tabindex="0" aria-label="Breakout game. Move with the mouse or left and right arrow keys.">
Your browser needs canvas support to play Breakout.
</canvas>
<div class="start-overlay" id="startScreen">
<div class="start-card">
<div class="ai-label"><i></i> SELF-PLAYING DEMO</div>
<div class="start-title">CLICK OR PRESS A KEY TO START</div>
<div class="start-sub">Mouse / ← → to move · Catch <b>↔ Wide</b> and <b>••• Multiball</b></div>
</div>
</div>
<div class="pause-overlay" id="pauseOverlay" hidden>
<div class="pause-card">
<strong>PAUSED</strong>
<span>Press P or click to resume</span>
</div>
</div>
<div class="end-overlay" id="endOverlay" role="dialog" aria-modal="true" aria-labelledby="endTitle" hidden>
<div class="end-panel">
<div class="end-kicker" id="endKicker">RUN TERMINATED</div>
<h2 id="endTitle">GAME OVER</h2>
<p class="end-message" id="endMessage">The grid held this time. Recalibrate and retry.</p>
<div class="end-stats">
<div class="end-stat">
<span>FINAL SCORE</span>
<strong id="finalScore">000000</strong>
</div>
<div class="end-stat">
<span>BRICKS CLEARED</span>
<strong id="finalBricks">0 / 60</strong>
</div>
</div>
<button class="restart-button" id="restartButton" type="button">PLAY AGAIN</button>
<p class="restart-hint">or press any key to restart</p>
</div>
</div>
</div>
<footer class="footer">
<div class="power-legend">
<span><i>↔</i> WIDE · 10s</span>
<span><i>•••</i> MULTIBALL · ×3</span>
</div>
<div class="controls">
<span>MOUSE / ← → MOVE</span>
<span>P PAUSE</span>
<span>M SOUND</span>
</div>
</footer>
</div>
<script>
(() => {
"use strict";
const $ = id => document.getElementById(id);
const canvas = $("game");
const canvasWrap = $("canvasWrap");
const ctx = canvas.getContext("2d");
const scoreEl = $("score");
const livesEl = $("lives");
const remainingEl = $("remaining");
const modeLabel = $("modeLabel");
const startScreen = $("startScreen");
const pauseOverlay = $("pauseOverlay");
const endOverlay = $("endOverlay");
const endKicker = $("endKicker");
const endTitle = $("endTitle");
const endMessage = $("endMessage");
const finalScore = $("finalScore");
const finalBricks = $("finalBricks");
const restartButton = $("restartButton");
const soundButton = $("soundButton");
const soundGlyph = $("soundGlyph");
const soundText = $("soundText");
const W = 960;
const H = 600;
const LEFT = 24;
const RIGHT = 936;
const TOP = 24;
const ROWS = 6;
const COLS = 10;
const TOTAL_BRICKS = ROWS * COLS;
const BASE_PADDLE_WIDTH = 132;
const WIDE_PADDLE_WIDTH = 204;
const STEP = 1 / 240;
const MAX_BALL_SPEED = 540;
const DPR = Math.min(2, window.devicePixelRatio || 1);
canvas.width = W * DPR;
canvas.height = H * DPR;
ctx.setTransform(DPR, 0, 0, DPR, 0, 0);
const clamp = (v, min, max) => Math.max(min, Math.min(max, v));
let seed = 0x4a31bc29;
const seededRandom = () => {
seed = (Math.imul(seed, 1664525) + 1013904223) >>> 0;
return seed / 4294967296;
};
const stars = Array.from({ length: 95 }, () => ({
x: seededRandom() * W,
y: seededRandom() * H,
r: 0.35 + seededRandom() * 1.15,
phase: seededRandom() * Math.PI * 2,
speed: 0.35 + seededRandom() * 1.1
}));
const powerMap = new Map([
["0,1", "wide"], ["0,8", "multi"],
["2,0", "multi"], ["2,8", "wide"],
["3,4", "multi"], ["4,6", "wide"],
["5,4", "multi"], ["5,5", "wide"],
["5,1", "wide"], ["5,9", "multi"]
]);
const rowHues = [187, 214, 258, 315, 14, 42];
const paddle = {
x: W / 2,
y: 546,
w: BASE_PADDLE_WIDTH,
h: 14,
vx: 0
};
let mode = "demo";
let ended = false;
let paused = false;
let score = 0;
let lives = 3;
let remaining = TOTAL_BRICKS;
let gameTime = 0;
let visualTime = 0;
let wideTimer = 0;
let serveTimer = 0;
let shake = 0;
let demoResetTimer = 0;
let demoServeCount = 0;
let accumulator = 0;
let balls = [];
let bricks = [];
let particles = [];
let powerups = [];
let floaters = [];
let keys = Object.create(null);
let controlMode = "mouse";
let pointerTarget = W / 2;
let lastScore = -1;
let lastLives = -1;
let lastRemaining = -1;
let soundOn = true;
let audioCtx = null;
let lastBrickSound = -1;
function roundPath(x, y, w, h, r) {
const rr = Math.max(0, Math.min(r, Math.abs(w) / 2, Math.abs(h) / 2));
ctx.beginPath();
ctx.moveTo(x + rr, y);
ctx.arcTo(x + w, y, x + w, y + h, rr);
ctx.arcTo(x + w, y + h, x, y + h, rr);
ctx.arcTo(x, y + h, x, y, rr);
ctx.arcTo(x, y, x + w, y, rr);
ctx.closePath();
}
function initAudio() {
if (!soundOn) return;
try {
if (!audioCtx) {
const AudioContextClass = window.AudioContext || window.webkitAudioContext;
if (AudioContextClass) audioCtx = new AudioContextClass();
}
if (audioCtx && audioCtx.state === "suspended") {
audioCtx.resume().catch(() => {});
}
} catch (_) {
audioCtx = null;
}
}
function tone(freq, duration, type = "sine", volume = 0.025, slide = 0, delay = 0) {
if (!audioCtx || !soundOn || mode !== "play") return;
try {
const now = audioCtx.currentTime + delay;
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.type = type;
osc.frequency.setValueAtTime(Math.max(30, freq), now);
if (slide) {
osc.frequency.exponentialRampToValueAtTime(Math.max(30, freq + slide), now + duration);
}
gain.gain.setValueAtTime(0.0001, now);
gain.gain.exponentialRampToValueAtTime(volume, now + 0.008);
gain.gain.exponentialRampToValueAtTime(0.0001, now + duration);
osc.connect(gain);
gain.connect(audioCtx.destination);
osc.start(now);
osc.stop(now + duration + 0.025);
osc.onended = () => {
osc.disconnect();
gain.disconnect();
};
} catch (_) {}
}
function toggleSound() {
soundOn = !soundOn;
soundButton.classList.toggle("muted", !soundOn);
soundButton.setAttribute("aria-pressed", String(soundOn));
soundGlyph.textContent = soundOn ? "♪" : "×";
soundText.textContent = soundOn ? "SOUND ON" : "SOUND OFF";
if (soundOn) {
initAudio();
tone(520, 0.07, "sine", 0.025, 120);
}
}
function updateHUD() {
if (score !== lastScore) {
scoreEl.textContent = String(score).padStart(6, "0");
lastScore = score;
}
if (remaining !== lastRemaining) {
remainingEl.textContent = `${remaining} BRICK${remaining === 1 ? "" : "S"}`;
lastRemaining = remaining;
}
if (lives !== lastLives) {
livesEl.replaceChildren();
for (let i = 0; i < 3; i++) {
const dot = document.createElement("span");
dot.className = "life" + (i < lives ? "" : " lost");
livesEl.appendChild(dot);
}
livesEl.setAttribute("aria-label", `${lives} ${lives === 1 ? "life" : "lives"}`);
lastLives = lives;
}
}
function buildLevel() {
bricks = [];
const bw = 80;
const bh = 26;
const gapX = 10;
const gapY = 10;
const startX = 35;
const startY = 82;
for (let r = 0; r < ROWS; r++) {
for (let c = 0; c < COLS; c++) {
const tough =
((r === 1 || r === 4) && c % 3 === 1) ||
(r === 2 && (c === 4 || c === 5)) ||
(r === 5 && c % 4 === 0);
bricks.push({
x: startX + c * (bw + gapX),
y: startY + r * (bh + gapY),
w: bw,
h: bh,
row: r,
col: c,
hue: rowHues[r],
hp: tough ? 2 : 1,
maxHp: tough ? 2 : 1,
power: powerMap.get(`${r},${c}`) || null,
alive: true,
flash: 0,
seed: r * COLS + c
});
}
}
remaining = bricks.length;
}
function newBall(x, y, speed = 410, stuck = false) {
return {
x, y,
vx: 0,
vy: 0,
r: 7,
speed,
stuck,
dead: false,
hue: 188
};
}
function serveBall() {
const ball = newBall(paddle.x, paddle.y - 11, 410, true);
balls = [ball];
serveTimer = mode === "demo" ? 0.35 : 0.9;
}
function clearWorld() {
buildLevel();
balls = [];
particles = [];
powerups = [];
floaters = [];
paddle.x = W / 2;
paddle.w = BASE_PADDLE_WIDTH;
paddle.vx = 0;
pointerTarget = W / 2;
wideTimer = 0;
serveTimer = 0;
shake = 0;
demoResetTimer = 0;
demoServeCount = 0;
gameTime = 0;
accumulator = 0;
}
function resetDemo() {
mode = "demo";
ended = false;
paused = false;
score = 0;
lives = 3;
clearWorld();
wideTimer = 8;
serveBall();
startScreen.hidden = false;
pauseOverlay.hidden = true;
endOverlay.hidden = true;
modeLabel.textContent = "AI DEMO";
updateHUD();
}
function startGame() {
initAudio();
mode = "play";
ended = false;
paused = false;
score = 0;
lives = 3;
keys = Object.create(null);
clearWorld();
serveBall();
startScreen.hidden = true;
pauseOverlay.hidden = true;
endOverlay.hidden = true;
endOverlay.classList.remove("victory");
modeLabel.textContent = "LIVE PLAY";
updateHUD();
tone(420, 0.08, "sine", 0.025, 160);
tone(650, 0.1, "sine", 0.018, 100, 0.07);
}
function launchStuckBall(ball) {
let angle;
if (mode === "demo") {
angle = demoServeCount === 0 ? -0.18 : (Math.random() - 0.5) * 0.46;
demoServeCount++;
} else {
angle = (Math.random() - 0.5) * 0.24;
}
ball.stuck = false;
ball.vx = Math.sin(angle) * ball.speed;
ball.vy = -Math.cos(angle) * ball.speed;
}
function addFloater(x, y, text, color = "#dffaff", life = 0.85) {
floaters.push({ x, y, text, color, life, maxLife: life });
if (floaters.length > 35) floaters.shift();
}
function burst(x, y, hue, count, force = 1) {
for (let i = 0; i < count && particles.length < 520; i++) {
const a = Math.random() * Math.PI * 2;
const speed = (55 + Math.random() * 190) * force;
const life = 0.35 + Math.random() * 0.55;
particles.push({
x, y,
vx: Math.cos(a) * speed,
vy: Math.sin(a) * speed - 28 * force,
gravity: 145 + Math.random() * 85,
hue,
size: 1.8 + Math.random() * 4.1,
rotation: Math.random() * Math.PI,
spin: (Math.random() - 0.5) * 10,
life,
maxLife: life
});
}
}
function damageBrick(brick, ball) {
brick.flash = 1;
if (brick.hp > 1) {
brick.hp--;
score += 25;
ball.speed = Math.min(MAX_BALL_SPEED, ball.speed + 1);
shake = Math.max(shake, 2.2);
burst(ball.x, ball.y, brick.hue, 7, 0.48);
addFloater(brick.x + brick.w / 2, brick.y - 3, "CRACKED", "#eaf7ff", 0.55);
if (gameTime - lastBrickSound > 0.025) {
tone(205 + brick.hue * 0.35, 0.045, "square", 0.014, 45);
lastBrickSound = gameTime;
}
} else {
brick.hp = 0;
brick.alive = false;
remaining--;
const points = brick.maxHp === 2 ? 200 : 100;
score += points;
ball.speed = Math.min(MAX_BALL_SPEED, ball.speed + 3.5);
shake = Math.max(shake, brick.maxHp === 2 ? 5 : 3.2);
burst(brick.x + brick.w / 2, brick.y + brick.h / 2, brick.hue, brick.maxHp === 2 ? 25 : 19, 1.05);
addFloater(brick.x + brick.w / 2, brick.y, `+${points}`, brick.maxHp === 2 ? "#fff1b8" : "#dffaff");
if (brick.power) {
powerups.push({
x: brick.x + brick.w / 2,
baseX: brick.x + brick.w / 2,
y: brick.y + brick.h / 2,
vy: mode === "demo" ? 145 : 128,
type: brick.power,
phase: Math.random() * Math.PI * 2,
rotation: 0,
dead: false
});
}
if (gameTime - lastBrickSound > 0.025) {
tone(310 + brick.hue * 0.6, 0.065, "triangle", 0.024, 125);
lastBrickSound = gameTime;
}
}
updateHUD();
}
function collectPowerup(powerup) {
const hue = powerup.type === "wide" ? 184 : 278;
burst(powerup.x, powerup.y, hue, 25, 0.9);
shake = Math.max(shake, 3.5);
if (powerup.type === "wide") {
wideTimer = 10;
score += 100;
addFloater(paddle.x, paddle.y - 30, "WIDE PADDLE · 10s", "#8bf4ff", 1.15);
tone(430, 0.09, "sine", 0.03, 250);
tone(720, 0.12, "sine", 0.02, 160, 0.07);
} else {
const needed = Math.max(0, 3 - balls.length);
if (needed === 0) {
score += 300;
addFloater(paddle.x, paddle.y - 30, "MULTI MAX · +300", "#d8b6ff", 1.05);
} else {
score += 150;
const baseSpeed = balls.reduce((m, b) => Math.max(m, b.speed), 410);
const angles = needed === 2 ? [-0.52, 0.52] : (needed === 3 ? [-0.55, 0, 0.55] : [Math.random() < 0.5 ? -0.48 : 0.48]);
for (let i = 0; i < needed; i++) {
const nb = newBall(paddle.x + (i - (needed - 1) / 2) * 13, paddle.y - 31, baseSpeed, false);
nb.hue = i % 2 === 0 ? 278 : 43;
nb.vx = Math.sin(angles[i]) * nb.speed;
nb.vy = -Math.cos(angles[i]) * nb.speed;
balls.push(nb);
}
addFloater(paddle.x, paddle.y - 30, "MULTIBALL · 3 BALLS", "#ddbaff", 1.15);
}
tone(360, 0.08, "triangle", 0.03, 200);
tone(590, 0.1, "triangle", 0.025, 250, 0.07);
tone(850, 0.12, "sine", 0.018, 120, 0.14);
}
updateHUD();
}
function finish(victory) {
ended = true;
paused = false;
pauseOverlay.hidden = true;
startScreen.hidden = true;
endOverlay.hidden = false;
endOverlay.classList.toggle("victory", victory);
endKicker.textContent = victory ? "SECTOR COMPLETE" : "RUN TERMINATED";
endTitle.textContent = victory ? "VICTORY!" : "GAME OVER";
endMessage.textContent = victory
? "The Neon Grid is clear. Perfectly played."
: "The grid held this time. Recalibrate and retry.";
finalScore.textContent = String(score).padStart(6, "0");
finalBricks.textContent = `${TOTAL_BRICKS - remaining} / ${TOTAL_BRICKS}`;
modeLabel.textContent = victory ? "VICTORY" : "GAME OVER";
if (victory) {
tone(523, 0.16, "triangle", 0.028, 35);
tone(659, 0.16, "triangle", 0.028, 35, 0.13);
tone(784, 0.25, "triangle", 0.03, 70, 0.26);
} else {
tone(210, 0.22, "sawtooth", 0.024, -90);
tone(120, 0.3, "sine", 0.025, -45, 0.17);
}
}
function loseLife() {
powerups = [];
wideTimer = 0;
shake = Math.max(shake, 8);
if (mode === "demo") {
serveBall();
return;
}
lives--;
updateHUD();
if (lives <= 0) {
finish(false);
} else {
serveBall();
addFloater(W / 2, 410, `${lives} ${lives === 1 ? "LIFE" : "LIVES"} LEFT`, "#ffafc2", 1.2);
tone(180, 0.2, "sawtooth", 0.022, -75);
}
}
function reflectedFieldX(x) {
const min = LEFT + 7;
const max = RIGHT - 7;
const span = max - min;
let n = (x - min) % (span * 2);
if (n < 0) n += span * 2;
return min + (n <= span ? n : span * 2 - n);
}
function updatePaddle(dt) {
const oldX = paddle.x;
const targetWidth = wideTimer > 0 ? WIDE_PADDLE_WIDTH : BASE_PADDLE_WIDTH;
paddle.w += (targetWidth - paddle.w) * Math.min(1, dt * 8);
if (mode === "demo") {
const active = balls.filter(b => !b.stuck && !b.dead);
active.sort((a, b) => b.y - a.y);
const threat = active.find(b => b.vy > 0) || active[0];
let target = paddle.x;
if (threat) {
if (threat.vy > 0) {
const t = (paddle.y - threat.r - threat.y) / threat.vy;
target = t > 0 ? reflectedFieldX(threat.x + threat.vx * t) : threat.x;
} else {
target = threat.x + threat.vx * 0.12;
}
}
const drops = powerups
.filter(p => !p.dead && p.y > 170 && p.y < paddle.y)
.sort((a, b) => b.y - a.y);
const drop = drops[0];
if (drop) {
const ballTime = threat && threat.vy > 0
? (paddle.y - threat.y) / Math.max(1, threat.vy)
: Infinity;
if (!threat || threat.vy < 0 || ballTime > 1.1) target = drop.x;
}
target += Math.sin(gameTime * 2.1) * 1.5;
const maxMove = 700 * dt;
paddle.x += clamp(target - paddle.x, -maxMove, maxMove);
} else {
const direction = (keys.ArrowRight ? 1 : 0) - (keys.ArrowLeft ? 1 : 0);
if (direction) {
controlMode = "keys";
paddle.x += direction * 620 * dt;
} else if (controlMode === "mouse") {
paddle.x += (pointerTarget - paddle.x) * Math.min(1, dt * 28);
}
}
paddle.x = clamp(paddle.x, LEFT + paddle.w / 2 + 2, RIGHT - paddle.w / 2 - 2);
paddle.vx = (paddle.x - oldX) / dt;
}
function sweepAABB(px, py, dx, dy, left, top, right, bottom) {
let txEntry = -Infinity;
let txExit = Infinity;
let tyEntry = -Infinity;
let tyExit = Infinity;
if (Math.abs(dx) < 1e-10) {
if (px < left || px > right) return null;
} else {
const a = (left - px) / dx;
const b = (right - px) / dx;
txEntry = Math.min(a, b);
txExit = Math.max(a, b);
}
if (Math.abs(dy) < 1e-10) {
if (py < top || py > bottom) return null;
} else {
const a = (top - py) / dy;
const b = (bottom - py) / dy;
tyEntry = Math.min(a, b);
tyExit = Math.max(a, b);
}
const entry = Math.max(txEntry, tyEntry);
const exit = Math.min(txExit, tyExit);
if (entry > exit || entry < 0 || entry > 1 || exit < 0) return null;
let nx = 0;
let ny = 0;
if (txEntry > tyEntry) nx = dx > 0 ? -1 : 1;
else ny = dy > 0 ? -1 : 1;
return { t: entry, nx, ny };
}
function reflectBall(ball, nx, ny) {
if (nx) ball.vx = -ball.vx;
if (ny) ball.vy = -ball.vy;
}
function normalizeBall(ball) {
const magnitude = Math.hypot(ball.vx, ball.vy) || 1;
ball.vx = ball.vx / magnitude * ball.speed;
ball.vy = ball.vy / magnitude * ball.speed;
}
function paddleBounce(ball) {
const offset = clamp((ball.x - paddle.x) / (paddle.w / 2), -1, 1);
const angle = offset * 1.15;
ball.speed = Math.min(MAX_BALL_SPEED, ball.speed + 1.5);
ball.vx = Math.sin(angle) * ball.speed;
ball.vy = -Math.cos(angle) * ball.speed;
ball.y = paddle.y - ball.r - 0.08;
burst(ball.x, paddle.y, 188, 5, 0.35);
tone(175 + Math.abs(offset) * 65, 0.055, "sine", 0.022, 80);
}
function moveBall(ball, dt) {
let timeLeft = dt;
for (let iteration = 0; iteration < 7 && timeLeft > 0.000001; iteration++) {
const dx = ball.vx * timeLeft;
const dy = ball.vy * timeLeft;
let hit = null;
const offer = (t, nx, ny, type, object = null) => {
if (t < -1e-8 || t > 1) return;
if (ball.vx * nx + ball.vy * ny >= -1e-7) return;
t = Math.max(0, t);
if (!hit || t < hit.t) hit = { t, nx, ny, type, object };
};
if (dx < 0) offer((LEFT + ball.r - ball.x) / dx, 1, 0, "wall");
if (dx > 0) offer((RIGHT - ball.r - ball.x) / dx, -1, 0, "wall");
if (dy < 0) offer((TOP + ball.r - ball.y) / dy, 0, 1, "wall");
if (ball.vy > 0) {
const result = sweepAABB(
ball.x, ball.y, dx, dy,
paddle.x - paddle.w / 2 - ball.r,
paddle.y - ball.r,
paddle.x + paddle.w / 2 + ball.r,
paddle.y + paddle.h + ball.r
);
if (result) offer(result.t, result.nx, result.ny, "paddle", paddle);
}
for (const brick of bricks) {
if (!brick.alive) continue;
if (
ball.x + Math.abs(dx) + ball.r < brick.x ||
ball.x - Math.abs(dx) - ball.r > brick.x + brick.w ||
ball.y + Math.abs(dy) + ball.r < brick.y ||
ball.y - Math.abs(dy) - ball.r > brick.y + brick.h
) continue;
const result = sweepAABB(
ball.x, ball.y, dx, dy,
brick.x - ball.r,
brick.y - ball.r,
brick.x + brick.w + ball.r,
brick.y + brick.h + ball.r
);
if (result) offer(result.t, result.nx, result.ny, "brick", brick);
}
if (!hit) {
ball.x += dx;
ball.y += dy;
break;
}
ball.x += dx * hit.t + hit.nx * 0.055;
ball.y += dy * hit.t + hit.ny * 0.055;
if (hit.type === "paddle" && hit.ny === -1) {
paddleBounce(ball);
} else {
reflectBall(ball, hit.nx, hit.ny);
if (hit.type === "brick") {
damageBrick(hit.object, ball);
normalizeBall(ball);
}
}
timeLeft *= 1 - hit.t;
if (hit.t < 1e-7) timeLeft = Math.max(0, timeLeft - 0.00001);
}
if (ball.y - ball.r > H + 26) ball.dead = true;
}
function updatePowerups(dt) {
for (const p of powerups) {
p.y += p.vy * dt;
p.x = p.baseX + Math.sin(gameTime * 3.8 + p.phase) * 8;
p.rotation += dt * 1.8;
const caught =
p.y + 15 >= paddle.y &&
p.y - 15 <= paddle.y + paddle.h &&
p.x + 14 >= paddle.x - paddle.w / 2 &&
p.x - 14 <= paddle.x + paddle.w / 2;
if (caught) {
p.dead = true;
collectPowerup(p);
} else if (p.y > H + 35) {
p.dead = true;
}
}
powerups = powerups.filter(p => !p.dead);
}
function updateEffects(dt) {
for (const brick of bricks) {
if (brick.flash > 0) brick.flash = Math.max(0, brick.flash - dt * 5.5);
}
for (const p of particles) {
p.life -= dt;
p.x += p.vx * dt;
p.y += p.vy * dt;
p.vy += p.gravity * dt;
const drag = Math.exp(-1.25 * dt);
p.vx *= drag;
p.rotation += p.spin * dt;
}
particles = particles.filter(p => p.life > 0);
for (const f of floaters) {
f.life -= dt;
f.y -= 27 * dt;
}
floaters = floaters.filter(f => f.life > 0);
shake = Math.max(0, shake - dt * 19);
}
function update(dt) {
if (ended) return;
gameTime += dt;
if (mode === "demo" && demoResetTimer > 0) {
demoResetTimer -= dt;
if (demoResetTimer <= 0) {
resetDemo();
return;
}
}
wideTimer = Math.max(0, wideTimer - dt);
updatePaddle(dt);
if (serveTimer > 0) {
serveTimer -= dt;
const ball = balls[0];
if (ball && ball.stuck) {
ball.x = paddle.x;
ball.y = paddle.y - ball.r - 4;
}
if (serveTimer <= 0 && ball && ball.stuck) launchStuckBall(ball);
}
for (const ball of balls) {
if (!ball.stuck && !ball.dead) moveBall(ball, dt);
}
balls = balls.filter(b => !b.dead);
updatePowerups(dt);
updateEffects(dt);
if (remaining === 0) {
if (mode === "demo") {
if (demoResetTimer <= 0) {
demoResetTimer = 1.8;
addFloater(W / 2, 390, "SECTOR CLEARED", "#8bf4ff", 1.5);
}
} else {
finish(true);
}
} else if (balls.length === 0) {
loseLife();
}
}
function drawBackground() {
const gradient = ctx.createLinearGradient(0, 0, 0, H);
gradient.addColorStop(0, "#0b1730");
gradient.addColorStop(0.48, "#081222");
gradient.addColorStop(1, "#050a15");
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, W, H);
const aura = ctx.createRadialGradient(W * 0.5, 85, 10, W * 0.5, 85, 520);
aura.addColorStop(0, "rgba(64, 132, 218, 0.16)");
aura.addColorStop(0.5, "rgba(65, 70, 160, 0.045)");
aura.addColorStop(1, "rgba(0, 0, 0, 0)");
ctx.fillStyle = aura;
ctx.fillRect(0, 0, W, H);
for (const star of stars) {
const alpha = 0.13 + (Math.sin(visualTime * star.speed + star.phase) + 1) * 0.12;
ctx.fillStyle = `rgba(177, 215, 255, ${alpha})`;
ctx.beginPath();
ctx.arc(star.x, star.y, star.r, 0, Math.PI * 2);
ctx.fill();
}
ctx.strokeStyle = "rgba(103, 151, 214, 0.035)";
ctx.lineWidth = 1;
ctx.beginPath();
for (let x = LEFT; x <= RIGHT; x += 48) {
ctx.moveTo(x, TOP);
ctx.lineTo(x, H);
}
for (let y = TOP; y <= H; y += 48) {
ctx.moveTo(LEFT, y);
ctx.lineTo(RIGHT, y);
}
ctx.stroke();
const danger = ctx.createLinearGradient(0, 510, 0, H);
danger.addColorStop(0, "rgba(255, 74, 116, 0)");
danger.addColorStop(1, "rgba(255, 65, 111, 0.045)");
ctx.fillStyle = danger;
ctx.fillRect(LEFT, 500, RIGHT - LEFT, 100);
ctx.save();
ctx.strokeStyle = "rgba(113, 187, 244, 0.23)";
ctx.lineWidth = 1.4;
ctx.shadowColor = "rgba(73, 180, 255, 0.28)";
ctx.shadowBlur = 10;
ctx.beginPath();
ctx.moveTo(LEFT, H - 18);
ctx.lineTo(LEFT, TOP + 12);
ctx.quadraticCurveTo(LEFT, TOP, LEFT + 12, TOP);
ctx.lineTo(RIGHT - 12, TOP);
ctx.quadraticCurveTo(RIGHT, TOP, RIGHT, TOP + 12);
ctx.lineTo(RIGHT, H - 18);
ctx.stroke();
ctx.restore();
}
function drawFieldStatus() {
ctx.save();
ctx.textBaseline = "middle";
ctx.font = "750 10px ui-sans-serif, system-ui, sans-serif";
ctx.textAlign = "left";
if (wideTimer > 0) {
ctx.fillStyle = "#7cefff";
ctx.shadowColor = "rgba(104,232,255,.55)";
ctx.shadowBlur = 8;
ctx.fillText(`↔ WIDE PADDLE ${wideTimer.toFixed(1)}s`, 36, 47);
} else {
ctx.fillStyle = "rgba(155, 179, 213, 0.72)";
ctx.fillText("SECTOR 01 // BREAK THE GRID", 36, 47);
}
ctx.shadowBlur = 0;
if (balls.length > 1) {
ctx.textAlign = "center";
ctx.fillStyle = "#d3b2ff";
ctx.fillText(`● ● ● MULTIBALL ×${balls.length}`, W / 2 + 40, 47);
}
ctx.textAlign = "right";
ctx.fillStyle = "rgba(168, 192, 224, 0.8)";
ctx.fillText(`${remaining} REMAINING`, 924, 47);
const progress = (TOTAL_BRICKS - remaining) / TOTAL_BRICKS;
roundPath(35, 59, 890, 3, 1.5);
ctx.fillStyle = "rgba(125, 164, 216, 0.1)";
ctx.fill();
if (progress > 0) {
const pg = ctx.createLinearGradient(35, 0, 925, 0);
pg.addColorStop(0, "#56e1f5");
pg.addColorStop(0.55, "#879cff");
pg.addColorStop(1, "#e498e4");
roundPath(35, 59, Math.max(3, 890 * progress), 3, 1.5);
ctx.fillStyle = pg;
ctx.shadowColor = "rgba(104,232,255,.45)";
ctx.shadowBlur = 7;
ctx.fill();
}
ctx.restore();
}
function drawPowerIcon(type, x, y, scale = 1, color = "#ffffff") {
ctx.save();
ctx.translate(x, y);
ctx.scale(scale, scale);
ctx.strokeStyle = color;
ctx.fillStyle = color;
ctx.lineWidth = 2;
ctx.lineCap = "round";
ctx.lineJoin = "round";
if (type === "wide") {
ctx.beginPath();
ctx.moveTo(-9, 0);
ctx.lineTo(9, 0);
ctx.moveTo(-9, 0);
ctx.lineTo(-5, -4);
ctx.moveTo(-9, 0);
ctx.lineTo(-5, 4);
ctx.moveTo(9, 0);
ctx.lineTo(5, -4);
ctx.moveTo(9, 0);
ctx.lineTo(5, 4);
ctx.stroke();
} else {
ctx.beginPath();
ctx.arc(-6, 2, 2.6, 0, Math.PI * 2);
ctx.arc(0, -3, 2.6, 0, Math.PI * 2);
ctx.arc(6, 2, 2.6, 0, Math.PI * 2);
ctx.fill();
}
ctx.restore();
}
function drawCracks(brick) {
const x = brick.x;
const y = brick.y;
const shift = (brick.seed % 5) - 2;
ctx.beginPath();
ctx.moveTo(x + brick.w * 0.43 + shift, y + 1);
ctx.lineTo(x + brick.w * 0.38 + shift, y + 7);
ctx.lineTo(x + brick.w * 0.48 + shift, y + 12);
ctx.lineTo(x + brick.w * 0.41 + shift, y + 18);
ctx.lineTo(x + brick.w * 0.46 + shift, y + brick.h - 1);
ctx.moveTo(x + brick.w * 0.48 + shift, y + 12);
ctx.lineTo(x + brick.w * 0.63 + shift, y + 8);
ctx.lineTo(x + brick.w * 0.72 + shift, y + 11);
ctx.moveTo(x + brick.w * 0.38 + shift, y + 7);
ctx.lineTo(x + brick.w * 0.27 + shift, y + 10);
ctx.strokeStyle = "rgba(3, 8, 19, 0.82)";
ctx.lineWidth = 2.2;
ctx.stroke();
ctx.strokeStyle = "rgba(242, 251, 255, 0.72)";
ctx.lineWidth = 0.7;
ctx.stroke();
}
function drawBrick(brick) {
if (!brick.alive) return;
const { x, y, w, h, hue } = brick;
ctx.save();
ctx.shadowColor = `hsla(${hue}, 90%, 62%, ${brick.power ? 0.42 : 0.2})`;
ctx.shadowBlur = brick.power ? 13 : 7;
const gradient = ctx.createLinearGradient(0, y, 0, y + h);
gradient.addColorStop(0, `hsl(${hue}, 92%, ${brick.maxHp === 2 ? 65 : 67}%)`);
gradient.addColorStop(0.46, `hsl(${hue}, 79%, 51%)`);
gradient.addColorStop(1, `hsl(${hue}, 72%, 29%)`);
roundPath(x, y, w, h, 6);
ctx.fillStyle = gradient;
ctx.fill();
ctx.shadowBlur = 0;
roundPath(x + 1, y + 1, w - 2, h - 2, 5);
ctx.strokeStyle = brick.maxHp === 2
? "rgba(235, 246, 255, 0.66)"
: "rgba(255, 255, 255, 0.24)";
ctx.lineWidth = brick.maxHp === 2 ? 1.25 : 0.8;
ctx.stroke();
ctx.beginPath();
ctx.moveTo(x + 7, y + 4);
ctx.lineTo(x + w - 7, y + 4);
ctx.strokeStyle = "rgba(255, 255, 255, 0.27)";
ctx.lineWidth = 1;
ctx.stroke();
if (brick.maxHp === 2) {
roundPath(x + 4, y + 4, w - 8, h - 8, 3);
ctx.strokeStyle = "rgba(9, 20, 38, 0.26)";
ctx.lineWidth = 1;
ctx.stroke();
for (let i = 0; i < 2; i++) {
ctx.fillStyle = i < brick.hp ? "rgba(255,255,255,.88)" : "rgba(7,14,28,.35)";
ctx.fillRect(x + 7 + i * 5, y + 7, 2.5, 6);
}
ctx.fillStyle = "rgba(255,255,255,.54)";
for (const rx of [x + w - 8, x + 8]) {
ctx.beginPath();
ctx.arc(rx, y + h - 7, 1.25, 0, Math.PI * 2);
ctx.fill();
}
}
if (brick.maxHp === 2 && brick.hp === 1) drawCracks(brick);
if (brick.flash > 0) {
roundPath(x, y, w, h, 6);
ctx.fillStyle = `rgba(255,255,255,${brick.flash * 0.52})`;
ctx.fill();
}
if (brick.power) {
const tokenColor = brick.power === "wide" ? "#8bf4ff" : "#e0b9ff";
roundPath(x + w / 2 - 17, y + h / 2 - 9, 34, 18, 7);
ctx.fillStyle = "rgba(4, 10, 24, 0.66)";
ctx.fill();
ctx.strokeStyle = brick.power === "wide"
? "rgba(139,244,255,.62)"
: "rgba(224,185,255,.62)";
ctx.lineWidth = 0.9;
ctx.stroke();
drawPowerIcon(brick.power, x + w / 2, y + h / 2, 0.75, tokenColor);
}
ctx.restore();
}
function drawParticles() {
ctx.save();
ctx.globalCompositeOperation = "lighter";
for (const p of particles) {
const alpha = clamp(p.life / p.maxLife, 0, 1);
ctx.save();
ctx.globalAlpha = alpha;
ctx.translate(p.x, p.y);
ctx.rotate(p.rotation);
ctx.fillStyle = `hsl(${p.hue}, 95%, 66%)`;
ctx.shadowColor = `hsl(${p.hue}, 95%, 62%)`;
ctx.shadowBlur = 5;
const size = p.size * (0.45 + alpha * 0.55);
ctx.fillRect(-size / 2, -size / 2, size * 1.35, size);
ctx.restore();
}
ctx.restore();
}
function drawPowerup(p) {
const color = p.type === "wide" ? "#78edff" : "#d6a8ff";
const hue = p.type === "wide" ? 185 : 278;
const pulse = 1 + Math.sin(visualTime * 7 + p.phase) * 0.05;
ctx.save();
ctx.translate(p.x, p.y);
ctx.scale(pulse, pulse);
const trail = ctx.createLinearGradient(0, -34, 0, -8);
trail.addColorStop(0, `hsla(${hue},90%,65%,0)`);
trail.addColorStop(1, `hsla(${hue},90%,65%,.32)`);
ctx.fillStyle = trail;
ctx.fillRect(-2, -34, 4, 23);
ctx.shadowColor = color;
ctx.shadowBlur = 20;
const g = ctx.createRadialGradient(-4, -5, 1, 0, 0, 17);
g.addColorStop(0, "rgba(255,255,255,.95)");
g.addColorStop(0.2, color);
g.addColorStop(1, `hsl(${hue},65%,27%)`);
ctx.fillStyle = g;
ctx.beginPath();
ctx.arc(0, 0, 15, 0, Math.PI * 2);
ctx.fill();
ctx.shadowBlur = 0;
ctx.strokeStyle = "rgba(255,255,255,.7)";
ctx.lineWidth = 1;
ctx.beginPath();
ctx.arc(0, 0, 12.5, 0, Math.PI * 2);
ctx.stroke();
drawPowerIcon(p.type, 0, 0, 0.9, "#07101e");
ctx.font = "800 7px ui-sans-serif, system-ui, sans-serif";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillStyle = color;
ctx.shadowColor = color;
ctx.shadowBlur = 7;
ctx.fillText(p.type === "wide" ? "WIDE" : "MULTI", 0, 24);
ctx.restore();
}
function drawPaddle() {
const x = paddle.x - paddle.w / 2;
const y = paddle.y;
ctx.save();
if (Math.abs(paddle.vx) > 100 && !paused) {
const direction = Math.sign(paddle.vx);
roundPath(x - direction * 7, y + 3, paddle.w, paddle.h - 3, 7);
ctx.fillStyle = "rgba(78, 195, 255, 0.09)";
ctx.fill();
}
ctx.shadowColor = wideTimer > 0 ? "#60efff" : "#52c8ff";
ctx.shadowBlur = wideTimer > 0 ? 24 : 15;
const gradient = ctx.createLinearGradient(x, 0, x + paddle.w, 0);
gradient.addColorStop(0, "#328ac9");
gradient.addColorStop(0.18, "#72dffa");
gradient.addColorStop(0.5, "#f2feff");
gradient.addColorStop(0.82, "#72dffa");
gradient.addColorStop(1, "#328ac9");
roundPath(x, y, paddle.w, paddle.h, 7);
ctx.fillStyle = gradient;
ctx.fill();
ctx.shadowBlur = 0;
roundPath(x + 4, y + 2, paddle.w - 8, 3, 1.5);
ctx.fillStyle = "rgba(255,255,255,.68)";
ctx.fill();
ctx.fillStyle = "rgba(5,28,49,.46)";
roundPath(paddle.x - 15, y + 7, 30, 3, 1.5);
ctx.fill();
if (wideTimer > 0) {
const barWidth = paddle.w * 0.62;
roundPath(paddle.x - barWidth / 2, y + 21, barWidth, 3, 1.5);
ctx.fillStyle = "rgba(103,212,239,.15)";
ctx.fill();
roundPath(paddle.x - barWidth / 2, y + 21, barWidth * wideTimer / 10, 3, 1.5);
ctx.fillStyle = "#71eaff";
ctx.shadowColor = "#71eaff";
ctx.shadowBlur = 7;
ctx.fill();
}
ctx.restore();
}
function drawBall(ball) {
ctx.save();
if (!ball.stuck) {
const magnitude = Math.hypot(ball.vx, ball.vy) || 1;
const ux = ball.vx / magnitude;
const uy = ball.vy / magnitude;
for (let i = 5; i >= 1; i--) {
const alpha = (6 - i) * 0.025;
ctx.fillStyle = `hsla(${ball.hue},95%,70%,${alpha})`;
ctx.beginPath();
ctx.arc(
ball.x - ux * i * 6,
ball.y - uy * i * 6,
ball.r * (0.42 + (5 - i) * 0.075),
0,
Math.PI * 2
);
ctx.fill();
}
} else {
ctx.setLineDash([3, 7]);
ctx.strokeStyle = "rgba(134,229,255,.25)";
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(ball.x, ball.y - 13);
ctx.lineTo(ball.x, ball.y - 62);
ctx.stroke();
ctx.setLineDash([]);
ctx.font = "700 8px ui-sans-serif, system-ui, sans-serif";
ctx.textAlign = "center";
ctx.fillStyle = "rgba(151,195,224,.65)";
ctx.fillText("AUTO LAUNCH", ball.x, ball.y - 71);
}
ctx.shadowColor = `hsl(${ball.hue},95%,65%)`;
ctx.shadowBlur = 19;
const gradient = ctx.createRadialGradient(
ball.x - 2.3, ball.y - 2.7, 0.5,
ball.x, ball.y, ball.r
);
gradient.addColorStop(0, "#ffffff");
gradient.addColorStop(0.32, "#e9fdff");
gradient.addColorStop(0.7, `hsl(${ball.hue},90%,68%)`);
gradient.addColorStop(1, `hsl(${ball.hue},80%,40%)`);
ctx.fillStyle = gradient;
ctx.beginPath();
ctx.arc(ball.x, ball.y, ball.r, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
function drawFloaters() {
ctx.save();
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.font = "800 11px ui-sans-serif, system-ui, sans-serif";
for (const f of floaters) {
const alpha = clamp(f.life / f.maxLife, 0, 1);
ctx.globalAlpha = Math.min(1, alpha * 1.8);
ctx.fillStyle = f.color;
ctx.shadowColor = f.color;
ctx.shadowBlur = 8;
ctx.fillText(f.text, f.x, f.y);
}
ctx.restore();
}
function drawVignette() {
const vignette = ctx.createRadialGradient(W / 2, H / 2, 190, W / 2, H / 2, 590);
vignette.addColorStop(0, "rgba(0,0,0,0)");
vignette.addColorStop(0.75, "rgba(0,0,0,.04)");
vignette.addColorStop(1, "rgba(0,0,0,.34)");
ctx.fillStyle = vignette;
ctx.fillRect(0, 0, W, H);
}
function render() {
ctx.setTransform(DPR, 0, 0, DPR, 0, 0);
ctx.clearRect(0, 0, W, H);
drawBackground();
drawFieldStatus();
ctx.save();
if (shake > 0 && !paused) {
ctx.translate(
Math.sin(visualTime * 91) * shake * 0.55,
Math.cos(visualTime * 77) * shake * 0.45
);
}
for (const brick of bricks) drawBrick(brick);
drawParticles();
for (const powerup of powerups) drawPowerup(powerup);
drawPaddle();
for (const ball of balls) drawBall(ball);
drawFloaters();
ctx.restore();
drawVignette();
}
function setPointer(event) {
const rect = canvas.getBoundingClientRect();
if (!rect.width) return;
pointerTarget = clamp((event.clientX - rect.left) / rect.width * W, LEFT, RIGHT);
}
function togglePause() {
if (mode !== "play" || ended) return;
paused = !paused;
pauseOverlay.hidden = !paused;
modeLabel.textContent = paused ? "PAUSED" : "LIVE PLAY";
accumulator = 0;
}
canvasWrap.addEventListener("pointerdown", event => {
if (event.button !== undefined && event.button !== 0) return;
event.preventDefault();
canvas.focus({ preventScroll: true });
if (mode === "demo") startGame();
if (paused) togglePause();
if (!ended) {
setPointer(event);
controlMode = "mouse";
}
}, { passive: false });
canvasWrap.addEventListener("pointermove", event => {
if (mode === "play" && !ended) {
setPointer(event);
controlMode = "mouse";
}
});
endOverlay.addEventListener("pointerdown", event => event.stopPropagation());
restartButton.addEventListener("click", event => {
event.stopPropagation();
startGame();
canvas.focus({ preventScroll: true });
});
soundButton.addEventListener("click", event => {
event.stopPropagation();
toggleSound();
});
window.addEventListener("keydown", event => {
const key = event.key;
if (key === "ArrowLeft" || key === "ArrowRight" || key === " " || key === "Spacebar") {
event.preventDefault();
}
let justStarted = false;
if (mode === "demo") {
startGame();
justStarted = true;
} else if (ended) {
if (!event.repeat) startGame();
justStarted = true;
}
const lower = key.toLowerCase();
if (!justStarted && lower === "p" && !event.repeat) {
togglePause();
return;
}
if (!justStarted && lower === "m" && !event.repeat) {
toggleSound();
return;
}
if (!justStarted && lower === "r" && !event.repeat && mode === "play") {
startGame();
return;
}
if (key === "ArrowLeft" || key === "ArrowRight") {
keys[key] = true;
controlMode = "keys";
}
}, { passive: false });
window.addEventListener("keyup", event => {
if (event.key === "ArrowLeft" || event.key === "ArrowRight") keys[event.key] = false;
});
window.addEventListener("blur", () => {
keys = Object.create(null);
});
let lastFrame = performance.now();
function frame(now) {
let dt = (now - lastFrame) / 1000;
lastFrame = now;
if (!Number.isFinite(dt) || dt < 0) dt = 0;
dt = Math.min(dt, 0.05);
visualTime += dt;
if (!paused) {
accumulator = Math.min(0.12, accumulator + dt);
let steps = 0;
while (accumulator >= STEP && steps < 30) {
update(STEP);
accumulator -= STEP;
steps++;
}
} else {
accumulator = 0;
}
render();
requestAnimationFrame(frame);
}
resetDemo();
render();
requestAnimationFrame(frame);
})();
</script>
</body>
</html>Reasoning trace (25,628 tokens)
I need build polished breakout single HTML canvas vanilla JS. I'll output exactly one html fenced. Should think through quality, self-playing demo behind "Click or press a key to start" message per instructions. Should have visual impressive at first paint with self playing demo. Full features robust collision no tunneling. I'll code responsive 1280x800 down to 375. Should handle scaling canvas and mouse. Should have powerups icons, wide 10sec, multi 3 balls. 6 rows normal/tough 2 hits crack. Lives score UI game over/victory restart. Self-playing AI demonstration no interaction; clear overlay start. Click starts real game resets. keyboard after click. But if pressing key within iframe initial perhaps keyboard not received but clicks works. Should pointer. Should self-playing demo active immediately. Could start game in demo mode with AI and overlay modest; bricks breaking and powerups visible. If click/key start resets to player game. Lives? demo should be self playing perhaps ball launch and AI follows. Should not end weird before screenshot. Fine.
I'll include no external resources. Use CSS system fonts, canvas gradients etc. Should have canvas full design within page. Could canvas area fixed internal 960x600 and scale CSS max. Layout header stats. To look polished, use dark space/navy background, neon glass. One page header title, subtitle, HUD top with score/lives, progress perhaps powerup pill, canvas 960x600, controls underneath. Render all in canvas including screens? Better DOM HUD for score lives and powerups? Requirement canvas game. Could entire game rendered on canvas and decorative DOM around. UI via DOM for controls overlays perhaps easier polished responsive. Canvas aspect 960x600. At 1280x800 page should fit no scrolling ideal. Header ~90, canvas 900x562? Controls footer. At 1280 with max width 1040. Canvas aspect 16:10. If width 960 height 600. At 1280x800 top 70, HUD 64, canvas height 540 perhaps controls 50. Should layout adapt. Use main max-width 1080; header 60; game-shell canvas ratio 16/10. At 1280x800 if width 960 canvas 600 + top 100 + footer 40 =740 fits. max-width 1000 perhaps 625 plus 100 40 =765. Could use width min(1000, calc((100vh-170)*1.6)) via CSS aspect ratio 16/10 and max-height calc(100dvh - 190px). At mobile width 375, height perhaps canvas ~222, too short for game but scales and text small. Could set container aspect 8/5, width. Adapt UI top. Should have no horizontal scrollbar. On mobile game playable although tiny. Could allow portrait canvas taller? Game internal 960x600 and CSS width 100%, fine. Overlay text perhaps scale. I can render responsive via canvas scaling coordinates. DOM overlay "Click or press a key to start" message perhaps center with backdrop. If canvas 222 high mobile message fits. Could use DOM overlay style clamp.
Should game logic precise.
Canvas logical W=960,H=600. Playfield boundaries perhaps x=24 y=24 to x=936 y=576. Bricks start y=86, 6 rows x 10 cols? 60 bricks. W internal. top area score in DOM, canvas no HUD except powerup indicators perhaps. Bricks width around 78, gap 8. x=(960 - (10*80+9*10))/2 =35. Use cols=10 rows=6, brickW=80, brickH=26, gapX=10,gapY=10, startX=35, startY=76. Width 890 => center 35. Good. Side walls at 18? I'll draw frame. Paddle y=544, h=14, default width=132. Ball radius=7. Ball speed ~360 px/s perhaps 400. Game fits.
Physics: continuous collision to ensure no tunneling. Use fixed timestep 1/120 with substeps based on distance and speed. rAF accumulate dt cap. Could implement swept circle vs AABB for bricks and paddle, and wall. Simpler fixed update dt 1/240 at speed 500 => 2px per step less than brick dimensions ensures no tunnel. But requirements "never pass through" perhaps strong robust. At high speeds after multi etc <=520. dt max 1/120 and move 4 px per step, radius 7. Fine but paddle moving via mouse can tunnel into ball? Use collision sweep paddle based previous position and ball. Could make fixed steps 1/240, robust. frame dt max .033, loops up to 8 at 240. 3 balls ~24 loops fine. Bricks 60 collision each naive 1440 per frame manageable. Use swept circle versus expanded AABB exact time and normals for movement each substep. Continuouscollision within step ensures. Or use per-step axis previous positions to determine bounce.
Important ball never pass through bricks/paddle. Use swept point vs expanded rect (Minkowski AABB), using ray-AABB slab; handles corners not exact circle but safe expanded square (a bit more collision). For each step, determine earliest collision among walls, paddle, bricks, move to hit, reflect, remaining time. At most 4 collisions. This robust no tunneling. Ball starts inside no collisions issues. Should handle multiple bricks adjacent simultaneous perhaps one at a time with nudge. Ball may hit underside etc. Implement function sweepPointAABB(px,py,dx,dy, rect expanded by radius): returns t in [0,1], nx,ny. Standard:
if abs(dx)<eps and (px<minX || px>maxX) return null; tx1=(minX-px)/dx etc. tEnterX = min, tExitX=max. Similar y. tEnter=max(txEntry,tyEntry), tExit=min(...). if tEnter>tExit or tExit<0 or tEnter>1 or tEnter<0? If starting inside expanded rect, tEnter<0 and tExit>=0. Ball perhaps after spawn intersects? Should treat only tEnter>=0. Collision if tEnter between 0 and1. Determine normal based greater entry axis. For vertical movement dx=0 etc. Normal x = dx>0?-1:1. In corner simultaneous choose perhaps both? For brick simple. Reflection if velocity dot normal <0. Should have position to exact collision then nudge normal epsilon. But if at t=0 and after reflecting away, next sweep from on boundary with tEnter=0? Starting on expanded boundary moving away: ray entry negative? For x at minX and dx>0 moving in? Should reflect normal etc. I add epsilon to pos so no immediate.
Paddle collision only when ball moving down and crossing from above? Requirement cannot pass through paddle; physically collide from all surfaces? Paddles only ball moving down and top plus sides. Use sweep expanded AABB, only accept if velocity dot normal<0 and ball moving down or side? If moving up and below paddle should not bounce down unexpectedly? Paddle at bottom, ball below likely loss. So only collide when ball.vy>0 and ball.y < paddle.y + paddle.h perhaps. Use sweep. Should paddle movement accounted? fixedstep paddle x updates before ball; mouse can jump. Swept stationary new paddle at each step could hit ball unexpectedly but never pass. Could incorporate previous x for paddle collision translation, but not needed. I can track and clamp. For paddle top, bounce angle based hit: relative=(ball.x - paddleCenter)/(paddleWidth/2) clamped [-1,1]. max angle 65 deg from vertical. vx = speed*sin(angle); vy=-speed*cos(angle). Ensure min vertical. On side collision reflect x, perhaps position. Top normal if ny=-1. Ball speed maintain or gradually increase per brick up to max. No crazy.
Walls: play bounds left=22,right=938,top=24. Bottom y=H+30 as lose. Swept wall collision within dt: if vx<0 time to x=left+r etc. Could integrate continuous collisions including walls with candidates. Ceiling. Paddle. Bricks. Should not collision bottom bounce.
Simpler per ball per fixed step:
remaining=dt
for iter 0..5:
dx=vx*remaining,dy=vy*remaining
find earliest t in [0,1] collision.
wall tx,ty separately compute if cross boundaries. normals.
bricks loop active, sweep expanded rect. paddle if vy>0.
earliest object.
if none pos+=d; break
pos += d*t; pos += normal*0.02
reflect or custom.
remaining *= (1-t)
if t=0 and same collision could loop but nudge.
else.
Should avoid hitting multiple bricks at same time? earliest. For bricks, when hit call hitBrick immediate modifying alive false; fine. If multi-ball simultaneous same brick update sequential.
If ball goes below H + radius, mark dead. When lives etc handle after update. If balls array empty and state playing:
lives--;
updateHUD;
if lives<=0 endGame(false)
else reset ball & paddle and set serve wait perhaps 1.0 sec. Use launch delay to avoid instantly falling. Have "Ball ready — move to aim" small canvas message? Ball stuck to paddle for 0.8 sec then launches. Mouse controls paddle. Press Space to launch? Requirements not need. Better auto-launch after 0.9s, and click starts game rather than launch. State playing serveTimer. During serve, ball position follows paddle and not moving. On start game initial serveTimer 1.0. In demo mode auto quick0.8. Overlay start stays. Real game overlay hidden on start.
Game states: mode = 'demo' or 'play'; screen = 'playing'|'gameover'|'victory'|'paused'? Should game over/victory DOM overlay. Demo attract always running; startPrompt visible only mode demo, but if demo reaches gameover or victory reset demo after delay so don't show game over. Demo lives perhaps infinite to keep action; AI strong. Demo reset when no balls after perhaps all bricks etc. I can ensure AI perfect enough and respawn without showing. Demo uses lives=3 but on lost reset ball no decrement? demo score perhaps HUD label "DEMO". Better HUD shows score but attract overlay. I can keep demoLives infinite, lives display 3 fixed. If all bricks broken victory then demo rebuild. Demo no power timers weird.
User input starts on any click or key in page if mode demo. But clicking mobile control buttons perhaps start then action. Keyboard event arrow first should start then controls. Should prevent arrows page scroll. Mouse movement before start perhaps no need start? Requirement says click or key start; mouse movement only controls paddle after started. pointerdown starts and set paddle from pointer location. pointermove when play update target. Overlay uses pointer-events none to let canvas click? DOM overlay whole canvas pointer events perhaps click listener window anyway. Use overlay pointer-events:none except restart button. Start prompt button-like but click anywhere. Restart option actual button. Buttons focus etc.
Keyboard: keys set; window keydown. If mode demo and !restart overlay button? startGame() on keydown (any perhaps except tab?) Could only recognized Enter Space arrows. Requirement click or press a key, any key. Prevent arrow/space. Start then keys. Escape perhaps pause? Add P pause polished perhaps optional. Keyboard controls left/right. I can include P pause and R restart? Avoid complicating. Should pause visibility when tab hidden? Could reset lastTime to avoid leaps. rAF dt cap. If paused P show overlay. Not required. Could implement keyboard only. Add control hint bottom "Mouse / ← → move • P pause". Click starts.
Demo AI: target chooses ball with largest y (closest to paddle) and moving down else any; desired x predict intercept perhaps simply ball.x + ball.vx*0.08 + slight. Paddle smooth via max speed 520, perhaps ideal. For self playing robust not lose. AI target most threatening ball. Expand if powerup falling near andsafe? Could target powerup if no immediate ball. Use pdemo speed 580. Make sure visually moving naturally. For demo ball served auto. In demo, paddle wide default perhaps 140. Use AI movement approach with smoothing.
When powerup drops, could catch sometimes. Use if ball high and powerup near bottom target power x else ball. AI should not miss. At each update move paddle toward target with acceleration? simple delta clamp target-x, maxSpeed*dt. prediction:
if ball.vy>0:
time=(paddle.y-ball.y)/ball.vy
predicted=ball.x+ball.vx*time
reflect predicted within bounds using fold.
target=predicted perhaps plus sin noise.
else target=ball.x
This is near perfect. Powerup may intercept occasionally.
Demo mode is reset upon click. Start overlay not blocking full view; place top-ish center or bottom? Should game in action visible. Overlay could be glass panel at center around y=350, covering some bricks but translucent. Prompt "CLICK OR PRESS A KEY TO START" with small "Demo playing • mouse or arrow keys" and start icon. Semi-transparent pill centered near lower area above paddle not covering bricks. Since screenshots after 3 sec, bricks action visible. Place overlay at 50%? Standard game attract panel at center perhaps glass background .88, but demo behind. Requirement behind message. Fine.
Visual style: dark neon arcade. Body background radial gradients stars grid. Canvas draw:
- backdrop gradient #081327 to #050914
- subtle star field static precomputed with deterministic RNG, twinkling.
- dotted grid lines with low alpha.
- outer playfield rounded corners border.
- glow at top.
- bricks each with own hue rows: cyan, teal, violet, magenta, orange, gold perhaps cohesive. 6 rows. Rounded rect gradient and top highlights. Tough bricks visually metallic with silver border and small "2" shield? requirement tougher 2 hits visibly crack after first. Set hp=2 pattern deterministic e.g. rows 1 and 4 every other? 12 tough. Normal hp1. Tough bricks use double border, bolt icon? Add small diamond/core. On hit hp 1 draw cracks: jagged white/dark lines via path deterministic from brick id, and perhaps chipped corners. Make sure visible. `drawBrick` if hp===2: outer metallic glow, corner rivets, double chevron; if maxHp2 && hp===1 draw crack paths using fixed arrays. Different hue but desaturated. Power-up carrying icon visible: brick has small centered capsule/circle icon W or ×3. Make some normal/tough `power:'wide'` or `'multi'`. Icon on brick with tiny rounded square dark overlay and W or 3 balls. Ensure visible at normal scale. On break spawn falling powerup with type and animated glow icon. Catch gives either:
wide sets paddle.powerUntil=time+10, width target 200 (normal 132) smooth. If repeated reset 10s. Multi-ball adds balls up to total 3: requirement multi-ball (3 balls). On catch if current count n, ensure exactly 3 (add 3-n clones from existing or caught power position). Each new ball velocity angles. If already 3 perhaps score bonus. Multi-ball no duration, remain until lost. Balls lost individually; when no balls lose life. New life resets one ball and wide? likely clear powerups and width. wide timer DOM indicator countdown. Paddle visibly wider with cyan glow and timer bar. Catch effects text float.
Drops move 115 px/s slight sway; rotation perhaps. Catch when intersects paddle AABB; swept to avoid miss? requirement ball never pass; powerup can normal. Use powerup y speed dt <=2 so fine. Check circle/rect. On miss remove.
Particles brick break: 12-22 rectangles with color, velocities, gravity, fade. crack first hit smaller sparks, screen shake slight. On break 18. Particle cap e.g. 500. Add score popup and floating text. On paddle hit tiny particles. On power catch rings. Use additive `lighter` perhaps. Trails ball as array previous positions? Each ball track trail 8 positions updated per fixed step can huge; update per frame or at collisions. Draw ball trail from `trail` push per frame before update? Could each ball store array with x,y and update every fixed 1/120 throttled. Simpler draw trailing circles opposite velocity normalized with fading, not actual path but nice. Ball glow radial gradient. Different balls hues? white core cyan. Multi added gold/lilac but consistent.
Score display DOM. Score per brick normal 100, tough first 25? On break 100 or 180 for tough; multiball bonus 250 etc. HUD values with animated perhaps. Score `scoreEl.textContent=pad`. Lives display heart icons via CSS inline SVG? No external. Could use three small ball/lifes dots or hearts using text ♥ (system glyph). Use `span` hearts. Should score displayed. Level indicator "01 / NEON GRID". Bricks left perhaps. HUD with three cards: score, level, lives and power indicator. Top header title BREAKOUT with logo. Could be visually polished but not clutter. Use HTML:
<div class=app>
<header class=topbar>
<div class=brand><span class=brand-mark>...</span><div><h1>NEON BREAKOUT</h1><p>SECTOR 01 ...</p></div></div>
<div class=top-actions><span class=sound ...>...</span> perhaps no audio needed. Could add sound toggle with WebAudio generated sound; polished. Should have no errors if AudioContext. Add sound button accessible; optional. Could implement simple WebAudio bleeps on hit, break, power, lose, start. Default sound on? Browsers block until gesture; demo silent until interaction. Initialize on user start. Sound toggle button. Good but not needed. Could add button with speaker SVG and label "SOUND ON". Browser can create on click. If audio fails guard. But evaluation zero errors. Use try catch. Perhaps default sound on and init user click. Demo collisions before audioCtx null no sound. On start init. Button click currently global click start would restart before toggle. Could handle. Better no audio toggle? Polished game benefits audio. I can include a small sound toggle. Global start listener on canvas and keydown not entire document? Requirement click or press key to start; click anywhere on game frame/overlay, top toggle could just toggle no start perhaps fine. Add canvas wrap click. sound button outside. On mobile keyboard note.
<main class=game-card>
<div class=hud> etc
<div class=canvas-wrap id=...>
<canvas width=960 height=600>
<div class=start-overlay>...
<div id=endOverlay class=end... hidden>...
<div pause...
</div>
</main>
<footer controls...
</div>
Should have canvas responsiveness. `.canvas-wrap{position:relative;width:100%;aspect-ratio:16/10;border-radius:...;overflow:hidden}` canvas width/height 100%; display:block. width of app perhaps `min(1040px, calc(100vw - 40px), calc((100dvh - 170px)*1.6))` but CSS min with calc supported. Simpler `.app{width:min(1000px,calc(100% - 32px));}` body min-height:100vh display grid place-items center padding 20px. At 1280x800 app canvas 1000x625, top brand 50, HUD 56, gaps, footer 30 total ~800 perhaps too tall. Should fit 1280x800 screenshot no vertical issue okay scroll vertical allowed but ideally full. Set app width:min(960px, calc(100vw - 32px)); canvas 600. top 50+hud 58+gap+footer=750 + padding 24=774 fits. 960x600 is good. At 1280, body p16, brand 48, gap10, hud54, canvas600, footer26 =>754+32=786. Good. At 800 exact. Could set max canvas via width: `width:min(960px, calc(100vw - 32px), calc((100dvh - 180px) * 1.6));` at 800 => (620*1.6)=992 so 960. At 700 =>832 width gives canvas 520 total ~700. Mobile vh perhaps width anyway. Use CSS `width: min(960px, calc(100vw - 28px), calc((100dvh - 178px) * 1.6)); min-width?` On 375x667 => calc((489)*1.6)=782 so width 347. Canvas 217. header/hud/footer ~170 total387 fits. Tiny but expected. Could switch HUD mobile smaller. Great.
Header HUD:
HUD separate within game-card top? Use `.hud{display:grid;grid-template-columns:1fr auto 1fr;...}` 64 high. Could fit mobile with smaller fonts.
score card left. level center pill. lives right.
Power status absolute in canvas top left or DOM below? Draw active power timer at canvas top 40 perhaps. Bricks start y=80 leaves top 50 for indicators. Use draw top labels e.g. left `WIDE PADDLE 8.4s` if active; right `3 BALLS IN PLAY` if multi. If no active, perhaps level name small centered top `BREAK ALL 60 BRICKS`? draw field at y=34 label and tiny progress bar? Top of canvas within play area. Bricks start y=86. In screenshot powerups visible on bricks.
Overlay start design:
`<div class="start-screen" id=startScreen><div class=start-card><span class=eyebrow>ATTRACT MODE · AI PLAYING</span><h2>READY TO BREAK?</h2><p><span class=mouse-icon>..</span>Click or press a key to start</p><div class=mini-controls>Mouse / ← → to move · Catch power-ups</div></div></div>`
If full overlay gradient might obscure demo; use pointer events none; background transparent and card at perhaps top 54%. Use backdrop blur 8. On small canvas text scale via clamp but card relative. `.start-card{transform:translateY(30px)}`. Canvas screenshot 3s perhaps overlay visible center. Attract mode text visible.
Game over/victory overlay full with dark gradient and panel. h2 different. final score. restart button. Clicking restart calls startGame and initializes audio; keydown also restart if end. Start overlay hidden in play. On victory overlay display.
Pause overlay perhaps reuse message and no button. Add `paused` flag; on P toggle, overlay small. In demo ignore P? start game instead. On blur not auto pause because keyboard? Could set paused when visibility hidden but not needed.
Canvas focus: tabindex=0? after click `canvas.focus({preventScroll:true})` makes keyboard. Window listener receives anyway iframe after click. Canvas outline none. Should accessible instructions.
Collision code detail:
Constants:
```
const W=960,H=600, WALL=22,TOP=22;
const paddle={x:W/2,y:548,w:132,targetW:132,h:14,vx:0};
let balls=[], bricks=[], particles=[], powerups=[], floaters=[];
let score=0,lives=3,state='demo', outcome=null, paused=false, serveTimer=0, elapsed=0, wideTime=0;
```
Use time seconds `gameTime` advanced fixed update. wideRemaining numeric decrement dt easier. On catch wideTimer=10. paddle.targetW = wideTimer>0?202:132. update width approach with dt 8? Collision should paddle actual w. `paddle.w += (targetW-paddle.w)*Math.min(1,dt*8)`, clamp x so within. On life loss clear wideTimer and drop powerups perhaps yes.
`paddle.x` center; render rect x-w/2.
Build bricks:
colors array values {h,s,l?}. Use hue numbers [188,166,262,318,24,44] perhaps order top purple? Polished. rows top 190 cyan, 214 blue, 258 violet, 310 pink, 18 coral, 42 gold. But tougher distinct.
for r=0..5, c=0..9:
`tough = ((r===1||r===4) && c%3===1) || (r===2 && (c===4||c===5)) || (r===5 && c%4===0)` total.
power pattern predetermined positions to guarantee visible: coordinates `${r},${c}` e.g. (0,2) wide, (0,7) multi, (2,0) multi, (2,8) wide, (3,4) multi, (4,6) wide, (5,2) multi, (5,9) wide. 8 power bricks. Icons visible. On break spawn every power.
Could some bricks visibly carry power-up but when broken drop; yes.
`brick={x:startX+c*(bw+gap), y:...,w,h,hp,maxHp,power,hue,alive:true,seed,flash:0}`
Tough hp2. On first hit `hp--`, score+=20; brick.flash=1; shake=2; spawn 5 sparks; audio.
`destroyBrick`: alive=false, brokenCount++; score += maxHp===2?200:100; spawn particles hue; if power spawn; float text; shake=4.
UpdateHUD every score/life change perhaps function.
Victory when brokenCount===total and mode play. But if last brick broken while balls moving, call endGame in update after collisions perhaps immediately. Set state='over', outcome victory; update overlay after step. In demo schedule reset after 2.5 and perhaps display "SECTOR CLEARED" canvas but start overlay remains. Should avoid `endGame` switching start overlay. separate if demo set demoResetTimer=2 and rebuild after, perhaps freeze? Could `buildLevel()` immediately? Would make continuous. Add demo banner float and rebuild after 1 sec. At victory game play `finish(true)`.
Scoring update many times. `setScore` not necessary.
Ball spawn:
```
function makeBall(x,y,vx,vy) { return {x,y,vx,vy,r:7,speed:380, stuck:false,...};}
function serveBall() {
const speed = 390;
balls=[{x:paddle.x,y:paddle.y-10,vx:0,vy:0,r:7,stuck:true,angle:-Math.PI/2...}]
serveTimer= mode==='demo'?0.65:1.1
}
```
During update if serveTimer>0:
serveTimer-=dt; ball stuck at paddle.x, y=paddle.y-11;
if <=0 launch angle = mode demo random -0.25 etc; for play based paddle movement perhaps -PI/2 + paddle.vx*0.0004? Could just angle -Math.PI/2 + (Math.random()-.5)*0.25. Should deterministic-ish no issue. Set vx = sin angle*speed, vy=-cos angle*speed.
Better initial game requires paddle move not click to launch. auto.
For multiball no stuck.
`launchBall(ball, angle=something)`.
Lives display as 3 hearts. Use HTML update. Avoid injecting unsafe no user input. `livesEl.innerHTML = Array.from...` okay static. Use characters `◆` perhaps accessible label. hearts `●` with small ball icon. Life icons spans with CSS. Could initial static and update. `.life{width:10px;height:10px;border-radius:50%;...}`. `livesEl` as div.
updatePlayer(dt):
Keyboard movement:
```
let dir=(keys.ArrowRight?1:0)-(keys.ArrowLeft?1:0);
if(dir){paddle.x += dir*560*dt; paddle.keyboardActive=true; pointerTarget=null?}
else if(pointerActive){ paddle.x += (pointerTarget-paddle.x)*min(1,dt*22) }
```
Mouse expected controlled directly. Smoothing with 25 enough. But if pointer moves rapidly, paddle not exact but okay. Mouse pointer event set x target. To avoid keyboard fighting mouse, `controlMode='mouse'` on pointermove, `'keys'` on keydown. In key mode only arrows. If no keys in mouse mode smooth. `paddle.vx=(x-oldX)/dt` for paddle influence perhaps not bounce angle per req only location.
Demo AI takes precedence.
clamp center between left+w/2+4,right-w/2-4.
When wide changing clamp.
Collision sweep:
Rect bricks use {x,y,w,h}. Expanded [x-r,x+w+r]. `sweep(ball, rx,ry,rw,rh,dx,dy)` returns t,nx,ny.
Pseudo:
```
function sweepAABB(px,py,dx,dy,left,top,right,bottom){
let txEntry=-Infinity, txExit=Infinity, ...
if (Math.abs(dx)<1e-9) { if(px<left||px>right)return null; }
else { let tx1=(left-px)/dx,tx2=(right-px)/dx; txEntry=Math.min...}
...
const entry=Math.max(txEntry,tyEntry);
const exit=Math.min(txExit,tyExit);
if(entry>exit || entry<0 || entry>1 || exit<0) return null;
let nx=0,ny=0;
if(txEntry>tyEntry) nx=dx>0?-1:1; else ny=dy>0?-1:1;
return...
}
```
At exactly touching and moving parallel could entry 0 and dot? Filter dot `vx*nx+vy*ny < -1e-6` later. For top paddle if start at y=paddle.y-r-0.03 moving down, good. Starting inside expanded due to paddle expanding underneath ball, entry negative -> no collision; could pass. Wide expansion occurs around center horizontal not top, y constant; ball could be alongside and now inside horizontally, but not major. Paddle movement into ball from side could put ball inside expanded rectangle and sweep misses. To strictly never pass through, add overlap resolution for paddle before movement:
- after paddle update for each non-stuck ball if circle intersects paddle actual rounded? AABB expansion check and moving down or above: if ball y < paddle.y and ball y+r > paddle.y etc, set ball.y=paddle.y-r-0.1 and bounce. Could handle. But continuous movement of paddle perhaps. Requirements typical no tunneling with speed; fixed sweep sufficient.
Ball vs brick start inside due to multi spawn? Spawn balls at source existing safe location. Fine.
Find collisions:
```
let hit=null;
function consider(t,nx,ny,type,obj) if valid and t>=0... earliest
walls:
if dx<0 sweep plane; t=(LEFT+r - px)/dx if between... normal 1
...
```
Using sweep against wall as rectangles? plane formula simpler:
`t=(LEFT+r-px)/dx` where dx<0 gives positive. Accept t <=1, and at hit y perhaps within? walls infinite vertical so fine.
Ceiling t=(TOP+r-py)/dy.
No bottom.
Paddle if vy>0:
`res=sweepAABB(... paddle.x-w/2-r, paddle.y-r, paddle.x+w/2+r,paddle.y+h+r)`;
Only accept if res normal top or side and dot. If bottom normal from below while vy>0 impossible? if below and moving down away dot positive filtered. good.
Could give paddle priority on equal t vs brick not near.
Bricks loop if alive and near broadphase:
if ball x + abs(dx)+r < b.x etc skip. Not necessary.
`sweepAABB`.
Filter `res && b.vx*nx+b.vy*ny<0`.
On earliest:
```
ball.x += dx*t; ball.y += dy*t;
ball.x += nx*0.05; ball.y...
switch wall:
if nx ball.vx=-ball.vx etc
brick:
if nx... reflect; if ny...
damageBrick
paddle:
if ny===-1:
let offset=clamp((ball.x-paddle.x)/(paddle.w/2),-1,1)
let angle=offset*MAX_ANGLE;
let speed=Math.min(520,Math.hypot(...) *1.01?)
ball.vx=Math.sin(angle)*speed + paddle.vx*0.03? This alters speed and angle; no need.
ball.vy=-Math.cos(angle)*speed
ball.y=paddle.y-r-0.1
else reflect x and perhaps y if bottom.
remaining *= 1-t
if(t<1e-6) remaining -= 1e-5? avoid zero loops.
```
At corners normal based entry; okay.
After loop if iter reaches max move remaining? Could avoid stuck. 6 collisions max plenty.
Bricks adjacent: damage destroys and nudge then remaining. Good.
Global game speed increment on brick break: for each ball speed *=1.008 up to 540? Doing immediately while in collision could change direction after reflect? If scale both components retains. Or store speed in ball. `ball.speed=Math.min(540,ball.speed+3)` on brick destroy; normalize after brick reflect to new speed. Use each ball has speed; initial 390. `bounce` reflect then normalize speed. Multi inherit min current.
On paddle use `ball.speed=Math.min(540,Math.max(390,ball.speed)+2)` perhaps. Demo after minutes speeds max.
Ball trail angle. Draw streak:
```
const mag=Math.hypot(vx,vy)||1;
for i=4..1 draw at x-vx/mag*i*5...
```
When stuck no trail.
Loss:
after physics if ball.y-ball.r>H+20 remove. particles at bottom perhaps.
`balls=balls.filter(b=>!b.dead)`. If no balls and not serve pending? serveBall creates one. If multi all lost same step, handle once:
```
if(balls.length===0 && state !== over && serveTimer<=0) loseLife()
```
During serve stuck ball exists so fine. loseLife in demo `serveBall()` and perhaps reset wide; no lives change. Play lives--; if <=0 finish(false), else wideTimer=0,powerups=[],serveBall.
`finish` sets gameStatus='ended'; balls perhaps still rendered freeze? rAF render. end overlay show. Keep particles animate? update if ended? I can update background and particles perhaps not game. state variable mode 'demo'/'play', `ended=false`. In main loop if !paused && !ended update. For end no particles update but okay.
Victory triggered `if bricksLeft===0` right after updates and not ended. Demo rebuild. finish overlay.
Powerup drops:
object {x,y,vy:120,type,phase,rot,r:15}
Update y; x=baseX + sin(time*3+phase)*8? If x used as base and increments? store baseX, update x=baseX+sin*10. Check catch rect:
```
if(p.y+14>=paddle.y && p.y-14<=paddle.y+paddle.h && p.x ... paddle.x±w/2) catch
```
Since top crossing dt safe. Also only vy down.
Catch wideTimer=10, score+=50? multi:
```
let source=balls[0];
while balls.length<3:
const base=balls[i%balls.length]...
const angle = -Math.PI/2 + (balls.length===1?-0.45:0.45) etc
new at base.x,base.y speed base.speed
```
If source stuck serve ball? powerup can't be caught while stuck after life perhaps falling powerups cleared on loss, so no.
Better if n=1, original ball adjust angle? Requirement gives 3 balls. Add two with ±0.45 from existing velocity direction, ensure upwards/downwards same? Spawn at original and clone rotate current velocity ±0.5; if original moving down near paddle, new balls also down and perhaps lost quickly. Better launch clones upward with angles -55 and -35? Standard multiball spawn at caught location above paddle moving up angles. Set original if near paddle bounce? I can create at source x,y with `baseAngle=Math.atan2(vy,vx)` rotate ±0.45, if abs vy small ensure. To guarantee visual, catch near paddle and new balls upward: set existing ball if vy>0 then bounce paddle? Powerup catches not ball. Could just create two at paddle.x,paddle.y-25 with vx ±speed*0.45, vy=-speed*0.89 and leave existing. 3 balls total, visually clear. New balls can't spawn intersect brick (paddle low). Fine. If n=2 add one at paddle.x,paddle.y-24 angle up. `spawnMulti()` adds needed at paddle.x±12,y-25 with angle[-0.55,0.55,0] based index. Balls have speed max existing or400. If existing count 0 impossible.
If already3, +300 score and float "MULTI MAX +300".
When multiball active draw top right `● ● ● MULTIBALL`. If n=2 perhaps 2. Requirement multi-ball (3 balls) first catch always likely one ->3.
wide and multi can coexist.
Powerup icons:
On brick, center small circular token 20px. Wide icon horizontal line with arrowheads perhaps `↔` text using canvas font bold 13; `W` more readable. Requirement visibly carry icon: use `↔` for wide and three small dots for multi. Also legend footer "↔ WIDE 10s ••• MULTIBALL ×3". Draw icon in brick:
- rounded pill 26x18 fill rgba(4,10,25,.55), stroke white .5
- if wide draw horizontal double arrow path manually.
- multi draw 3 circles radius2.3 positions.
Dropping token: circle radius 15 fill by type cyan or purple, glow, icon white; label beneath perhaps `WIDE` / `MULTI` tiny at y+26? Since drop height 36, draw capsule 34x30 with type icon; text "W" etc. Use icon plus small ring. Animated pulse.
Bricks drawing:
Canvas scaled coordinates unaffected by devicePixelRatio? Canvas fixed width=960 height=600; CSS scales. Could support high DPI by backing store W*dpr with ctx.setTransform(dpr...) but HTML canvas width attribute then 960? Requirement canvas logical. For crisp on high DPI, resize backing based displayed dimensions and DPR, use logical coordinates. If canvas.width=W*dpr and CSS aspect. Set canvas.height=H*dpr, ctx.setTransform(dpr,0,0,dpr,0,0), no need actual CSS pixel ratio because browser scales W*dpr to displayed. DPR cap2. On resize dpr perhaps. If set width=1920 and CSS width100%, drawing W=960 scaled with dpr; at display 960 CSS, ideal 2x. At display 375, still 2x 1920 overkill but okay. Use DPR=Math.min(2,window.devicePixelRatio||1), canvas.width=W*DPR etc once. context setTransform each frame? assigning width resets, do on resize. `ctx.setTransform(DPR,0,0,DPR,0,0)`. Render clearRect 0,0,W,H under transform clears logical yes. Better `ctx.clearRect(0,0,W,H)`.
No external fonts. Use `font-family: Inter, ui-sans-serif...` but Inter not loaded. System.
Canvas roundRect supported latest Chrome. Could implement helper path for compatibility, use `ctx.roundRect`. latest Chrome yes. For robust own function `roundedRect(ctx,x,y,w,h,r)`. Fine.
Gradients per brick every frame 60* gradient could okay. 60 createLinearGradient at 60 fps 3600, fine. Could optimize color functions.
Use hsl strings. `color = hsl(hue 90% 60%)` supported modern. Better `hsl(${hue}, 85%, 60%)`.
Brick body:
```
ctx.save();
ctx.shadowColor=hsla;ctx.shadowBlur=b.power?12:6;
grad = ctx.createLinearGradient(x,y,x,y+h); add lighten to dark;
roundRect fill.
shadowBlur=0;
top highlight line;
if tough fill pattern rivets corners; border color hp2? white.
if cracked path:
begin move x+w*.28,y+2 line...; stroke dark rgba then white offset .6
...
```
flash decrement in update dt `b.flash=Math.max(0,b.flash-dt*5)`; render if flash >0 overlay white alpha.
Destruction particles color from b hue. use hsla.
Background stars precompute with seeded rng:
```
let seed=...
function rand(){seed=(seed*...);...}
stars = Array.from({length:90},...)
```
Draw some stars behind bricks. `twinkle=0.3+0.5*sin(time*...+phase)`. draw grid lines every48, low alpha. Top ambient radial gradients create each frame. Can pre-render background to offscreen canvas to optimize? No need. Offscreen no external. Polished moving background subtle.
Walls draw left/right vertical lines neon. Top line. Paddle area bottom subtle gradient.
Screen shake applied to play objects not background. In render:
```
ctx.save();
if(shake>0) ctx.translate((Math.random()-.5)*shake,...)
draw bricks/powerups/particles/paddle/balls/floaters
ctx.restore()
```
Random shake only visual no physics. Decay shake update.
At end overlay DOM darkens.
Draw launch guide if stuck: dotted line upwards from ball based paddle offset? Could draw at ball x, vertical line 50 high and "AUTO LAUNCH" tiny. Demo overlay enough.
Progress bar at top perhaps bricksLeft. `drawFieldHud`: tiny label `SECTOR 01` left at 38,52, right `${remaining} BRICKS`; line from x150 to... but bricks start at86. Canvas HUD at y45. Power indicators need not overlap. Could show left power status at x40 y52, right bricks. On no wide show "CORE BALL • 390". But HUD DOM has level. Perhaps:
left `WIDE 09.8s` if active else `60 BRICKS REMAIN`
center `MULTIBALL ×3` if balls>1
right small colored dots power legend? Better uncluttered.
Progress line y62 from 36 to924 width; fill fraction destroyed using cyan gradient. Nice.
Top of playfield border at 20. Bricks y84 so progress at56.
Paddle draw:
- shadowBlur 20 cyan if wide, white normal
- gradient cyan/blue perhaps paddle normal #ecfaff center, edges #46d7ff.
- base rounded capsule x,y,w,h r7
- inner top white line; small central notch perhaps.
- wide timer bar under paddle at y+20 with width ratio wideTimer/10, color.
- engine glow underneath.
Paddle movement particles occasional? can add small trail but particle budget. Perhaps draw afterimage based vx.
Balls: shadowBlur, gradient. Draw ring.
Particles update:
`p.x+=vx*dt;p.y+=vy*dt;p.vy+=gravity*dt;p.vx*=Math.pow(.15,dt)??` just *=0.99 per fixed (too strong) use Math.exp(-dt*1.2). life-=dt; rotation. Render rect alpha life/max.
Break brick shards are rectangular and small. Use 16+.
Floating text {x,y,text,color,life,maxLife}. update y-=25dt; render text with alpha.
Audio:
Could implement WebAudio simple oscillator. Polished but task not require, adding sound toggle might risk click start semantics. Could still implement safe. Desired quality 4 perhaps visuals/game enough. Sound can be subtle. Perhaps no audio toggle to reduce complexity, use a small button `SOUND ON` with generated. But screenshots side by side judged visual primarily. A sound button top right polished. Make sure initial no interaction demo no audio errors. On `startGame` call `initAudio` if soundOn. `tone(freq,dur,type,vol,slide)`:
```
if(!audioCtx||!soundOn)return;
const now=audioCtx.currentTime; ...
```
On toggle click:
```
soundOn=!soundOn; initAudio if true; update...
event.stopPropagation()
```
No global click except canvasWrap, so okay.
Audio contexts don't throw after user event. `audioCtx.resume().catch(()=>{})` no unhandled. Could sound on click start. On first canvas pointerdown call `startGame(); initAudio();` order init before? startGame calls tone perhaps audio null, then init. Fine.
I can default `soundOn=true`, label SOUND. Icon via inline SVG markup, allowed no external. On mobile hide text. Top brand perhaps add `span class=status-dot>LIVE DEMO</span>` and sound.
Interaction state:
Variables `mode='demo'`, `ended=false`, `paused=false`.
`startGame()`:
```
mode='play'; score=0;lives=3;ended=false;paused=false;gameTime=0;...
buildLevel(); paddle reset; serveBall(); overlays...
```
Keydown:
```
if (['ArrowLeft','ArrowRight',' ','Spacebar'].includes(e.key)) e.preventDefault();
if(mode==='demo'){startGame(); initAudio();}
else if(ended && (e.key...any?)){startGame();}
if(e.key.toLowerCase()==='p' && mode==='play'&&!ended){togglePause();return;}
if(e.key.toLowerCase()==='r' ...){startGame();}
keys[e.key]=true; controlMode='keys'
```
"press a key to start" any key including sound? Fine. Avoid starting when key is Tab? Could any except modifiers. Should have P handling not start then immediately pause. Structure:
```
if(mode==='demo'){startGame(); if key arrows set; return?}
```
If key p starts game but not pause. okay.
If ended any key restart perhaps requirement restart option button; okay.
Space perhaps not control after game start but prevent scroll.
Pointer:
`canvasWrap.addEventListener('pointerdown', e=>{ if(mode==='demo'||ended){ startGame(); initAudio(); } setPointer(e); canvas.focus(); })`
If ended overlay has restart button with pointer events; event bubbles to wrap causing start twice. Add overlay end `pointerdown e.stopPropagation()` and button click start. But start on click anywhere end acceptable; restart button clear. If event pointerdown starts before button click, then click button start again resetting minor. Stop on button. Simpler wrap click:
- startScreen pointer-events:none
- endOverlay pointer-events:auto and on pointerdown if target button? global listener wrap could. Add endOverlay `pointerdown` stopPropagation; restartBtn click `startGame`.
- canvasWrap pointerdown for demo only. For play update pointer target. If ended clicking non-button no action except restart button, fine. Key restarts.
Start requirement click anywhere game area works demo.
`pointermove` attach to canvas wrap; compute `getBoundingClientRect`, x=(clientX-left)/width*W, set pointerTarget=clamp; if mode play controlMode mouse. In demo mouse shouldn't influence AI perhaps update pointerTarget only no control.
Pointerdown in play set mouse target, perhaps immediate paddle.x=target? Mouse control responsive. On pointermove don't set directly physics update smoothing. `pointerTarget`.
Touch pointermove supported; CSS `touch-action:none` on wrap prevents scroll while playing. On mobile page canvas 217 high, paddle tiny but playable.
Keyboard input after click. `keys` object clear on blur to prevent stuck. blur not pause perhaps.
P pause overlay `#pauseOverlay` hidden; toggle text. Pause stops update but draws; demo time background continues based performance `visualTime` perhaps okay. Escape also pause? Footer P Pause.
Demo start overlay hidden? Initially visible. End overlay hidden. Demo updates. `startGame` calls updateHUD, overlay add hidden. CSS `[hidden]{display:none!important}`.
Demo game HUD score values increases, lives 3. `modeBadge` text "AI DEMO" and class. DOM level badge center `SECTOR 01` with small `AI DEMO` perhaps update. In play `60 BRICKS`. On score updates update `remainingEl`.
Perhaps header status in brand `ATTRACT MODE` changes to `LIVE`.
HTML:
```
<div class=brand><div class=logo><i...></div><div><h1>BREAKOUT<span>//</span></h1><p>NEON SECTOR <b id=modeLabel>AI DEMO</b></p></div></div>
<button id=soundButton...>
```
HUD score panel with "SCORE" and span id score `000000`. lives. Center:
`<div class=level-meta><span>SECTOR 01</span><div class=brick-progress><i id=domProgress></i></div><small id=remaining>60 BRICKS</small></div>`.
Could be too much. Canvas also progress. I can DOM HUD with three equal cards.
First paint: JS executes end body builds level demo and rAF. Canvas initial render immediate `render()` before rAF to guarantee. CSS background. Overlay visible.
Timing:
```
let last=performance.now(),acc=0;
function frame(now){
const frameDt=Math.min(0.05,(now-last)/1000);last=now;
visualTime+=frameDt;
if(!paused){
acc+=frameDt;
const step=1/240; // 12 steps at .05; 60 bricks*3 balls*12 =2160 sweeps per frame worst 130k/s fine.
while(acc>=step){update(step);acc-=step;}
}
render();
requestAnimationFrame(frame);
}
```
At 240Hz 4 steps per 60fps, each 60 bricks*3=720 sweeps/frame ~43k, okay. Actually 4*180=720. Good. Particles update per fixed perhaps 400*4 ~1600. fine.
Should update visual effects in demo/ended. If ended no update but visualTime twinkle continues. Could update particles if ended? not needed.
Cap accumulated .1 to avoid spiral.
`update(dt)`:
- gameTime+=dt
- update paddle
- update serve/balls
- update powers etc
- particles even ended perhaps called until finish sets ended mid step then subsequent frame no update.
- shake=Math.max(0,shake-dt*18)
- brick.flash decays.
- if demoResetTimer >0...
Demo auto-reset if game lasts and all bricks clear. On life lose no lives.
Victory check only after update balls. If brick destroyed last inside move, can finish right away after updating all perhaps. `if(bricksLeft===0)` after.
Demo game rebuild after timer; freeze balls? set timer 1.4 and perhaps no update bricks left 0 but balls bounce. In update if demoResetTimer>0 decrement and when<=0 resetDemo full. Avoid repeated setting by check.
`resetDemo()` preserve mode, score=0,lives=3, ended=false, buildLevel, balls clear etc, wide=0, serve. start overlay still visible.
When demo all bricks broken:
```
if(mode==='demo' && demoResetTimer<=0){demoResetTimer=1.8; addFloater center ...}
```
But next update `demoResetTimer>0` decrement; condition guard. Should initialize `demoResetTimer=0`; while countdown positive. after reset build etc.
`resetDemo` perhaps score resets after 1.8.
When play `finish('victory')`.
Power drops in demo can affect paddle wide. When demo reset click `startGame` all clean.
Collision and state transitions within fixed step:
If `finish` sets ended during update but loops ball update still perhaps okay. Check `if(ended) return` after physics? I can set and update overlays. rAF continues render.
When mode play lives 0 finish false clears? End overlay final score. Keep balls perhaps none. Victory balls may still visible but overlay covers.
`showEnd(win)` sets title "SECTOR CLEARED"/"GAME OVER", kicker, message, final score, bricks count etc. restart text "PLAY AGAIN". `endOverlay.hidden=false`.
Start overlay `hidden = mode!=='demo'`.
`modeLabel.textContent=demo?'AI DEMO':'LIVE PLAY'`.
`document.body.classList.toggle('is-playing')` perhaps cursor hide over canvas during play? `.canvas-wrap.playing{cursor:none}` but keyboard user no pointer okay. Do not hide cursor perhaps crosshair.
Aim bounce angle:
`offset = clamp((ball.x-paddle.x)/(paddle.w/2),-1,1)`;
maxAngle=1.15 rad (66°). At edges vx~0.91speed vy~-0.40speed. "sharp angle" good. Center straight up exactly. Perhaps nonlinear `Math.sign(offset)*Math.pow(abs(offset),0.9)` not necessary.
Ensure ball horizontal not too high after brick side collisions? Brick reflect naturally can yield vx high vy low from paddle edges but min vertical 0.4. Could add `enforceBallAngle` after brick bounce: if abs(vy)<speed*0.22 set vy sign*0.22 speed and adjust vx. Not needed but avoids endless horizontal.
Side paddle collision reflect x but if ball hits side moving down, might continue down and be lost, okay.
On paddle top speed exactly. Position top.
Swept wall function consider t values. Starting at x=left+r and vx<0? if after nudge left+0.05 and moving right no. If numerical t small negative ignore? `if(t>=0 && t<=1)`. Good.
Collision ordering: `best` and consider if t<best.t - eps; if equal prefer brick? No big.
Paddle is checked after bricks perhaps bricks never near bottom. Walls then paddle then bricks.
After brick damage, `ball.speed` increase on break. `reflectBall(ball,nx,ny)`:
```
if(nx) ball.vx=-ball.vx;
if(ny) ball.vy=-ball.vy;
normalize to speed
```
If corner returns only one normal, acceptable.
`hitBrick` may trigger score but no victory until later.
Paddle collision offset depends actual contact x at collision. Good.
Wall bounce audio tone for every wall perhaps many with 3 balls, throttle audio? audio nodes manageable. Sound only real play. I can play wall very low and throttle via lastWallSound time based audioCtx. Could skip wall sound.
Particles when brick breaks definitely.
Crack visual: On tough brick first hit hp becomes1; draw line patterns. Should tough hp2 initial visible perhaps small `II` at right? Icon plus power might overlap. Design:
if maxHp2:
- double outline silver.
- two small metallic rectangles at top-left `2` text. On hp1 crack and only one pip? Use two tiny diamonds at right x+w-13 and x+w-20 with opacity hp index. But power icon centered. label `2` at left top in small. `ctx.font='700 8px ...'`, text '2H' perhaps. Better draw 2 rivets and top stripe.
Cracks:
```
const sx=b.x+b.w*(0.3+(seed%3)*.15)
ctx.moveTo(sx,b.y+1)
lineTo(sx-5,b.y+7)
lineTo(sx+3,b.y+12)
lineTo(sx-4,b.y+b.h-1)
...
second branch
```
white/light and dark line 1.4.
Bricks power icon visible carry. color wide cyan (#7df9ff), multi violet (#e1a6ff). On any hue token background dark ensures.
Wide 10 seconds accurate game time excluding pause. `wideTimer=Math.max(0,wideTimer-dt)`, target based. On catch reset. HUD canvas timer one decimal. DOM perhaps power chip. Footer legend static.
Lives update DOM create spans once:
```
function updateHUD(){
scoreEl.textContent=String(score).padStart(6,'0');
livesEl.innerHTML='';
for i 0..2 span class=life+(i<lives?'':' lost')
livesEl.setAttribute('aria-label',`${lives} lives`)
...
}
```
Frequent score update recreating lives many times (each brick) okay but unnecessary. Could cache last values.
`hudScore.classList.add bump` perhaps no timeout. Avoid layout animations repeatedly. Could CSS transition. Use `scoreEl` transform? On update remove/reflow not needed.
`remainingEl.textContent=remaining+' BRICKS'`.
Victory if bricks left 0. Multi balls may destroy several, score pop.
Responsive: Canvas internal fixed and DOM overlay text using container query? Could use CSS `container-type:inline-size` and cqw units but support latest. Simpler clamp based viewport width. At 960 card h2 24, padding 20; at 347 canvas height217 card too tall if h2+ etc ~130, fits but bricks cover. Use media max600: start card padding10 14, h2 14, eyebrow 8, start prompt 11, mini display:none. End panel stats smaller. Canvas border radius 12.
HUD at mobile grid; hide labels perhaps.
Body header h1 20. App width calc. footer wraps. No horizontal scrollbar due box-sizing border-box and max. Use `overflow-x:hidden` body perhaps.
At height under600 desktop width adjusts via vh formula. Should have CSS `width:min(960px, calc(100vw - 28px), calc((100dvh - 184px) * 1.6));` On mobile 375x667 gives min347,782 =347. On 1280x800 gives min960,1252,985.6 =960. body app overall heights: brand45+margin10 + HUD60+gap10 + canvas600 + footer36 + perhaps padding top/bottom16*2 =783. Good. At 800. `app` contains topbar, hud, canvas, footer with gaps8 perhaps 45+56+600+24 + gaps24 +32=781. Great.
If viewport height 600 desktop, calc((416)*1.6)=665 width canvas416 + noncanvas181=597. Good.
At mobile landscape height375, calc(191*1.6)=305 width, canvas190 +181=371, okay but width unnecessarily tiny in landscape; still fit. Fine.
`min()` with negative if extremely height<184, not relevant.
App topbar perhaps 42. Body padding12.
No horizontal scroll.
End overlay:
Position absolute inset0 display:grid place-items:center;background:rgba...; backdrop-filter blur(5px). `end-panel` max width 420; border. Stats row score and bricks. Restart button gradient. `#endOverlay[hidden]`.
Start overlay background radial transparent, no full dark. card at perhaps `margin-top:80px`? display flex align center. Game action behind visible. At 3sec ball likely mid field. Put card near bottom 63% to not cover bricks. `align-items:flex-end; padding-bottom:74px` since paddle at548/600 91%, card around y390? Card height120, bottom74 => y406, covers lower empty field. Great. Desktop start card from y390 to516, paddle y548 visible. Mobile canvas scale .36; CSS pixels are display, card height perhaps80 and bottom30 => y107, covers center but okay. CSS overlay based actual canvas CSS not logical. At mobile card 70 high within217.
Start card title perhaps "READY, PLAYER ONE?" and main prompt. Requirement exact message clear. Use `CLICK OR PRESS A KEY TO START`. AI tag above.
Attract mode card perhaps no huge h2 to minimize.
```
<div class=start-card>
<div class=ai-row><span class=pulse></span> SELF-PLAYING DEMO</div>
<div class=start-title>CLICK OR PRESS A KEY TO START</div>
<div class=start-sub>Mouse / ← → to move · Catch <b>↔</b> wide & <b>•••</b> multiball</div>
</div>
```
On mobile start title 11, sub hidden.
End button actual.
Sound icon initial button:
Inline svg `<path d=...>`; CSS. If muted toggle slash. Could avoid dynamic SVG by text `SOUND ON` with speaker `◖` perhaps. Use two inline svg spans. Simpler `<span id=soundIcon>♪</span><span id=soundText>SOUND ON</span>`. System glyph. Button title. Initial sound on. When click, no global start. Keyboard `M` toggles? Could add M mute. Footer perhaps `P PAUSE · M SOUND`.
Should avoid pressing M in demo starts game and toggles? Could ignore.
Audio tone functions. Sounds:
- paddle `tone(190,0.055,'sine',0.035,90)`
- brick hp `tone(240+hue, .04,'square',.018,50)`
- break `tone(360+hue,...triangle)`
- power two tones perhaps.
Audio max with multiball can be noisy. Throttle hit via `lastToneAt` gameTime? Each audio call can create. Limit globally:
```
let lastHitAudio=0;
if(gameTime-lastHitAudio>0.035)...
```
Not essential.
No audio autoplay in demo `audioCtx` null. start game click init and resume. If start by key user gesture also init. `startGame()` perhaps called from restart click, audio exists.
`soundBtn.addEventListener('click', e=>{...})` click user. Doesn't start demo. Good.
Testing mental:
On load `resetDemo()` sets mode demo but start screen initial. Should have `buildLevel` uses power coordinates. `serveBall` uses mode. `updateHUD`.
`requestAnimationFrame(loop)`, render initially.
At each update paddle AI. serve timer, ball stuck. After 0.55 launch. 3sec screenshot ball bounces and perhaps bricks not yet broken? Ball speed400, distance from paddle537 to bricks bottom ~300, takes .6 sec up, first brick at t1.2, then breaks. By3 sec several bricks, particles at moments. Good. Overlay bottom and visible power icons. AI paddle follows.
Brick top y86 row6 bottom y296? 6*26+5*10=206, start82 => end288. Ball distance paddle548 to bottom288=260 .65 sec. Good.
One level at least6 yes exactly6 60.
Ball speeds 400. Falls after brick up etc. Self demo continuous.
Powerup drops 120 px/s from y~100 to548 takes3.7sec; first power brick if broken early perhaps screenshot drop visible. Demo AI should catch eventually. Some powers on top row may break at1sec, drop at5sec. First 3 sec perhaps not catch but visible icons enough. Requirement everything interesting visible first seconds per instructions "Everything that makes demo interesting must be visible within first seconds: nothing hidden behind high score..." Power-up icons and particles visible. Wide/multi may not trigger first seconds. Could design demo to showcase powerups quickly: Place one power brick in bottom row at central likely hit initial ball. Initial ball angle perhaps aimed at bottom row power. Place bottom row c4 multi and c5 wide? Initial vertical center x480 hits bottom row between c4/c5? StartX35, width80 gap10: c0 35, c1 125,c2 215,c3 305,c4 395,c5 485,c6 575,... center x480 at right edge c4 (395-475) gap475-485. It may hit c4/c5 corner. Place power multi at r5,c4, wide at r5,c5 to ensure drop early. Initial angle slight -0.12 => x~440 by bottom bricks, hits c4. Multi drops from y272 to548 2.3sec, catch around3.7sec (after launch .6 +travel .65=1.25 +2.3=3.55) screenshot 3 perhaps drop. Could accelerate drops demo to160 ->1.7 catch3.0. Real 130. I can place power at r5,c3 (x305) if angle initial -0.25. Better initial demo ball angle=-0.20 (vx negative if angle formula) moves left x~425 hits c4. Yes multi c4. After 3 sec perhaps catch. `serveTimer=.35` demo, launch speed410, travel260/390=.67 first hit1.0, drop160 1.7 catch2.7, multiball active by screenshot 3! Great. AI catch power while also tracking ball? If demo AI always ball target, may not catch drop unless coincident. Make AI prioritize powerup when it is y>300 and no ball y>450 perhaps. After ball hit bottom brick, ball moving up, so AI can target drop. It should catch at2.7. Then 3 balls visible by screenshot. Good. Real gameplay initial serve1.0 and random slight angle, power bricks distributed.
Set demo first launch deterministic angle=-0.18 so hits c4 multi. `launchBall`: if mode demo and `demoServeCount===0` angle=-0.18 else random. `demoServeCount++`.
Bottom row powers include c4 multi and c5 wide. Good.
Powerups normal speed=145 in demo,120 play. Catch.
AI targeting:
```
const imminent=balls.filter...sort y desc[0]
let catchable=powerups.filter(p=>p.y>240 && p.y<paddle.y).sort y desc[0]
if(catchable && (!imminent || imminent.vy<0 || imminent.y<410)) target=catchable.x else predict(imminent)
```
After break ball vy<0 so target power. Catch. If ball descending high perhaps prediction.
Wide may happen later but visible legend and icons. Could pre-activate wide in demo? That would showcase but then start overlay says demo. Could make first drop wide rather than multi to show wide timer by3 sec. Requirement both interesting perhaps choose multi first yields 3 balls; wide icons visible. Later wide. Could set bottom c4 wide and c5 multi and initial hits c4 -> wide active by3. But multiball then later. I can make first brick drops a special? No each either. Could initial demo start with wideTimer=6? That is artificial but demo mode showcasing wider paddle while game self plays. However user clicks resets. Could do demo initial wideTimer=8 so first paint wide paddle and HUD countdown. But demo mode self playing can legitimately be mid-game with power-up active. Score could start 0 though. It is okay. Yet initial width should show standard vs wide? Screenshot only one. Perhaps start demo with wideTimer=7 to feature. But then by3 secs remaining4, clear visible. Multi caught by3. Excellent. Demo can be "AI playing" with active wide. This doesn't violate. However first game on click starts normal. Set `wideTimer=8` in resetDemo after serve. This makes power HUD and wide paddle visible immediately. Good.
But demo eventually wide expires and reobtains. Fine.
First screenshot 3sec: multiball and wide active perhaps stunning.
Game demo start overlay card and wide status.
Do not show end screens hidden obviously not needed first seconds.
Should ensure restart after game over. end overlay button. Keyboard any key if ended currently if key p? structure.
Pause:
If play pressing P toggles and overlay. If mode demo pressing P starts game, no pause. If ended pressing key restart before p. Fine.
Pause overlay can be simple div with `PAUSED` and `Press P to resume`; pointer click perhaps resume? Add button? footer. Canvas pointerdown while paused currently just pointer target, game remains paused. Perhaps click should resume? User might expect; add if paused `paused=false;hide`. But keyboard hint. Start overlay no.
`togglePause` set paused, pauseOverlay hidden attr. `pauseOverlay` pointer-events:none. In loop if paused no acc? currently if !paused accumulate, else acc=0 to avoid catch-up. Visual render continues. `last` still updates.
End overlay can show victory and game over with `data-result` for colors. `endTitle`, `endKicker`, `endMessage`, `finalScore`, `finalBricks`. On gameover bricks remaining perhaps stats label "BRICKS LEFT"; on victory "TIME" could track elapsed. Simpler both final score and bricks cleared.
```
<stat><span>FINAL SCORE</span><b id=finalScore>0</b></stat>
<stat><span>BRICKS CLEARED</span><b id=finalBricks>0 / 60</b></stat>
```
Works both.
Win overlay glow gold, gameover red.
Potential context errors: Canvas getContext always. Avoid roundRect helper incorrectly. I'll implement:
```
function pathRoundRect(x,y,w,h,r){
const rr=Math.min(r,w/2,h/2);
ctx.beginPath();ctx.moveTo...
}
```
Can use current ctx only.
Color utility `hsla(${hue}, 90%, 65%, ${alpha})`. Modern CSS canvas supports comma syntax `hsla(180, 90%, 65%, 0.5)` safe.
Gradients with `hsl`.
Background radial gradient create radial. Use `ctx.fillStyle=...`.
LineWidth under transform DPR okay.
Rendering order:
1 background full.
2 apply shake? playfield border perhaps stable.
3 draw field markings/progress stable.
4 save shake draw bricks, drops, particles (particles behind balls perhaps).
5 paddle, balls, floaters.
6 restore.
7 perhaps vignette.
DOM overlays.
Particles use globalCompositeOperation lighter only glows, then source-over reset.
Ball trail if speed.
`drawPowerup` uses `visualTime` not gameTime so pulse even pause but okay.
Bricks flash uses b.flash updated.
Static stars seeded but visualTime.
No collision with wall border visual thickness exact.
Paddle particles on hit: spawn 5 with white/cyan, but `spawnParticles` requires hue etc. implement `burst(x,y,hue,count,power=1)` with positions.
On break `burst(b.x+b.w/2,...,b.hue,20,1.2)` and perhaps shards particle shape. First hit `burst(contact point? currently damageBrick(b,ball) call after ball pos, use ball.x/y)`.
`damageBrick(brick,ball)`:
```
if hp>1:
hp--;score+=25;brick.flash=1; burst(ball.x,ball.y,hue,6,.45); addFloater(...'CRACKED',..) perhaps too many floaters, show small.
else ...
updateHUD
```
Crack visually after first. Score floats +100.
Power drop from center.
No actual brick removal animation besides particles. Could have `b.alive=false` instantly.
`brickCount=bricks.filter...` costly updateHUD every hit but 60 fine. Keep `remaining` global decrement. `bricksRemaining`.
Build sets60. damage break decrements.
End stats uses60-remain.
Powerup catching updates score by50 and HUD.
Floaters perhaps text drawn at actual brick width, 'CRACKED' 10 font. Good.
Combo? Not requirement. Could add combo score multiplier? Avoid complexity.
One level.
Lives 3 display. Demo lives never decrease, good.
Game over when 3 lost. Reset clears.
Victory when all60. Restart.
Mouse initial `pointerTarget=W/2`; in startGame if click at x? startGame resets then setPointer afterwards in event. good. Keyboard keydown starts then keys set. Make sure startGame calls `keys={}`? If keydown before set no issue. On restart click keys may remain; clear keys in startGame to prevent stuck. But keydown start then startGame clears then sets current. Good.
Pointerdown when demo calls startGame clears keys. Fine.
`startGame` calling `initAudio` itself perhaps ensure tone. Put at top if soundOn `initAudio()`, user triggered except perhaps end keyboard yes. Demo initial doesn't call startGame. Restart button user. Good.
`resetDemo` no audio.
`soundBtn` click in play toggles but if audio off no sounds. Mode demo if sound toggled on initializes audio but then demo sounds? `tone` could play in demo if audioCtx now exists, causing attract audio after only sound button click. Is that okay? User interacted sound. Perhaps tone checks mode==='play' so demo always silent. `tone` if mode!=='play' return. Toggle button in demo just on for future.
Sound button should not start via key? no.
Audio init on `startGame`. `tone(420,...)` startup perhaps after build.
`finish` plays win/lose tones via sequences setTimeout? setTimeout allowed but could keep simple two tone calls with oscillator delayed parameter. Add `tone(freq,duration,type,volume,slide=0,delay=0)` and start now+delay. Victory 523,659,784 delays. Game over descending. `tone` checks mode play and audio. Fine.
On life lose tone 140.
On power tones.
Audio oscillators stop and disconnect onended perhaps:
```
osc.onended=()=>{osc.disconnect();gain.disconnect();}
```
No errors.
Fixed loop with sound `gameTime` stops ended. okay.
Collision accuracy test with sweep:
Ball above brick moving down: expanded top=b.y-r. py < top, dy>0; tx perhaps entry negative if x within and dx0: txEntry=-inf, txExit=inf; y entry positive; normal ny=-1; dot vy*ny<0 good. Move to top, bounce vy=-. Nudge y-=0.05. damage.
Ball moving up from below: expanded bottom=b.y+b.h+r, py>bottom, dy<0; y1=(top-py)/neg larger, y2=(bottom-py)/neg smaller; entry=min = bottom => normal dy<0 -> ny=1; bounce down.
Ball from left dx>0, x entry left; normal -1. good.
Walls t formula:
left if dx<0, boundary=left+r. if px=500 dx=-100, t=(29-500)/-100=4.71 >1 until close. good.
Right t=(right-r-px)/dx positive.
Top same.
Paddle expanded left etc. moving down from above, top normal -1. bounce custom. Side collision at corner might normal top depending equal. fine.
No bottom boundary, ball loss.
When ball inside expanded brick due to exact adjacency after reflection from neighboring brick, entry<0 skip and could pass through current overlapped brick. Nudge prevents typical. At seam hitting two bricks, first destroyed, nudge normal. okay.
Multiple collisions cap6 at high cluster perhaps enough.
`remaining` update after collision t=0 with nudge ensures progress. Could add if t<1e-7 `remaining=Math.max(0,remaining-1e-5)` to avoid iteration all zero. Use.
At end if iteration max and remaining>0, do not move (safe, no tunneling), might briefly stick but next step resolves. fine.
Paddle speed keyboard 610px/s, mouse smoothing 28. Wide target 204. AI max700 with wide.
`paddle.y=546`, h=14, H600 bottom 40. Loss at y>620. Ball takes below paddle. Footer outside.
Playable bounds LEFT=24 RIGHT=936 TOP=24. Brick startX35 leaves 11 to wall; expanded ball may fit side gaps. Brick width80 etc right last x845? c9=35+810=845, end925, right wall936 leaves11. Good.
Power drop x within.
Ball stuck y=paddle.y-r-4 =535. On launch safe.
Bottom info perhaps draw small "BALL READY" at y paddle.y-32 if serve, but float? overlay bottom card in demo could cover. Real nice.
Paddle bounce angle max=68 deg. `offset` clamp; if hit near edge but ball center beyond paddle half due radius, clamp.
`ball.speed` initial 410 perhaps. Lives new speed reset410. Level difficulty moderate 60 bricks.
Power wide timer begins decreasing immediately catch. Paddle width target 204, actual smooth. 10sec.
If wideTimer reaches0 target132. Draw timer only >0.
When lose life wideTimer=0. Multi resets as one serve.
Powerups cleared on loss. good.
Score values display leading zeros. End score locale string perhaps `score.toLocaleString()` no issue. Avoid locale inconsistent but okay.
HUD initial 000000.
`remainingEl` "60 LEFT".
Mode badge.
CSS details:
body background:
```
background:
radial-gradient(circle at 20% -10%, #172758 0,transparent 40%),
radial-gradient(...),
#040711;
color...
font...
```
body::before fixed grid subtle with linear-gradients and mask. no external.
App perhaps no card background around canvas except HUD. Canvas wrap border1 #263a61 box-shadow. Header.
HUD backdrop blur background rgba, border.
Buttons.
Start overlay CSS animations `float` perhaps reduced motion respect? Canvas continuously moves, no need. Add prompt glow pulse via keyframes.
`@media(prefers-reduced-motion)` can't stop game requirement, ignore.
Focus visible restart/sound.
Canvas display:block;width:100%;height:100%;.
Touch action none.
Text user-select none.
Footer `display:flex;justify-content:space-between` with power legend and controls. On mobile perhaps justify center and hide power legend? Power legend helpful. At 375 width footer text 9; wrap.
Canvas overlay class uses z index.
No horizontal scrollbar: `*{box-sizing:border-box}`, body margin0 overflow-x:hidden. Start card max-width calc100%-16. End panel same.
At 1280x800 width formula uses `100dvh` perhaps supported. If sandbox screenshot 800. Great.
App perhaps header top 45, hud 58, canvas 600, footer 30, gaps 8*3=24, body padding 20? total45+58+600+30+24+24=781. Good. Body minheight100dvh grid center. At 800 vertical margin9.5. fine.
Set body padding:10px 14px. total 20. `.app{display:flex;flex-direction:column;gap:8px}` 4 children =3 gaps 24. topbar 42 hud56 footer26. 42+56+600+26+24+20=768. good.
Header h1 line 23 + p12=35 plus logo38 =>42.
HUD 56.
Footer two lines mobile but 26 desktop.
Use CSS width formula subtract 172? At 800 max app noncanvas168 => max canvas 632;960 canvas600 fits. `calc((100dvh - 170px)*1.6)` at800=1008 so960. At600=688 canvas430 plus168=598. Good.
App width `min(960px,calc(100vw - 28px),calc((100dvh - 170px)*1.6))`.
At mobile total noncanvas perhaps150 so okay.
Demo overlay start-card actual bottom72. At CSS canvas960x600 card perhaps110 high from418 to528, just above paddle at logical548 scaled =548. Good. Ball may pass behind but transparency.
`start-overlay{align-items:flex-end;justify-content:center;padding:0 16px 66px;}`. On mobile padding-bottom:30px. Card width auto max 560. backdrop blur.
End panel 420.
Could add scanline overlay canvas CSS pseudo `.canvas-wrap::after` pointer none background repeating-linear-gradient transparent etc opacity .1, but might reduce clarity. Polished subtle.
Vignette canvas.
Should have exact output one code block no text. I need compose full HTML perhaps ~1000 lines? Code necessarily large. Aim ~700 lines. I have plenty token. Should ensure no syntax errors. I'll draft systematically.
HTML skeleton:
```
<!DOCTYPE html>
<html lang=en>
<head>...
<style>...</style>
</head>
<body>
<div class=app>
<header...
...
<script>
(()=>{'use strict';
...
})();
</script>
</body>
</html>
```
No external.
SVG in sound button? Use `span class=sound-glyph` `◕` perhaps. Could use inline svg:
```
<svg viewBox...><path ...></path></svg><span ...>
```
If muted update button class and icon path? Use CSS `.muted .waves{opacity:0}` and `.mute-line{opacity:0}` etc. Not necessary for game. `<span id=soundGlyph>♪</span>`. Good.
Brand mark CSS bricks:
```
<div class=logo aria-hidden=true><i></i><i></i><i></i><b></b></div>
```
Use three mini bricks and ball. Polished.
HUD:
```
<section class=hud aria-label...>
<div class="hud-block score-block"><span class=hud-label>SCORE</span><strong id=score>000000</strong></div>
<div class=sector><span>SECTOR 01</span><small id=remaining>60 BRICKS</small></div>
<div class="hud-block lives-block"><span...>LIVES</span><div id=lives ...></div></div>
</section>
```
Add `modeLabel` in topbar brand p.
Canvas fallback text.
StartScreen ARIA perhaps role status. `startScreen` not hidden initial.
Pause hidden.
End hidden with role dialog and button.
Footer:
`<div class=power-legend><span><i class=wide-icon>↔</i> WIDE · 10s</span>...`
`<div class=controls><span>MOUSE / ← → MOVE</span><span>P PAUSE</span></div>`.
On mobile hide P? okay.
JS declarations.
Use `$=id=>document.getElementById(id)`.
DPR set:
```
const DPR=Math.min(2,window.devicePixelRatio||1);
canvas.width=W*DPR;...
ctx.setTransform...
```
DPR fixed. No resize listener needed because backing constant and CSS scales. If devicePixelRatio changes not important. Canvas CSS width100 height100.
Stars rand:
```
let starSeed=0x...;
function random() { ... } // used only stars? Math.random game.
```
Perhaps game random `Math.random`.
Stars generated using seeded.
Power map:
```
const powerMap = new Map([
['0,1','wide'],...
['5,4','multi'], ['5,5','wide']
]);
```
Ensure no duplicate key.
Tough formula.
Build level:
```
bricks=[];remaining=0;
for...
bricks.push({... seed:(r*cols+c)*...});
remaining=bricks.length;
```
Total constant set after build? `TOTAL_BRICKS=ROWS*COLS`.
Rows constants defined.
PowerMap uses rows.
Brick top 82.
Colors perhaps top r0=187,r1=213,r2=256,r3=315,r4=14,r5=42. Bottom gold with power tokens.
Power icon tokens contrast.
Tough bricks hp2 color still row but outer white. normal.
State initialization:
```
let mode='demo',ended=false,paused=false;
let score=0,lives=3,gameTime=0,visualTime=0,...
let paddle={...}
let controlMode='ai',pointerTarget=W/2,keys=Object.create(null);
```
`resetWorld(demo)` function perhaps avoid duplicate:
```
function clearWorld(){
particles.length=0;powerups.length=0;floaters.length=0;...
paddle.x=W/2;paddle.w=BASE_W;paddle.vx=0;
wideTimer=0;serveTimer=0;shake=0;
buildLevel();
}
function resetDemo(){
mode='demo';...
clearWorld();
wideTimer=8;
serveBall();
updateUI();
}
function startGame(){
initAudio();
mode='play';...
clearWorld();
serveBall();
...
tone...
}
```
`clearWorld` calls build level but updateHUD after.
`balls=[]` before serve.
`demoServeCount=0`.
`demoResetTimer=0`.
`endOverlay.hidden=true`, `pauseOverlay.hidden=true`; startScreen hidden mode play.
`canvasWrap.classList.toggle('live',mode==='play')`.
`updateHUD` uses remaining.
`clearWorld` doesn't set score/lives? each caller set.
`resetDemo` score=0 lives=3.
StartGame keys clear? `keys=Object.create(null)` but event listeners reference variable dynamic yes.
pointerTarget perhaps W/2 and controlMode mouse? Set keys. If start click then pointermove set.
`paddle.w=132`.
`serveBall` sets `serveTimer=mode==='demo'?0.35:0.9`, `balls=[create...]`.
`createBall(x,y,speed=410)` returns vx0 vy0 r7,stuck true? For multi use false. Define:
```
function newBall(x,y,speed=410,stuck=false){...}
```
serve passes true. Multi false.
`launchStuck`:
```
const a = mode==='demo' ? (demoServeCount++===0?-0.18:(Math.random()*.5-.25)) : (Math.random()*.24-.12);
b.stuck=false;b.vx=Math.sin(a)*b.speed;b.vy=-Math.cos(a)*b.speed;
```
Angle convention 0 straight up. negative left.
`newBall` has `id` perhaps for hue trail? `tint` optional 185. multi colors array [188,275,48]. Main ball cyan, added violet/gold. Draw with b.hue.
When serve new hue188.
In multi choose hue 280,42.
Physics bounce uses ball hue unaffected.
Update:
```
function update(dt){
if(ended)return;
gameTime+=dt;
if(demoResetTimer>0){... perhaps if reset return}
updatePaddle(dt);
wideTimer=max...
for b flash...
if(serveTimer>0){
serveTimer-=dt;
const b=balls[0]; if b... stuck position;
if<=0 launch...
}
for(const b of balls) if(!b.stuck) moveBall...
balls=balls.filter...
updatePowerups...
updateParticles...
updateFloaters...
shake...
if(!ended&&balls.length===0) loseLife();
if(!ended&&remaining===0){...}
}
```
Order if ball launched during serve same update then move with full dt causing tiny extra okay.
If all balls lost and loseLife serve before victory check. If last brick destroyed and ball lost same update, life lost before victory undesirable. Check victory before lose life:
```
if(remaining===0) handleClear else if balls empty loseLife
```
Good.
`demoResetTimer` if remaining0 set and then next updates decrement at top. On countdown, still balls move/power. if reset calls resetDemo and return.
At top:
```
if(mode==='demo'&&demoResetTimer>0){
demoResetTimer-=dt;
if(demoResetTimer<=0){resetDemo();return;}
}
```
But resetDemo changes UI and startScreen remains. good.
`ended` update particles freeze. okay.
Update paddle before wideTimer decrement perhaps target previous. Do wideTimer first then paddle.
`updatePaddle`:
```
let old=paddle.x;
if(mode==='demo'){...}
else {
let dir...
if(dir){controlMode='keys';paddle.x+=...}
else if(controlMode==='mouse'){...}
}
paddle.w +=...
clamp...
paddle.vx=(paddle.x-old)/dt;
```
Width update before clamp. If keyboard mode and no dir, paddle stays. On pointermove sets controlMode mouse. On keydown left sets keys. On keyup no pointer switch.
Demo prediction function fold:
```
function reflectX(x){
const min=LEFT+7,max=RIGHT-7,span=max-min;
let v=(x-min)%(2*span);if(v<0)v+=2*span;
return min+(v<=span?v:2*span-v);
}
```
AI select:
```
let target=W/2;
const sorted=balls.filter(!stuck).sort((a,b)=>b.y-a.y);
const threat=sorted.find(b=>b.vy>0)||sorted[0];
if(threat){
if(threat.vy>0){
let t=(paddle.y-threat.r-threat.y)/threat.vy;
if(t>0)target=reflectX(threat.x+threat.vx*t); else target=threat.x;
} else target=threat.x+threat.vx*.12;
}else target=paddle.x;
const p=powerups.filter(...).sort(...)[0];
if(p&&(!threat||threat.vy<0||threat.y<370))target=p.x;
const wobble=Math.sin(gameTime*2.1)*2; target+=wobble;
move clamp diff maxSpeed*dt with `1-Math.exp`? simple.
```
If stuck ball exists, threat omitted, p perhaps, else target paddle.x stays center; good.
AI maxSpeed 680. Prediction includes time ignoring brick bounces, but path to paddle after bricks may differ; updates continuously and near perfect.
Power target p.x is sway current.
`paddle.x += clamp(target-paddle.x,-maxDelta,maxDelta)`.
Player mouse smoothing:
`paddle.x += (pointerTarget-paddle.x)*Math.min(1,dt*28)` at240 factor.117, time response fast.
Mouse direct would be 1:1 but smoothing polished. "controlled with mouse" yes.
Keyboard speed 620.
moveBall uses `hit` objects allocation many but okay.
`consider` closure inside move perhaps many function allocations. fine.
Wall check if t valid and normal moving. For `best` tie use.
```
function offer(t,nx,ny,type,obj){
if(t>=-1e-8&&t<=1&&...){
t=Math.max(0,t);
if(!hit||t<hit.t)...
}
}
```
Dot computed with nx ny.
For sweep if t=0 and moving away dot filter.
Paddle collision if ball.vy>0, but side while vy<0 moving up could pass through paddle from below; below paddle already lost area and should not bounce. Fine.
"The ball must never pass through bricks or paddle." A ball moving upward underneath paddle shouldn't normally occur without already missed. Typical only top collision. Good.
Could collide paddle regardless vy for side? If vy<0 and below, pass up through paddle could violate but rare after side bounce? Ball side collision can reflect x but vy remains down, no. Brick can't be below paddle. no.
Offer paddle only when `b.vy>0`.
`hitPaddle` top vs side. For top offset. For side reflect both?
```
if(hit.ny===-1)...
else {reflect}
```
Side might return normal x. Set ball.x accordingly via nudge already.
Audio only on paddle top.
`burst` at paddle contact.
After each hit `iter` continue.
At no hit set x+=dx y+=dy, remaining=0.
After loop if b.y-b.r>H+30 b.dead=true.
Wall sounds perhaps call tone(120...) on top only but skip.
After wall reflect perhaps add tiny particles? no.
Bricks damage updates remaining; if ball hits cracked tough and destroys, score etc.
After damage normalize speed? `reflectBall` before damage; damage increments speed and then `setVelocitySpeed(b)`:
```
reflect;
damage;
normalizeVelocity(b);
```
damage for each ball only current speed? Implement in move:
```
if type brick {
reflect;
damageBrick(hit.obj,b);
normalizeBall(b);
}
```
`damageBrick` on break increments `b.speed=Math.min(MAX_SPEED,b.speed+4)` via passed ball. On first hit +1 perhaps.
Wall no speed normalize needed.
Paddle top uses b.speed. Side `reflect`.
`reflectBall` if nx !==0 etc.
`normalizeBall` to b.speed preserving direction. For paddle top direct already.
At brick horizontal if resulting abs(vy)<b.speed*.18:
```
let sign=b.vy===0?(Math.random()<.5?-1:1):Math.sign(b.vy)
b.vy=sign*b.speed*.18;
b.vx=Math.sign(b.vx||1)*Math.sqrt(...);
```
Could alter bounce unnatural, skip since min paddle vertical 0.375 (max68 cos=.375) and walls/brick only flip so remains >=.375. Initial vertical high. fine.
Damage:
```
function damageBrick(brick,ball){
brick.flash=1;
if(brick.hp>1){... addFloater brick center "CRACKED" perhaps; tone}
else{
brick.alive=false;brick.hp=0;remaining--;score...
ball.speed=Math.min(540,ball.speed+3.5)
burst center ...
addFloater '+100'
if power...
}
updateHUD();
}
```
Score first hit25. `updateHUD` cache.
Powerup drop `powerups.push({x:center,baseX:center,y:center,...})`.
Float text colors string perhaps `hsl`.
Particle count 22. `shake=Math.max(shake, brick.maxHp===2?5:3)`.
`burst` cap particles 480:
```
for count if particles.length>500 break
const angle=Math.random()*2pi,speed...
life=.35+Math.random()*.55
particles.push({x,y,vx:cos*speed,vy:sin*speed-30,gravity:180,hue,size,...,rot,spin,life,maxLife:life})
```
Brick shards initial velocities in all directions. Power catch `burst` hue power wide185 multi280 count20.
Update particles uses p.maxLife set. `p.size*life/max`.
Floater maxLife 0.9. score float.
When cracked add floater "CRACKED" life0.55 perhaps.
Power update:
```
for p:
p.y+=p.vy*dt;p.x=p.baseX+sin(gameTime*4+p.phase)*8;p.rot+=...
if intersects:
p.dead=true; collect
else if y>H+30 dead
filter
```
If paddle wide width changes no issue.
Collect:
```
if wide: wideTimer=10;addFloater(paddle.x,paddle.y-24,'WIDE PADDLE · 10s','#79f2ff')
else:
let needed=3-balls.length;
for i... {
let angles= needed===2?[-.5,.5]:[Math.random()<0...];
const nb=newBall(paddle.x,paddle.y-34,Math.max(410,...),false);
nb.hue=...
...
}
if needed0 score+=300 else score+=150;
```
If current balls stuck? new balls launched but serve timer still, possible catch while existing stuck only if power leftover from prior and life not cleared? I clear on loss. no.
Set new balls positions `paddle.x+(i?12:-12)` y=paddle.y-30.
Original remains. If original near paddle and moving down, clones upward. good.
`score+=150` and update HUD.
`burst(p.x,p.y,hue,24,1)`, `shake=3`.
Powerup label on canvas active. For multi `balls.length` perhaps includes stuck. draw if >1.
Catch p dead, but burst uses before filter.
Tone.
Wide target updated next fixed step immediate enough.
Powerup drops paused no movement.
`loseLife`:
```
if(mode==='demo'){
wideTimer=0;powerups=[];serveBall();return;
}
lives--;
powerups=[];
wideTimer=0;
shake=8;
if lives<=0 {updateHUD();finish(false)} else {serveBall();addFloater...;tone}
```
If demo wide goes away on first miss but AI good.
Play add floater at center x W/2 y400 `2 LIVES LEFT`.
`finish` no serve.
`updateHUD`.
`finish(win)` ended=true pausedfalse, final, etc.
Set startScreen hidden true. end overlay. sound. Update mode label `GAME OVER`? perhaps top mode "RUN ENDED" or "SECTOR CLEAR".
`modeLabel.textContent=win?'VICTORY':'GAME OVER'`.
StartGame resets to LIVE PLAY.
EndOverlay class `victory` toggle.
Button focus? On finish `restartBtn.focus({preventScroll:true})` would focus and key events; pressing space triggers button click plus global keydown start perhaps duplicate. Avoid auto focus. Button clickable.
End overlay keydown global any restarts; if focused no.
Key handler robust:
```
window.addEventListener('keydown',e=>{
const k=e.key;
if(['ArrowLeft','ArrowRight',' ','Spacebar'].includes(k))e.preventDefault();
if(e.repeat && mode==='demo') return? start only once mode changes.
if(mode==='demo'){startGame();}
else if(ended){if(!e.repeat)startGame();}
if(mode==='play'&&!ended){
if(k.toLowerCase()==='p'&&!e.repeat){togglePause();return;}
if(k.toLowerCase()==='m'...){toggleSound();return;}
}
if(k==='ArrowLeft'||...){keys[k]=true;controlMode='keys';}
},{passive:false});
```
If demo pressing p: startGame then immediately togglePause. User pressed a key to start but now paused blank action. Could return after start:
```
let justStarted=false;
if demo {startGame();justStarted=true;}
...
if(!justStarted&&p)...
```
Any key start. If arrow set.
If ended start and justStarted true.
R restart optional:
```
if(k.toLowerCase()==='r'&&!justStarted){startGame();return;}
```
Button restart.
P toggle:
```
paused=!paused; pauseOverlay.hidden=!paused;acc=0;
```
`acc` declared later let but handler executed after initialization fine.
M sound toggle function.
keyup delete keys. On blur reset.
Canvas pointerdown:
```
if(e.button!==undefined&&e.button!==0)return;
canvas.focus...
if(mode==='demo'){startGame();}
if(paused){paused=false;...}
setPointer(e);controlMode='mouse';
```
Ended overlay intercept; if somehow canvas ended hidden? no.
StartScreen pointer events none.
`pointermove`: if mode play&&!ended {setPointer;controlMode=mouse}` even paused okay.
Prevent default pointerdown? `e.preventDefault()` to avoid focus? I want focus, touch prevent scroll CSS. okay.
`canvasWrap.addEventListener('pointerdown',...,{passive:false})`.
Restart `click` e=>{e.stopPropagation();startGame();canvas.focus...}`.
Sound button `click` toggle and no start.
`toggleSound` update classes/text; if enabling init. `soundBtn.setAttribute('aria-pressed',String(soundOn))`.
No localStorage.
Loop:
```
function frame(now){
let dt=(now-last)/1000;last=now;
if(!Number.isFinite(dt)||dt<0)dt=0;
dt=Math.min(dt,.05);
visualTime+=dt;
if(!paused){
acc=Math.min(acc+dt,.12);
let n=0;
while(acc>=STEP&&n<30){update(STEP);acc-=STEP;n++;}
} else acc=0;
render();
requestAnimationFrame(frame);
}
resetDemo();
render();
requestAnimationFrame(frame);
```
If ended update returns, while consumes acc fine.
STEP=1/240 n max30 dt cap.05 =12.
`last=performance.now()` before render.
Render helpers.
`drawBackground`:
```
let g=ctx.createLinearGradient...
fillRect.
let rg=ctx.createRadialGradient(W*.5,120,0,...);
...
for stars...
ctx.strokeStyle rgba...
for x=LEFT;x<=RIGHT;x+=48 lines fromTOP toH...
...
// playfield top/side glow
ctx.shadowBlur=12; stroke...
```
Drawing full grid lines each frame ~30 okay.
Canvas field outside border background same.
At top `drawTopStatus`:
- fill small text.
```
ctx.font='600 10px ...';ctx.letterSpacing not supported? ctx.letterSpacing latest but skip. `fillText`.
ctx.textAlign left; fillStyle...
let leftText=wideTimer>0?`WIDE PADDLE ${wideTimer.toFixed(1)}s`:'BREAK THE GRID';
...
right `${remaining} BRICKS`;
center if balls.length>1 `MULTIBALL ×${balls.length}` perhaps ball count can3.
progress bar y62 h3 from35 to925 fill destroyed/total.
```
Top line y24, status y47. Brick top82.
If wide no wide show `SECTOR 01 // NEON GRID`.
DOM already sector. Fine.
Wide bar gradient.
Small power active pills could be drawn rounded backgrounds. Use text enough plus dot.
`ctx.textBaseline='middle'`.
Progress background.
Draw bottom faint line? no.
`drawBrick` uses `b.flash`. Body coordinates perhaps h=26. radius6.
For power icon needs not obscure cracks? cracks behind token.
Tough hp2:
```
ctx.strokeStyle='rgba(255,255,255,.65)'; lineWidth1.2; pathRoundRect x+2...
for rivet...
```
Normal outline alpha .25.
hp1 cracked border color orange.
Power icon last on top.
`drawPowerIcon(type,cx,cy,scale)` with current ctx and strokes:
wide:
```
line(-8s,0 to8s) arrows; arrow left at -8 line to-4,-4 etc.
```
multi three circles.
Use scale perhaps0.8 brick and1 drops.
Brick token dark background 30x18. Tough hp text `2` perhaps at x+7,y+7. With font 7. `ctx.fillText(brick.hp,...)`; only tough and no? Could display `II` top-right. Might be visible. Do two small bars at top-left x+7 andx+11 width2 h5, second alpha based hp? When hp1 first bright second faint. This signals hit points.
Crack path dark + light.
`drawPaddle`:
x = paddle.x-paddle.w/2. Wide hue185 else197? Base cyan. if wide shadow.
Gradient x horizontal perhaps center white. createLinearGradient(x,0,x+w,0), stops edges blue center white. Fill capsule.
Draw lower base dark.
Paddle `vx` afterimage:
if abs(vx)>100 draw translucent rect at x - sign(vx)*6? optional.
Wide timer small bar `w*0.6` at y+22. In game power text top.
Stuck ball auto launch:
draw dashed line:
```
ctx.save();setLineDash([3,7]);... from b.x,b.y-12 to b.x,b.y-58; restore
```
Text `AUTO LAUNCH` at paddle.y-40 perhaps ball y535, text y503. Only play and serve; demo overlay may cover but okay.
Draw paddle before balls so ball visible.
Powerup drawn before paddle so catch overlays perhaps.
Particles before paddle and after bricks? break particles visible over bricks. Order bricks->particles->powerups->paddle->balls->floaters.
`drawParticles` set globalCompositeOperation='lighter' and each fill rect rotated. 400 save/restore expensive but okay. Can per particle save. Use ctx.translate rotate. Fine.
`drawBall`:
Trail uses velocity; if not stuck. for i=5..1 alpha; draw circle radius r*(1-i*.09) perhaps.
Set shadowBlur18; radial gradient at x-2,y-2.
Ball hue hsl.
After draw small white center.
`drawFloater`: global alpha life/max, font 700 11 etc, textAlign center, shadow. Score popups visible.
`drawPowerup`: token capsule perhaps draw glow circle, outer ring rotate? Use p.rot but icon should upright. Draw outer ring arc with dashed?
```
ctx.save();translate(p.x,p.y);
shadow...
fill circle gradient...
ctx.rotate(sin...*.1)
...
drawPowerIcon
ctx.font7; text type below?
```
Drop width 34 and label tiny inside bottom perhaps token circle 28 and type label at y23; label could look good.
Power icon wide or multi.
Draw trail vertical line alpha.
`p.y` etc.
`render`:
```
ctx.setTransform(DPR,...); // ensure each frame
ctx.clearRect...
drawBackground();
ctx.save();
if(shake...) translate...
for bricks...
...
ctx.restore();
drawVignette();
```
If top status should shake? keep stable perhaps draw after objects? It overlays brick no.
Vignette radial gradient centered transparent to rgba black .3 edges. create every frame.
Flash on wide catch could draw full screen overlay? `flashScreen` var perhaps not needed.
Screen shake random under paused could still shake static random but shake only >0 and paused render jitter forever; if pause right after hit, random jitter while paused undesired. Use deterministic `Math.sin(visualTime*80)*shake` so movement with visual time anyway. Fine.
Could not shake if paused: `const s=paused?0:shake`.
No errors.
HUD update caching:
```
let lastScore=-1,lastLives=-1,lastRemaining=-1;
function updateHUD(){
if score changed...
if lives changed { livesEl.replaceChildren(...)}
...
}
```
Document.createElement. Hearts dots. `span.className='life'+(i<lives?'':' lost')`.
Mode update separate.
`scoreEl.textContent=String(score).padStart(6,'0')` if score>999999 no truncate.
`remainingEl.textContent=remaining+' BRICKS'`.
Perhaps CSS score `font-variant-numeric:tabular-nums`.
Final values.
Demo mode wide timer:
resetDemo calls clearWorld sets wide=0 then wide=8. serveBall. `modeLabel` etc.
Start overlay initially not hidden; resetDemo ensure false.
`endOverlay.hidden=true`.
`updateHUD`.
`resetDemo` called after demo victory while startScreen visible.
When game start, `clearWorld` removes all particles then power.
`gameTime=0`, visualTime not reset to keep stars.
`score=0,lives=3`.
`demoResetTimer=0`.
`demoServeCount=0`.
`ended=false`, paused=false.
`acc=0` but acc declared before reset call? Define `let accumulator=0` near state before functions. startGame handler uses. At initialization yes.
`startGame` initAudio only user but perhaps restart from key. fine.
`tone` start after all set.
Audio:
```
function initAudio(){
if(!soundOn)return;
try{
if(!audioCtx){const AC=window.AudioContext||window.webkitAudioContext;if(AC)audioCtx=new AC();}
if(audioCtx&&audioCtx.state==='suspended')audioCtx.resume().catch(()=>{});
}catch(_){audioCtx=null;}
}
```
No console errors. tone:
```
if(!audioCtx||!soundOn||mode!=='play')return;
try {...}catch{}
```
Catching avoids.
Gain exponential ramp cannot to0 use .0001. oscillator frequency set and exponential ramp with positive.
`delay` allowed.
`osc.start(now+delay);osc.stop(now+delay+duration+.02)`.
Sound button update `soundGlyph.textContent=soundOn?'♪':'×';`.
Click mute in demo no game start. Keyboard M in play toggle and returns; if demo pressing M starts game but doesn't toggle due justStarted; okay.
Instruction "Do not use ... alert etc" no. No network. No local storage.
Exactly one html code block.
Perhaps "Games must start in a self-playing demo mode behind Click or press a key to start". Yes resetDemo and overlay exact uppercase.
Power-up caught gives either wide or multi. Some bricks icons. yes.
Never pass through bricks or paddle continuous.
One level6 rows. yes.
Particles.
3lives and score.
Game over/victory restart.
Potential issue user clicking canvas in demo: pointerdown calls startGame, which calls initAudio and `tone(420...)`. startGame clears pointerTarget center; then setPointer event. good. Overlay start remains? startGame startScreen.hidden=true. yes.
Keydown arrow in demo calls startGame then keys Arrow true. good.
On end keydown any key calls startGame and then if p perhaps justStarted prevents pause. good.
On game over canvas overlay intercept pointer, button restart only. requirement restart option visible.
On victory same.
Pause overlay perhaps clicking canvas to resume code pointerdown sees paused, sets paused=false and hides, good.
Space key in play does nothing but prevented.
Mouse pointer leaves keeps last target, okay.
End overlay button pointerdown propagation stopped by overlay itself; click handler button start. Add `endOverlay.addEventListener('pointerdown',e=>e.stopPropagation())`; canvasWrap pointerdown listener is on ancestor bubble so stopped. Click on end panel nonbutton does nothing. Keydown any restart. Clear message "PRESS ANY KEY OR" perhaps under button.
End overlay `<p class=restart-hint>Press any key to restart</p>`.
Button restart click works.
Score popup on wide `updateHUD`. Good.
Physics serve ball stuck and paddle expands wide demo; ball follows center. update serve before move, if launch set stuck false then moves. `updatePaddle` AI no nonstuck balls so paddle stays perhaps wide. initial angle left hits multi.
AI after ball launch: during updatePaddle before launch no nonstuck target, stays center. Next step predicts ball vy<0 target x+vx*.12 ~471, moves left slightly. Fine.
Bottom row r5 y=82+5*36=262, h26 end288. Ball from535 up410, travel247/.~395=.625, launch .35 so first hit~.975. x=480+sin(-.18)*410*.625=480-45.9=434 hits brick c4 395-475. multi drop center435 y275. Catch drop speed145, distance paddle top546 - power radius? update catch when p.y+15>=546 =>531; from275 to531=256/145=1.77, total2.75. AI target drop since ball vy<0 after hit. It moves to435, catches. New 2 balls at paddle around2.75. By3 sec screenshot 3 balls and wideTimer~5.25 (initial8 minus2.75=5.25; collect multi doesn't reset wide), perfect. Power c4 removed, particles at1sec not at3 but other bricks perhaps ball after destroy continues upward (bounce downward actually ball moving down? initial ball moving up hits brick from below: hits bottom edge, reflect down. After breaking bottom row, vy down toward paddle. AI conflict: `threat.vy>0` and y around288 <370, power priority condition `threat.y<370` true even though descending, so target power and may miss ball while catching. At first hit from below moving up, after bounce down, both ball and power drop: ball speed410 down reaches paddle in .65 sec around1.6; power catch2.75. AI must prioritize ball or lose. Condition p && threat.y<370 chooses power, bad; ball descends and AI sits at435 (actually predicted ball paddle x~550? ball vx left -73, after .65 x386; power435 close perhaps can hit both? Paddle wide204 centered435 spans333-537, ball x386 within, so catches anyway thanks wide. Great initial wide demo ensures. Then ball paddle bounce at1.6, angle offset (386-435)/102=-.48 -> up-left. Power continues, AI follows power and catches2.75. Works. Without wide player may need choose.
AI general should prioritize immediate ball if time to paddle<~0.9 and horizontal difference large. Better logic:
```
if p && (!threat || threat.vy<0 || (threat.vy>0 && (paddle.y-threat.y)/threat.vy>1.15)) target=p.x
```
At first threat time .62<1.15 so target ball, after paddle bounce vy<0 then power target. Still catches power around2.75 because after1.6 target435, yes. This better. Use this condition. At later imminent protect ball.
Demo wide helps no misses.
After catch multi wide timer visible. Great.
Player initial real bottom power perhaps first brick multi too, power drops but user handles.
Power fall speed normal128, demo145.
Paddle default132 and wide204.
Multi source initial existing ball after paddle bounce upward by2.75 likely hits other bricks. New balls3.
No high score gating.
Sound toggle label on first paint "SOUND ON". okay.
Canvas scaling logical star positions etc.
Use `ctx.font='... system-ui'` canvas supports.
CSS sound `white-space:nowrap`.
DOM overlay pointer events end auto, start none, pause none.
One issue backdrop-filter over canvas can be expensive but okay.
One issue canvas CSS height: `.canvas-wrap{aspect-ratio:8/5}` and canvas `height:100%` with parent no explicit height but aspect ratio computes yes.
Width app dynamic based dvh.
At 375, start title "CLICK OR PRESS A KEY TO START" long ~31 chars at11px average6=186 fits card width315. good.
HUD score strong 20 desktop, 15 mobile.
Lives dots.
Footer at375 two groups perhaps wrap no horizontal.
Body app width formula at mobile height667, fine.
I'll now formulate code fully. Should not include comments too many but helpful. Ensure no external. I can include comments for collision etc.
I'll estimate code output ~35k tokens? I can fit. Desired perhaps 20k. I have max. Should be accurate syntax.
Write CSS then JS.
Detailed helper `roundPath`:
```
function roundPath(x,y,w,h,r){
r=Math.max(0,Math.min(r,w/2,h/2));
ctx.beginPath();
ctx.moveTo(x+r,y);
ctx.arcTo(x+w,y,x+w,y+h,r);
...
ctx.closePath();
}
```
ArcTo with negative w never. fine.
Background border `roundPath(LEFT-8,TOP-8,RIGHT-LEFT+16,H-TOP? ... )` perhaps outer full toH-14. Draw border at x14,y14,w932,h572 r16; playable bottom open but border all around could visually imply bottom wall though ball dies below. Better draw only left/top/right lines not bottom. `ctx.beginPath();moveTo(LEFT,H-22);lineTo(LEFT,TOP+12);quadratic...` etc. Or full subtle rounded frame and bottom reddish line signaling danger. Standard breakout has three walls no bottom. Draw:
```
ctx.strokeStyle rgba;begin move left,bottom; line left,top+12;quad left,top left+12,top;line right-12,top;quad...line right,bottom;stroke
```
TOP=24.
No bottom line.
Danger zone gradient bottom from y520 to600 red faint.
Background stars behind walls. `drawBackground` fill.
draw top status progress bar coordinates x35..925.
Render field objects within no clipping? Particles may go outside walls; okay visual.
Wall collision uses TOP24, borders.
Power icons on bricks use `drawPowerIcon` which calls begin paths and sets colors, should save/restore. Function:
```
function drawPowerIcon(type,x,y,s=1,color='#fff'){
ctx.save();ctx.translate(x,y);ctx.scale(s,s);ctx.strokeStyle=color;...
if wide ...
else...
ctx.restore();
}
```
`ctx.scale` affects lineWidth? lineWidth scales, okay set2/s? not necessary.
Brick scale .7 lineWidth2 ->1.4.
Drop scale1 line2.
Three dots fill.
Crack path:
```
function drawCracks(b){
const x=b.x,y=b.y;
const offset=(b.seed%7)-3;
ctx.beginPath...
ctx.strokeStyle='rgba(3,8,20,.75)';lineWidth=2;stroke();
ctx.translate(.7,.4)? Avoid transform. just second path same with strokeStyle rgba white .45 lineWidth.7 and same path still current; after stroke path remains. set lineWidth .65 stroke again gives centered light, looks bright crack. Could dark thick then white thin. good.
}
```
Branches path.
Tough hp pips at left top could be under crack okay.
Brick gradients:
```
const g=ctx.createLinearGradient(0,b.y,0,b.y+b.h);
g.addColorStop(0,`hsl(${hue},92%,${hp===1?56:68}%)`);
...
```
Bright top, dark bottom.
shadowBlur power? 14:7.
When cracked hue color still.
Flash overlay after icon? Flash should cover all but icon perhaps draw before icon. do body, cracks, flash, power icon.
Tough border before flash.
Particles color alpha using globalAlpha easier fillStyle `hsl(${p.hue},95%,65%)`; globalAlpha. reset at end. Save overall.
Floaters color supplied valid.
Wide status text toFixed. Progress fraction `(TOTAL_BRICKS-remaining)/TOTAL_BRICKS`.
If remaining0 progress full.
End screen stats use score `String(score).padStart(6,'0')`.
FinalBricks `${TOTAL_BRICKS-remaining} / ${TOTAL_BRICKS}`.
`finish` tone call after ended true; tone checks mode only not ended, good.
`startGame` tone after mode play.
`loseLife` lives0 finish game over tone plus earlier loss tone? avoid if0 only finish.
`handle victory` in update:
```
if(remaining===0){
if(mode==='demo'){
if(demoResetTimer<=0){demoResetTimer=1.6;addFloater(...);}
} else finish(true);
} else if(balls.length===0) ...
```
When demoResetTimer reaches0 at top resetDemo. But if demoResetTimer is set then next update top decrement, good. However at same update after set, subsequent fixed step top, okay. `demoResetTimer<=0` when countdown exactly perhaps reset return.
Play finish then else if no ball? use if/else ensures.
After finish update particles later? Order particles before end check, fine.
Update order code:
```
updatePowerups(dt);
updateEffects(dt);
if(remaining===0)...else if balls empty...
```
Powerup catch could add balls when all original died same step, preventing life loss if caught after balls lost; powerup paddle can catch even no balls and multi spawns 2 only, total2 not3 because needed3 but code loop needed=3 and adds3 at paddle, okay. But normally should lose life before catching power if no balls. I can handle no balls before power update? At moment balls fall and power catches same step rare. Not important. To ensure multi always 3 if none and catch, `needed=3-balls.length` adds3 ->3. Good.
If no balls and power no catch then life loss.
New balls spawn when no original speed max via reduce initial410.
`balls` reassigned filter before powerups; powerups collect uses current.
`serveTimer` after life new ball.
If balls empty and remaining0 victory priority.
`updateEffects` loops bricks flash perhaps at beginning. Particles and floaters.
`shake` decays.
`wideTimer` decays even demoReset countdown, fine.
On play ended update stops.
`paddle.target` computed from wideTimer inside updatePaddle.
Life HUD initially demo3.
Mode label resetDemo "AI DEMO"; startGame "LIVE PLAY"; paused set "PAUSED" and resume "LIVE PLAY"; end.
When togglePause use modeLabel. Sound unaffected.
Start screen says AI playing always while demo even demo reset.
Top status left wide, right bricks. Score.
I should include `aria-live=polite` end overlay perhaps.
No horizontal scrollbar from end panel width min. Good.
Potential Chrome console error from AudioContext not allowed? Created in click only, safe. If key start recognized user gesture. On sound button click in demo `toggleSound` initAudio user. `resume().catch` handles. No console.
No attempt at fullscreen or storage.
Now respond.