hypeorcode
Network offOpen alone (new tab)

Run details

Model
deepseek/deepseek-v4.1-flash-1017
Route
OpenRouter API, one user message
Reasoning
provider default, 14,992 reasoning tokens
Limits
60 min · 393,216 output tokens
Date
21 Oct 2026
Time
69 s
Size
27 kB
Tokens
106 in, 25,220 out
List price
$0.015 ($0.15 in / $0.6 out per M)
Paid
$0.030

Raw files: output · meta.json

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 973 lines Raw

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<title>Neon Breakout</title>
<style>
  html, body {
    margin: 0; padding: 0; height: 100%; overflow: hidden;
    background: #05070d;
    font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  }
  #wrap {
    position: fixed; inset: 0;
    display: flex; align-items: center; justify-content: center;
    background: radial-gradient(circle at 50% 35%, #101a2e 0%, #05070d 72%);
  }
  canvas {
    width: min(100vw, calc(100vh * 900 / 620));
    height: min(100vh, calc(100vw * 620 / 900));
    display: block;
    border-radius: 14px;
    box-shadow: 0 0 70px rgba(80,180,255,.16), 0 0 0 1px rgba(120,180,255,.12);
    touch-action: none;
    cursor: crosshair;
  }
</style>
</head>
<body>
<div id="wrap"><canvas id="c"></canvas></div>
<script>
(() => {
'use strict';

// ---------------------------------------------------------------- setup
const W = 900, H = 620;
const canvas = document.getElementById('c');
const ctx = canvas.getContext('2d');

function resize() {
  const dpr = Math.min(2, window.devicePixelRatio || 1);
  canvas.width = Math.round(W * dpr);
  canvas.height = Math.round(H * dpr);
  ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
}
window.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);

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();
}

function circleRect(cx, cy, r, rx, ry, rw, rh) {
  const nx = clamp(cx, rx, rx + rw);
  const ny = clamp(cy, ry, ry + rh);
  const dx = cx - nx, dy = cy - ny;
  return dx * dx + dy * dy <= r * r;
}

// ---------------------------------------------------------------- audio
let actx = null;
function initAudio() {
  if (!actx) {
    try { actx = new (window.AudioContext || window.webkitAudioContext)(); } catch (e) { actx = null; }
  }
  if (actx && actx.state === 'suspended') actx.resume();
}
function tone(freq, dur, type, vol, slideTo) {
  if (!actx || game.muted) return;
  const t0 = actx.currentTime;
  const o = actx.createOscillator();
  const g = actx.createGain();
  o.type = type || 'square';
  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 || 0.05, t0 + 0.006);
  g.gain.exponentialRampToValueAtTime(0.0001, t0 + dur);
  o.connect(g); g.connect(actx.destination);
  o.start(t0); o.stop(t0 + dur + 0.03);
}
function chord(freqs, dur, vol) {
  freqs.forEach((f, i) => setTimeout(() => tone(f, dur, 'triangle', vol || 0.05), i * 55));
}

// ---------------------------------------------------------------- levels
const COLORS = {
  A: '#ff4d6d', B: '#ff8c42', C: '#ffd93d', D: '#4ade80',
  E: '#38bdf8', F: '#a78bfa', G: '#f472b6'
};

const LEVELS = [
  [ // 1 - warmup
    "cccccccccccc",
    "bbbbbbbbbbbb",
    "aaaaaaaaaaaa"
  ],
  [ // 2 - pyramid
    "....dddd....",
    "...dddddd...",
    "..dddddddd..",
    ".dddddddddd.",
    "dddddddddddd"
  ],
  [ // 3 - checker
    "a.a.a.a.a.a.",
    "b.b.b.b.b.b.",
    "c.c.c.c.c.c.",
    "d.d.d.d.d.d.",
    "e.e.e.e.e.e."
  ],
  [ // 4 - diamond
    ".....ee.....",
    "....dddd....",
    "...cccccc...",
    "..bbbbbbbb..",
    "...cccccc...",
    "....dddd...."
  ],
  [ // 5 - fortress (uppercase = armored, 2 hits)
    "gggggggggggg",
    "gFggggggggFg",
    "gggggggggggg",
    "ggFFggggFFgg",
    "gggggggggggg"
  ]
];

