Run details
- Model
- moonshotai/kimi-k3.1-pro
- Route
- OpenRouter API, one user message
- Served by
- Moonshot AI
- Effort
- max, OpenRouter's default for this model
- Limits
- 60 min · 300,000 output tokens (our $3.00 stop per request at $10/M)
- Date
- 23 Oct 2026
- Time
- 8 min 18 s
- Size
- 44 kB
- Tokens
- 162 in, 34,869 out (22,394 of them thinking)
- List price
- $0.35 at today's price ($2 in / $10 out per M)
- Paid
- $0.35
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
44 kB 1,575 lines Raw
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="theme-color" content="#070817">
<link rel="icon" href="data:,">
<title>Neon Breakout</title>
<style>
* { box-sizing: border-box; }
:root {
color-scheme: dark;
--cyan: #44e7ff;
--pink: #ff4fa3;
--violet: #8b6cff;
--ink: #070817;
--muted: #7f89ad;
}
html, body {
margin: 0;
min-height: 100%;
background: #070817;
}
body {
min-height: 100vh;
min-height: 100dvh;
display: grid;
place-items: center;
overflow: hidden;
padding: 12px;
color: #edf3ff;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
user-select: none;
background:
radial-gradient(circle at 15% 15%, rgba(71, 52, 170, .18), transparent 36%),
radial-gradient(circle at 85% 85%, rgba(0, 194, 255, .10), transparent 34%),
#070817;
}
#app {
width: min(1080px, 96vw, calc(160vh - 190px));
}
.topline {
min-height: 25px;
margin-bottom: 9px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
font-size: 11px;
letter-spacing: .16em;
}
.brand {
display: flex;
align-items: center;
gap: 9px;
font-weight: 800;
}
.brand-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--cyan);
box-shadow: 0 0 14px var(--cyan);
}
.tag {
color: var(--muted);
font-size: 9px;
}
#stage {
position: relative;
width: 100%;
aspect-ratio: 8 / 5;
overflow: hidden;
line-height: 0;
border: 1px solid rgba(119, 149, 255, .28);
border-radius: 14px;
background: #090b1c;
box-shadow:
0 28px 90px rgba(0, 0, 0, .55),
0 0 45px rgba(81, 77, 255, .10),
inset 0 0 30px rgba(0, 0, 0, .4);
}
canvas {
display: block;
width: 100%;
height: 100%;
border-radius: inherit;
outline: none;
touch-action: none;
cursor: crosshair;
}
canvas:focus-visible {
box-shadow: inset 0 0 0 2px rgba(68, 231, 255, .6);
}
.scanlines {
position: absolute;
inset: 0;
pointer-events: none;
border-radius: inherit;
opacity: .16;
background: repeating-linear-gradient(
to bottom,
transparent 0,
transparent 3px,
rgba(255, 255, 255, .025) 4px
);
box-shadow: inset 0 0 70px rgba(0, 0, 0, .45);
}
.help {
margin-top: 10px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
color: #8993b5;
font-size: 10px;
line-height: 22px;
}
kbd {
display: inline-block;
min-width: 20px;
padding: 0 5px;
margin: 0 1px;
border: 1px solid rgba(143, 159, 207, .28);
border-bottom-color: rgba(143, 159, 207, .5);
border-radius: 4px;
background: rgba(255, 255, 255, .04);
color: #cbd5f3;
font: inherit;
line-height: 17px;
text-align: center;
}
@media (max-width: 620px) {
.tag, .help span:last-child { display: none; }
.help { justify-content: center; font-size: 9px; }
}
@media (max-height: 560px) {
body { padding: 8px; }
.topline, .help { display: none; }
#app { width: min(1080px, 96vw, calc(160vh - 26px)); }
}
</style>
</head>
<body>
<main id="app">
<div class="topline">
<div class="brand"><span class="brand-dot"></span>NEON // BREAKOUT</div>
<div class="tag">FIVE LEVELS · ONE VERY RESPONSIBLE PADDLE</div>
</div>
<div id="stage">
<canvas
id="game"
width="960"
height="600"
tabindex="0"
aria-label="Neon Breakout. Move with the mouse, arrow keys, or A and D. Press Space to launch, P to pause, M to toggle sound, and R to restart."
></canvas>
<div class="scanlines"></div>
</div>
<div class="help">
<span><kbd>←</kbd><kbd>→</kbd> / <kbd>A</kbd><kbd>D</kbd> or mouse to move</span>
<span><kbd>Space</kbd> launch · <kbd>P</kbd> pause · <kbd>M</kbd> sound · <kbd>R</kbd> restart</span>
</div>
</main>
<script>
(() => {
"use strict";
const W = 960;
const H = 600;
const TOP = 62;
const WALL_LEFT = 13;
const WALL_RIGHT = W - 13;
const canvas = document.getElementById("game");
const ctx = canvas.getContext("2d");
const levels = [
{
name: "IGNITION",
speed: 380,
palette: ["#22d3ee", "#6366f1", "#a855f7"],
rows: [
"111111111111",
"111111111111",
"111111111111"
]
},
{
name: "PYRAMID",
speed: 400,
palette: ["#f472b6", "#a855f7", "#6366f1"],
rows: [
"....2222....",
"...111111...",
"..22222222..",
".1111111111."
]
},
{
name: "FORTRESS",
speed: 425,
palette: ["#fbbf24", "#fb7185", "#f472b6"],
rows: [
"222222222222",
"2..........2",
"2.11111111.2",
"2..........2",
"222222222222"
]
},
{
name: "CHECKER",
speed: 450,
palette: ["#34d399", "#22d3ee", "#818cf8"],
rows: [
"3.3.3.3.3.3.",
".2.2.2.2.2.2",
"1.1.1.1.1.1.",
".2.2.2.2.2.2",
"3.3.3.3.3.3."
]
},
{
name: "THE CORE",
speed: 475,
palette: ["#ff4d8d", "#a855f7", "#22d3ee"],
rows: [
"....3333....",
"..22222222..",
".3333333333.",
"111111111111"
]
}
];
const POWERS = {
wide: { color: "#42f5ff", icon: "W", name: "WIDE PADDLE" },
slow: { color: "#8292ff", icon: "S", name: "SLOW BALL" },
multi: { color: "#ff4fd8", icon: "×3", name: "MULTIBALL" },
life: { color: "#ffd166", icon: "+", name: "EXTRA LIFE" }
};
const clamp = (value, min, max) => Math.max(min, Math.min(max, value));
let dpr = 1;
let state = "ready";
let serveType = "start";
let levelIndex = 0;
let score = 0;
let lives = 3;
let best = loadBest();
let combo = 0;
let levelBonus = 0;
let bricks = [];
let balls = [];
let particles = [];
let powerups = [];
let floaters = [];
let wideTimer = 0;
let shake = 0;
let flash = 0;
let time = 0;
let bannerTimer = 0;
let muted = false;
const paddle = {
x: W / 2,
y: H - 43,
w: 122,
h: 14,
vx: 0
};
const keys = { left: false, right: false };
const pointer = { x: W / 2, active: false };
let audio = null;
let master = null;
let lastFrame = performance.now();
const stars = Array.from({ length: 90 }, () => ({
x: 18 + Math.random() * (W - 36),
y: TOP + Math.random() * (H - TOP),
size: .4 + Math.random() * 1.4,
phase: Math.random() * Math.PI * 2,
speed: .4 + Math.random() * 1.5
}));
function loadBest() {
try {
return Number(localStorage.getItem("neon-breakout-best")) || 0;
} catch (_) {
return 0;
}
}
function saveBest() {
try {
localStorage.setItem("neon-breakout-best", String(best));
} catch (_) {}
}
function resizeCanvas() {
dpr = Math.min(window.devicePixelRatio || 1, 2);
canvas.width = Math.round(W * dpr);
canvas.height = Math.round(H * dpr);
}
function makeBall(x, y, stuck = false) {
return {
x, y,
r: 7,
vx: 0,
vy: 0,
stuck,
dead: false,
trail: []
};
}
function resetBall() {
balls = [makeBall(paddle.x, paddle.y - 10, true)];
}
function createBricks(config) {
const result = [];
const cols = 12;
const gap = 6;
const boardX = 48;
const boardW = 864;
const brickW = (boardW - gap * (cols - 1)) / cols;
const brickH = 24;
config.rows.forEach((row, r) => {
for (let c = 0; c < cols; c++) {
const char = row[c] || ".";
const hp = Number(char);
if (!Number.isFinite(hp) || hp <= 0) continue;
result.push({
x: boardX + c * (brickW + gap),
y: 92 + r * 32,
w: brickW,
h: brickH,
row: r,
col: c,
hp,
maxHp: hp,
alive: true,
flash: 0,
color: config.palette[(r + c) % config.palette.length]
});
}
});
return result;
}
function loadLevel(index) {
levelIndex = index;
bricks = createBricks(levels[index]);
powerups = [];
floaters = [];
combo = 0;
wideTimer = 0;
paddle.x = W / 2;
paddle.w = 122;
paddle.vx = 0;
resetBall();
bannerTimer = 2.2;
}
function resetGame() {
if (score > 0) saveBest();
score = 0;
lives = 3;
combo = 0;
levelBonus = 0;
particles = [];
shake = 0;
flash = .25;
keys.left = false;
keys.right = false;
loadLevel(0);
state = "ready";
serveType = "start";
}
function addScore(points) {
score += points;
if (score > best) best = score;
}
function launchBall() {
if (state !== "ready") return;
const ball = balls[0];
if (!ball) return;
state = "playing";
const speed = levels[levelIndex].speed;
const angle = (Math.random() - .5) * .55;
ball.stuck = false;
ball.vx = Math.sin(angle) * speed;
ball.vy = -Math.cos(angle) * speed;
spawnBurst(ball.x, ball.y, "#55edff", 12, 135);
tone(240, .12, "triangle", .05, 1.8);
}
function completeLevel() {
if (state !== "playing") return;
levelBonus = 500 * (levelIndex + 1) + lives * 100;
addScore(levelBonus);
saveBest();
powerups = [];
shake = 10;
flash = .3;
if (levelIndex === levels.length - 1) {
state = "won";
sfxClear(true);
} else {
state = "levelclear";
sfxClear(false);
}
}
function nextLevel() {
if (levelIndex >= levels.length - 1) return;
loadLevel(levelIndex + 1);
state = "ready";
launchBall();
}
function loseLife() {
lives = Math.max(0, lives - 1);
combo = 0;
wideTimer = 0;
powerups = [];
bannerTimer = 0;
shake = 14;
flash = .5;
tone(220, .22, "sawtooth", .045, .45);
tone(130, .28, "triangle", .04, .55, .09);
if (lives === 0) {
state = "gameover";
balls = [];
saveBest();
} else {
resetBall();
state = "ready";
serveType = "lost";
}
}
function handleAction() {
initAudio();
if (state === "ready") {
launchBall();
} else if (state === "paused") {
state = "playing";
} else if (state === "levelclear") {
nextLevel();
} else if (state === "gameover" || state === "won") {
resetGame();
launchBall();
}
}
function togglePause() {
if (state === "playing") {
state = "paused";
} else if (state === "paused") {
state = "playing";
}
}
function initAudio() {
if (audio) {
if (audio.state === "suspended") audio.resume().catch(() => {});
return;
}
try {
const AudioContext = window.AudioContext || window.webkitAudioContext;
if (!AudioContext) return;
audio = new AudioContext();
master = audio.createGain();
master.gain.value = .5;
master.connect(audio.destination);
} catch (_) {}
}
function tone(freq, duration = .08, type = "sine", volume = .04, slide = 1, delay = 0) {
if (muted || !audio || !master) return;
try {
const start = audio.currentTime + delay;
const oscillator = audio.createOscillator();
const gain = audio.createGain();
oscillator.type = type;
oscillator.frequency.setValueAtTime(Math.max(30, freq), start);
if (slide !== 1) {
oscillator.frequency.exponentialRampToValueAtTime(
Math.max(30, freq * slide),
start + duration
);
}
gain.gain.setValueAtTime(.0001, start);
gain.gain.exponentialRampToValueAtTime(volume, start + .008);
gain.gain.exponentialRampToValueAtTime(.0001, start + duration);
oscillator.connect(gain);
gain.connect(master);
oscillator.start(start);
oscillator.stop(start + duration + .025);
} catch (_) {}
}
function sfxClear(won) {
const notes = won ? [523, 659, 784, 1047] : [440, 554, 659];
notes.forEach((note, i) => {
tone(note, .2, "triangle", .05, 1, i * .09);
});
}
function spawnBurst(x, y, color, count = 12, power = 170) {
for (let i = 0; i < count; i++) {
const angle = Math.random() * Math.PI * 2;
const speed = power * (.25 + Math.random() * .85);
const life = .3 + Math.random() * .45;
particles.push({
x, y,
vx: Math.cos(angle) * speed,
vy: Math.sin(angle) * speed,
gravity: 75,
life,
maxLife: life,
size: 1 + Math.random() * 2.2,
color
});
}
if (particles.length > 650) {
particles.splice(0, particles.length - 650);
}
}
function addFloater(x, y, text, color = "#ffffff") {
floaters.push({
x, y, text, color,
life: 1,
maxLife: 1
});
}
function comboMultiplier() {
return Math.min(5, 1 + Math.floor(combo / 5));
}
function setBallSpeed(ball, speed) {
const current = Math.hypot(ball.vx, ball.vy) || 1;
ball.vx = ball.vx / current * speed;
ball.vy = ball.vy / current * speed;
}
function tameBall(ball) {
const speed = Math.hypot(ball.vx, ball.vy);
if (speed === 0 || Math.abs(ball.vy) >= speed * .24) return;
const verticalDirection = ball.vy <= 0 ? -1 : 1;
const horizontalDirection = ball.vx < 0 ? -1 : 1;
ball.vy = verticalDirection * speed * .24;
ball.vx = horizontalDirection * Math.sqrt(speed * speed - ball.vy * ball.vy);
}
function damageBrick(brick, ball) {
brick.hp--;
brick.flash = 1;
if (brick.hp > 0) {
addScore(15 * brick.maxHp);
spawnBurst(ball.x, ball.y, brick.color, 5, 95);
shake = Math.min(8, shake + 1.3);
tone(190 + brick.hp * 45, .055, "square", .025, .8);
return;
}
brick.alive = false;
combo++;
const multiplier = comboMultiplier();
const points = 100 * brick.maxHp * multiplier;
addScore(points);
const cx = brick.x + brick.w / 2;
const cy = brick.y + brick.h / 2;
addFloater(cx, cy, `+${points}`, brick.color);
spawnBurst(cx, cy, brick.color, 15, 190);
shake = Math.min(11, shake + 3.2);
flash = Math.max(flash, .08);
tone(310 + Math.min(combo, 15) * 16, .075, "square", .032, 1.55);
if (combo > 0 && combo % 5 === 0) {
addFloater(cx, cy - 20, `COMBO ×${multiplier}`, "#ffffff");
tone(660, .12, "triangle", .04, 1.3, .04);
}
if (Math.random() < .15) {
spawnPowerup(cx, cy);
}
if (bricks.every(item => !item.alive)) {
completeLevel();
}
}
function spawnPowerup(x, y) {
const roll = Math.random();
const type =
roll < .34 ? "wide" :
roll < .58 ? "slow" :
roll < .92 ? "multi" : "life";
powerups.push({
x, y,
vy: 125,
type,
rot: Math.random() * Math.PI * 2,
dead: false
});
}
function applyPowerup(type) {
const info = POWERS[type];
let label = info.name;
if (type === "wide") {
wideTimer = 14;
} else if (type === "slow") {
balls.forEach(ball => {
const speed = Math.hypot(ball.vx, ball.vy);
if (speed > 0) setBallSpeed(ball, Math.max(290, speed * .8));
});
} else if (type === "multi") {
const source = balls.find(ball => !ball.dead);
if (source) {
const baseAngle = Math.atan2(source.vy, source.vx);
const speed = Math.hypot(source.vx, source.vy) || levels[levelIndex].speed;
[-.48, .48].forEach(offset => {
if (balls.length >= 6) return;
const ball = makeBall(source.x, source.y);
ball.vx = Math.cos(baseAngle + offset) * speed;
ball.vy = Math.sin(baseAngle + offset) * speed;
balls.push(ball);
});
}
} else if (type === "life") {
if (lives < 5) {
lives++;
} else {
addScore(500);
label = "LIFE MAX · +500";
}
}
addScore(50);
addFloater(paddle.x, paddle.y - 28, label, info.color);
spawnBurst(paddle.x, paddle.y, info.color, 18, 180);
shake = Math.min(9, shake + 2);
tone(440, .1, "triangle", .04, 1.15);
tone(660, .12, "triangle", .04, 1.1, .07);
tone(880, .14, "triangle", .035, 1.05, .14);
}
function updatePaddle(dt) {
const oldX = paddle.x;
const targetWidth = wideTimer > 0 ? 170 : 122;
paddle.w += (targetWidth - paddle.w) * Math.min(1, dt * 8);
const direction = (keys.right ? 1 : 0) - (keys.left ? 1 : 0);
if (direction !== 0) {
paddle.x += direction * 690 * dt;
} else if (pointer.active) {
paddle.x += (pointer.x - paddle.x) * Math.min(1, dt * 22);
}
paddle.x = clamp(
paddle.x,
paddle.w / 2 + 17,
W - paddle.w / 2 - 17
);
paddle.vx = dt > 0 ? (paddle.x - oldX) / dt : 0;
}
function updatePowerups(dt) {
for (const powerup of powerups) {
if (powerup.dead) continue;
powerup.y += powerup.vy * dt;
powerup.rot += dt * 3;
const caught =
powerup.y + 13 >= paddle.y &&
powerup.y - 13 <= paddle.y + paddle.h &&
Math.abs(powerup.x - paddle.x) <= paddle.w / 2 + 14;
if (caught) {
powerup.dead = true;
applyPowerup(powerup.type);
} else if (powerup.y > H + 30) {
powerup.dead = true;
}
}
powerups = powerups.filter(powerup => !powerup.dead);
}
function stepBall(ball, dt) {
const previousX = ball.x;
const previousY = ball.y;
ball.x += ball.vx * dt;
ball.y += ball.vy * dt;
let wallHit = false;
if (ball.x - ball.r < WALL_LEFT) {
ball.x = WALL_LEFT + ball.r;
ball.vx = Math.abs(ball.vx);
wallHit = true;
} else if (ball.x + ball.r > WALL_RIGHT) {
ball.x = WALL_RIGHT - ball.r;
ball.vx = -Math.abs(ball.vx);
wallHit = true;
}
if (ball.y - ball.r < TOP) {
ball.y = TOP + ball.r;
ball.vy = Math.abs(ball.vy);
wallHit = true;
}
if (wallHit) {
spawnBurst(ball.x, ball.y, "#7186ff", 3, 70);
tone(150, .035, "square", .014, 1.1);
}
const paddleHit =
ball.vy > 0 &&
ball.y + ball.r >= paddle.y &&
ball.y - ball.r <= paddle.y + paddle.h &&
ball.y < paddle.y + paddle.h + 4 &&
ball.x >= paddle.x - paddle.w / 2 - ball.r &&
ball.x <= paddle.x + paddle.w / 2 + ball.r;
if (paddleHit) {
ball.y = paddle.y - ball.r;
const english = clamp(paddle.vx / 1800, -.12, .12);
const offset = clamp(
(ball.x - paddle.x) / (paddle.w / 2) + english,
-1,
1
);
const angle = offset * 1.08;
const speed = clamp(
Math.hypot(ball.vx, ball.vy) * 1.008,
levels[levelIndex].speed,
640
);
ball.vx = Math.sin(angle) * speed;
ball.vy = -Math.cos(angle) * speed;
combo = 0;
spawnBurst(ball.x, paddle.y, "#55edff", 7, 105);
shake = Math.min(7, shake + 1.2);
tone(250 + Math.abs(offset) * 80, .065, "triangle", .032, 1.35);
return;
}
for (const brick of bricks) {
if (!brick.alive) continue;
const nearestX = clamp(ball.x, brick.x, brick.x + brick.w);
const nearestY = clamp(ball.y, brick.y, brick.y + brick.h);
const dx = ball.x - nearestX;
const dy = ball.y - nearestY;
if (dx * dx + dy * dy > ball.r * ball.r) continue;
if (previousY + ball.r <= brick.y + 1 && ball.vy > 0) {
ball.y = brick.y - ball.r;
ball.vy = -Math.abs(ball.vy);
} else if (previousY - ball.r >= brick.y + brick.h - 1 && ball.vy < 0) {
ball.y = brick.y + brick.h + ball.r;
ball.vy = Math.abs(ball.vy);
} else if (previousX + ball.r <= brick.x + 1 && ball.vx > 0) {
ball.x = brick.x - ball.r;
ball.vx = -Math.abs(ball.vx);
} else if (previousX - ball.r >= brick.x + brick.w - 1 && ball.vx < 0) {
ball.x = brick.x + brick.w + ball.r;
ball.vx = Math.abs(ball.vx);
} else {
const positions = [
{ side: "left", value: Math.abs(ball.x - (brick.x - ball.r)) },
{ side: "right", value: Math.abs(ball.x - (brick.x + brick.w + ball.r)) },
{ side: "top", value: Math.abs(ball.y - (brick.y - ball.r)) },
{ side: "bottom", value: Math.abs(ball.y - (brick.y + brick.h + ball.r)) }
].sort((a, b) => a.value - b.value);
const side = positions[0].side;
if (side === "left") {
ball.x = brick.x - ball.r;
ball.vx = -Math.abs(ball.vx);
} else if (side === "right") {
ball.x = brick.x + brick.w + ball.r;
ball.vx = Math.abs(ball.vx);
} else if (side === "top") {
ball.y = brick.y - ball.r;
ball.vy = -Math.abs(ball.vy);
} else {
ball.y = brick.y + brick.h + ball.r;
ball.vy = Math.abs(ball.vy);
}
}
damageBrick(brick, ball);
setBallSpeed(
ball,
Math.min(640, Math.hypot(ball.vx, ball.vy) + 2.5)
);
tameBall(ball);
return;
}
if (ball.y - ball.r > H + 25) {
ball.dead = true;
}
}
function updateBalls(dt) {
for (const ball of balls) {
if (state !== "playing") break;
if (ball.dead) continue;
if (ball.stuck) {
ball.x = paddle.x;
ball.y = paddle.y - ball.r - 3;
continue;
}
ball.trail.unshift({ x: ball.x, y: ball.y });
if (ball.trail.length > 15) ball.trail.pop();
const speed = Math.hypot(ball.vx, ball.vy);
const steps = clamp(Math.ceil(speed * dt / 5), 1, 10);
const stepDt = dt / steps;
for (let i = 0; i < steps; i++) {
if (state !== "playing" || ball.dead) break;
stepBall(ball, stepDt);
}
}
balls = balls.filter(ball => !ball.dead);
}
function updateEffects(dt) {
for (let i = particles.length - 1; i >= 0; i--) {
const particle = particles[i];
particle.life -= dt;
if (particle.life <= 0) {
particles.splice(i, 1);
continue;
}
particle.vy += particle.gravity * dt;
particle.x += particle.vx * dt;
particle.y += particle.vy * dt;
particle.vx *= Math.pow(.97, dt * 60);
particle.vy *= Math.pow(.985, dt * 60);
}
for (let i = floaters.length - 1; i >= 0; i--) {
const floater = floaters[i];
floater.life -= dt;
floater.y -= 30 * dt;
if (floater.life <= 0) floaters.splice(i, 1);
}
for (const brick of bricks) {
brick.flash = Math.max(0, brick.flash - dt * 5);
}
shake = Math.max(0, shake - dt * 25);
flash = Math.max(0, flash - dt * 1.8);
}
function update(dt) {
time += dt;
if (state !== "paused") {
updateEffects(dt);
}
if (state === "ready" || state === "playing") {
updatePaddle(dt);
}
if (state === "ready") {
for (const ball of balls) {
if (ball.stuck) {
ball.x = paddle.x;
ball.y = paddle.y - ball.r - 3;
}
}
}
if (state === "playing") {
wideTimer = Math.max(0, wideTimer - dt);
updatePowerups(dt);
updateBalls(dt);
if (state === "playing" && balls.length === 0) {
loseLife();
}
bannerTimer = Math.max(0, bannerTimer - dt);
}
}
function roundedPath(x, y, w, h, radius) {
const r = Math.min(radius, w / 2, h / 2);
ctx.beginPath();
ctx.moveTo(x + r, y);
ctx.lineTo(x + w - r, y);
ctx.quadraticCurveTo(x + w, y, x + w, y + r);
ctx.lineTo(x + w, y + h - r);
ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
ctx.lineTo(x + r, y + h);
ctx.quadraticCurveTo(x, y + h, x, y + h - r);
ctx.lineTo(x, y + r);
ctx.quadraticCurveTo(x, y, x + r, y);
ctx.closePath();
}
function drawText(text, x, y, size, color, align = "left", weight = 600, mono = true) {
ctx.save();
ctx.font = `${weight} ${size}px ${
mono
? "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace"
: "system-ui, -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif"
}`;
ctx.textAlign = align;
ctx.textBaseline = "middle";
ctx.fillStyle = color;
ctx.fillText(text, x, y);
ctx.restore();
}
function drawBackground() {
const gradient = ctx.createLinearGradient(0, 0, 0, H);
gradient.addColorStop(0, "#0d1028");
gradient.addColorStop(.55, "#090c20");
gradient.addColorStop(1, "#080917");
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, W, H);
const glow = ctx.createRadialGradient(
W * .5, H * .85, 0,
W * .5, H * .85, W * .68
);
glow.addColorStop(0, "rgba(73, 67, 190, .12)");
glow.addColorStop(1, "rgba(20, 20, 60, 0)");
ctx.fillStyle = glow;
ctx.fillRect(0, TOP, W, H - TOP);
ctx.save();
for (const star of stars) {
const alpha = .09 + (.5 + .5 * Math.sin(time * star.speed + star.phase)) * .23;
ctx.globalAlpha = alpha;
ctx.fillStyle = star.size > 1.2 ? "#8fdcff" : "#a7b5ef";
ctx.fillRect(star.x, star.y, star.size, star.size);
}
ctx.restore();
ctx.save();
ctx.strokeStyle = "rgba(109, 129, 205, .045)";
ctx.lineWidth = 1;
ctx.beginPath();
for (let x = 0; x <= W; x += 60) {
ctx.moveTo(x, TOP);
ctx.lineTo(x, H);
}
for (let y = TOP; y <= H; y += 40) {
ctx.moveTo(0, y);
ctx.lineTo(W, y);
}
ctx.stroke();
ctx.restore();
const danger = ctx.createLinearGradient(0, H - 90, 0, H);
danger.addColorStop(0, "rgba(255, 45, 110, 0)");
danger.addColorStop(1, "rgba(255, 45, 110, .065)");
ctx.fillStyle = danger;
ctx.fillRect(0, H - 90, W, 90);
}
function drawBounds() {
ctx.save();
ctx.strokeStyle = "rgba(91, 191, 255, .18)";
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(WALL_LEFT, H);
ctx.lineTo(WALL_LEFT, TOP);
ctx.lineTo(WALL_RIGHT, TOP);
ctx.lineTo(WALL_RIGHT, H);
ctx.stroke();
ctx.strokeStyle = "rgba(82, 232, 255, .45)";
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(WALL_LEFT, TOP + 20);
ctx.lineTo(WALL_LEFT, TOP);
ctx.lineTo(WALL_LEFT + 20, TOP);
ctx.moveTo(WALL_RIGHT - 20, TOP);
ctx.lineTo(WALL_RIGHT, TOP);
ctx.lineTo(WALL_RIGHT, TOP + 20);
ctx.stroke();
ctx.restore();
}
function drawBricks() {
for (const brick of bricks) {
if (!brick.alive) continue;
const ratio = brick.hp / brick.maxHp;
ctx.save();
roundedPath(brick.x, brick.y, brick.w, brick.h, 5);
ctx.shadowColor = brick.color;
ctx.shadowBlur = 9;
ctx.globalAlpha = .28 + ratio * .43;
ctx.fillStyle = brick.color;
ctx.fill();
ctx.shadowBlur = 0;
ctx.globalAlpha = .85;
ctx.strokeStyle = brick.color;
ctx.lineWidth = 1;
ctx.stroke();
ctx.globalAlpha = .26;
ctx.strokeStyle = "#ffffff";
ctx.beginPath();
ctx.moveTo(brick.x + 7, brick.y + 5);
ctx.lineTo(brick.x + brick.w - 7, brick.y + 5);
ctx.stroke();
if (brick.maxHp > 1) {
ctx.globalAlpha = .75;
ctx.fillStyle = "#ffffff";
for (let i = 0; i < brick.hp; i++) {
ctx.fillRect(
brick.x + brick.w - 8 - i * 6,
brick.y + brick.h - 6,
3,
2
);
}
}
if (brick.flash > 0) {
roundedPath(brick.x, brick.y, brick.w, brick.h, 5);
ctx.globalAlpha = brick.flash * .75;
ctx.fillStyle = "#ffffff";
ctx.fill();
}
ctx.restore();
}
}
function drawParticles() {
ctx.save();
ctx.globalCompositeOperation = "lighter";
ctx.lineCap = "round";
for (const particle of particles) {
const alpha = clamp(particle.life / particle.maxLife, 0, 1);
ctx.globalAlpha = alpha;
ctx.strokeStyle = particle.color;
ctx.lineWidth = particle.size;
ctx.beginPath();
ctx.moveTo(particle.x, particle.y);
ctx.lineTo(
particle.x - particle.vx * .024,
particle.y - particle.vy * .024
);
ctx.stroke();
}
ctx.restore();
}
function drawPowerups() {
for (const powerup of powerups) {
const info = POWERS[powerup.type];
ctx.save();
ctx.translate(powerup.x, powerup.y);
ctx.rotate(Math.sin(powerup.rot) * .16);
ctx.shadowColor = info.color;
ctx.shadowBlur = 18;
roundedPath(-14, -14, 28, 28, 8);
ctx.globalAlpha = .25;
ctx.fillStyle = info.color;
ctx.fill();
ctx.globalAlpha = 1;
ctx.strokeStyle = info.color;
ctx.lineWidth = 1.5;
ctx.stroke();
ctx.shadowBlur = 0;
drawText(
info.icon,
0,
1,
info.icon.length > 1 ? 11 : 15,
"#ffffff",
"center",
800
);
ctx.restore();
}
}
function drawPaddle() {
const x = paddle.x - paddle.w / 2;
const gradient = ctx.createLinearGradient(x, 0, x + paddle.w, 0);
gradient.addColorStop(0, "#35e7ff");
gradient.addColorStop(.52, "#8174ff");
gradient.addColorStop(1, "#ff50bf");
ctx.save();
ctx.shadowColor = wideTimer > 0 ? "#45f3ff" : "#756cff";
ctx.shadowBlur = wideTimer > 0 ? 24 : 16;
roundedPath(x, paddle.y, paddle.w, paddle.h, 7);
ctx.fillStyle = gradient;
ctx.fill();
ctx.shadowBlur = 0;
roundedPath(x + 4, paddle.y + 3, paddle.w - 8, 3, 2);
ctx.globalAlpha = .45;
ctx.fillStyle = "#ffffff";
ctx.fill();
ctx.globalAlpha = .9;
ctx.fillStyle = "#ffffff";
ctx.fillRect(paddle.x - 1, paddle.y + 3, 2, paddle.h - 6);
ctx.restore();
}
function drawBalls() {
for (const ball of balls) {
ctx.save();
ctx.globalCompositeOperation = "lighter";
for (let i = ball.trail.length - 1; i >= 0; i--) {
const point = ball.trail[i];
const strength = 1 - i / ball.trail.length;
ctx.globalAlpha = strength * .18;
ctx.fillStyle = i % 2 ? "#6b8cff" : "#43eaff";
ctx.beginPath();
ctx.arc(
point.x,
point.y,
ball.r * (.25 + strength * .55),
0,
Math.PI * 2
);
ctx.fill();
}
ctx.restore();
ctx.save();
ctx.shadowColor = "#54dcff";
ctx.shadowBlur = 20;
const gradient = ctx.createRadialGradient(
ball.x - 2, ball.y - 2, 1,
ball.x, ball.y, ball.r * 1.8
);
gradient.addColorStop(0, "#ffffff");
gradient.addColorStop(.4, "#d8fbff");
gradient.addColorStop(1, "#55bfff");
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();
for (const floater of floaters) {
const alpha = clamp(floater.life / floater.maxLife, 0, 1);
ctx.globalAlpha = Math.min(1, alpha * 2);
drawText(
floater.text,
floater.x,
floater.y,
12 + (1 - alpha) * 2,
floater.color,
"center",
800
);
}
ctx.restore();
}
function drawHUD() {
ctx.save();
ctx.fillStyle = "rgba(5, 7, 22, .74)";
ctx.fillRect(0, 0, W, TOP);
ctx.strokeStyle = "rgba(112, 137, 218, .18)";
ctx.beginPath();
ctx.moveTo(0, TOP - .5);
ctx.lineTo(W, TOP - .5);
ctx.stroke();
ctx.restore();
drawText("SCORE", 24, 17, 9, "#7481aa", "left", 700);
drawText(String(score).padStart(6, "0"), 24, 39, 20, "#edf6ff", "left", 800);
drawText("BEST", 190, 17, 9, "#7481aa", "left", 700);
drawText(String(best).padStart(6, "0"), 190, 39, 15, "#9aa8d2", "left", 700);
drawText(
`LEVEL ${levelIndex + 1} / ${levels.length}`,
W / 2, 17, 9, "#7481aa", "center", 700
);
drawText(
levels[levelIndex].name,
W / 2, 39, 15, "#b9c8ff", "center", 800
);
drawText("LIVES", 716, 39, 9, "#7481aa", "left", 700);
ctx.save();
for (let i = 0; i < lives; i++) {
const x = 780 + i * 22;
ctx.shadowColor = "#ff5ca8";
ctx.shadowBlur = 9;
ctx.fillStyle = "#ff70b4";
ctx.beginPath();
ctx.arc(x, 39, 4.5, 0, Math.PI * 2);
ctx.fill();
}
ctx.restore();
drawText(
muted ? "M · SOUND OFF" : "M · SOUND ON",
W - 24, 17, 8,
muted ? "#626b8b" : "#66b9c9",
"right",
700
);
if (wideTimer > 0 && state === "playing") {
drawText(
`WIDE ${wideTimer.toFixed(1)}s`,
24, H - 17, 10, "#54e9ff", "left", 700
);
}
if (combo >= 2 && state === "playing") {
drawText(
`${combo} HIT COMBO · ×${comboMultiplier()}`,
W - 24, H - 17, 10, "#ff88ca", "right", 800
);
}
}
function drawBanner() {
if (state !== "playing" || bannerTimer <= 0) return;
const elapsed = 2.2 - bannerTimer;
const alpha = clamp(Math.min(elapsed * 3.5, bannerTimer * 1.4), 0, 1);
ctx.save();
ctx.globalAlpha = alpha * .75;
drawText(
`LEVEL ${levelIndex + 1}`,
W / 2, 466, 10, "#7f8fb9", "center", 700
);
drawText(
levels[levelIndex].name,
W / 2, 491, 25, "#c6d2ff", "center", 900, false
);
ctx.restore();
}
function drawOverlay() {
if (state === "playing") return;
let kicker = "";
let title = "";
let line1 = "";
let line2 = "";
let action = "";
if (state === "ready" && serveType === "start") {
kicker = "NO COIN REQUIRED";
title = "NEON BREAKOUT";
line1 = "Clear five levels. Catch power-ups. Chase the high score.";
line2 = "Move with mouse, touch, ← →, or A / D";
action = "CLICK OR PRESS SPACE TO LAUNCH";
} else if (state === "ready") {
kicker = `LEVEL ${levelIndex + 1} · ${levels[levelIndex].name}`;
title = "BALL LOST";
line1 = `${lives} ${lives === 1 ? "life" : "lives"} remaining`;
line2 = "The next ball is ready when you are.";
action = "CLICK OR PRESS SPACE TO RELAUNCH";
} else if (state === "paused") {
kicker = "TAKE A BREATH";
title = "PAUSED";
line1 = "Your run is safe.";
line2 = "Press P or Escape to jump back in.";
action = "CLICK OR PRESS SPACE TO RESUME";
} else if (state === "levelclear") {
kicker = `LEVEL ${levelIndex + 1} COMPLETE`;
title = "NICE WORK";
line1 = `Clear bonus +${levelBonus} · Score ${score}`;
line2 = `Next up: ${levels[levelIndex + 1].name}`;
action = "CLICK OR PRESS SPACE FOR NEXT LEVEL";
} else if (state === "gameover") {
kicker = "THE GRID WINS THIS ROUND";
title = "GAME OVER";
line1 = `Score ${score} · Best ${best}`;
line2 = `You reached level ${levelIndex + 1}: ${levels[levelIndex].name}`;
action = "CLICK OR PRESS SPACE TO TRY AGAIN";
} else if (state === "won") {
kicker = "ALL FIVE LEVELS CLEARED";
title = "GRID CLEARED!";
line1 = `Final score ${score} · Best ${best}`;
line2 = "You are officially a brick-breaking menace.";
action = "CLICK OR PRESS SPACE TO PLAY AGAIN";
} else {
return;
}
ctx.save();
ctx.fillStyle = "rgba(4, 5, 18, .69)";
ctx.fillRect(0, TOP, W, H - TOP);
const panelX = W / 2 - 300;
const panelY = 196;
const panelW = 600;
const panelH = 242;
ctx.shadowColor = "rgba(75, 100, 255, .35)";
ctx.shadowBlur = 35;
roundedPath(panelX, panelY, panelW, panelH, 18);
ctx.fillStyle = "rgba(10, 13, 36, .94)";
ctx.fill();
ctx.shadowBlur = 0;
ctx.strokeStyle = "rgba(119, 150, 255, .3)";
ctx.lineWidth = 1;
ctx.stroke();
ctx.restore();
drawText(kicker, W / 2, 224, 9, "#6ed8e8", "center", 800);
drawText(title, W / 2, 272, 41, "#f2f5ff", "center", 900, false);
drawText(line1, W / 2, 318, 13, "#b1bddc", "center", 500);
drawText(line2, W / 2, 342, 11, "#7785ac", "center", 500);
const pulse = .65 + Math.sin(time * 4) * .15;
ctx.save();
ctx.globalAlpha = pulse;
roundedPath(W / 2 - 210, 365, 420, 42, 10);
ctx.fillStyle = "rgba(56, 221, 255, .10)";
ctx.fill();
ctx.strokeStyle = "rgba(73, 226, 255, .62)";
ctx.stroke();
ctx.restore();
drawText(action, W / 2, 386, 11, "#a9f4ff", "center", 800);
}
function render() {
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
ctx.clearRect(0, 0, W, H);
drawBackground();
ctx.save();
if (shake > 0) {
ctx.translate(
(Math.random() * 2 - 1) * shake,
(Math.random() * 2 - 1) * shake
);
}
drawBounds();
drawBricks();
drawParticles();
drawPowerups();
drawPaddle();
drawBalls();
drawFloaters();
ctx.restore();
drawHUD();
drawBanner();
drawOverlay();
if (flash > 0) {
ctx.fillStyle = `rgba(190, 225, 255, ${flash * .16})`;
ctx.fillRect(0, 0, W, H);
}
}
function frame(now) {
const dt = Math.min(.033, Math.max(0, (now - lastFrame) / 1000));
lastFrame = now;
update(dt);
render();
requestAnimationFrame(frame);
}
function pointerXFromEvent(event) {
const rect = canvas.getBoundingClientRect();
return clamp((event.clientX - rect.left) / rect.width * W, 0, W);
}
canvas.addEventListener("pointermove", event => {
pointer.x = pointerXFromEvent(event);
pointer.active = true;
});
canvas.addEventListener("pointerdown", event => {
event.preventDefault();
canvas.focus({ preventScroll: true });
pointer.x = pointerXFromEvent(event);
pointer.active = true;
if (state === "ready") {
paddle.x = clamp(
pointer.x,
paddle.w / 2 + 17,
W - paddle.w / 2 - 17
);
for (const ball of balls) {
if (ball.stuck) {
ball.x = paddle.x;
ball.y = paddle.y - ball.r - 3;
}
}
}
handleAction();
});
canvas.addEventListener("contextmenu", event => event.preventDefault());
window.addEventListener("keydown", event => {
if (event.ctrlKey || event.metaKey || event.altKey) return;
const controlledKeys = [
"ArrowLeft", "ArrowRight", "Space",
"KeyA", "KeyD", "KeyP", "KeyM", "KeyR",
"Escape", "Enter"
];
if (controlledKeys.includes(event.code)) {
event.preventDefault();
}
if (event.code === "ArrowLeft" || event.code === "KeyA") {
keys.left = true;
pointer.active = false;
}
if (event.code === "ArrowRight" || event.code === "KeyD") {
keys.right = true;
pointer.active = false;
}
if (event.repeat) return;
if (event.code === "Space" || event.code === "Enter") {
handleAction();
} else if (event.code === "KeyP" || event.code === "Escape") {
togglePause();
} else if (event.code === "KeyM") {
muted = !muted;
if (!muted) {
initAudio();
tone(660, .09, "triangle", .04, 1.25);
}
} else if (event.code === "KeyR") {
resetGame();
}
});
window.addEventListener("keyup", event => {
if (event.code === "ArrowLeft" || event.code === "KeyA") {
keys.left = false;
}
if (event.code === "ArrowRight" || event.code === "KeyD") {
keys.right = false;
}
});
document.addEventListener("visibilitychange", () => {
if (document.hidden && state === "playing") {
state = "paused";
}
});
window.addEventListener("blur", () => {
if (state === "playing") state = "paused";
});
window.addEventListener("resize", resizeCanvas);
resizeCanvas();
resetGame();
requestAnimationFrame(frame);
})();
</script>
</body>
</html>

