Run details
- Model
- reflection/beam-1.0-501b-a23b
- Route
- OpenRouter API, one user message
- Reasoning
- provider default, 10,917 reasoning tokens
- Limits
- 60 min · 600,000 output tokens
- Date
- 21 Oct 2026
- Time
- 51 s
- Size
- 27 kB
- Tokens
- 122 in, 20,062 out
- List price
- $0.020 ($0.3 in / $1 out per M)
- Paid
- $0.020
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). Reply with the file in a single ```html code block.
Source
27 kB 974 lines Raw
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Breakout: Neon Bricks</title>
<style>
* { box-sizing: border-box; }
html, body {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
background: #05070d;
color: #f5f7ff;
font-family: "Segoe UI", system-ui, -apple-system, sans-serif;
overflow: hidden;
}
body {
display: flex;
align-items: center;
justify-content: center;
}
canvas {
display: block;
max-width: 100vw;
max-height: 100vh;
width: auto;
height: auto;
box-shadow: 0 0 48px rgba(0, 0, 0, 0.75);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 8px;
background: #05070d;
}
</style>
</head>
<body>
<canvas id="game" width="960" height="640" tabindex="0"></canvas>
<script>
(() => {
const W = 960;
const H = 640;
const COLS = 10;
const BRICK_W = 80;
const BRICK_H = 24;
const GAP_X = 8;
const GAP_Y = 8;
const FIELD_W = COLS * BRICK_W + (COLS - 1) * GAP_X;
const START_X = (W - FIELD_W) / 2;
const START_Y = 92;
const MAX_LEVEL = 5;
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
let state = 'start'; // start, playing, paused, levelclear, gameover, victory
let score = 0;
let highScore = 0;
let lives = 3;
let level = 1;
let combo = 0;
let muted = false;
let mouseActive = true;
let keyboardActive = false;
let stuckTimer = 0;
let levelClearTimer = 0;
let lastTime = 0;
const paddle = {
x: W / 2,
y: H - 52,
w: 140,
h: 18,
vx: 0,
targetX: W / 2
};
const ball = {
x: W / 2,
y: H - 72,
r: 9,
vx: 0,
vy: 0,
stuck: true,
squash: 0,
trail: []
};
let bricks = [];
let particles = [];
let floats = [];
let stars = [];
let shakeX = 0, shakeY = 0, shakeTime = 0, shakeDur = 0.2, shakeMag = 0;
let audioCtx = null;
// ---------- utility ----------
function clamp(v, a, b) { return Math.max(a, Math.min(b, v)); }
function roundRect(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();
}
function tryStorage(fn) {
try { return fn(); } catch (e) { return null; }
}
function loadHighScore() {
const v = tryStorage(() => localStorage.getItem('breakout_neon_hs'));
highScore = v ? Number(v) || 0 : 0;
}
function saveHighScore() {
if (score > highScore) {
highScore = score;
tryStorage(() => localStorage.setItem('breakout_neon_hs', String(highScore)));
}
}
// ---------- audio ----------
function ensureAudio() {
if (!audioCtx) {
try {
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
} catch (e) {
audioCtx = null;
}
}
if (audioCtx && audioCtx.state === 'suspended') audioCtx.resume();
}
function playSound(type, pitch = 1) {
if (muted || !audioCtx) return;
const now = audioCtx.currentTime;
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.connect(gain);
gain.connect(audioCtx.destination);
let freq = 440, dur = 0.08, vol = 0.07, wave = 'square';
switch (type) {
case 'paddle': freq = 300; dur = 0.07; wave = 'triangle'; vol = 0.06; break;
case 'brick': freq = 520; dur = 0.06; wave = 'square'; vol = 0.05; break;
case 'wall': freq = 220; dur = 0.05; wave = 'square'; vol = 0.04; break;
case 'launch': freq = 380; dur = 0.12; wave = 'sawtooth'; vol = 0.06; break;
case 'lose': freq = 160; dur = 0.35; wave = 'sawtooth'; vol = 0.09; break;
case 'clear': freq = 660; dur = 0.25; wave = 'triangle'; vol = 0.09; break;
case 'over': freq = 110; dur = 0.6; wave = 'sawtooth'; vol = 0.1; break;
case 'ui': freq = 420; dur = 0.05; wave = 'sine'; vol = 0.05; break;
case 'steel': freq = 180; dur = 0.08; wave = 'square'; vol = 0.05; break;
}
osc.type = wave;
osc.frequency.setValueAtTime(freq * pitch, now);
if (type === 'clear') {
osc.frequency.exponentialRampToValueAtTime(freq * pitch * 1.5, now + dur);
}
gain.gain.setValueAtTime(vol, now);
gain.gain.exponentialRampToValueAtTime(0.0001, now + dur);
osc.start(now);
osc.stop(now + dur + 0.03);
}
// ---------- level generation ----------
function hpAt(level, r, c, rows) {
if (level === 1) return 1;
if (level === 2) return r < 2 ? 2 : 1;
if (level === 3) {
const half = r + 1;
if (Math.abs(c + 0.5 - COLS / 2) > half) return 0;
return Math.min(3, Math.floor(r / 2) + 1);
}
if (level === 4) {
if (c === 0 || c === COLS - 1) return 3;
if ((r + c) % 4 === 0) return 0;
if ((r * c) % 7 === 0) return 2;
return 1;
}
// level 5
const v = (r * 31 + c * 17 + level * 13) % 9;
if (v === 0 || v === 8) return 0;
if (v === 7) return 999;
return 1 + (v % 3);
}
function buildLevel(n) {
bricks = [];
const rows = 5 + Math.min(n - 1, 2);
for (let r = 0; r < rows; r++) {
for (let c = 0; c < COLS; c++) {
const hp = hpAt(n, r, c, rows);
if (hp === 0) continue;
const hue = (c * 34 + r * 22 + n * 45) % 360;
let color;
if (hp >= 999) color = '#5f6b7a';
else color = `hsl(${hue}, 74%, ${52 + hp * 6}%)`;
bricks.push({
x: START_X + c * (BRICK_W + GAP_X),
y: START_Y + r * (BRICK_H + GAP_Y),
w: BRICK_W,
h: BRICK_H,
hp,
maxHp: hp,
alive: true,
flash: 0,
color
});
}
}
}
function remainingBreakable() {
let n = 0;
for (const b of bricks) if (b.alive && b.maxHp < 999) n++;
return n;
}
// ---------- effects ----------
function spawnParticles(x, y, color, count = 12, speed = 220, life = 0.65, gravity = 320, size = 3) {
for (let i = 0; i < count; i++) {
const a = Math.random() * Math.PI * 2;
const s = Math.random() * speed;
particles.push({
x, y,
vx: Math.cos(a) * s,
vy: Math.sin(a) * s,
life,
maxLife: life,
color,
size: Math.random() * size + 1,
gravity
});
}
if (particles.length > 700) particles.splice(0, particles.length - 700);
}
function spawnFloat(x, y, text, color = '#ffffff') {
floats.push({
x, y,
text,
life: 1.0,
maxLife: 1.0,
color,
vy: -60,
scale: 0.6
});
}
function triggerShake(mag, dur) {
shakeMag = Math.max(shakeMag, mag);
shakeDur = dur;
shakeTime = dur;
}
function updateParticles(dt) {
for (let i = particles.length - 1; i >= 0; i--) {
const p = particles[i];
p.vy += p.gravity * dt;
p.x += p.vx * dt;
p.y += p.vy * dt;
p.vx *= 0.985;
p.life -= dt;
if (p.life <= 0) particles.splice(i, 1);
}
for (let i = floats.length - 1; i >= 0; i--) {
const f = floats[i];
f.y += f.vy * dt;
f.vy *= 0.96;
f.life -= dt;
f.scale = Math.min(1, f.scale + dt * 4);
if (f.life <= 0) floats.splice(i, 1);
}
for (let i = ball.trail.length - 1; i >= 0; i--) {
ball.trail[i].life -= dt;
if (ball.trail[i].life <= 0) ball.trail.splice(i, 1);
}
if (shakeTime > 0) {
shakeTime -= dt;
const t = Math.max(0, shakeTime / shakeDur);
const m = shakeMag * t;
shakeX = (Math.random() * 2 - 1) * m;
shakeY = (Math.random() * 2 - 1) * m;
if (shakeTime <= 0) {
shakeX = shakeY = 0;
shakeMag = 0;
}
}
}
// ---------- game flow ----------
function resetBallOnPaddle() {
ball.stuck = true;
ball.trail = [];
ball.x = paddle.x;
ball.y = paddle.y - ball.r - 2;
ball.vx = 0;
ball.vy = 0;
ball.squash = 0;
stuckTimer = 0;
}
function startGame() {
score = 0;
lives = 3;
level = 1;
combo = 0;
buildLevel(1);
paddle.w = 140;
paddle.x = W / 2;
paddle.targetX = W / 2;
paddle.vx = 0;
mouseActive = true;
keyboardActive = false;
resetBallOnPaddle();
state = 'playing';
ensureAudio();
playSound('ui');
}
function launchBall() {
if (!ball.stuck || state !== 'playing') return;
ball.stuck = false;
const speed = 330 + (level - 1) * 38;
ball.vx = (Math.random() * 0.7 - 0.35) * speed;
ball.vy = -speed;
stuckTimer = 0;
playSound('launch');
}
function loseLife() {
lives--;
spawnParticles(ball.x, ball.y, '#ff4d6d', 45, 320, 0.85, 360, 4);
triggerShake(18, 0.45);
playSound('lose');
combo = 0;
if (lives <= 0) {
state = 'gameover';
saveHighScore();
playSound('over');
} else {
resetBallOnPaddle();
}
}
function startLevelClear() {
state = 'levelclear';
ball.vx = 0;
ball.vy = 0;
ball.stuck = false;
levelClearTimer = 0;
const bonus = lives * 500 + level * 1000;
score += bonus;
saveHighScore();
spawnFloat(W / 2, H / 2 - 50, `LEVEL CLEAR +${bonus}`, '#9ef0ff');
// confetti
const colors = ['#ff595e', '#ffca3a', '#8ac926', '#1982c4', '#c77dff', '#f15bb5'];
for (let i = 0; i < 90; i++) {
spawnParticles(
Math.random() * W, -20,
colors[Math.floor(Math.random() * colors.length)],
1, 120 + Math.random() * 200, 1.2, 120, 4
);
}
triggerShake(12, 0.35);
playSound('clear');
}
function nextLevel() {
if (state !== 'levelclear') return;
level++;
if (level > MAX_LEVEL) {
state = 'victory';
saveHighScore();
playSound('clear');
return;
}
buildLevel(level);
paddle.w = Math.max(92, 140 - (level - 1) * 10);
paddle.x = W / 2;
paddle.targetX = W / 2;
paddle.vx = 0;
resetBallOnPaddle();
state = 'playing';
playSound('ui');
}
// ---------- collisions ----------
function circleRectHit(b) {
const cx = clamp(ball.x, b.x, b.x + b.w);
const cy = clamp(ball.y, b.y, b.y + b.h);
const dx = ball.x - cx;
const dy = ball.y - cy;
return dx * dx + dy * dy <= ball.r * ball.r;
}
function reflectBrick(b) {
const leftOverlap = ball.x + ball.r - b.x;
const rightOverlap = b.x + b.w - (ball.x - ball.r);
const topOverlap = ball.y + ball.r - b.y;
const bottomOverlap = b.y + b.h - (ball.y - ball.r);
const minOverlapX = Math.min(leftOverlap, rightOverlap);
const minOverlapY = Math.min(topOverlap, bottomOverlap);
if (minOverlapX < minOverlapY) {
ball.vx = -ball.vx;
if (leftOverlap < rightOverlap) ball.x = b.x - ball.r - 0.1;
else ball.x = b.x + b.w + ball.r + 0.1;
} else {
ball.vy = -ball.vy;
if (topOverlap < bottomOverlap) ball.y = b.y - ball.r - 0.1;
else ball.y = b.y + b.h + ball.r + 0.1;
}
}
function handleBrickHit(b) {
b.flash = 0.18;
const destroyed = b.maxHp < 999;
if (destroyed) {
b.hp -= 1;
if (b.hp <= 0) b.alive = false;
}
combo++;
let pts = 50 * combo;
if (destroyed && !b.alive) pts += 100 * b.maxHp;
score += pts;
if (score > highScore) highScore = score;
const cx = b.x + b.w / 2;
const cy = b.y + b.h / 2;
spawnParticles(cx, cy, b.color, 16, 260, 0.7, 340, 3);
spawnFloat(cx, cy, `+${pts}`, '#ffffff');
if (combo >= 3) spawnFloat(cx, cy - 18, `x${combo} COMBO`, '#ffd166');
triggerShake(4, 0.12);
if (b.maxHp >= 999) {
playSound('steel');
} else {
playSound('brick', 1 + Math.min(combo, 12) * 0.045);
}
if (remainingBreakable() === 0) startLevelClear();
}
// ---------- input ----------
function setKeyboardControl() {
if (!keyboardActive) {
keyboardActive = true;
mouseActive = false;
paddle.vx = 0;
}
}
function handlePrimary() {
ensureAudio();
if (state === 'start') startGame();
else if (state === 'playing') {
if (ball.stuck) launchBall();
} else if (state === 'levelclear') nextLevel();
else if (state === 'paused') state = 'playing';
else if (state === 'gameover' || state === 'victory') startGame();
}
window.addEventListener('keydown', (e) => {
const k = e.key.toLowerCase();
if (k === 'arrowleft' || k === 'a') { e.preventDefault(); setKeyboardControl(); }
if (k === 'arrowright' || k === 'd') { e.preventDefault(); setKeyboardControl(); }
if (k === 'arrowleft' || k === 'a') { /* handled below */ }
if (['arrowleft', 'a'].includes(k)) { /* noop */ }
if (k === 'arrowleft' || k === 'a') { /* placeholder */ }
});
// cleaner key handling
const keys = { left: false, right: false };
window.addEventListener('keydown', (e) => {
const k = e.key.toLowerCase();
if (k === 'arrowleft' || k === 'a') {
keys.left = true;
setKeyboardControl();
e.preventDefault();
}
if (k === 'arrowright' || k === 'd') {
keys.right = true;
setKeyboardControl();
e.preventDefault();
}
if (k === ' ') {
e.preventDefault();
handlePrimary();
}
if (k === 'p') {
if (state === 'playing') state = 'paused';
else if (state === 'paused') state = 'playing';
}
if (k === 'm') muted = !muted;
if (k === 'r') {
if (state !== 'playing') startGame();
}
if (k === 'escape') {
if (state === 'playing') state = 'paused';
else if (state === 'paused') state = 'playing';
}
});
window.addEventListener('keyup', (e) => {
const k = e.key.toLowerCase();
if (k === 'arrowleft' || k === 'a') keys.left = false;
if (k === 'arrowright' || k === 'd') keys.right = false;
});
function canvasPos(e) {
const rect = canvas.getBoundingClientRect();
return (e.clientX - rect.left) * (W / rect.width);
}
canvas.addEventListener('mousemove', (e) => {
paddle.targetX = canvasPos(e);
if (!mouseActive) {
mouseActive = true;
keyboardActive = false;
paddle.vx = 0;
}
});
canvas.addEventListener('mousedown', (e) => {
canvas.focus();
ensureAudio();
paddle.targetX = canvasPos(e);
mouseActive = true;
keyboardActive = false;
paddle.vx = 0;
handlePrimary();
});
canvas.addEventListener('touchstart', (e) => {
e.preventDefault();
canvas.focus();
ensureAudio();
const t = e.touches[0];
const rect = canvas.getBoundingClientRect();
paddle.targetX = (t.clientX - rect.left) * (W / rect.width);
mouseActive = true;
keyboardActive = false;
paddle.vx = 0;
handlePrimary();
}, { passive: false });
canvas.addEventListener('touchmove', (e) => {
e.preventDefault();
const t = e.touches[0];
const rect = canvas.getBoundingClientRect();
paddle.targetX = (t.clientX - rect.left) * (W / rect.width);
mouseActive = true;
keyboardActive = false;
}, { passive: false });
// ---------- stars ----------
function initStars() {
stars = [];
for (let i = 0; i < 110; i++) {
stars.push({
x: Math.random() * W,
y: Math.random() * H,
z: Math.random() * 0.9 + 0.15,
phase: Math.random() * Math.PI * 2
});
}
}
// ---------- update ----------
function update(dt) {
if (state === 'playing') {
// keyboard paddle
if (keyboardActive) {
paddle.vx = 0;
if (keys.left) paddle.vx -= 620;
if (keys.right) paddle.vx += 620;
}
// mouse smoothing
if (mouseActive) {
paddle.x += (paddle.targetX - paddle.x) * Math.min(1, dt * 16);
} else {
paddle.x += paddle.vx * dt;
}
const half = paddle.w / 2;
paddle.x = clamp(paddle.x, half, W - half);
if (ball.stuck) {
ball.x = paddle.x;
ball.y = paddle.y - ball.r - 2;
ball.trail = [];
stuckTimer += dt;
if (stuckTimer > 4.5) launchBall();
} else {
// trail
ball.trail.push({ x: ball.x, y: ball.y, life: 0.22 });
if (ball.trail.length > 26) ball.trail.shift();
ball.x += ball.vx * dt;
ball.y += ball.vy * dt;
ball.squash = Math.max(0, ball.squash - dt * 7);
// walls
if (ball.x - ball.r < 0) {
ball.x = ball.r;
ball.vx = Math.abs(ball.vx);
spawnParticles(ball.x, ball.y, '#7dd3fc', 6, 100, 0.35, 120, 2);
playSound('wall');
}
if (ball.x + ball.r > W) {
ball.x = W - ball.r;
ball.vx = -Math.abs(ball.vx);
spawnParticles(ball.x, ball.y, '#7dd3fc', 6, 100, 0.35, 120, 2);
playSound('wall');
}
if (ball.y - ball.r < 0) {
ball.y = ball.r;
ball.vy = Math.abs(ball.vy);
spawnParticles(ball.x, ball.y, '#7dd3fc', 6, 100, 0.35, 120, 2);
playSound('wall');
}
if (ball.y + ball.r > H) {
loseLife();
return;
}
// paddle
const ph = paddle.h;
if (ball.vy > 0 &&
ball.y + ball.r >= paddle.y &&
ball.y - ball.r <= paddle.y + ph &&
ball.x >= paddle.x - paddle.w / 2 - ball.r &&
ball.x <= paddle.x + paddle.w / 2 + ball.r) {
const hit = clamp((ball.x - paddle.x) / (paddle.w / 2), -1, 1);
const speed = Math.hypot(ball.vx, ball.vy);
const maxAngle = 72 * Math.PI / 180;
const angle = hit * maxAngle;
ball.vx = speed * Math.sin(angle);
ball.vy = -Math.abs(speed * Math.cos(angle));
ball.y = paddle.y - ball.r - 0.1;
ball.squash = 0.4;
const minVy = speed * 0.35;
if (Math.abs(ball.vy) < minVy) {
ball.vy = -minVy;
ball.vx = Math.sign(ball.vx || 1) * Math.sqrt(Math.max(0, speed * speed - ball.vy * ball.vy));
}
spawnParticles(ball.x, paddle.y, '#7dd3fc', 12, 150, 0.45, 180, 2);
playSound('paddle');
if (combo > 2) spawnFloat(ball.x, paddle.y - 24, `x${combo} COMBO`, '#ffd166');
combo = 0;
}
// bricks
let hitBrick = null;
for (const b of bricks) {
if (!b.alive) continue;
if (circleRectHit(b)) {
hitBrick = b;
break;
}
}
if (hitBrick) {
reflectBrick(hitBrick);
handleBrickHit(hitBrick);
}
}
} else if (state === 'levelclear') {
levelClearTimer += dt;
if (levelClearTimer > 2.4) nextLevel();
}
updateParticles(dt);
}
// ---------- rendering ----------
function drawBackground() {
const grad = ctx.createLinearGradient(0, 0, 0, H);
grad.addColorStop(0, '#07111f');
grad.addColorStop(0.45, '#0b1026');
grad.addColorStop(1, '#02040a');
ctx.fillStyle = grad;
ctx.fillRect(0, 0, W, H);
// nebula
const t = performance.now() / 1000;
const g2 = ctx.createRadialGradient(W * 0.7, H * 0.25, 10, W * 0.7, H * 0.25, 260);
g2.addColorStop(0, 'rgba(125, 90, 255, 0.16)');
g2.addColorStop(1, 'rgba(125, 90, 255, 0)');
ctx.fillStyle = g2;
ctx.fillRect(0, 0, W, H);
const g3 = ctx.createRadialGradient(W * 0.2, H * 0.75, 10, W * 0.2, H * 0.75, 220);
g3.addColorStop(0, 'rgba(0, 200, 255, 0.10)');
g3.addColorStop(1, 'rgba(0, 200, 255, 0)');
ctx.fillStyle = g3;
ctx.fillRect(0, 0, W, H);
for (const s of stars) {
const tw = 0.35 + 0.65 * (0.5 + 0.5 * Math.sin(t * 1.7 + s.phase));
ctx.globalAlpha = tw * s.z;
ctx.fillStyle = '#ffffff';
ctx.fillRect(s.x, s.y, s.z * 1.6, s.z * 1.6);
}
ctx.globalAlpha = 1;
}
function drawBricks() {
for (const b of bricks) {
if (!b.alive) continue;
if (b.flash > 0) b.flash -= 0.016;
// body
roundRect(b.x, b.y, b.w, b.h, 5);
ctx.fillStyle = b.color;
ctx.fill();
// top highlight
const hg = ctx.createLinearGradient(b.x, b.y, b.x, b.y + b.h);
hg.addColorStop(0, 'rgba(255,255,255,0.28)');
hg.addColorStop(0.45, 'rgba(255,255,255,0.04)');
hg.addColorStop(1, 'rgba(0,0,0,0.18)');
ctx.fillStyle = hg;
roundRect(b.x, b.y, b.w, b.h, 5);
ctx.fill();
if (b.maxHp >= 999) {
ctx.strokeStyle = 'rgba(255,255,255,0.18)';
ctx.lineWidth = 1;
for (let i = 6; i < b.w; i += 10) {
ctx.beginPath();
ctx.moveTo(b.x + i, b.y + 2);
ctx.lineTo(b.x + i - 6, b.y + b.h - 2);
ctx.stroke();
}
} else if (b.maxHp > 1) {
ctx.fillStyle = 'rgba(255,255,255,0.85)';
ctx.font = 'bold 11px system-ui, sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(String(b.hp), b.x + b.w / 2, b.y + b.h / 2);
ctx.textBaseline = 'alphabetic';
}
if (b.flash > 0) {
ctx.globalAlpha = Math.min(1, b.flash * 5);
ctx.fillStyle = '#ffffff';
roundRect(b.x, b.y, b.w, b.h, 5);
ctx.fill();
ctx.globalAlpha = 1;
}
}
}
function drawPaddle() {
const half = paddle.w / 2;
const x = paddle.x - half;
const y = paddle.y;
ctx.save();
ctx.shadowColor = '#4cc9f0';
ctx.shadowBlur = 20;
const grad = ctx.createLinearGradient(x, y, x, y + paddle.h);
grad.addColorStop(0, '#9be7ff');
grad.addColorStop(0.45, '#4cc9f0');
grad.addColorStop(1, '#1b7ea6');
ctx.fillStyle = grad;
roundRect(x, y, paddle.w, paddle.h, 9);
ctx.fill();
ctx.shadowBlur = 0;
// sheen
ctx.fillStyle = 'rgba(255,255,255,0.35)';
roundRect(x + 6, y + 3, paddle.w - 12, 5, 3);
ctx.fill();
ctx.restore();
}
function drawBall() {
// trail
for (let i = 0; i < ball.trail.length; i++) {
const t = ball.trail[i];
const a = (i + 1) / ball.trail.length;
ctx.globalAlpha = a * 0.45;
ctx.fillStyle = '#7dd3fc';
ctx.beginPath();
ctx.arc(t.x, t.y, ball.r * (0.5 + a * 0.5), 0, Math.PI * 2);
ctx.fill();
}
ctx.globalAlpha = 1;
const sx = 1 + ball.squash * 0.75;
const sy = 1 - ball.squash * 0.45;
ctx.save();
ctx.shadowColor = '#7dd3fc';
ctx.shadowBlur = 18;
const grad = ctx.createRadialGradient(ball.x - 2, ball.y - 2, 1, ball.x, ball.y, ball.r * 1.3);
grad.addColorStop(0, '#ffffff');
grad.addColorStop(0.35, '#a5f3fc');
grad.addColorStop(1, '#0ea5e9');
ctx.fillStyle = grad;
ctx.beginPath();
ctx.ellipse(ball.x, ball.y, ball.r * sx, ball.r * sy, 0, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
if (ball.stuck && state === 'playing') {
ctx.fillStyle = 'rgba(255,255,255,0.75)';
ctx.font = 'bold 13px system-ui, sans-serif';
ctx.textAlign = 'center';
ctx.fillText('SPACE / CLICK TO LAUNCH', ball.x, ball.y - 26);
}
}
function drawParticles() {
for (const p of particles) {
const a = Math.max(0, p.life / p.maxLife);
ctx.globalAlpha = a;
ctx.fillStyle = p.color;
ctx.fillRect(p.x - p.size / 2, p.y - p.size / 2, p.size, p.size);
}
ctx.globalAlpha = 1;
}
function drawFloats() {
for (const f of floats) {
const a = Math.max(0, f.life / f.maxLife);
ctx.globalAlpha = a;
ctx.fillStyle = f.color;
ctx.font = `bold ${Math.round(16 * f.scale)}px system-ui, sans-serif`;
ctx.textAlign = 'center';
ctx.fillText(f.text, f.x, f.y);
}
ctx.globalAlpha = 1;
}
function drawUI() {
ctx.fillStyle = 'rgba(0,0,0,0.35)';
ctx.fillRect(0, 0, W, 58);
ctx.font = 'bold 22px system-ui, sans-serif';
ctx.textAlign = 'left';
ctx.fillStyle = '#ffffff';
ctx.fillText(`SCORE ${score.toLocaleString()}`, 24, 36);
ctx.textAlign = 'center';
ctx.fillStyle = '#ffd166';
ctx.fillText(`BEST ${highScore.toLocaleString()}`, W / 2, 36);
ctx.textAlign = 'right';
ctx.fillStyle = '#ffffff';
ctx.fillText(`LEVEL ${level}`, W - 150, 36);
// lives
ctx.font = '22px system-ui, sans-serif';
ctx.fillStyle = '#ff4d6d';
for (let i = 0; i < 3; i++) {
ctx.globalAlpha = i < lives ? 1 : 0.25;
ctx.fillText('♥', W - 118 + i * 26, 36);
}
ctx.globalAlpha = 1;
// combo
if (combo > 1 && state === 'playing') {
ctx.textAlign = 'center';
ctx.fillStyle = '#9ef0ff';
ctx.font = 'bold 16px system-ui, sans-serif';
ctx.fillText(`COMBO x${combo}`, W / 2, 54);
}
ctx.textAlign = 'left';
ctx.font = '12px system-ui, sans-serif';
ctx.fillStyle = 'rgba(255,255,255,0.55)';
ctx.fillText(muted ? 'M: muted' : 'M: mute P: pause R: restart', 24, H - 16);
}
function drawOverlay() {
if (state === 'start') {
panel('NEON BREAKOUT', 'Clear every brick. Catch the ball. Climb the levels.', [
'Mouse or A / D / Arrow Keys: move paddle',
'Space or Click: launch ball / start',
'P or Esc: pause R: restart M: mute',
'',
`Best score: ${highScore.toLocaleString()}`,
'Press SPACE or CLICK to begin'
]);
} else if (state === 'paused') {
panel('PAUSED', '', ['Press P or Esc to resume']);
} else if (state === 'levelclear') {
panel(`LEVEL ${level - 1} CLEAR`, 'Get ready for the next wave.', ['Press SPACE or CLICK to continue']);
} else if (state === 'gameover') {
panel('GAME OVER', `Final score: ${score.toLocaleString()}`, [
`Best score: ${highScore.toLocaleString()}`,
'Press R or SPACE to play again'
]);
} else if (state === 'victory') {
panel('YOU WIN!', `Final score: ${score.toLocaleString()}`, [
`Best score: ${highScore.toLocaleString()}`,
'Press R or SPACE to play again'
]);
}
}
function panel(title, subtitle, lines) {
ctx.fillStyle = 'rgba(0, 0, 0, 0.55)';
ctx.fillRect(0, 0, W, H);
ctx.textAlign = 'center';
ctx.fillStyle = '#ffffff';
ctx.shadowColor = 'rgba(76, 201, 240, 0.8)';
ctx.shadowBlur = 24;
ctx.font = 'bold 58px system-ui, sans-serif';
ctx.fillText(title, W / 2, H / 2 - 60);
ctx.shadowBlur = 0;
if (subtitle) {
ctx.font = '22px system-ui, sans-serif';
ctx.fillStyle = '#9ef0ff';
ctx.fillText(subtitle, W / 2, H / 2 - 18);
}
ctx.font = '18px system-ui, sans-serif';
ctx.fillStyle = 'rgba(255,255,255,0.85)';
let y = H / 2 + 28;
for (const line of lines) {
ctx.fillText(line, W / 2, y);
y += 28;
}
}
function render() {
canvas.style.cursor = state === 'playing' ? 'none' : 'default';
ctx.save();
ctx.translate(shakeX, shakeY);
drawBackground();
drawBricks();
drawParticles();
drawPaddle();
drawBall();
drawFloats();
ctx.restore();
drawUI();
drawOverlay();
}
// ---------- main loop ----------
function frame(ts) {
if (!lastTime) lastTime = ts;
const dt = Math.min((ts - lastTime) / 1000, 1 / 30);
lastTime = ts;
if (state === 'playing' || state === 'levelclear') {
update(dt);
} else {
updateParticles(dt);
}
render();
requestAnimationFrame(frame);
}
// ---------- init ----------
loadHighScore();
initStars();
buildLevel(1);
// show start with a prepared board
paddle.x = W / 2;
paddle.targetX = W / 2;
resetBallOnPaddle();
canvas.focus();
requestAnimationFrame(frame);
})();
</script>
</body>
</html>