// ---------------------------------------------------------------- state
const game = {
  state: 'menu',
  level: 0,
  score: 0,
  lives: 3,
  best: 0,
  combo: 0,
  shake: 0,
  flash: 0,
  stateTimer: 0,
  time: 0,
  muted: false
};

const paddle = {
  x: W / 2, y: H - 56, w: 130, baseW: 130, h: 16,
  targetX: W / 2, squash: 0, wideTimer: 0
};

let balls = [];
let bricks = [];
let particles = [];
let popups = [];
let powerups = [];
let speedFactor = 1;
const keys = { left: false, right: false };

const PU_TYPES = [
  { type: 'wide',  color: '#38bdf8', letter: 'W', weight: 30 },
  { type: 'multi', color: '#f472b6', letter: 'M', weight: 30 },
  { type: 'slow',  color: '#a78bfa', letter: 'S', weight: 25 },
  { type: 'life',  color: '#4ade80', letter: '+', weight: 15 }
];

// ---------------------------------------------------------------- level build
const MARGIN_X = 40, TOP_Y = 96, CELL_H = 28;

function buildLevel(idx) {
  bricks = [];
  const rows = LEVELS[idx];
  const cols = rows[0].length;
  const cellW = (W - MARGIN_X * 2) / cols;
  for (let r = 0; r < rows.length; r++) {
    for (let c = 0; c < cols; c++) {
      const ch = rows[r][c];
      if (ch === '.') continue;
      const strong = ch === ch.toUpperCase();
      const color = COLORS[ch.toUpperCase()] || '#94a3b8';
      bricks.push({
        x: MARGIN_X + c * cellW + 3,
        y: TOP_Y + r * CELL_H + 3,
        w: cellW - 6,
        h: CELL_H - 6,
        color,
        hp: strong ? 2 : 1,
        maxHp: strong ? 2 : 1,
        points: strong ? 100 : 50,
        alive: true,
        flash: 0
      });
    }
  }
}

// ---------------------------------------------------------------- ball
function makeBall(x, y, stuck) {
  return { x, y, r: 8, vx: 0, vy: 0, stuck: !!stuck, trail: [], dead: false };
}

function baseSpeed() { return 330 + game.level * 24; }

function resetBall() {
  balls = [makeBall(paddle.x, paddle.y - paddle.h / 2 - 12, true)];
  speedFactor = 1;
}

function setSpeedFactor(f) {
  const ratio = f / speedFactor;
  for (const b of balls) { b.vx *= ratio; b.vy *= ratio; }
  speedFactor = f;
}

function launch() {
  let did = false;
  for (const b of balls) {
    if (b.stuck) {
      b.stuck = false;
      const a = -Math.PI / 2 + rand(-0.28, 0.28);
      const sp = baseSpeed() * speedFactor;
      b.vx = Math.cos(a) * sp;
      b.vy = Math.sin(a) * sp;
      did = true;
    }
  }
  if (did) tone(660, 0.09, 'square', 0.05, 1100);
  return did;
}

function fixStuck(b) {
  const sp = Math.hypot(b.vx, b.vy);
  if (sp < 1) return;
  const minVy = sp * 0.24;
  if (Math.abs(b.vy) < minVy) {
    b.vy = (b.vy < 0 ? -1 : 1) * minVy;
    b.vx = (b.vx < 0 ? -1 : 1) * Math.sqrt(Math.max(0, sp * sp - b.vy * b.vy));
  }
}

// ---------------------------------------------------------------- fx
function shakeAdd(v) { game.shake = Math.min(26, game.shake + v); }

function spawnParticles(x, y, color, n, power) {
  if (particles.length > 500) return;
  for (let i = 0; i < n; i++) {
    const a = Math.random() * Math.PI * 2;
    const sp = rand(40, 250) * power;
    const life = rand(0.35, 0.9);
    particles.push({
      x, y,
      vx: Math.cos(a) * sp,
      vy: Math.sin(a) * sp - 40,
      s: rand(2, 6) * (0.6 + power * 0.6),
      color,
      rot: Math.random() * Math.PI,
      vr: rand(-10, 10),
      life, max: life
    });
  }
}

function popup(x, y, text, color) {
  popups.push({ x, y, text, color, life: 0.85, max: 0.85 });
}

// ---------------------------------------------------------------- score
function comboMult() { return Math.min(5, 1 + Math.floor(game.combo / 4)); }

