Run details
- Model
- particle-ai/particle-1
- Route
- OpenRouter API, one user message
- Served by
- Particle AI
- Effort
- high, OpenRouter's default for this model
- Limits
- 60 min · 131,072 output tokens (the model's limit)
- Date
- 28 Oct 2026
- Time
- 2 min 10 s
- Size
- 34 kB
- Tokens
- 158 in, 30,859 out (18,168 of them thinking)
- List price
- $0.015 at today's price ($0.05 in / $0.5 out per M)
- Paid
- $0.031
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
34 kB 1,295 lines Raw
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,user-scalable=no">
<title>Neon Breakout</title>
<style>
html, body {
margin: 0;
padding: 0;
height: 100%;
background: #05070d;
overflow: hidden;
-webkit-user-select: none;
user-select: none;
font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
}
body {
display: flex;
align-items: center;
justify-content: center;
}
canvas {
display: block;
border-radius: 14px;
cursor: crosshair;
touch-action: none;
box-shadow:
0 0 70px rgba(70, 230, 255, 0.16),
0 0 0 1px rgba(70, 230, 255, 0.16),
0 30px 80px rgba(0, 0, 0, 0.8);
}
</style>
</head>
<body>
<canvas id="game" width="900" height="640"></canvas>
<script>
(function () {
"use strict";
// ---------------------------------------------------------------- setup
const canvas = document.getElementById("game");
const ctx = canvas.getContext("2d");
const W = 900, H = 640;
const TAU = Math.PI * 2;
const HUD_H = 56;
const PADDLE_Y = 596;
const PADDLE_H = 14;
const BALL_R = 8;
const clamp = (v, a, b) => (v < a ? a : v > b ? b : v);
const rand = (a, b) => a + Math.random() * (b - a);
function roundRect(c, x, y, w, h, r) {
r = Math.min(r, w / 2, h / 2);
c.beginPath();
c.moveTo(x + r, y);
c.lineTo(x + w - r, y);
c.quadraticCurveTo(x + w, y, x + w, y + r);
c.lineTo(x + w, y + h - r);
c.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
c.lineTo(x + r, y + h);
c.quadraticCurveTo(x, y + h, x, y + h - r);
c.lineTo(x, y + r);
c.quadraticCurveTo(x, y, x + r, y);
c.closePath();
}
// responsive scale
function resize() {
const pad = 28;
const s = Math.min(
(window.innerWidth - pad * 2) / W,
(window.innerHeight - pad * 2) / H,
1.35
);
canvas.style.width = Math.round(W * s) + "px";
canvas.style.height = Math.round(H * s) + "px";
}
window.addEventListener("resize", resize);
resize();
// ---------------------------------------------------------------- audio
let actx = null, masterGain = null, muted = false;
let lastBlip = 0;
function initAudio() {
if (actx) {
if (actx.state === "suspended") actx.resume();
return;
}
try {
actx = new (window.AudioContext || window.webkitAudioContext)();
masterGain = actx.createGain();
masterGain.gain.value = 0.32;
masterGain.connect(actx.destination);
} catch (e) {
actx = null;
}
}
function tone(freq, dur, type, vol, when, slideTo) {
if (!actx || muted) return;
const t0 = actx.currentTime + (when || 0);
const o = actx.createOscillator();
const g = actx.createGain();
o.type = type || "sine";
o.frequency.setValueAtTime(freq, t0);
if (slideTo) o.frequency.exponentialRampToValueAtTime(Math.max(20, slideTo), t0 + dur);
g.gain.setValueAtTime(0.0001, t0);
g.gain.linearRampToValueAtTime(vol, t0 + 0.008);
g.gain.exponentialRampToValueAtTime(0.0001, t0 + dur);
o.connect(g);
g.connect(masterGain);
o.start(t0);
o.stop(t0 + dur + 0.03);
}
const sfx = {
wall() {
const now = performance.now();
if (now - lastBlip < 24) return;
lastBlip = now;
tone(300, 0.045, "square", 0.05, 0, 240);
},
paddle() { tone(230, 0.09, "square", 0.11, 0, 430); },
hit(hp) { tone(500 + hp * 60, 0.06, "square", 0.07, 0, 380); },
brk(hp) {
tone(360 + hp * 130, 0.12, "triangle", 0.12, 0, 900);
tone(720 + hp * 90, 0.07, "square", 0.05, 0.01, 1300);
},
clang() { tone(160, 0.09, "sawtooth", 0.06, 0, 120); },
death() {
tone(320, 0.55, "sawtooth", 0.16, 0, 55);
tone(160, 0.55, "square", 0.08, 0.02, 40);
},
power() {
tone(520, 0.09, "square", 0.10, 0);
tone(700, 0.09, "square", 0.10, 0.07);
tone(980, 0.12, "square", 0.10, 0.14);
},
level() {
[523, 659, 784, 1046].forEach((f, i) =>
tone(f, 0.16, "triangle", 0.12, i * 0.09)
);
},
win() {
[523, 659, 784, 1046, 1318].forEach((f, i) =>
tone(f, 0.22, "triangle", 0.13, i * 0.11)
);
}
};
// ---------------------------------------------------------------- levels
const COLS = 12;
const BRICK_LEFT = 36, BRICK_RIGHT = 36, BRICK_TOP = 92;
const BRICK_GAP = 6, BRICK_H = 26;
const BRICK_W = (W - BRICK_LEFT - BRICK_RIGHT - BRICK_GAP * (COLS - 1)) / COLS;
const LEVELS = [
{
name: "WARM UP",
rows: [
"111111111111",
"111111111111",
"222222222222",
"222222222222"
]
},
{
name: "CHECKER",
rows: [
"1.1.1.1.1.1.",
".2.2.2.2.2.2",
"3.3.3.3.3.3.",
".1.1.1.1.1.1",
"2.2.2.2.2.2."
]
},
{
name: "FORTRESS",
rows: [
"X2222222222X",
"X1111111111X",
"X.22222222.X",
"X..333333..X",
"X...1111...X"
]
},
{
name: "PYRAMID",
rows: [
".....33.....",
"....3223....",
"...322223...",
"..32222223..",
".3222222223.",
"111111111111"
]
},
{
name: "CROSSFIRE",
rows: [
"2..111111..2",
"22.222222.22",
"322.3333.223",
"3222.33.2223",
"322.3333.223",
"22.222222.22",
"2..111111..2"
]
},
{
name: "THE VAULT",
rows: [
"333333333333",
"3XX333333XX3",
"3X11111111X3",
"3X12222221X3",
"3X11111111X3",
"3XX333333XX3",
"333333333333"
]
}
];
const BRICK_COLORS = {
1: ["#2ad4ff", "#0a4f6b"],
2: ["#7bff6a", "#1c6b16"],
3: ["#ff5fd2", "#7a1858"],
X: ["#7d879e", "#333a4a"]
};
// ---------------------------------------------------------------- state
const paddle = {
x: W / 2,
y: PADDLE_Y,
w: 118,
h: PADDLE_H,
baseW: 118,
wideW: 182,
vx: 0,
squash: 0
};
const G = {
mode: "title",
score: 0,
best: 0,
lives: 3,
levelIndex: 0,
bricks: [],
balls: [],
particles: [],
powerups: [],
popups: [],
banner: null,
shake: 0,
flash: 0,
combo: 0,
ballSpeed: 330,
effects: { wide: 0, slow: 0 },
clearTimer: 0,
time: 0
};
try {
const b = localStorage.getItem("neonBreakoutBest");
if (b) G.best = parseInt(b, 10) || 0;
} catch (e) {}
function saveBest() {
try {
localStorage.setItem("neonBreakoutBest", String(G.best));
} catch (e) {}
}
// ---------------------------------------------------------------- input
const keys = { left: false, right: false };
let mouseX = W / 2;
let usingMouse = false;
window.addEventListener("keydown", (e) => {
const k = e.key.toLowerCase();
if (
k === "arrowleft" || k === "arrowright" || k === "arrowup" ||
k === "arrowdown" || k === " "
) {
e.preventDefault();
}
if (k === "arrowleft" || k === "a") { keys.left = true; usingMouse = false; }
if (k === "arrowright" || k === "d") { keys.right = true; usingMouse = false; }
if (k === " " || k === "enter") { action(); }
if (k === "p" || k === "escape") { togglePause(); }
if (k === "m") {
muted = !muted;
toast(muted ? "SOUND OFF" : "SOUND ON");
}
if (k === "r" && (G.mode === "playing" || G.mode === "paused")) {
startGame();
}
});
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 pointerToX(clientX) {
const r = canvas.getBoundingClientRect();
return ((clientX - r.left) / r.width) * W;
}
canvas.addEventListener("mousemove", (e) => {
mouseX = pointerToX(e.clientX);
usingMouse = true;
});
canvas.addEventListener("mousedown", (e) => {
e.preventDefault();
initAudio();
action();
});
canvas.addEventListener("touchstart", (e) => {
e.preventDefault();
initAudio();
mouseX = pointerToX(e.touches[0].clientX);
usingMouse = true;
action();
}, { passive: false });
canvas.addEventListener("touchmove", (e) => {
e.preventDefault();
mouseX = pointerToX(e.touches[0].clientX);
usingMouse = true;
}, { passive: false });
function togglePause() {
if (G.mode === "playing") G.mode = "paused";
else if (G.mode === "paused") G.mode = "playing";
}
function action() {
initAudio();
if (G.mode === "title") startGame();
else if (G.mode === "playing") launch();
else if (G.mode === "paused") G.mode = "playing";
else if (G.mode === "levelclear") G.clearTimer = 0;
else if (G.mode === "gameover" || G.mode === "win") startGame();
}
// ---------------------------------------------------------------- game flow
function startGame() {
initAudio();
G.score = 0;
G.lives = 3;
G.combo = 0;
G.particles.length = 0;
G.popups.length = 0;
G.powerups.length = 0;
G.banner = null;
G.shake = 0;
G.flash = 0;
G.effects.wide = 0;
G.effects.slow = 0;
paddle.x = W / 2;
paddle.w = paddle.baseW;
loadLevel(0);
G.mode = "playing";
}
function loadLevel(i) {
G.levelIndex = i;
const lv = LEVELS[i];
G.bricks = [];
for (let r = 0; r < lv.rows.length; r++) {
const row = lv.rows[r];
for (let c = 0; c < COLS; c++) {
const ch = row[c];
if (!ch || ch === ".") continue;
const hp = ch === "X" ? Infinity : parseInt(ch, 10);
G.bricks.push({
x: BRICK_LEFT + c * (BRICK_W + BRICK_GAP),
y: BRICK_TOP + r * (BRICK_H + BRICK_GAP),
w: BRICK_W,
h: BRICK_H,
hp: hp,
maxHp: hp,
flash: 0,
dead: false
});
}
}
G.ballSpeed = 330 + i * 22;
G.effects.wide = 0;
G.effects.slow = 0;
G.combo = 0;
G.powerups.length = 0;
paddle.x = W / 2;
paddle.w = paddle.baseW;
resetBall();
toast("LEVEL " + (i + 1) + " — " + lv.name);
}
function resetBall() {
G.balls = [{
x: paddle.x,
y: paddle.y - BALL_R - 1,
vx: 0,
vy: 0,
r: BALL_R,
stuck: true,
trail: []
}];
}
function speedForBall() {
return G.ballSpeed * (G.effects.slow > 0 ? 0.72 : 1);
}
function launch() {
let launched = false;
for (const b of G.balls) {
if (b.stuck) {
b.stuck = false;
const ang = rand(-0.3, 0.3);
const sp = speedForBall();
b.vx = Math.sin(ang) * sp;
b.vy = -Math.cos(ang) * sp;
launched = true;
}
}
if (launched) sfx.paddle();
}
function levelClear() {
G.mode = "levelclear";
G.clearTimer = 2.2;
const bonus = 500 + G.lives * 150;
G.score += bonus;
G.popups.push({
x: W / 2, y: H / 2 + 60,
text: "+" + bonus + " BONUS",
life: 1.6, maxLife: 1.6, color: "#ffd166", size: 22
});
sfx.level();
addShake(8);
}
function nextLevel() {
if (G.levelIndex + 1 >= LEVELS.length) {
G.mode = "win";
G.best = Math.max(G.best, G.score);
saveBest();
sfx.win();
} else {
loadLevel(G.levelIndex + 1);
G.mode = "playing";
}
}
function loseLife() {
G.lives--;
G.combo = 0;
G.shake = 16;
G.flash = 0.55;
sfx.death();
if (G.lives <= 0) {
G.mode = "gameover";
G.best = Math.max(G.best, G.score);
saveBest();
G.balls = [];
} else {
resetBall();
}
}
// ---------------------------------------------------------------- update
function update(dt) {
G.time += dt;
if (G.effects.wide > 0) G.effects.wide = Math.max(0, G.effects.wide - dt);
if (G.effects.slow > 0) G.effects.slow = Math.max(0, G.effects.slow - dt);
const targetW = G.effects.wide > 0 ? paddle.wideW : paddle.baseW;
paddle.w += (targetW - paddle.w) * Math.min(1, dt * 10);
paddle.squash = Math.max(0, paddle.squash - dt * 6);
if (G.banner) {
G.banner.life -= dt;
if (G.banner.life <= 0) G.banner = null;
}
if (G.mode === "playing") {
updatePaddle(dt);
updateBalls(dt);
updatePowerups(dt);
updatePopups(dt);
if (!G.bricks.some((b) => !b.dead && b.hp !== Infinity)) levelClear();
} else if (G.mode === "levelclear") {
G.clearTimer -= dt;
updatePopups(dt);
if (G.clearTimer <= 0) nextLevel();
} else if (G.mode === "gameover" || G.mode === "win") {
updatePopups(dt);
}
updateParticles(dt);
for (const b of G.bricks) {
if (b.flash > 0) b.flash = Math.max(0, b.flash - dt * 5);
}
G.shake *= Math.pow(0.0018, dt);
if (G.shake < 0.06) G.shake = 0;
G.flash = Math.max(0, G.flash - dt * 2.4);
}
function updatePaddle(dt) {
const prevX = paddle.x;
if (usingMouse) {
const target = clamp(mouseX, paddle.w / 2, W - paddle.w / 2);
paddle.x += (target - paddle.x) * Math.min(1, dt * 26);
} else {
let dir = 0;
if (keys.left) dir -= 1;
if (keys.right) dir += 1;
paddle.x += dir * 760 * dt;
}
paddle.x = clamp(paddle.x, paddle.w / 2, W - paddle.w / 2);
paddle.vx = (paddle.x - prevX) / Math.max(dt, 1e-4);
}
function updateBalls(dt) {
const balls = G.balls;
for (let i = balls.length - 1; i >= 0; i--) {
const b = balls[i];
if (b.stuck) {
b.x = paddle.x;
b.y = paddle.y - b.r - 1;
b.trail.length = 0;
continue;
}
// smooth speed correction (handles slow powerup + level changes)
const target = speedForBall();
const cur = Math.hypot(b.vx, b.vy);
if (cur > 0.5) {
const k = 1 + (target / cur - 1) * Math.min(1, dt * 7);
b.vx *= k;
b.vy *= k;
}
// avoid near-horizontal crawling
const sp = Math.hypot(b.vx, b.vy);
if (sp > 1 && Math.abs(b.vy) < sp * 0.22) {
const sign = b.vy <= 0 ? -1 : 1;
b.vy = sign * sp * 0.22;
const m = Math.hypot(b.vx, b.vy);
b.vx = (b.vx / m) * sp;
b.vy = (b.vy / m) * sp;
}
// substep movement for reliable collisions
const dist = Math.hypot(b.vx, b.vy) * dt;
const steps = Math.max(1, Math.ceil(dist / (b.r * 0.85)));
const sdt = dt / steps;
let alive = true;
for (let s = 0; s < steps && alive; s++) {
b.x += b.vx * sdt;
b.y += b.vy * sdt;
collideWalls(b);
collidePaddle(b);
collideBricks(b);
if (b.y - b.r > H + 30) alive = false;
}
if (!alive) {
balls.splice(i, 1);
} else {
b.trail.push({ x: b.x, y: b.y });
if (b.trail.length > 9) b.trail.shift();
}
}
if (balls.length === 0 && G.mode === "playing") loseLife();
}
function collideWalls(b) {
if (b.x - b.r < 0) {
b.x = b.r;
b.vx = Math.abs(b.vx);
sfx.wall();
spawnParticles(0, b.y, "#7fe9ff", 3, 80);
} else if (b.x + b.r > W) {
b.x = W - b.r;
b.vx = -Math.abs(b.vx);
sfx.wall();
spawnParticles(W, b.y, "#7fe9ff", 3, 80);
}
if (b.y - b.r < HUD_H) {
b.y = HUD_H + b.r;
b.vy = Math.abs(b.vy);
sfx.wall();
spawnParticles(b.x, HUD_H, "#7fe9ff", 3, 80);
}
}
function collidePaddle(b) {
if (b.vy <= 0) return;
const px = paddle.x - paddle.w / 2;
if (
b.y + b.r >= paddle.y &&
b.y - b.r <= paddle.y + paddle.h &&
b.x + b.r >= px &&
b.x - b.r <= px + paddle.w
) {
b.y = paddle.y - b.r - 0.01;
const rel = clamp((b.x - paddle.x) / (paddle.w / 2), -1, 1);
let ang = clamp(rel * 1.05 + paddle.vx * 0.00011, -1.2, 1.2);
const sp = speedForBall();
b.vx = Math.sin(ang) * sp;
b.vy = -Math.cos(ang) * sp;
paddle.squash = 1;
G.combo = 0;
addShake(2);
sfx.paddle();
spawnParticles(b.x, paddle.y, "#46e6ff", 6, 150);
}
}
function collideBricks(b) {
for (const br of G.bricks) {
if (br.dead) continue;
if (
b.x + b.r < br.x || b.x - b.r > br.x + br.w ||
b.y + b.r < br.y || b.y - b.r > br.y + br.h
) continue;
const cx = clamp(b.x, br.x, br.x + br.w);
const cy = clamp(b.y, br.y, br.y + br.h);
const dx = b.x - cx, dy = b.y - cy;
if (dx * dx + dy * dy > b.r * b.r) continue;
const bcx = br.x + br.w / 2;
const bcy = br.y + br.h / 2;
const ox = br.w / 2 + b.r - Math.abs(b.x - bcx);
const oy = br.h / 2 + b.r - Math.abs(b.y - bcy);
if (ox < oy) {
b.vx = -b.vx;
b.x += b.x < bcx ? -ox : ox;
} else {
b.vy = -b.vy;
b.y += b.y < bcy ? -oy : oy;
}
hitBrick(br, b);
break;
}
}
function hitBrick(br, ball) {
if (br.hp === Infinity) {
br.flash = 0.2;
addShake(1.6);
sfx.clang();
spawnParticles(ball.x, ball.y, "#c8d2e6", 4, 110);
return;
}
br.hp--;
br.flash = 0.24;
const base = br.maxHp;
const col = BRICK_COLORS[base] ? BRICK_COLORS[base][0] : "#2ad4ff";
if (br.hp <= 0) {
br.dead = true;
G.combo++;
const bonus = Math.min(G.combo, 12) * 10;
const pts = 50 * base + bonus;
G.score += pts;
spawnParticles(br.x + br.w / 2, br.y + br.h / 2, col, 12 + base * 5, 250);
G.popups.push({
x: br.x + br.w / 2, y: br.y + br.h / 2,
text: "+" + pts, life: 0.85, maxLife: 0.85,
color: col, size: 13
});
addShake(2.5 + base);
maybeDropPowerup(br);
sfx.brk(base);
} else {
G.score += 10;
spawnParticles(ball.x, ball.y, col, 4, 130);
sfx.hit(base);
}
}
function maybeDropPowerup(br) {
if (Math.random() > 0.17) return;
const pool = ["wide", "multi", "slow", "wide", "multi", "slow", "life"];
const kind = pool[Math.floor(Math.random() * pool.length)];
G.powerups.push({
x: br.x + br.w / 2,
y: br.y + br.h / 2,
vy: 135,
kind: kind,
t: 0,
w: 30,
h: 22
});
}
function updatePowerups(dt) {
const px = paddle.x - paddle.w / 2;
for (let i = G.powerups.length - 1; i >= 0; i--) {
const p = G.powerups[i];
p.y += p.vy * dt;
p.t += dt;
if (
p.y + p.h / 2 > paddle.y &&
p.y - p.h / 2 < paddle.y + paddle.h &&
p.x + p.w / 2 > px &&
p.x - p.w / 2 < px + paddle.w
) {
applyPowerup(p.kind);
G.powerups.splice(i, 1);
continue;
}
if (p.y - p.h > H) G.powerups.splice(i, 1);
}
}
function applyPowerup(kind) {
sfx.power();
addShake(4);
if (kind === "wide") {
G.effects.wide = 12;
toast("WIDE PADDLE");
} else if (kind === "slow") {
G.effects.slow = 8;
toast("SLOW BALL");
} else if (kind === "life") {
G.lives = Math.min(G.lives + 1, 6);
toast("EXTRA LIFE");
} else if (kind === "multi") {
const active = G.balls.filter((b) => !b.stuck);
if (!active.length) {
toast("MULTIBALL");
return;
}
const added = [];
for (const b of active) {
if (G.balls.length + added.length >= 8) break;
const sp = Math.hypot(b.vx, b.vy) || speedForBall();
const baseAng = Math.atan2(b.vy, b.vx);
for (const off of [-0.45, 0.45]) {
const a = baseAng + off;
added.push({
x: b.x, y: b.y,
vx: Math.cos(a) * sp, vy: Math.sin(a) * sp,
r: BALL_R, stuck: false, trail: []
});
}
}
G.balls.push.apply(G.balls, added);
toast("MULTIBALL");
}
}
function spawnParticles(x, y, color, n, spread) {
if (G.particles.length > 520) n = Math.min(n, 3);
for (let i = 0; i < n; i++) {
const a = Math.random() * TAU;
const sp = rand(30, spread);
const life = rand(0.28, 0.7);
G.particles.push({
x: x, y: y,
vx: Math.cos(a) * sp,
vy: Math.sin(a) * sp - 40,
life: life,
maxLife: life,
size: rand(1.6, 4.2),
color: color
});
}
}
function updateParticles(dt) {
for (let i = G.particles.length - 1; i >= 0; i--) {
const p = G.particles[i];
p.life -= dt;
if (p.life <= 0) {
G.particles.splice(i, 1);
continue;
}
p.x += p.vx * dt;
p.y += p.vy * dt;
p.vy += 620 * dt;
p.vx *= Math.pow(0.12, dt);
p.vy *= Math.pow(0.55, dt);
}
}
function updatePopups(dt) {
for (let i = G.popups.length - 1; i >= 0; i--) {
const p = G.popups[i];
p.life -= dt;
p.y -= 42 * dt;
if (p.life <= 0) G.popups.splice(i, 1);
}
}
function addShake(v) {
G.shake = Math.min(26, G.shake + v);
}
function toast(text) {
G.banner = { text: text, life: 1.5 };
}
// ---------------------------------------------------------------- drawing
const bgGrad = ctx.createLinearGradient(0, 0, 0, H);
bgGrad.addColorStop(0, "#0b1224");
bgGrad.addColorStop(0.55, "#070b16");
bgGrad.addColorStop(1, "#04060c");
const stars = [];
for (let i = 0; i < 95; i++) {
stars.push({
x: Math.random() * W,
y: Math.random() * H,
r: rand(0.4, 1.7),
p: Math.random() * TAU,
s: rand(0.2, 0.8)
});
}
function drawBackground() {
ctx.fillStyle = bgGrad;
ctx.fillRect(0, 0, W, H);
// faint grid
ctx.strokeStyle = "rgba(70,180,255,0.045)";
ctx.lineWidth = 1;
ctx.beginPath();
for (let x = 0; x <= W; x += 45) {
ctx.moveTo(x + 0.5, HUD_H);
ctx.lineTo(x + 0.5, H);
}
for (let y = HUD_H; y <= H; y += 45) {
ctx.moveTo(0, y + 0.5);
ctx.lineTo(W, y + 0.5);
}
ctx.stroke();
// stars
for (const s of stars) {
const a = 0.18 + 0.32 * (0.5 + 0.5 * Math.sin(G.time * s.s + s.p));
ctx.fillStyle = "rgba(150,215,255," + a.toFixed(3) + ")";
ctx.beginPath();
ctx.arc(s.x, s.y, s.r, 0, TAU);
ctx.fill();
}
}
function drawFieldEdges() {
ctx.save();
ctx.strokeStyle = "rgba(70,230,255,0.22)";
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(1, HUD_H);
ctx.lineTo(1, H);
ctx.moveTo(W - 1, HUD_H);
ctx.lineTo(W - 1, H);
ctx.stroke();
ctx.restore();
}
function drawBricks() {
for (const br of G.bricks) {
if (br.dead) continue;
const key = br.maxHp === Infinity ? "X" : br.maxHp;
const col = BRICK_COLORS[key] || BRICK_COLORS[1];
const main = col[0], dark = col[1];
ctx.save();
if (br.flash > 0) {
ctx.shadowColor = main;
ctx.shadowBlur = 20;
}
ctx.fillStyle = dark;
roundRect(ctx, br.x, br.y, br.w, br.h, 5);
ctx.fill();
ctx.fillStyle = main;
roundRect(ctx, br.x + 1.5, br.y + 1.5, br.w - 3, br.h - 4, 4);
ctx.fill();
ctx.restore();
// top highlight
ctx.fillStyle = "rgba(255,255,255,0.30)";
roundRect(ctx, br.x + 4, br.y + 3.5, br.w - 8, 3.5, 1.8);
ctx.fill();
// damage hint
if (br.hp !== Infinity && br.hp < br.maxHp) {
ctx.fillStyle = "rgba(0,0,0,0.22)";
roundRect(ctx, br.x + 1.5, br.y + 1.5, br.w - 3, br.h - 4, 4);
ctx.fill();
}
if (br.maxHp === Infinity) {
ctx.strokeStyle = "rgba(255,255,255,0.25)";
ctx.lineWidth = 1.5;
ctx.beginPath();
ctx.moveTo(br.x + 8, br.y + br.h - 8);
ctx.lineTo(br.x + br.w - 8, br.y + 8);
ctx.moveTo(br.x + 8, br.y + 8);
ctx.lineTo(br.x + br.w - 8, br.y + br.h - 8);
ctx.stroke();
}
if (br.flash > 0) {
ctx.fillStyle = "rgba(255,255,255," + (br.flash * 3.2).toFixed(3) + ")";
roundRect(ctx, br.x, br.y, br.w, br.h, 5);
ctx.fill();
}
}
}
function drawParticles() {
for (const p of G.particles) {
const a = Math.max(0, p.life / p.maxLife);
ctx.globalAlpha = a;
ctx.fillStyle = p.color;
ctx.beginPath();
ctx.arc(p.x, p.y, p.size * (0.35 + a * 0.75), 0, TAU);
ctx.fill();
}
ctx.globalAlpha = 1;
}
const PU_STYLE = {
wide: { c: "#5cff9d", t: "W" },
multi: { c: "#ffb545", t: "M" },
slow: { c: "#5cb8ff", t: "S" },
life: { c: "#ff6bd6", t: "+" }
};
function drawPowerups() {
for (const p of G.powerups) {
const st = PU_STYLE[p.kind] || PU_STYLE.wide;
const bob = Math.sin(p.t * 8) * 1.6;
ctx.save();
ctx.translate(p.x, p.y + bob);
ctx.shadowColor = st.c;
ctx.shadowBlur = 16;
ctx.fillStyle = "rgba(6,12,22,0.9)";
roundRect(ctx, -p.w / 2, -p.h / 2, p.w, p.h, 6);
ctx.fill();
ctx.strokeStyle = st.c;
ctx.lineWidth = 2;
roundRect(ctx, -p.w / 2, -p.h / 2, p.w, p.h, 6);
ctx.stroke();
ctx.shadowBlur = 0;
ctx.fillStyle = st.c;
ctx.font = "bold 15px ui-monospace, Menlo, Consolas, monospace";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText(st.t, 0, 0.5);
ctx.restore();
}
}
function drawPaddle() {
const sq = paddle.squash;
const h = paddle.h * (1 - 0.45 * sq);
const w = paddle.w;
const x = paddle.x - w / 2;
const y = paddle.y + (paddle.h - h) / 2;
ctx.save();
ctx.shadowColor = "#46e6ff";
ctx.shadowBlur = 20;
const g = ctx.createLinearGradient(0, y, 0, y + h);
g.addColorStop(0, "#ccfaff");
g.addColorStop(0.45, "#46e6ff");
g.addColorStop(1, "#0d6f92");
ctx.fillStyle = g;
roundRect(ctx, x, y, w, h, h / 2);
ctx.fill();
ctx.restore();
ctx.fillStyle = "rgba(255,255,255,0.55)";
roundRect(ctx, x + 6, y + 2, w - 12, 2.5, 1.2);
ctx.fill();
}
function drawBalls() {
for (const b of G.balls) {
// trail
for (let i = 0; i < b.trail.length; i++) {
const t = b.trail[i];
const a = (i / b.trail.length) * 0.28;
ctx.fillStyle = "rgba(120,235,255," + a.toFixed(3) + ")";
ctx.beginPath();
ctx.arc(t.x, t.y, b.r * (0.3 + 0.7 * (i / b.trail.length)), 0, TAU);
ctx.fill();
}
ctx.save();
ctx.shadowColor = "#7ef0ff";
ctx.shadowBlur = 18;
const g = ctx.createRadialGradient(
b.x - b.r * 0.3, b.y - b.r * 0.3, b.r * 0.15,
b.x, b.y, b.r
);
g.addColorStop(0, "#ffffff");
g.addColorStop(0.45, "#b6f4ff");
g.addColorStop(1, "#2aa9d6");
ctx.fillStyle = g;
ctx.beginPath();
ctx.arc(b.x, b.y, b.r, 0, TAU);
ctx.fill();
ctx.restore();
}
}
function drawPopups() {
ctx.textAlign = "center";
ctx.textBaseline = "middle";
for (const p of G.popups) {
const a = Math.max(0, p.life / p.maxLife);
ctx.globalAlpha = a;
ctx.fillStyle = p.color;
ctx.font = "bold " + (p.size || 14) + "px ui-monospace, Menlo, Consolas, monospace";
ctx.fillText(p.text, p.x, p.y);
}
ctx.globalAlpha = 1;
}
function drawBanner() {
if (!G.banner) return;
const a = Math.min(1, G.banner.life * 2.2);
ctx.globalAlpha = a;
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.font = "bold 20px ui-monospace, Menlo, Consolas, monospace";
ctx.fillStyle = "rgba(4,10,20,0.7)";
const w = ctx.measureText(G.banner.text).width + 40;
roundRect(ctx, W / 2 - w / 2, 400, w, 38, 19);
ctx.fill();
ctx.fillStyle = "#8ff4ff";
ctx.shadowColor = "#46e6ff";
ctx.shadowBlur = 14;
ctx.fillText(G.banner.text, W / 2, 420);
ctx.shadowBlur = 0;
ctx.globalAlpha = 1;
}
function drawHUD() {
ctx.fillStyle = "rgba(6,11,22,0.92)";
ctx.fillRect(0, 0, W, HUD_H);
ctx.strokeStyle = "rgba(70,230,255,0.28)";
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(0, HUD_H - 1);
ctx.lineTo(W, HUD_H - 1);
ctx.stroke();
const mono = "ui-monospace, Menlo, Consolas, monospace";
ctx.textBaseline = "alphabetic";
// score
ctx.textAlign = "left";
ctx.fillStyle = "rgba(140,200,230,0.65)";
ctx.font = "600 11px " + mono;
ctx.fillText("SCORE", 24, 22);
ctx.fillStyle = "#e2f8ff";
ctx.font = "bold 24px " + mono;
ctx.fillText(String(G.score).padStart(6, "0"), 24, 46);
// lives
ctx.textAlign = "center";
ctx.fillStyle = "rgba(140,200,230,0.65)";
ctx.font = "600 11px " + mono;
ctx.fillText("LIVES", W / 2, 22);
const n = Math.max(0, G.lives);
const iconW = 22, gap = 8;
const totalW = n > 0 ? n * iconW + (n - 1) * gap : 0;
let sx = W / 2 - totalW / 2;
for (let i = 0; i < n; i++) {
ctx.save();
ctx.shadowColor = "#46e6ff";
ctx.shadowBlur = 8;
ctx.fillStyle = "#46e6ff";
roundRect(ctx, sx, 33, iconW, 6, 3);
ctx.fill();
ctx.restore();
sx += iconW + gap;
}
// level
ctx.textAlign = "right";
ctx.fillStyle = "rgba(140,200,230,0.65)";
ctx.font = "600 11px " + mono;
ctx.fillText("LEVEL", W - 24, 22);
ctx.fillStyle = "#e2f8ff";
ctx.font = "bold 24px " + mono;
ctx.fillText((G.levelIndex + 1) + " / " + LEVELS.length, W - 24, 46);
// combo indicator
if (G.combo > 1) {
ctx.textAlign = "center";
ctx.fillStyle = "rgba(255,214,102," + (0.5 + 0.5 * Math.sin(G.time * 14)) + ")";
ctx.font = "bold 13px " + mono;
ctx.fillText("COMBO x" + G.combo, W / 2, HUD_H + 24);
}
}
function drawOverlay() {
if (G.mode === "playing") return;
ctx.fillStyle = "rgba(3,6,14,0.74)";
ctx.fillRect(0, 0, W, H);
const mono = "ui-monospace, Menlo, Consolas, monospace";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
const blink = 0.55 + 0.45 * Math.sin(G.time * 4.5);
if (G.mode === "title") {
ctx.save();
ctx.shadowColor = "#46e6ff";
ctx.shadowBlur = 44;
const g = ctx.createLinearGradient(0, 190, 0, 270);
g.addColorStop(0, "#ffffff");
g.addColorStop(0.5, "#8bf2ff");
g.addColorStop(1, "#2a8bd6");
ctx.fillStyle = g;
ctx.font = "bold 82px " + mono;
ctx.fillText("BREAKOUT", W / 2, 232);
ctx.restore();
ctx.fillStyle = "rgba(120,220,255,0.75)";
ctx.font = "bold 16px " + mono;
ctx.fillText("N E O N E D I T I O N", W / 2, 288);
ctx.fillStyle = "rgba(200,225,245,0.8)";
ctx.font = "14px " + mono;
ctx.fillText("MOVE: MOUSE or \u2190 \u2192 / A D", W / 2, 372);
ctx.fillText("LAUNCH: SPACE or CLICK", W / 2, 398);
ctx.fillText("PAUSE: P MUTE: M RESTART: R", W / 2, 424);
ctx.globalAlpha = blink;
ctx.fillStyle = "#ffd166";
ctx.font = "bold 22px " + mono;
ctx.fillText("PRESS SPACE OR CLICK TO START", W / 2, 512);
ctx.globalAlpha = 1;
if (G.best > 0) {
ctx.fillStyle = "rgba(140,200,230,0.6)";
ctx.font = "13px " + mono;
ctx.fillText("BEST SCORE: " + G.best, W / 2, 566);
}
} else if (G.mode === "paused") {
ctx.fillStyle = "#e2f8ff";
ctx.font = "bold 56px " + mono;
ctx.fillText("PAUSED", W / 2, H / 2 - 20);
ctx.globalAlpha = blink;
ctx.fillStyle = "#ffd166";
ctx.font = "bold 18px " + mono;
ctx.fillText("PRESS P OR SPACE TO RESUME", W / 2, H / 2 + 50);
ctx.globalAlpha = 1;
} else if (G.mode === "levelclear") {
ctx.save();
ctx.shadowColor = "#7bff6a";
ctx.shadowBlur = 30;
ctx.fillStyle = "#c9ffbe";
ctx.font = "bold 54px " + mono;
ctx.fillText("LEVEL CLEAR", W / 2, H / 2 - 30);
ctx.restore();
ctx.fillStyle = "#ffd166";
ctx.font = "bold 20px " + mono;
ctx.fillText("+" + (500 + G.lives * 150) + " BONUS", W / 2, H / 2 + 30);
ctx.globalAlpha = blink;
ctx.fillStyle = "rgba(200,225,245,0.9)";
ctx.font = "bold 16px " + mono;
ctx.fillText("NEXT LEVEL INCOMING\u2026", W / 2, H / 2 + 90);
ctx.globalAlpha = 1;
} else if (G.mode === "gameover") {
ctx.save();
ctx.shadowColor = "#ff4d6d";
ctx.shadowBlur = 30;
ctx.fillStyle = "#ff8fa3";
ctx.font = "bold 62px " + mono;
ctx.fillText("GAME OVER", W / 2, H / 2 - 60);
ctx.restore();
ctx.fillStyle = "#e2f8ff";
ctx.font = "bold 26px " + mono;
ctx.fillText("SCORE " + G.score, W / 2, H / 2 + 10);
ctx.fillStyle = "rgba(140,200,230,0.8)";
ctx.font = "16px " + mono;
ctx.fillText("BEST " + G.best, W / 2, H / 2 + 48);
ctx.globalAlpha = blink;
ctx.fillStyle = "#ffd166";
ctx.font = "bold 20px " + mono;
ctx.fillText("PRESS SPACE OR CLICK TO PLAY AGAIN", W / 2, H / 2 + 122);
ctx.globalAlpha = 1;
} else if (G.mode === "win") {
ctx.save();
ctx.shadowColor = "#ffd166";
ctx.shadowBlur = 36;
const g = ctx.createLinearGradient(0, H / 2 - 110, 0, H / 2 - 40);
g.addColorStop(0, "#ffffff");
g.addColorStop(1, "#ffb545");
ctx.fillStyle = g;
ctx.font = "bold 64px " + mono;
ctx.fillText("YOU WIN!", W / 2, H / 2 - 75);
ctx.restore();
ctx.fillStyle = "#e2f8ff";
ctx.font = "bold 26px " + mono;
ctx.fillText("FINAL SCORE " + G.score, W / 2, H / 2 + 10);
ctx.fillStyle = "rgba(140,200,230,0.8)";
ctx.font = "16px " + mono;
ctx.fillText("ALL " + LEVELS.length + " LEVELS CLEARED", W / 2, H / 2 + 48);
ctx.globalAlpha = blink;
ctx.fillStyle = "#ffd166";
ctx.font = "bold 20px " + mono;
ctx.fillText("PRESS SPACE OR CLICK TO PLAY AGAIN", W / 2, H / 2 + 122);
ctx.globalAlpha = 1;
}
}
function render() {
ctx.setTransform(1, 0, 0, 1, 0, 0);
drawBackground();
ctx.save();
if (G.shake > 0.1) {
ctx.translate(
(Math.random() - 0.5) * G.shake,
(Math.random() - 0.5) * G.shake
);
}
drawFieldEdges();
drawBricks();
drawParticles();
drawPowerups();
drawPaddle();
drawBalls();
drawPopups();
drawBanner();
ctx.restore();
drawHUD();
if (G.flash > 0.001) {
ctx.fillStyle = "rgba(255,70,100," + (G.flash * 0.45).toFixed(3) + ")";
ctx.fillRect(0, 0, W, H);
}
drawOverlay();
}
// ---------------------------------------------------------------- loop
let lastT = performance.now();
function frame(now) {
let dt = (now - lastT) / 1000;
lastT = now;
if (dt > 1 / 30) dt = 1 / 30;
if (dt < 0) dt = 0;
update(dt);
render();
requestAnimationFrame(frame);
}
// start with a placeholder level so the title screen has something behind it
loadLevel(0);
G.mode = "title";
requestAnimationFrame(frame);
})();
</script>
</body>
</html>

