Run details
- Model
- claude-opus-5-5
- Route
- Claude Code CLI
- Reasoning
- provider default
- Limits
- 60 min · 128,000 output tokens
- Date
- 21 Oct 2026
- Time
- 4 min 39 s
- Size
- 31 kB
- Tokens
- 6 in, 31,455 out
- List price
- $0.63 ($4 in / $20 out per M)
- Paid
- Not billed per run (CLI plan)
Exact prompt
Make a Breakout game I can play right now with the keyboard or the mouse. Give it several levels, a score and some juice. It must be one complete, self-contained HTML file: inline all CSS and JavaScript, and make no network requests of any kind (no CDNs, web fonts or remote images). Save it as index.html in the current directory, then stop. Do not ask questions.
Source
31 kB 718 lines Raw
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Breakout</title>
<style>
html,body{margin:0;height:100%;background:#05050c;overflow:hidden;font-family:system-ui,-apple-system,"Segoe UI",Roboto,sans-serif}
canvas{display:block;position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);cursor:none;border-radius:10px;
box-shadow:0 0 80px rgba(80,120,255,.25),0 0 0 1px rgba(120,150,255,.15)}
</style>
</head>
<body>
<canvas id="c"></canvas>
<script>
(() => {
'use strict';
const W = 800, H = 600, PLAY_TOP = 50;
const cv = document.getElementById('c'), ctx = cv.getContext('2d');
let K = 1;
function resize() {
const dpr = window.devicePixelRatio || 1;
const s = Math.min(innerWidth / W, innerHeight / H) * 0.97;
cv.style.width = (W * s) + 'px'; cv.style.height = (H * s) + 'px';
cv.width = Math.round(W * s * dpr); cv.height = Math.round(H * s * dpr);
K = s * dpr;
}
addEventListener('resize', resize); resize();
// ---------- helpers ----------
const clamp = (v, a, b) => v < a ? a : v > b ? b : v;
const rand = (a, b) => a + Math.random() * (b - a);
const easeOutBack = t => { const c1 = 1.70158, c3 = c1 + 1; return 1 + c3 * Math.pow(t - 1, 3) + c1 * Math.pow(t - 1, 2); };
function rr(x, y, w, h, r) {
r = Math.min(r, w / 2, h / 2);
ctx.beginPath(); ctx.moveTo(x + r, y);
ctx.arcTo(x + w, y, x + w, y + h, r); ctx.arcTo(x + w, y + h, x, y + h, r);
ctx.arcTo(x, y + h, x, y, r); ctx.arcTo(x, y, x + w, y, r); ctx.closePath();
}
// ---------- audio (synthesized, no assets) ----------
let AC = null, muted = false, noiseBuf = null;
function audio() {
if (!AC) {
try {
AC = new (window.AudioContext || window.webkitAudioContext)();
noiseBuf = AC.createBuffer(1, AC.sampleRate * 0.3, AC.sampleRate);
const d = noiseBuf.getChannelData(0);
for (let i = 0; i < d.length; i++) d[i] = Math.random() * 2 - 1;
} catch (e) { AC = null; }
}
if (AC && AC.state === 'suspended') AC.resume();
}
function tone(freq, dur = 0.08, type = 'square', vol = 0.06, slide = 0, delay = 0) {
if (!AC || muted) return;
const t = AC.currentTime + delay;
const o = AC.createOscillator(), g = AC.createGain();
o.type = type; o.frequency.setValueAtTime(freq, t);
if (slide) o.frequency.exponentialRampToValueAtTime(Math.max(30, freq * slide), t + dur);
g.gain.setValueAtTime(vol, t); g.gain.exponentialRampToValueAtTime(0.0001, t + dur);
o.connect(g); g.connect(AC.destination); o.start(t); o.stop(t + dur + 0.02);
}
function noise(dur = 0.08, vol = 0.05, freq = 2000) {
if (!AC || muted) return;
const t = AC.currentTime, s = AC.createBufferSource(), f = AC.createBiquadFilter(), g = AC.createGain();
s.buffer = noiseBuf; f.type = 'lowpass'; f.frequency.value = freq;
g.gain.setValueAtTime(vol, t); g.gain.exponentialRampToValueAtTime(0.0001, t + dur);
s.connect(f); f.connect(g); g.connect(AC.destination); s.start(t); s.stop(t + dur);
}
const sfx = {
paddle() { tone(220, 0.1, 'square', 0.05, 1.8); },
wall() { tone(520, 0.04, 'square', 0.025); },
crack() { tone(760, 0.06, 'triangle', 0.06, 0.8); noise(0.04, 0.03, 3000); },
metal() { tone(1400, 0.09, 'square', 0.025, 0.6); tone(2100, 0.05, 'sine', 0.03); },
brk(c) { const f = 330 * Math.pow(2, Math.min(c, 24) / 12); tone(f, 0.13, 'triangle', 0.08, 1.5); noise(0.07, 0.05, 2500); },
launch() { tone(300, 0.15, 'sine', 0.08, 2.5); },
lose() { tone(420, 0.7, 'sawtooth', 0.07, 0.12); noise(0.4, 0.08, 600); },
power() { [0, 4, 7, 12].forEach((s, i) => tone(523 * Math.pow(2, s / 12), 0.1, 'square', 0.04, 0, i * 0.05)); },
clear() { [0, 4, 7, 12, 16, 19, 24].forEach((s, i) => tone(392 * Math.pow(2, s / 12), 0.16, 'triangle', 0.07, 0, i * 0.07)); },
over() { [12, 7, 3, 0, -5].forEach((s, i) => tone(330 * Math.pow(2, s / 12), 0.3, 'triangle', 0.07, 0, i * 0.18)); },
};
// ---------- levels ----------
// '.' empty, '1'-'3' hit points, '#' unbreakable
const LEVELS = [
{ name: 'Warm Up', map: [
'............',
'111111111111',
'111111111111',
'111111111111',
'111111111111',
'111111111111']},
{ name: 'Pyramid', map: [
'.....22.....',
'....2112....',
'...211112...',
'..21111112..',
'.2111111112.',
'211111111112',
'222222222222']},
{ name: 'Checkers', map: [
'2.2.2.2.2.2.',
'.2.2.2.2.2.2',
'3.1.3.1.3.1.',
'.1.3.1.3.1.3',
'1.1.1.1.1.1.',
'............',
'##.##..##.##']},
{ name: 'Invader', map: [
'..1......1..',
'...1....1...',
'..22222222..',
'.22.2222.22.',
'222222222222',
'2.22222222.2',
'2.2......2.2',
'...33..33...']},
{ name: 'Fortress', map: [
'333333333333',
'#2222222222#',
'#2111111112#',
'#2133333312#',
'#2111111112#',
'#2222222222#',
'####....####']},
{ name: 'Diamond Rush', map: [
'.....33.....',
'....3223....',
'...321123...',
'..32111123..',
'.3211111123.',
'..32111123..',
'...321123...',
'....3223....',
'#....33....#']},
];
const PALETTE = ['#ff4d6d', '#ff8c42', '#ffd23f', '#3ddc97', '#3fa7ff', '#9b5de5', '#f15bb5', '#00e5c4', '#fee440'];
const COLS = 12, GAP = 4, BX0 = 20, BY0 = 92, BH = 22;
const BW = (W - BX0 * 2 - GAP * (COLS - 1)) / COLS;
const POWERS = {
W: { name: 'WIDE', col: '#4ee6ff', ch: '↔', w: 3 },
M: { name: 'MULTIBALL', col: '#ffd23f', ch: '●', w: 3 },
S: { name: 'SLOW', col: '#3ddc97', ch: '◷', w: 2 },
F: { name: 'FIREBALL', col: '#ff6a2a', ch: '✸', w: 2 },
L: { name: '+1 LIFE', col: '#ff4d9d', ch: '♥', w: 1 },
};
const POWER_BAG = Object.keys(POWERS).flatMap(k => Array(POWERS[k].w).fill(k));
// ---------- state ----------
let state = 'title', paused = false, time = 0;
let score = 0, shownScore = 0, lives = 3, level = 0, combo = 0, comboPulse = 0;
let best = 0; try { best = +localStorage.getItem('breakout-best') || 0; } catch (e) {}
let shake = 0, hitstop = 0, flashA = 0, flashCol = '#fff', stateT = 0, levelT = 0;
let banner = { t: 99, title: '', sub: '' };
let bricks = [], balls = [], parts = [], pops = [], caps = [];
let wideT = 0, slowT = 0, fireT = 0;
const paddle = { x: W / 2, y: H - 38, w: 110, tw: 110, h: 14, vx: 0, sq: 0 };
const MAXSP = 820;
const stars = Array.from({ length: 90 }, () => ({ x: Math.random() * W, y: Math.random() * H, z: Math.random() }));
const bg = document.createElement('canvas'); bg.width = W; bg.height = H;
{
const b = bg.getContext('2d');
const g = b.createLinearGradient(0, 0, 0, H); g.addColorStop(0, '#0d0b24'); g.addColorStop(1, '#060a18');
b.fillStyle = g; b.fillRect(0, 0, W, H);
b.strokeStyle = 'rgba(90,120,255,0.05)'; b.lineWidth = 1;
for (let x = 0; x <= W; x += 40) { b.beginPath(); b.moveTo(x + .5, 0); b.lineTo(x + .5, H); b.stroke(); }
for (let y = 0; y <= H; y += 40) { b.beginPath(); b.moveTo(0, y + .5); b.lineTo(W, y + .5); b.stroke(); }
const v = b.createRadialGradient(W / 2, H / 2, H * 0.3, W / 2, H / 2, H * 0.85);
v.addColorStop(0, 'rgba(0,0,0,0)'); v.addColorStop(1, 'rgba(0,0,0,0.55)');
b.fillStyle = v; b.fillRect(0, 0, W, H);
}
function loadLevel(n) {
const L = LEVELS[n];
bricks = [];
L.map.forEach((row, r) => [...row].forEach((ch, c) => {
if (ch === '.') return;
const unb = ch === '#', hp = unb ? 1 : +ch;
bricks.push({ x: BX0 + c * (BW + GAP), y: BY0 + r * (BH + GAP), w: BW, h: BH, hp, max: hp, unb,
col: unb ? '#8a90a6' : PALETTE[r % PALETTE.length], flash: 0, wob: 0, delay: r * 0.05 + c * 0.018, alive: true });
}));
caps = []; wideT = slowT = fireT = 0; levelT = 0;
resetBalls();
banner = { t: 0, title: 'LEVEL ' + (n + 1), sub: L.name };
}
function newBall() { return { x: paddle.x, y: paddle.y - paddle.h / 2 - 7, vx: 0, vy: 0, r: 7, sp: 0, stuck: true, off: rand(-15, 15), trail: [] }; }
function resetBalls() { balls = [newBall()]; combo = 0; }
const baseSpeed = () => 390 + level * 30;
function startGame() {
score = 0; shownScore = 0; lives = 3; level = 0; paused = false;
parts = []; pops = [];
loadLevel(0); state = 'play';
}
function launch() {
let any = false;
for (const b of balls) if (b.stuck) {
const a = clamp(b.off / (paddle.w / 2) * 0.8 + paddle.vx * 0.0003 + rand(-0.12, 0.12), -0.9, 0.9);
b.sp = baseSpeed(); b.vx = Math.sin(a) * b.sp; b.vy = -Math.cos(a) * b.sp; b.stuck = false; any = true;
}
if (any) { sfx.launch(); paddle.sq = 0.6; burst(paddle.x, paddle.y - 10, 10, '#9ef', 160); }
}
// ---------- effects ----------
function burst(x, y, n, col, sp = 220, opt = {}) {
for (let i = 0; i < n && parts.length < 900; i++) {
const a = opt.ang !== undefined ? opt.ang + rand(-opt.spread, opt.spread) : rand(0, Math.PI * 2);
const v = rand(sp * 0.3, sp);
parts.push({ x, y, vx: Math.cos(a) * v, vy: Math.sin(a) * v, life: rand(0.4, 0.9) * (opt.life || 1), max: 0,
col: Array.isArray(col) ? col[(Math.random() * col.length) | 0] : col, s: rand(2, opt.size || 5),
rot: rand(0, 6), vr: rand(-12, 12), g: opt.g ?? 500 });
parts[parts.length - 1].max = parts[parts.length - 1].life;
}
}
function popup(x, y, txt, col, size = 18) { pops.push({ x, y, txt, col, t: 0, size }); }
function addShake(v) { shake = Math.min(18, shake + v); }
// ---------- game events ----------
function hitBrick(br, b) {
br.flash = 1; br.wob = 1;
if (br.unb) { sfx.metal(); burst(b.x, b.y, 5, '#dde', 180, { size: 3 }); addShake(1.5); return; }
br.hp--;
if (br.hp <= 0) destroyBrick(br);
else { sfx.crack(); score += 5; burst(b.x, b.y, 6, br.col, 180, { size: 3 }); addShake(2); }
}
function destroyBrick(br) {
br.alive = false; combo++; comboPulse = 1;
const mult = Math.min(8, 1 + Math.floor((combo - 1) / 3));
const pts = 10 * br.max * mult;
score += pts;
const cx = br.x + br.w / 2, cy = br.y + br.h / 2;
popup(cx, cy, '+' + pts + (mult > 1 ? ' x' + mult : ''), mult > 1 ? '#ffe066' : '#fff', mult > 1 ? 20 : 16);
burst(cx, cy, 16, [br.col, br.col, '#fff'], 280, { size: 7 });
sfx.brk(combo);
addShake(3 + Math.min(combo, 10) * 0.3);
if (combo % 5 === 0) { hitstop = 0.05; flashA = 0.12; flashCol = '#fff'; }
if (Math.random() < 0.14) {
const k = POWER_BAG[(Math.random() * POWER_BAG.length) | 0];
caps.push({ x: cx, y: cy, k, vy: 140, t: 0 });
}
if (!bricks.some(b => b.alive && !b.unb)) levelClear();
}
function levelClear() {
state = 'clear'; stateT = 0;
const bonus = 1000 + level * 500 + lives * 100;
score += bonus;
banner = { t: 0, title: 'LEVEL CLEAR!', sub: 'Bonus +' + bonus };
sfx.clear(); flashA = 0.5; flashCol = '#bff'; addShake(10);
for (const b of balls) burst(b.x, b.y, 30, ['#fff', '#9ef', '#ffd23f'], 400);
balls = []; caps = [];
for (let i = 0; i < 6; i++) burst(rand(100, W - 100), rand(120, 300), 25, PALETTE, 350, { size: 6 });
}
function nextLevel() {
level++;
if (level >= LEVELS.length) { state = 'win'; stateT = 0; saveBest(); sfx.clear(); }
else { loadLevel(level); state = 'play'; }
}
function loseLife() {
lives--; addShake(16); flashA = 0.45; flashCol = '#ff2850'; sfx.lose();
burst(paddle.x, paddle.y, 40, ['#ff4d6d', '#ff8c42', '#fff'], 380);
wideT = slowT = fireT = 0; caps = [];
if (lives <= 0) { state = 'over'; stateT = 0; saveBest(); setTimeout(sfx.over, 400); balls = []; }
else { resetBalls(); popup(paddle.x, paddle.y - 30, lives === 1 ? 'LAST LIFE!' : 'OUCH!', '#ff6680', 22); }
}
function saveBest() {
if (score > best) { best = score; try { localStorage.setItem('breakout-best', best); } catch (e) {} }
}
function applyPower(k) {
const P = POWERS[k];
sfx.power(); flashA = 0.15; flashCol = P.col;
popup(paddle.x, paddle.y - 34, P.name, P.col, 22);
burst(paddle.x, paddle.y, 24, [P.col, '#fff'], 300, { ang: -Math.PI / 2, spread: 1.2 });
score += 50;
if (k === 'W') wideT = 14;
else if (k === 'S') slowT = 10;
else if (k === 'F') fireT = 8;
else if (k === 'L') lives = Math.min(7, lives + 1);
else if (k === 'M') {
if (balls.some(b => b.stuck)) launch();
const add = [];
for (const b of balls.slice(0, 4)) for (const d of [-0.45, 0.45]) {
const a = Math.atan2(b.vy, b.vx) + d;
const nb = { ...b, trail: [], vx: Math.cos(a) * b.sp, vy: Math.sin(a) * b.sp };
fixAngle(nb); add.push(nb);
}
balls.push(...add);
}
}
// ---------- physics ----------
function fixAngle(b) {
const min = b.sp * 0.3;
if (Math.abs(b.vy) < min) {
b.vy = (b.vy < 0 ? -1 : 1) * min;
b.vx = (b.vx < 0 ? -1 : 1) * Math.sqrt(b.sp * b.sp - min * min);
}
}
function moveBall(b, dt) {
if (b.stuck) {
b.off = clamp(b.off, -paddle.w / 2 + 8, paddle.w / 2 - 8);
b.x = paddle.x + b.off; b.y = paddle.y - paddle.h / 2 * (1 - 0.4 * paddle.sq) - b.r; return;
}
const dist = b.sp * dt, steps = Math.max(1, Math.ceil(dist / 3)), sdt = dt / steps;
for (let i = 0; i < steps; i++) {
if (state !== 'play') return;
b.x += b.vx * sdt; b.y += b.vy * sdt;
// walls
if (b.x < b.r) { b.x = b.r; b.vx = Math.abs(b.vx); wallFx(b, 0); }
else if (b.x > W - b.r) { b.x = W - b.r; b.vx = -Math.abs(b.vx); wallFx(b, Math.PI); }
if (b.y < PLAY_TOP + b.r) { b.y = PLAY_TOP + b.r; b.vy = Math.abs(b.vy); wallFx(b, Math.PI / 2); }
// paddle
const pw = paddle.w, ph = paddle.h, px = paddle.x - pw / 2, py = paddle.y - ph / 2;
if (b.vy > 0 && b.y < paddle.y) {
const cx = clamp(b.x, px, px + pw), cy = clamp(b.y, py, py + ph);
if ((b.x - cx) ** 2 + (b.y - cy) ** 2 <= b.r * b.r) {
const rel = clamp((b.x - paddle.x) / (pw / 2), -1, 1);
const a = rel * 1.05;
b.sp = Math.min(MAXSP, b.sp + 6);
b.vx = Math.sin(a) * b.sp; b.vy = -Math.cos(a) * b.sp;
b.y = py - b.r;
paddle.sq = 1; combo = 0; sfx.paddle();
burst(b.x, py, 8, '#9ef', 200, { ang: -Math.PI / 2, spread: 1.1, size: 3 });
addShake(1.5);
}
}
// bricks
for (const br of bricks) {
if (!br.alive) continue;
const cx = clamp(b.x, br.x, br.x + br.w), cy = clamp(b.y, br.y, br.y + br.h);
let nx = b.x - cx, ny = b.y - cy;
const d2 = nx * nx + ny * ny;
if (d2 > b.r * b.r) continue;
if (fireT > 0 && !br.unb) {
br.hp = 0; destroyBrick(br);
if (state !== 'play') return;
continue;
}
if (d2 === 0) { nx = -b.vx; ny = -b.vy; }
const d = Math.hypot(nx, ny) || 1;
if (Math.abs(nx) > Math.abs(ny)) b.vx = Math.sign(nx) * Math.abs(b.vx);
else b.vy = Math.sign(ny) * Math.abs(b.vy);
if (d2 > 0) { b.x = cx + nx / d * (b.r + 0.5); b.y = cy + ny / d * (b.r + 0.5); }
fixAngle(b);
hitBrick(br, b);
break;
}
if (b.y - b.r > H) { b.dead = true; return; }
}
}
function wallFx(b, ang) {
sfx.wall(); fixAngle(b);
burst(b.x, b.y, 5, '#8af', 150, { ang, spread: 1.0, size: 3, g: 0 });
}
// ---------- input ----------
const keys = {};
let mouseX = W / 2, useMouse = true;
function action() {
audio();
if (state === 'title' || ((state === 'over' || state === 'win') && stateT > 0.8)) { startGame(); return; }
if (paused) { paused = false; return; }
if (state === 'play') launch();
}
addEventListener('keydown', e => {
const k = e.key;
if ([' ', 'ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'].includes(k)) e.preventDefault();
if (k === 'ArrowLeft' || k === 'a' || k === 'A') keys.left = true;
if (k === 'ArrowRight' || k === 'd' || k === 'D') keys.right = true;
if (k === ' ' || k === 'ArrowUp' || k === 'Enter' || k === 'w' || k === 'W') { if (!e.repeat) action(); }
if ((k === 'p' || k === 'P' || k === 'Escape') && (state === 'play' || state === 'clear')) { audio(); paused = !paused; }
if (k === 'm' || k === 'M') { audio(); muted = !muted; }
});
addEventListener('keyup', e => {
const k = e.key;
if (k === 'ArrowLeft' || k === 'a' || k === 'A') keys.left = false;
if (k === 'ArrowRight' || k === 'd' || k === 'D') keys.right = false;
});
function toGame(clientX) { const r = cv.getBoundingClientRect(); return (clientX - r.left) / r.width * W; }
addEventListener('mousemove', e => { mouseX = toGame(e.clientX); useMouse = true; });
addEventListener('mousedown', e => { mouseX = toGame(e.clientX); useMouse = true; action(); });
addEventListener('touchstart', e => { e.preventDefault(); mouseX = toGame(e.touches[0].clientX); useMouse = true; action(); }, { passive: false });
addEventListener('touchmove', e => { e.preventDefault(); mouseX = toGame(e.touches[0].clientX); useMouse = true; }, { passive: false });
addEventListener('blur', () => { keys.left = keys.right = false; if (state === 'play') paused = true; });
// ---------- update ----------
function update(dt) {
time += dt;
shake *= Math.exp(-dt * 9); if (shake < 0.05) shake = 0;
flashA *= Math.exp(-dt * 6);
if (paused) return;
stateT += dt; banner.t += dt;
shownScore += (score - shownScore) * Math.min(1, dt * 10);
if (Math.abs(score - shownScore) < 1) shownScore = score;
comboPulse *= Math.exp(-dt * 6);
// stars
for (const s of stars) { s.y += (8 + s.z * 30) * dt; if (s.y > H) { s.y = 0; s.x = Math.random() * W; } }
// particles & popups (keep going even in hitstop for feel)
for (const p of parts) { p.vy += p.g * dt; p.x += p.vx * dt; p.y += p.vy * dt; p.vx *= Math.exp(-dt * 1.5); p.rot += p.vr * dt; p.life -= dt; }
parts = parts.filter(p => p.life > 0);
for (const p of pops) p.t += dt;
pops = pops.filter(p => p.t < 1.1);
for (const br of bricks) { br.flash *= Math.exp(-dt * 12); br.wob *= Math.exp(-dt * 8); }
if (state === 'win' && Math.random() < dt * 6) burst(rand(80, W - 80), rand(80, 300), 30, PALETTE, 320, { size: 6 });
if (hitstop > 0) { hitstop -= dt; return; }
// paddle
const prev = paddle.x;
const dir = (keys.right ? 1 : 0) - (keys.left ? 1 : 0);
if (dir) { useMouse = false; paddle.x += dir * 780 * dt; }
else if (useMouse) paddle.x += (mouseX - paddle.x) * Math.min(1, dt * 28);
paddle.tw = wideT > 0 ? 175 : 110;
paddle.w += (paddle.tw - paddle.w) * Math.min(1, dt * 10);
paddle.x = clamp(paddle.x, paddle.w / 2, W - paddle.w / 2);
paddle.vx = (paddle.x - prev) / Math.max(dt, 1e-4);
paddle.sq *= Math.exp(-dt * 10);
if (state === 'clear') { if (stateT > 2.4) nextLevel(); return; }
if (state !== 'play') return;
levelT += dt;
wideT = Math.max(0, wideT - dt); slowT = Math.max(0, slowT - dt); fireT = Math.max(0, fireT - dt);
const bdt = dt * (slowT > 0 ? 0.6 : 1);
for (const b of balls) {
moveBall(b, bdt);
if (state !== 'play') return;
b.trail.push({ x: b.x, y: b.y }); if (b.trail.length > 12) b.trail.shift();
if (fireT > 0 && !b.stuck && Math.random() < 0.8)
parts.push({ x: b.x + rand(-3, 3), y: b.y + rand(-3, 3), vx: rand(-30, 30), vy: rand(-60, -10), life: 0.35, max: 0.35,
col: Math.random() < 0.5 ? '#ffb02a' : '#ff4d1a', s: rand(3, 6), rot: 0, vr: 0, g: -100 });
}
balls = balls.filter(b => !b.dead);
if (!balls.length) { loseLife(); return; }
// capsules
for (const c of caps) {
c.t += dt; c.y += c.vy * dt;
const pw = paddle.w;
if (c.y + 9 > paddle.y - paddle.h / 2 && c.y - 9 < paddle.y + paddle.h / 2 && Math.abs(c.x - paddle.x) < pw / 2 + 18) {
c.dead = true; applyPower(c.k);
} else if (c.y > H + 20) c.dead = true;
}
caps = caps.filter(c => !c.dead);
}
// ---------- render ----------
function render() {
ctx.setTransform(K, 0, 0, K, 0, 0);
ctx.drawImage(bg, 0, 0);
for (const s of stars) {
ctx.globalAlpha = 0.25 + s.z * 0.6 * (0.7 + 0.3 * Math.sin(time * 3 + s.x));
ctx.fillStyle = '#cfe0ff'; const sz = 1 + s.z * 1.5; ctx.fillRect(s.x, s.y, sz, sz);
}
ctx.globalAlpha = 1;
ctx.save();
if (shake > 0) ctx.translate(rand(-shake, shake), rand(-shake, shake));
drawBricks();
drawCaps();
drawParticles();
if (state !== 'title') { drawPaddle(); drawBalls(); }
drawPops();
ctx.restore();
drawHUD();
if (flashA > 0.01) { ctx.globalAlpha = flashA; ctx.fillStyle = flashCol; ctx.fillRect(0, 0, W, H); ctx.globalAlpha = 1; }
drawOverlay();
}
function drawBricks() {
for (const br of bricks) {
if (!br.alive) continue;
const t = state === 'title' ? 1 : clamp((levelT - br.delay) / 0.45, 0, 1);
if (t <= 0) continue;
const oy = (1 - easeOutBack(t)) * -220;
const s = 1 + 0.14 * br.wob;
ctx.save();
ctx.globalAlpha = t;
ctx.translate(br.x + br.w / 2, br.y + br.h / 2 + oy + (state === 'title' ? Math.sin(time * 2 + br.x * 0.02) * 3 : 0));
ctx.scale(s, s);
const w = br.w, h = br.h;
if (br.unb) {
const g = ctx.createLinearGradient(0, -h / 2, 0, h / 2);
g.addColorStop(0, '#c9cfe0'); g.addColorStop(0.5, '#7d849a'); g.addColorStop(1, '#4b5063');
ctx.fillStyle = g; rr(-w / 2, -h / 2, w, h, 4); ctx.fill();
ctx.fillStyle = 'rgba(30,32,45,.7)';
for (const dx of [-w / 2 + 6, w / 2 - 6]) { ctx.beginPath(); ctx.arc(dx, 0, 2, 0, 7); ctx.fill(); }
} else {
ctx.fillStyle = br.col; rr(-w / 2, -h / 2, w, h, 4); ctx.fill();
ctx.fillStyle = 'rgba(255,255,255,.28)'; rr(-w / 2 + 2, -h / 2 + 2, w - 4, h * 0.32, 3); ctx.fill();
ctx.fillStyle = 'rgba(0,0,0,.22)'; ctx.fillRect(-w / 2 + 3, h / 2 - 4, w - 6, 2);
// toughness rings
ctx.strokeStyle = 'rgba(255,255,255,.6)'; ctx.lineWidth = 1.5;
for (let i = 1; i < br.max; i++) { rr(-w / 2 + 3 + i * 3, -h / 2 + 3 + i * 2, w - 6 - i * 6, h - 6 - i * 4, 3); ctx.stroke(); }
// cracks
if (br.hp < br.max) {
ctx.strokeStyle = 'rgba(0,0,0,.55)'; ctx.lineWidth = 1.5; ctx.beginPath();
ctx.moveTo(-w * 0.3, -h / 2); ctx.lineTo(-w * 0.15, -2); ctx.lineTo(-w * 0.25, 4); ctx.lineTo(-w * 0.1, h / 2);
if (br.max - br.hp > 1) { ctx.moveTo(w * 0.35, -h / 2); ctx.lineTo(w * 0.2, 0); ctx.lineTo(w * 0.3, h / 2); }
ctx.stroke();
}
}
if (br.flash > 0.02) { ctx.globalAlpha = br.flash * t; ctx.fillStyle = '#fff'; rr(-w / 2, -h / 2, w, h, 4); ctx.fill(); }
ctx.restore();
}
}
function drawPaddle() {
const pw = paddle.w, ph = paddle.h, sx = 1 + 0.2 * paddle.sq, sy = 1 - 0.45 * paddle.sq;
ctx.save(); ctx.translate(paddle.x, paddle.y); ctx.scale(sx, sy);
ctx.shadowColor = fireT > 0 ? '#ff7a2a' : wideT > 0 ? '#4ee6ff' : '#4aa3ff'; ctx.shadowBlur = 22;
const g = ctx.createLinearGradient(0, -ph / 2, 0, ph / 2);
g.addColorStop(0, '#e6fbff'); g.addColorStop(0.45, fireT > 0 ? '#ffb347' : '#5fd4ff'); g.addColorStop(1, fireT > 0 ? '#c3401a' : '#2a64d8');
ctx.fillStyle = g; rr(-pw / 2, -ph / 2, pw, ph, ph / 2); ctx.fill();
ctx.shadowBlur = 0;
ctx.fillStyle = 'rgba(255,255,255,.7)'; rr(-pw / 2 + 8, -ph / 2 + 2, pw - 16, 3, 2); ctx.fill();
ctx.restore();
}
function drawBalls() {
const fire = fireT > 0;
for (const b of balls) {
for (let i = 0; i < b.trail.length; i++) {
const p = b.trail[i], k = i / b.trail.length;
ctx.globalAlpha = k * 0.35; ctx.fillStyle = fire ? '#ff8a2a' : '#7fd8ff';
ctx.beginPath(); ctx.arc(p.x, p.y, b.r * (0.4 + 0.6 * k), 0, 7); ctx.fill();
}
ctx.globalAlpha = 1;
ctx.save(); ctx.translate(b.x, b.y);
if (!b.stuck) {
const st = 1 + Math.min(0.35, b.sp / 2400);
ctx.rotate(Math.atan2(b.vy, b.vx)); ctx.scale(st, 1 / st);
}
ctx.shadowColor = fire ? '#ff6a00' : '#9ef'; ctx.shadowBlur = 18;
ctx.fillStyle = fire ? '#ffe2a8' : '#ffffff';
ctx.beginPath(); ctx.arc(0, 0, b.r, 0, 7); ctx.fill();
ctx.restore();
}
}
function drawCaps() {
ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
for (const c of caps) {
const P = POWERS[c.k];
ctx.save(); ctx.translate(c.x, c.y); ctx.rotate(Math.sin(c.t * 6) * 0.15);
const s = 1 + Math.sin(c.t * 10) * 0.06; ctx.scale(s, s);
ctx.shadowColor = P.col; ctx.shadowBlur = 16; ctx.fillStyle = P.col;
rr(-20, -9, 40, 18, 9); ctx.fill(); ctx.shadowBlur = 0;
ctx.fillStyle = 'rgba(255,255,255,.35)'; rr(-17, -7, 34, 5, 3); ctx.fill();
ctx.fillStyle = '#10121e'; ctx.font = 'bold 13px system-ui,sans-serif'; ctx.fillText(P.ch, 0, 1);
ctx.restore();
}
}
function drawParticles() {
for (const p of parts) {
ctx.globalAlpha = clamp(p.life / p.max, 0, 1); ctx.fillStyle = p.col;
ctx.save(); ctx.translate(p.x, p.y); ctx.rotate(p.rot); ctx.fillRect(-p.s / 2, -p.s / 2, p.s, p.s); ctx.restore();
}
ctx.globalAlpha = 1;
}
function drawPops() {
ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
for (const p of pops) {
const k = p.t / 1.1, s = p.t < 0.12 ? 0.5 + p.t / 0.12 * 0.8 : 1.3 - Math.min(0.3, (p.t - 0.12) * 2);
ctx.globalAlpha = 1 - k * k;
ctx.save(); ctx.translate(p.x, p.y - p.t * 50); ctx.scale(s, s);
ctx.font = `800 ${p.size}px system-ui,sans-serif`;
ctx.lineWidth = 4; ctx.strokeStyle = 'rgba(0,0,0,.6)'; ctx.strokeText(p.txt, 0, 0);
ctx.fillStyle = p.col; ctx.fillText(p.txt, 0, 0);
ctx.restore();
}
ctx.globalAlpha = 1;
}
function drawHUD() {
ctx.fillStyle = 'rgba(10,12,30,.75)'; ctx.fillRect(0, 0, W, PLAY_TOP);
const g = ctx.createLinearGradient(0, 0, W, 0);
g.addColorStop(0, 'rgba(255,77,109,.6)'); g.addColorStop(0.5, 'rgba(63,167,255,.8)'); g.addColorStop(1, 'rgba(155,93,229,.6)');
ctx.fillStyle = g; ctx.fillRect(0, PLAY_TOP - 2, W, 2);
ctx.textBaseline = 'alphabetic';
ctx.textAlign = 'left';
ctx.font = '700 11px system-ui,sans-serif'; ctx.fillStyle = '#7f8bb5';
ctx.fillText('SCORE', 18, 18); ctx.fillText('BEST', 170, 18);
ctx.font = '800 22px ui-monospace,Menlo,Consolas,monospace'; ctx.fillStyle = '#fff';
ctx.fillText(String(Math.round(shownScore)).padStart(6, '0'), 18, 41);
ctx.fillStyle = '#ffd23f'; ctx.fillText(String(Math.max(best, score)).padStart(6, '0'), 170, 41);
ctx.textAlign = 'center';
if (state !== 'title') {
ctx.font = '700 11px system-ui,sans-serif'; ctx.fillStyle = '#7f8bb5';
ctx.fillText('LEVEL ' + Math.min(level + 1, LEVELS.length) + ' / ' + LEVELS.length, W / 2, 18);
ctx.font = '800 18px system-ui,sans-serif'; ctx.fillStyle = '#fff';
ctx.fillText(LEVELS[Math.min(level, LEVELS.length - 1)].name, W / 2, 40);
}
// combo
if (combo >= 4 && state === 'play') {
const mult = Math.min(8, 1 + Math.floor((combo - 1) / 3));
ctx.save(); ctx.translate(560, 30); const s = 1 + comboPulse * 0.35; ctx.scale(s, s); ctx.rotate(-0.05);
ctx.font = '900 18px system-ui,sans-serif'; ctx.fillStyle = `hsl(${(time * 300) % 360},100%,65%)`;
ctx.fillText(combo + ' COMBO' + (mult > 1 ? ' x' + mult : ''), 0, 0); ctx.restore();
}
// lives
ctx.textAlign = 'right'; ctx.font = '700 11px system-ui,sans-serif'; ctx.fillStyle = '#7f8bb5';
ctx.fillText('LIVES', W - 18, 18);
for (let i = 0; i < lives; i++) {
ctx.fillStyle = '#5fd4ff'; rr(W - 42 - i * 30, 28, 24, 8, 4); ctx.fill();
}
// active powerups
const act = [['WIDE', wideT, 14, POWERS.W.col], ['SLOW', slowT, 10, POWERS.S.col], ['FIRE', fireT, 8, POWERS.F.col]].filter(a => a[1] > 0);
ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.font = '800 10px system-ui,sans-serif';
act.forEach((a, i) => {
const x = 16 + i * 74, y = H - 14;
ctx.fillStyle = 'rgba(255,255,255,.08)'; rr(x, y - 7, 66, 14, 7); ctx.fill();
ctx.fillStyle = a[3]; rr(x, y - 7, 66 * a[1] / a[2], 14, 7); ctx.fill();
ctx.fillStyle = '#0b0d1a'; ctx.fillText(a[0], x + 33, y + 1);
});
ctx.textAlign = 'right'; ctx.fillStyle = 'rgba(160,175,220,.5)'; ctx.font = '600 10px system-ui,sans-serif';
ctx.fillText('[P] pause [M] sound ' + (muted ? 'off' : 'on'), W - 12, H - 12);
}
function bigText(txt, y, size, cols) {
ctx.font = `900 ${size}px system-ui,sans-serif`; ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
const chars = [...txt], widths = chars.map(c => ctx.measureText(c).width), tot = widths.reduce((a, b) => a + b, 0);
let x = W / 2 - tot / 2;
chars.forEach((c, i) => {
const cx = x + widths[i] / 2, yy = y + Math.sin(time * 4 + i * 0.6) * size * 0.08;
ctx.save(); ctx.translate(cx, yy); ctx.rotate(Math.sin(time * 3 + i) * 0.05);
ctx.shadowColor = cols[i % cols.length]; ctx.shadowBlur = 25;
ctx.fillStyle = cols[i % cols.length]; ctx.fillText(c, 0, 0);
ctx.shadowBlur = 0; ctx.fillStyle = 'rgba(255,255,255,.35)'; ctx.fillText(c, 0, -2);
ctx.restore();
x += widths[i];
});
}
function centerText(txt, y, font, col, alpha = 1) {
ctx.globalAlpha = alpha; ctx.font = font; ctx.fillStyle = col; ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
ctx.fillText(txt, W / 2, y); ctx.globalAlpha = 1;
}
function dim(a) { ctx.fillStyle = `rgba(5,6,16,${a})`; ctx.fillRect(0, PLAY_TOP, W, H - PLAY_TOP); }
function drawOverlay() {
const blink = 0.55 + 0.45 * Math.sin(time * 5);
if (state === 'title') {
dim(0.55);
bigText('BREAKOUT', 230, 92, PALETTE);
centerText('Smash every brick. Catch the capsules. Chain combos.', 310, '500 18px system-ui,sans-serif', '#c8d2f5');
centerText('Mouse / touch or ← → / A D to move · Click / Space to launch', 350, '600 14px system-ui,sans-serif', '#8f9cc8');
centerText('CLICK OR PRESS SPACE TO START', 430, '800 22px system-ui,sans-serif', '#fff', blink);
const ks = Object.values(POWERS);
ks.forEach((P, i) => {
const x = W / 2 - (ks.length - 1) * 70 + i * 140, y = 500;
ctx.fillStyle = P.col; rr(x - 20, y - 9, 40, 18, 9); ctx.fill();
ctx.fillStyle = '#10121e'; ctx.font = 'bold 13px system-ui,sans-serif'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText(P.ch, x, y + 1);
ctx.fillStyle = '#aab4dc'; ctx.font = '700 11px system-ui,sans-serif'; ctx.fillText(P.name, x, y + 24);
});
return;
}
if (state === 'over') {
dim(Math.min(0.7, stateT));
bigText('GAME OVER', 250, 80, ['#ff4d6d', '#ff8c42']);
centerText('Score ' + score + (score >= best && score > 0 ? ' — NEW BEST!' : ' · Best ' + best), 330, '700 22px system-ui,sans-serif', '#fff');
if (stateT > 0.8) centerText('Click or press Space to play again', 400, '700 18px system-ui,sans-serif', '#c8d2f5', blink);
return;
}
if (state === 'win') {
dim(Math.min(0.55, stateT));
bigText('YOU WIN!', 240, 90, PALETTE);
centerText('All ' + LEVELS.length + ' levels cleared · Final score ' + score, 330, '700 22px system-ui,sans-serif', '#fff');
if (score >= best) centerText('NEW BEST!', 370, '900 20px system-ui,sans-serif', '#ffd23f', blink);
if (stateT > 0.8) centerText('Click or press Space to play again', 430, '700 18px system-ui,sans-serif', '#c8d2f5', blink);
return;
}
// banner (level start / clear)
if (banner.t < 2.4) {
const t = banner.t, a = t < 0.25 ? t / 0.25 : t > 1.9 ? Math.max(0, (2.4 - t) / 0.5) : 1;
const s = t < 0.25 ? 0.6 + 0.4 * easeOutBack(t / 0.25) : 1;
ctx.save(); ctx.globalAlpha = a; ctx.translate(W / 2, 330); ctx.scale(s, s);
ctx.font = '900 54px system-ui,sans-serif'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
ctx.shadowColor = '#3fa7ff'; ctx.shadowBlur = 30; ctx.fillStyle = '#fff'; ctx.fillText(banner.title, 0, 0);
ctx.shadowBlur = 0; ctx.font = '700 22px system-ui,sans-serif'; ctx.fillStyle = '#ffd23f'; ctx.fillText(banner.sub, 0, 44);
ctx.restore();
}
if (state === 'play' && balls.some(b => b.stuck) && !paused && banner.t > 1.2)
centerText('Click / Space to launch', H - 90, '700 16px system-ui,sans-serif', '#c8d2f5', blink * 0.8);
if (paused) {
dim(0.6);
bigText('PAUSED', 270, 70, ['#3fa7ff', '#9b5de5']);
centerText('Click or press P to resume', 340, '700 18px system-ui,sans-serif', '#c8d2f5', blink);
}
}
// ---------- boot ----------
loadLevel(0); state = 'title';
let last = performance.now();
function frame(now) {
const dt = Math.min(0.033, (now - last) / 1000); last = now;
update(dt); render();
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
})();
</script>
</body>
</html>