// ---------------------------------------------------------------- bricks
function damageBrick(br) {
  br.hp--;
  br.flash = 1;
  const cx = br.x + br.w / 2, cy = br.y + br.h / 2;
  if (br.hp <= 0) {
    br.alive = false;
    game.combo++;
    const mult = comboMult();
    const pts = br.points * mult;
    game.score += pts;
    spawnParticles(cx, cy, br.color, 14, 1);
    popup(cx, cy, '+' + pts, mult > 1 ? '#ffd93d' : '#ffffff');
    shakeAdd(3.5);
    tone(420 + Math.random() * 90 + game.combo * 12, 0.07, 'square', 0.045);
    maybeDropPowerup(br);
  } else {
    spawnParticles(cx, cy, br.color, 5, 0.5);
    tone(190, 0.05, 'sawtooth', 0.03);
  }
}

function maybeDropPowerup(br) {
  if (Math.random() > 0.16) return;
  let total = 0;
  for (const p of PU_TYPES) total += p.weight;
  let roll = Math.random() * total;
  let pick = PU_TYPES[0];
  for (const p of PU_TYPES) { roll -= p.weight; if (roll <= 0) { pick = p; break; } }
  powerups.push({
    x: br.x + br.w / 2, y: br.y + br.h / 2,
    w: 30, h: 18, vy: 135,
    type: pick.type, color: pick.color, letter: pick.letter, spin: 0
  });
}

// ---------------------------------------------------------------- collision
function hitPaddle(b) {
  const rel = clamp((b.x - paddle.x) / (paddle.w / 2), -1, 1);
  let speed = Math.hypot(b.vx, b.vy);
  speed = Math.max(speed, baseSpeed() * 0.85);
  const angle = rel * 1.02;
  b.vx = Math.sin(angle) * speed;
  b.vy = -Math.cos(angle) * speed;
  b.y = paddle.y - paddle.h / 2 - b.r - 0.5;
  paddle.squash = 1;
  shakeAdd(1.6);
  tone(210 + rel * 70, 0.055, 'square', 0.045);
  spawnParticles(b.x, b.y, '#8be9fd', 5, 0.45);
}

function stepBall(b, dt) {
  b.x += b.vx * dt;
  b.y += b.vy * dt;

  if (b.x - b.r < 0) { b.x = b.r; b.vx = Math.abs(b.vx); tone(300, 0.03, 'sine', 0.025); }
  else if (b.x + b.r > W) { b.x = W - b.r; b.vx = -Math.abs(b.vx); tone(300, 0.03, 'sine', 0.025); }
  if (b.y - b.r < 0) { b.y = b.r; b.vy = Math.abs(b.vy); tone(300, 0.03, 'sine', 0.025); }
  if (b.y - b.r > H) { b.dead = true; return; }

  // paddle
  const px = paddle.x - paddle.w / 2;
  const py = paddle.y - paddle.h / 2;
  if (b.vy > 0 && circleRect(b.x, b.y, b.r, px, py, paddle.w, paddle.h)) {
    hitPaddle(b);
  }

  // bricks
  for (let i = 0; i < bricks.length; i++) {
    const br = bricks[i];
    if (!br.alive) continue;
    if (circleRect(b.x, b.y, b.r, br.x, br.y, br.w, br.h)) {
      const bcx = br.x + br.w / 2, bcy = br.y + br.h / 2;
      const ox = (b.r + br.w / 2) - Math.abs(b.x - bcx);
      const oy = (b.r + br.h / 2) - Math.abs(b.y - bcy);
      const sx = b.x < bcx ? -1 : 1;
      const sy = b.y < bcy ? -1 : 1;
      if (ox < oy) { b.vx = sx * Math.abs(b.vx); b.x += sx * ox; }
      else { b.vy = sy * Math.abs(b.vy); b.y += sy * oy; }
      damageBrick(br);
      break;
    }
  }

  fixStuck(b);
}

function updateBalls(dt) {
  for (let i = balls.length - 1; i >= 0; i--) {
    const b = balls[i];
    if (b.stuck) {
      b.x = paddle.x;
      b.y = paddle.y - paddle.h / 2 - b.r - 2;
      b.trail.length = 0;
      continue;
    }
    const speed = Math.hypot(b.vx, b.vy);
    const steps = Math.max(1, Math.ceil((speed * dt) / 5));
    for (let s = 0; s < steps; s++) {
      stepBall(b, dt / steps);
      if (b.dead) break;
    }
    if (b.dead) { balls.splice(i, 1); continue; }
    b.trail.push({ x: b.x, y: b.y });
    if (b.trail.length > 10) b.trail.shift();
  }
  if (balls.length === 0) loseLife();
}

// ---------------------------------------------------------------- life / level flow
function loseLife() {
  game.lives--;
  game.combo = 0;
  shakeAdd(16);
  game.flash = 0.55;
  tone(180, 0.35, 'sawtooth', 0.08, 55);
  setPaddleWidth(paddle.baseW);
  paddle.wideTimer = 0;
  speedFactor = 1;
  if (game.lives <= 0) {
    game.state = 'gameover';
    game.stateTimer = 0.9;
    game.best = Math.max(game.best, game.score);
    chord([330, 262, 196], 0.25, 0.05);
  } else {
    resetBall();
  }
}

function levelClear() {
  game.state = 'levelclear';
  game.stateTimer = 1.9;
  game.flash = 1;
  game.score += 250;
  shakeAdd(10);
  chord([523, 659, 784, 1046], 0.16, 0.05);
  for (const br of bricks) {
    if (br.alive) spawnParticles(br.x + br.w / 2, br.y + br.h / 2, br.color, 6, 1);
  }
}

function nextLevel() {
  if (game.level + 1 >= LEVELS.length) {
    game.state = 'win';
    game.stateTimer = 0.9;
    game.best = Math.max(game.best, game.score);
    chord([523, 659, 784, 1046, 1318], 0.2, 0.06);
  } else {
    loadLevel(game.level + 1);
  }
}

function loadLevel(i) {
  game.level = i;
  buildLevel(i);
  powerups = [];
  game.combo = 0;
  setPaddleWidth(paddle.baseW);
  paddle.wideTimer = 0;
  resetBall();
  game.state = 'playing';
  game.stateTimer = 0;
}

function startGame() {
  game.score = 0;
  game.lives = 3;
  game.combo = 0;
  game.flash = 0;
  game.shake = 0;
  particles = [];
  popups = [];
  powerups = [];
  loadLevel(0);
}

// ---------------------------------------------------------------- paddle
function setPaddleWidth(w) {
  paddle.w = w;
  paddle.targetX = clamp(paddle.targetX, w / 2, W - w / 2);
  paddle.x = clamp(paddle.x, w / 2, W - w / 2);
}

function updatePaddle(dt) {
  const sp = 680;
  if (keys.left) paddle.targetX -= sp * dt;
  if (keys.right) paddle.targetX += sp * dt;
  paddle.targetX = clamp(paddle.targetX, paddle.w / 2, W - paddle.w / 2);
  paddle.x += (paddle.targetX - paddle.x) * Math.min(1, dt * 30);
  paddle.x = clamp(paddle.x, paddle.w / 2, W - paddle.w / 2);
}

// ---------------------------------------------------------------- powerups
function updatePowerups(dt) {
  for (let i = powerups.length - 1; i >= 0; i--) {
    const p = powerups[i];
    p.y += p.vy * dt;
    p.spin += dt * 4;
    if (p.y - p.h / 2 > H) { powerups.splice(i, 1); continue; }
    const px = paddle.x - paddle.w / 2;
    const py = paddle.y - paddle.h / 2;
    if (p.y + p.h / 2 > py && p.y - p.h / 2 < py + paddle.h &&
        p.x + p.w / 2 > px && p.x - p.w / 2 < px + paddle.w) {
      applyPowerup(p);
      powerups.splice(i, 1);
    }
  }
}

function applyPowerup(p) {
  spawnParticles(p.x, p.y, p.color, 16, 1);
  shakeAdd(5);
  tone(880, 0.12, 'triangle', 0.06, 1400);
  popup(p.x, p.y - 10, p.type.toUpperCase(), p.color);
  if (p.type === 'wide') {
    setPaddleWidth(paddle.baseW * 1.55);
    paddle.wideTimer = 10;
  } else if (p.type === 'slow') {
    setSpeedFactor(0.68);
    paddle.slowTimer = 8;
  } else if (p.type === 'multi') {
    const src = balls.filter(b => !b.stuck);
    const use = src.length ? src : balls;
    const candidates = use.slice(0, 2);
    for (const b of candidates) {
      if (balls.length >= 7) break;
      for (const dir of [-0.45, 0.45]) {
        if (balls.length >= 7) break;
        const sp = Math.hypot(b.vx, b.vy) || baseSpeed();
        const ang = Math.atan2(b.vy, b.vx) + dir;
        const nb = makeBall(b.x, b.y, false);
        nb.vx = Math.cos(ang) * sp;
        nb.vy = Math.sin(ang) * sp;
        balls.push(nb);
      }
    }
  } else if (p.type === 'life') {
    game.lives = Math.min(9, game.lives + 1);
  }
}

function updatePowerTimers(dt) {
  if (paddle.wideTimer > 0) {
    paddle.wideTimer -= dt;
    if (paddle.wideTimer <= 0) { setPaddleWidth(paddle.baseW); paddle.wideTimer = 0; }
  }
  if (paddle.slowTimer > 0) {
    paddle.slowTimer -= dt;
    if (paddle.slowTimer <= 0) { setSpeedFactor(1); paddle.slowTimer = 0; }
  }
}

// ---------------------------------------------------------------- particles & popups
function updateParticles(dt) {
  for (let i = particles.length - 1; i >= 0; i--) {
    const p = particles[i];
    p.life -= dt;
    if (p.life <= 0) { particles.splice(i, 1); continue; }
    p.vy += 720 * dt;
    p.vx *= (1 - 1.7 * dt);
    p.x += p.vx * dt;
    p.y += p.vy * dt;
    p.rot += p.vr * dt;
  }
}

function updatePopups(dt) {
  for (let i = popups.length - 1; i >= 0; i--) {
    const p = popups[i];
    p.life -= dt;
    p.y -= 42 * dt;
    if (p.life <= 0) popups.splice(i, 1);
  }
}

// ---------------------------------------------------------------- update
function update(dt) {
  game.time += dt;
  game.shake = Math.max(0, game.shake - dt * 42);
  game.flash = Math.max(0, game.flash - dt * 3.2);
  paddle.squash = Math.max(0, paddle.squash - dt * 5);
  for (const br of bricks) if (br.flash > 0) br.flash = Math.max(0, br.flash - dt * 6);

  updateParticles(dt);
  updatePopups(dt);

  if (game.state === 'playing') {
    updatePaddle(dt);
    updateBalls(dt);
    updatePowerups(dt);
    updatePowerTimers(dt);
    if (!bricks.some(b => b.alive)) levelClear();
  } else if (game.state === 'levelclear') {
    game.stateTimer -= dt;
    if (game.stateTimer <= 0) nextLevel();
  } else if (game.state === 'gameover' || game.state === 'win') {
    game.stateTimer -= dt;
  } else if (game.state === 'paused') {
    // frozen
  }
}

// ---------------------------------------------------------------- drawing
function drawBackground() {
  const g = ctx.createLinearGradient(0, 0, 0, H);
  g.addColorStop(0, '#0a1020');
  g.addColorStop(1, '#05070d');
  ctx.fillStyle = g;
  ctx.fillRect(0, 0, W, H);

  ctx.strokeStyle = 'rgba(80,140,255,0.055)';
  ctx.lineWidth = 1;
  const gs = 45;
  const off = (game.time * 12) % gs;
  for (let x = -gs + off; x < W + gs; x += gs) {
    ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, H); ctx.stroke();
  }
  for (let y = 0; y < H; y += gs) {
    ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(W, y); ctx.stroke();
  }

  const rg = ctx.createRadialGradient(W / 2, 0, 0, W / 2, 0, 520);
  rg.addColorStop(0, 'rgba(60,130,255,0.13)');
  rg.addColorStop(1, 'rgba(0,0,0,0)');
  ctx.fillStyle = rg;
  ctx.fillRect(0, 0, W, H);
}

function drawBricks() {
  for (const br of bricks) {
    if (!br.alive) continue;
    ctx.fillStyle = br.color;
    roundRect(ctx, br.x, br.y, br.w, br.h, 5);
    ctx.fill();

    ctx.fillStyle = 'rgba(255,255,255,0.26)';
    roundRect(ctx, br.x + 2.5, br.y + 2, br.w - 5, br.h * 0.34, 3);
    ctx.fill();

    ctx.fillStyle = 'rgba(0,0,0,0.18)';
    roundRect(ctx, br.x + 2.5, br.y + br.h - 5.5, br.w - 5, 3.5, 2);
    ctx.fill();

    if (br.maxHp > 1) {
      ctx.strokeStyle = br.hp > 1 ? 'rgba(0,0,0,0.45)' : 'rgba(255,255,255,0.55)';
      ctx.lineWidth = 2.5;
      roundRect(ctx, br.x + 4, br.y + 4, br.w - 8, br.h - 8, 3);
      ctx.stroke();
    }

    if (br.flash > 0) {
      ctx.fillStyle = 'rgba(255,255,255,' + (br.flash * 0.85) + ')';
      roundRect(ctx, br.x, br.y, br.w, br.h, 5);
      ctx.fill();
    }
  }
}

function drawPaddle() {
  const sq = paddle.squash;
  const h = paddle.h * (1 - sq * 0.35);
  const y = paddle.y - h / 2 + sq * 2;
  const x = paddle.x - paddle.w / 2;
  const wide = paddle.wideTimer > 0;

  ctx.save();
  ctx.shadowColor = wide ? '#38bdf8' : '#7dd3fc';
  ctx.shadowBlur = 22 + sq * 26;

  const g = ctx.createLinearGradient(0, y, 0, y + h);
  g.addColorStop(0, wide ? '#bae6fd' : '#e0f2fe');
  g.addColorStop(0.55, wide ? '#38bdf8' : '#7dd3fc');
  g.addColorStop(1, wide ? '#0369a1' : '#0284c7');
  ctx.fillStyle = g;
  roundRect(ctx, x, y, paddle.w, h, h / 2);
  ctx.fill();
  ctx.restore();

  ctx.fillStyle = 'rgba(255,255,255,0.55)';
  roundRect(ctx, x + 8, y + 2.5, paddle.w - 16, h * 0.3, h * 0.15);
  ctx.fill();
}

function drawBalls() {
  for (const b of balls) {
    for (let i = 0; i < b.trail.length; i++) {
      const t = b.trail[i];
      const a = (i / b.trail.length) * 0.32;
      ctx.fillStyle = 'rgba(140,225,255,' + a + ')';
      ctx.beginPath();
      ctx.arc(t.x, t.y, b.r * (0.35 + 0.65 * (i / b.trail.length)), 0, Math.PI * 2);
      ctx.fill();
    }

    ctx.save();
    ctx.shadowColor = '#6ee7ff';
    ctx.shadowBlur = 20;
    const rg = ctx.createRadialGradient(b.x - b.r * 0.3, b.y - b.r * 0.3, 1, b.x, b.y, b.r);
    rg.addColorStop(0, '#ffffff');
    rg.addColorStop(1, '#7fd8ff');
    ctx.fillStyle = rg;
    ctx.beginPath();
    ctx.arc(b.x, b.y, b.r, 0, Math.PI * 2);
    ctx.fill();
    ctx.restore();
  }
}

function drawPowerups() {
  for (const p of powerups) {
    ctx.save();
    ctx.translate(p.x, p.y);
    const sc = 1 + Math.sin(p.spin) * 0.08;
    ctx.scale(sc, sc);
    ctx.shadowColor = p.color;
    ctx.shadowBlur = 16;
    ctx.fillStyle = p.color;
    roundRect(ctx, -p.w / 2, -p.h / 2, p.w, p.h, 6);
    ctx.fill();
    ctx.shadowBlur = 0;
    ctx.fillStyle = 'rgba(0,0,0,0.62)';
    ctx.font = 'bold 14px ui-monospace, monospace';
    ctx.textAlign = 'center';
    ctx.textBaseline = 'middle';
    ctx.fillText(p.letter, 0, 1);
    ctx.restore();
  }
  ctx.textBaseline = 'alphabetic';
}

function drawParticles() {
  for (const p of particles) {
    const a = clamp(p.life / p.max, 0, 1);
    ctx.globalAlpha = a;
    ctx.fillStyle = p.color;
    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 drawPopups() {
  ctx.textAlign = 'center';
  ctx.font = 'bold 17px ui-monospace, monospace';
  for (const p of popups) {
    ctx.globalAlpha = clamp(p.life / p.max, 0, 1);
    ctx.fillStyle = p.color;
    ctx.fillText(p.text, p.x, p.y);
  }
  ctx.globalAlpha = 1;
}

function drawHUD() {
  ctx.textBaseline = 'alphabetic';
  ctx.font = 'bold 20px ui-monospace, monospace';
  ctx.textAlign = 'left';
  ctx.fillStyle = '#cbd5e1';
  ctx.fillText('SCORE', 26, 34);
  ctx.fillStyle = '#7dd3fc';
  ctx.fillText(String(game.score).padStart(5, '0'), 106, 34);

  ctx.textAlign = 'center';
  ctx.fillStyle = '#94a3b8';
  ctx.font = 'bold 18px ui-monospace, monospace';
  ctx.fillText('LEVEL ' + (game.level + 1) + ' / ' + LEVELS.length, W / 2, 34);

  // lives
  ctx.textAlign = 'right';
  ctx.fillStyle = '#94a3b8';
  ctx.font = 'bold 16px ui-monospace, monospace';
  ctx.fillText('LIVES', W - 26 - game.lives * 26, 32);
  for (let i = 0; i < game.lives; i++) {
    const x = W - 26 - i * 26;
    ctx.fillStyle = '#7dd3fc';
    ctx.shadowColor = '#38bdf8';
    ctx.shadowBlur = 10;
    roundRect(ctx, x - 18, 22, 18, 7, 3.5);
    ctx.fill();
    ctx.shadowBlur = 0;
  }

  // combo
  if (game.combo >= 4 && (game.state === 'playing' || game.state === 'paused')) {
    const mult = comboMult();
    const pulse = 1 + Math.sin(game.time * 10) * 0.06;
    ctx.save();
    ctx.translate(W / 2, 62);
    ctx.scale(pulse, pulse);
    ctx.textAlign = 'center';
    ctx.font = 'bold 16px ui-monospace, monospace';
    ctx.fillStyle = mult >= 4 ? '#ffd93d' : '#f472b6';
    ctx.fillText('COMBO x' + mult, 0, 0);
    ctx.restore();
  }

  if (game.muted) {
    ctx.textAlign = 'left';
    ctx.font = 'bold 13px ui-monospace, monospace';
    ctx.fillStyle = '#64748b';
    ctx.fillText('MUTED', 26, 58);
  }
}

function centerText(text, y, size, color, glow) {
  ctx.textAlign = 'center';
  ctx.font = 'bold ' + size + 'px ui-monospace, monospace';
  if (glow) { ctx.shadowColor = glow; ctx.shadowBlur = 24; }
  ctx.fillStyle = color;
  ctx.fillText(text, W / 2, y);
  ctx.shadowBlur = 0;
}

function drawOverlays() {
  if (game.state === 'playing') return;

  ctx.fillStyle = 'rgba(4,8,16,0.74)';
  ctx.fillRect(0, 0, W, H);

  if (game.state === 'menu') {
    const p = 0.86 + Math.sin(game.time * 3) * 0.14;
    ctx.save();
    ctx.shadowColor = '#38bdf8';
    ctx.shadowBlur = 34 * p;
    centerText('NEON', 208, 80, '#7dd3fc');
    ctx.shadowColor = '#f472b6';
    centerText('BREAKOUT', 288, 80, '#f472b6');
    ctx.restore();

    centerText('CLICK  or  PRESS SPACE  TO START', 400, 22,
      'rgba(226,232,240,' + (0.6 + Math.sin(game.time * 4) * 0.4) + ')');

    ctx.font = 'bold 16px ui-monospace, monospace';
    ctx.textAlign = 'center';
    ctx.fillStyle = '#64748b';
    ctx.fillText('MOVE: mouse  /  \u2190 \u2192  or  A D', W / 2, 470);
    ctx.fillText('LAUNCH: space or click      PAUSE: P      MUTE: M', W / 2, 496);
    ctx.fillStyle = '#475569';
    ctx.fillText('Break every brick. Grab the falling power-ups. Chain bricks for combo multipliers.', W / 2, 540);
    return;
  }

  if (game.state === 'paused') {
    centerText('PAUSED', 290, 60, '#7dd3fc', '#38bdf8');
    centerText('press P to resume', 340, 20, '#94a3b8');
    return;
  }

  if (game.state === 'levelclear') {
    centerText('LEVEL CLEAR', 270, 58, '#4ade80', '#22c55e');
    centerText('+250 BONUS', 320, 26, '#ffd93d');
    return;
  }

  if (game.state === 'gameover') {
    centerText('GAME OVER', 262, 66, '#ff4d6d', '#ff4d6d');
    centerText('SCORE  ' + game.score, 330, 30, '#e2e8f0');
    centerText('BEST   ' + game.best, 368, 22, '#94a3b8');
    if (game.stateTimer <= 0) {
      centerText('press SPACE or click to play again', 470, 20,
        'rgba(226,232,240,' + (0.55 + Math.sin(game.time * 4) * 0.45) + ')');
    }
    return;
  }

  if (game.state === 'win') {
    const p = 0.9 + Math.sin(game.time * 5) * 0.1;
    ctx.save();
    ctx.shadowColor = '#ffd93d';
    ctx.shadowBlur = 30 * p;
    centerText('YOU WIN!', 260, 72, '#ffd93d');
    ctx.restore();
    centerText('ALL ' + LEVELS.length + ' LEVELS CLEARED', 320, 26, '#4ade80');
    centerText('SCORE  ' + game.score, 380, 30, '#e2e8f0');
    if (game.stateTimer <= 0) {
      centerText('press SPACE or click to play again', 470, 20,
        'rgba(226,232,240,' + (0.55 + Math.sin(game.time * 4) * 0.45) + ')');
    }
    return;
  }
}

function render() {
  ctx.save();
  if (game.shake > 0.15) {
    ctx.translate((Math.random() - 0.5) * game.shake, (Math.random() - 0.5) * game.shake);
  }
  drawBackground();
  drawBricks();
  drawPowerups();
  drawPaddle();
  drawBalls();
  drawParticles();
  drawPopups();
  ctx.restore();

  if (game.flash > 0.01) {
    ctx.fillStyle = 'rgba(255,255,255,' + (game.flash * 0.35) + ')';
    ctx.fillRect(0, 0, W, H);
  }

  drawHUD();
  drawOverlays();
}

// ---------------------------------------------------------------- input
function primaryAction() {
  initAudio();
  if (game.state === 'menu') {
    startGame();
  } else if (game.state === 'playing') {
    launch();
  } else if (game.state === 'paused') {
    game.state = 'playing';
  } else if ((game.state === 'gameover' || game.state === 'win') && game.stateTimer <= 0) {
    startGame();
  }
}

function togglePause() {
  if (game.state === 'playing') game.state = 'paused';
  else if (game.state === 'paused') game.state = 'playing';
}

window.addEventListener('keydown', (e) => {
  const k = e.key;
  if (k === 'ArrowLeft' || k === 'a' || k === 'A') { keys.left = true; e.preventDefault(); }
  else if (k === 'ArrowRight' || k === 'd' || k === 'D') { keys.right = true; e.preventDefault(); }
  else if (k === ' ' || k === 'Spacebar') { e.preventDefault(); primaryAction(); }
  else if (k === 'p' || k === 'P' || k === 'Escape') { e.preventDefault(); togglePause(); }
  else if (k === 'm' || k === 'M') { game.muted = !game.muted; }
  initAudio();
});

window.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 pointerX(clientX) {
  const r = canvas.getBoundingClientRect();
  return clamp(((clientX - r.left) / r.width) * W, 0, W);
}

canvas.addEventListener('mousemove', (e) => {
  paddle.targetX = clamp(pointerX(e.clientX), paddle.w / 2, W - paddle.w / 2);
});

canvas.addEventListener('mousedown', (e) => {
  e.preventDefault();
  paddle.targetX = clamp(pointerX(e.clientX), paddle.w / 2, W - paddle.w / 2);
  primaryAction();
});

canvas.addEventListener('touchstart', (e) => {
  e.preventDefault();
  initAudio();
  const t = e.touches[0];
  if (t) paddle.targetX = clamp(pointerX(t.clientX), paddle.w / 2, W - paddle.w / 2);
  primaryAction();
}, { passive: false });

canvas.addEventListener('touchmove', (e) => {
  e.preventDefault();
  const t = e.touches[0];
  if (t) paddle.targetX = clamp(pointerX(t.clientX), paddle.w / 2, W - paddle.w / 2);
}, { passive: false });

// ---------------------------------------------------------------- loop
let last = performance.now();
function frame(now) {
  let dt = (now - last) / 1000;
  last = now;
  if (dt > 1 / 25) dt = 1 / 25;
  if (dt < 0) dt = 0;
  update(dt);
  render();
  requestAnimationFrame(frame);
}

// initial idle scene
buildLevel(0);
resetBall();
requestAnimationFrame(frame);

})();
</script>
</body>
</html>

Compare with

DeepSeek V4.1 Flash 1017: other attempts