hypeorcode
Network offOpen alone (new tab)

Run details

Model
stepfun/step-5-preview
Route
OpenRouter API, one user message
Reasoning
provider default, 15,621 reasoning tokens
Limits
60 min · 64,000 output tokens
Date
21 Oct 2026
Time
1 min 55 s
Size
26 kB
Tokens
87 in, 25,139 out
List price
$0.068 ($1 in / $2.7 out per M)
Paid
$0.068

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

26 kB 1,008 lines Raw

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Neon Breakout</title>
<style>
  * { box-sizing: border-box; }
  html, body { margin: 0; padding: 0; background: #03030c; }
  body {
    font-family: "Courier New", Courier, monospace;
    color: #dff6fa;
    min-height: 100vh;
    display: flex;
    justify-content: center;
    background:
      radial-gradient(circle at 20% 0%, rgba(0,232,255,.12), transparent 55%),
      radial-gradient(circle at 85% 100%, rgba(255,43,214,.12), transparent 50%),
      #03030c;
    user-select: none;
  }

  #wrap { width: 100%; max-width: 860px; padding: 14px; }
  h1 {
    margin: 4px 0 12px;
    text-align: center;
    font-size: clamp(20px, 4vw, 34px);
    letter-spacing: 5px;
    color: #00e8ff;
    text-shadow: 0 0 10px #00e8ff, 0 0 26px rgba(0,232,255,.45);
  }

  #hud {
    display: flex;
    gap: 8px;
    margin-bottom: 10px;
    font-weight: bold;
    text-align: center;
  }
  #hud div {
    flex: 1;
    border: 1px solid rgba(0,232,255,.28);
    background: rgba(10,10,28,.7);
    border-radius: 8px;
    padding: 8px 4px;
    font-size: clamp(11px, 2vw, 15px);
    letter-spacing: 1px;
    text-shadow: 0 0 8px currentColor;
    box-shadow: inset 0 0 12px rgba(0,232,255,.08);
  }

  #canvasBox { position: relative; width: 100%; }
  canvas {
    display: block;
    width: 100%;
    height: auto;
    aspect-ratio: 4 / 3;
    background: #02020a;
    border: 2px solid rgba(0,232,255,.35);
    border-radius: 10px;
    box-shadow: 0 0 32px rgba(0,232,255,.16), inset 0 0 28px rgba(0,0,0,.7);
  }

  #overlay {
    position: absolute;
    inset: 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    text-align: center;
    padding: 18px;
    background: rgba(2,2,14,.84);
    border-radius: 10px;
    transition: opacity .22s ease;
    z-index: 2;
  }
  #overlay.hidden { opacity: 0; pointer-events: none; }
  #overlay h2 {
    margin: 0 0 12px;
    font-size: clamp(24px, 5vw, 44px);
    letter-spacing: 3px;
    color: #ff2bd6;
    text-shadow: 0 0 12px #ff2bd6, 0 0 28px rgba(255,43,214,.45);
  }
  #overlay p {
    margin: 0 0 20px;
    max-width: 560px;
    line-height: 1.6;
    color: #bfe4ec;
    font-size: clamp(12px, 2.2vw, 15px);
  }

  button {
    font-family: inherit;
    font-weight: bold;
    letter-spacing: 2px;
    font-size: 18px;
    color: #001018;
    padding: 12px 26px;
    border: none;
    border-radius: 8px;
    cursor: pointer;
    background: linear-gradient(180deg, #00e8ff, #00a2b8);
    box-shadow: 0 0 18px rgba(0,232,255,.55);
    transition: transform .12s ease, box-shadow .12s ease;
  }
  button:hover { transform: translateY(-2px); box-shadow: 0 0 28px rgba(0,232,255,.85); }
  button:active { transform: translateY(1px); }

  .controls {
    text-align: center;
    color: #7f9aa3;
    margin: 12px 0 0;
    font-size: 12px;
    line-height: 1.5;
  }
  .key {
    display: inline-block;
    border: 1px solid rgba(0,232,255,.35);
    border-radius: 4px;
    padding: 1px 6px;
    margin: 0 2px;
    color: #c8f6ff;
    box-shadow: inset 0 0 8px rgba(0,232,255,.12);
  }
</style>
</head>
<body>
<div id="wrap">
  <h1>NEON BREAKOUT</h1>
  <div id="hud">
    <div>SCORE <span id="scoreEl">0</span></div>
    <div>LIVES <span id="livesEl">3</span></div>
    <div>LEVEL <span id="levelEl">1</span></div>
    <div>COMBO <span id="comboEl">x1</span></div>
  </div>
  <div id="canvasBox">
    <canvas id="game" width="800" height="600"></canvas>
    <div id="overlay">
      <h2 id="ovTitle">READY PLAYER ONE</h2>
      <p id="ovText">Break every bright brick. Catch power-ups. Chain bricks for combo score.<br>Use the mouse or arrow keys. Click or press SPACE to launch.</p>
      <button id="startBtn">START</button>
    </div>
  </div>
  <p class="controls">
    <span class="key">←</span><span class="key">→</span> or <span class="key">A</span><span class="key">D</span> move &nbsp;•&nbsp;
    Mouse Move also moves &nbsp;•&nbsp; <span class="key">Click</span>/<span class="key">SPACE</span> launch &nbsp;•&nbsp;
    <span class="key">P</span> pause &nbsp;•&nbsp; <span class="key">M</span> mute
  </p>
</div>

<script>
(() => {
'use strict';

const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
const W = canvas.width;
const H = canvas.height;

const scoreEl = document.getElementById('scoreEl');
const livesEl = document.getElementById('livesEl');
const levelEl = document.getElementById('levelEl');
const comboEl = document.getElementById('comboEl');
const overlay = document.getElementById('overlay');
const ovTitle = document.getElementById('ovTitle');
const ovText = document.getElementById('ovText');
const startBtn = document.getElementById('startBtn');

const BALL_RADIUS = 7;
const PADDLE_BASE = 108;
const PADDLE_H = 16;
const PADDLE_Y = H - 56;
const BRICK_TOP = 82;
const BRICK_ROWS_PAD = 32;
const BRICK_GAP_X = 4;
const BRICK_GAP_Y = 6;
const COLS = 12;
const BRICK_W = (W - BRICK_ROWS_PAD * 2 - (COLS - 1) * BRICK_GAP_X) / COLS;
const BRICK_H = 22;

const LEVELS = [
  [
    "111111111111",
    "111111111111",
    "111111111111"
  ],
  [
    "111111111111",
    "101111111101",
    "100111111001",
    "100111111001",
    "101111111101",
    "111111111111"
  ],
  [
    "222222222222",
    "111111111111",
    "333333333333"
  ],
  [
    "000011000000",
    "000111100000",
    "001111110000",
    "011111111000",
    "111111111111",
    "011111111000",
    "001111110000",
    "000111100000",
    "000011000000"
  ],
  [
    "333333333333",
    "300000000003",
    "300444444003",
    "300000000003",
    "222222222222"
  ],
  [
    "121212121212",
    "212121212121",
    "131313131313",
    "313131313131"
  ],
  [
    "111111100000",
    "000001111110",
    "000000011111",
    "111110000000",
    "001111111111",
    "110000000000",
    "111111111000",
    "000000111111"
  ],
  [
    "444444444444",
    "441111111144",
    "442222222244",
    "442000002244",
    "442233223344",
    "442233223344",
    "442000002244",
    "442222222244",
    "441111111144"
  ]
];

const BRICK_STYLE = {
  1: { fill: "#00e8ff", stroke: "#008a99", value: 100 },
  2: { fill: "#ff2bd6", stroke: "#8b0075", value: 200 },
  3: { fill: "#ffe234", stroke: "#8c7800", value: 300 },
  4: { fill: "#6b7a95", stroke: "#2f3a4a", value: 0 }
};

let state = "menu"; // menu | serve | playing | paused | gameover | victory
let prevState = "playing";
let bricks = [];
let balls = [];
let powerups = [];
let particles = [];
let floaters = [];
let stars = [];

let score = 0;
let lives = 3;
let levelIndex = 0;
let combo = 1;
let mute = false;
let useMouse = false;
let mouseX = W / 2;
let shake = 0;
let flashRed = 0;

const keys = { left: false, right: false };
const effects = { expand: 0, slow: 0 };
const paddle = { x: W / 2, y: PADDLE_Y, w: PADDLE_BASE, h: PADDLE_H, flash: 0 };

/* ------------------------- audio ------------------------- */
let audioCtx = null;

function initAudio() {
  if (audioCtx) {
    if (audioCtx.state === "suspended") audioCtx.resume();
    return;
  }
  try {
    const AC = window.AudioContext || window.webkitAudioContext;
    if (AC) audioCtx = new AC();
  } catch (e) { audioCtx = null; }
}

function playTone(freq, dur = 0.07, type = "square", vol = 0.08, slideTo = null) {
  if (!audioCtx || mute) return;
  const t = audioCtx.currentTime;
  const osc = audioCtx.createOscillator();
  const gain = audioCtx.createGain();
  osc.type = type;
  osc.frequency.setValueAtTime(freq, t);
  if (slideTo) osc.frequency.exponentialRampToValueAtTime(Math.max(20, slideTo), t + dur);
  gain.gain.setValueAtTime(vol, t);
  gain.gain.exponentialRampToValueAtTime(0.0001, t + dur);
  osc.connect(gain);
  gain.connect(audioCtx.destination);
  osc.start(t);
  osc.stop(t + dur + 0.02);
}

const sfx = {
  paddle: () => playTone(210, 0.07, "square", 0.09, 150),
  wall: () => playTone(120, 0.05, "square", 0.05),
  brick: (hp) => playTone(280 + hp * 90, 0.07, "square", 0.08, 340 + hp * 90),
  unbreakable: () => playTone(90, 0.08, "triangle", 0.06, 60),
  power: () => playTone(520, 0.16, "triangle", 0.1, 900),
  lose: () => playTone(380, 0.45, "sawtooth", 0.12, 70),
  level: () => playTone(660, 0.18, "triangle", 0.1, 990)
};

/* ------------------------- helpers ------------------------- */
function clamp(v, a, b) { return Math.max(a, Math.min(b, v)); }
function rand(a, b) { return a + Math.random() * (b - a); }

function roundRect(x, y, w, h, r) {
  r = Math.min(r, w / 2, h / 2);
  ctx.beginPath();
  ctx.moveTo(x + r, y);
  ctx.arcTo(x + w, y, x + w, y + h, r);
  ctx.arcTo(x + w, y + h, x, y + h, r);
  ctx.arcTo(x, y + h, x, y, r);
  ctx.arcTo(x, y, x + w, y, r);
  ctx.closePath();
}

function updateHUD() {
  scoreEl.textContent = score;
  livesEl.textContent = Math.max(0, lives);
  levelEl.textContent = levelIndex + 1;
  comboEl.textContent = "x" + combo;
}

function showOverlay(title, html, btnLabel) {
  ovTitle.textContent = title;
  ovText.innerHTML = html;
  startBtn.textContent = btnLabel;
  overlay.classList.remove("hidden");
}
function hideOverlay() { overlay.classList.add("hidden"); }

/* ------------------------- game setup ------------------------- */
function loadLevel(i) {
  bricks = [];
  const rows = LEVELS[i];
  for (let r = 0; r < rows.length; r++) {
    let row = rows[r];
    while (row.length < COLS) row += "0";
    for (let c = 0; c < COLS; c++) {
      const ch = row[c];
      if (ch === "0") continue;
      const type = parseInt(ch, 10);
      const style = BRICK_STYLE[type] || BRICK_STYLE[1];
      bricks.push({
        x: BRICK_ROWS_PAD + c * (BRICK_W + BRICK_GAP_X),
        y: BRICK_TOP + r * (BRICK_H + BRICK_GAP_Y),
        w: BRICK_W,
        h: BRICK_H,
        type,
        maxHp: type === 4 ? 999 : type,
        hp: type === 4 ? 999 : type,
        flash: 0,
        dead: false
      });
    }
  }
}

function makeBall() {
  return {
    x: paddle.x,
    y: paddle.y - BALL_RADIUS - 2,
    vx: 0,
    vy: 0,
    r: BALL_RADIUS,
    speed: 340,
    stuck: true,
    trail: []
  };
}

function newServe() {
  powerups = [];
  effects.expand = 0;
  effects.slow = 0;
  combo = 1;
  paddle.w = PADDLE_BASE;
  paddle.x = W / 2;
  balls = [makeBall()];
  state = "serve";
  hideOverlay();
  updateHUD();
}

function startGame() {
  initAudio();
  score = 0;
  lives = 3;
  levelIndex = 0;
  particles = [];
  floaters = [];
  shake = 0;
  flashRed = 0;
  loadLevel(0);
  updateHUD();
  newServe();
}

function nextLevel() {
  const bonus = 250 + levelIndex * 120 + Math.max(0, lives) * 60;
  score += bonus;
  addFloater(W / 2, H / 2, "LEVEL CLEAR  +" + bonus, "#00e8ff", 1.6);
  sfx.level();
  levelIndex++;
  updateHUD();

  if (levelIndex >= LEVELS.length) {
    state = "victory";
    showOverlay("YOU WIN!", "Final Score: " + score + "<br>Press SPACE or click START to play again.", "PLAY AGAIN");
  } else {
    loadLevel(levelIndex);
    newServe();
  }
}

function launchBall(b) {
  if (!b.stuck) return;
  b.stuck = false;
  b.speed = 340 + Math.min(levelIndex * 16, 90);
  b.vx = rand(-150, 150);
  b.vy = -Math.sqrt(Math.max(50, b.speed * b.speed - b.vx * b.vx));
  playTone(420, 0.08, "square", 0.09, 640);
}

function launchAll() {
  let launched = false;
  for (const b of balls) {
    if (b.stuck) { launchBall(b); launched = true; }
  }
  if (launched) state = "playing";
}

function togglePause() {
  if (state === "playing" || state === "serve") {
    prevState = state;
    state = "paused";
    showOverlay("PAUSED", "Take a breath. The bricks will wait.<br>Press P, SPACE, or click RESUME.", "RESUME");
  } else if (state === "paused") {
    state = prevState;
    hideOverlay();
  }
}

/* ------------------------- fx ------------------------- */
function spawnParticles(x, y, color, count = 10, power = 160) {
  for (let i = 0; i < count; i++) {
    const a = Math.random() * Math.PI * 2;
    const s = rand(power * 0.25, power);
    particles.push({
      x, y,
      vx: Math.cos(a) * s,
      vy: Math.sin(a) * s,
      life: rand(0.3, 0.7),
      maxLife: 0.7,
      size: rand(2, 4),
      color
    });
  }
}

function addFloater(x, y, text, color = "#fff", scale = 1) {
  floaters.push({ x, y, text, color, scale, life: 1.0, vy: -34 });
}

function spawnPowerup(x, y) {
  if (Math.random() > 0.15) return;
  const types = ["EXP", "MULT", "SLOW", "LIFE"];
  const t = types[Math.floor(Math.random() * types.length)];
  powerups.push({ x: x - 9, y, w: 18, h: 12, type: t, vy: 165, flash: 0 });
}

function activatePower(p) {
  sfx.power();
  if (p.type === "EXP") {
    effects.expand = 14;
    addFloater(paddle.x, paddle.y - 30, "WIDE PADDLE", "#00e8ff");
  } else if (p.type === "SLOW") {
    effects.slow = 12;
    addFloater(paddle.x, paddle.y - 30, "SLOW BALL", "#ffe234");
  } else if (p.type === "LIFE") {
    lives = Math.min(9, lives + 1);
    addFloater(paddle.x, paddle.y - 30, "1UP", "#ff2bd6");
  } else if (p.type === "MULT") {
    const src = balls[Math.floor(Math.random() * balls.length)];
    if (src) {
      const count = Math.max(0, 4 - balls.length);
      for (let i = 0; i < count; i++) {
        const nb = {
          x: src.x, y: src.y, r: BALL_RADIUS,
          speed: src.speed, stuck: false, trail: []
        };
        const ang = rand(-Math.PI / 3, -Math.PI / 6);
        nb.vx = Math.sin(ang) * src.speed * (i % 2 ? 1 : -1) + src.vx * 0.2;
        nb.vy = Math.cos(ang) * src.speed;
        balls.push(nb);
      }
      spawnParticles(src.x, src.y, "#ff2bd6", 18, 220);
    }
    addFloater(paddle.x, paddle.y - 30, "MULTI BALL", "#ff2bd6");
  }
  updateHUD();
}

/* ------------------------- collisions ------------------------- */
function paddleBounce(b) {
  const rel = clamp((b.x - paddle.x) / (paddle.w / 2), -1, 1);
  const angle = rel * (Math.PI / 3);
  const speed = Math.min(b.speed + 6, 680);
  b.speed = speed;
  b.vx = Math.sin(angle) * speed;
  b.vy = -Math.cos(angle) * speed;
  b.y = paddle.y - b.r - 0.1;
  paddle.flash = 1;
  combo = 1;
  shake = Math.min(shake + 3, 12);
  sfx.paddle();
  spawnParticles(b.x, paddle.y, "#00e8ff", 6, 120);
}

function damageBrick(brick) {
  brick.flash = 1;
  const style = BRICK_STYLE[brick.type] || BRICK_STYLE[1];

  if (brick.type === 4) {
    sfx.unbreakable();
    spawnParticles(brick.x + brick.w / 2, brick.y + brick.h / 2, "#7f8fa8", 7, 120);
    shake = Math.min(shake + 2, 10);
    return;
  }

  brick.hp--;
  sfx.brick(brick.hp);
  shake = Math.min(shake + 2.2, 14);

  if (brick.hp <= 0) {
    brick.dead = true;
    combo = Math.min(combo + 1, 99);
    const points = style.value * combo;
    score += points;
    updateHUD();
    spawnParticles(brick.x + brick.w / 2, brick.y + brick.h / 2, style.fill, 16, 230);
    addFloater(brick.x + brick.w / 2, brick.y + brick.h / 2, "+" + points, style.fill, 0.95);
    spawnPowerup(brick.x + brick.w / 2, brick.y + brick.h / 2);
    flashRed = Math.min(flashRed + 0.08, 0.35);
  } else {
    spawnParticles(brick.x + brick.w / 2, brick.y + brick.h / 2, style.fill, 5, 110);
  }
}

function collideBricks(b) {
  for (const brick of bricks) {
    if (brick.dead) continue;

    const nx = clamp(b.x, brick.x, brick.x + brick.w);
    const ny = clamp(b.y, brick.y, brick.y + brick.h);
    const dx = b.x - nx;
    const dy = b.y - ny;
    if (dx * dx + dy * dy > b.r * b.r) continue;

    const fromLeft = b.x + b.r - brick.x;
    const fromRight = brick.x + brick.w - (b.x - b.r);
    const fromTop = b.y + b.r - brick.y;
    const fromBottom = brick.y + brick.h - (b.y - b.r);
    const min = Math.min(fromLeft, fromRight, fromTop, fromBottom);

    if (min === fromLeft) {
      b.x = brick.x - b.r - 0.01;
      b.vx = -Math.abs(b.vx);
    } else if (min === fromRight) {
      b.x = brick.x + brick.w + b.r + 0.01;
      b.vx = Math.abs(b.vx);
    } else if (min === fromTop) {
      b.y = brick.y - b.r - 0.01;
      b.vy = -Math.abs(b.vy);
    } else {
      b.y = brick.y + brick.h + b.r + 0.01;
      b.vy = Math.abs(b.vy);
    }

    damageBrick(brick);
    return true;
  }
  return false;
}

/* ------------------------- update ------------------------- */
function updateStars(dt) {
  for (const s of stars) {
    s.y += (8 + s.z * 34) * dt;
    if (s.y > H) { s.y = -2; s.x = Math.random() * W; }
  }
}

function updateFx(dt) {
  for (const p of particles) {
    p.life -= dt;
    p.x += p.vx * dt;
    p.y += p.vy * dt;
    p.vy += 320 * dt;
    p.vx *= 0.99;
  }
  particles = particles.filter(p => p.life > 0);

  for (const f of floaters) {
    f.life -= dt * 0.85;
    f.y += f.vy * dt;
  }
  floaters = floaters.filter(f => f.life > 0);

  paddle.flash = Math.max(0, paddle.flash - dt * 4);
  for (const b of bricks) b.flash = Math.max(0, b.flash - dt * 4.5);
}

function moveBall(b, dt) {
  const bdt = dt * (effects.slow > 0 ? 0.62 : 1);
  const dx = b.vx * bdt;
  const dy = b.vy * bdt;
  const dist = Math.hypot(dx, dy);
  const steps = Math.max(1, Math.ceil(dist / (BALL_RADIUS * 0.65)));
  const sx = dx / steps;
  const sy = dy / steps;

  for (let i = 0; i < steps && !b.dead; i++) {
    b.x += sx;
    b.y += sy;

    if (b.x - b.r < 0) { b.x = b.r; b.vx = Math.abs(b.vx); sfx.wall(); }
    else if (b.x + b.r > W) { b.x = W - b.r; b.vx = -Math.abs(b.vx); sfx.wall(); }
    if (b.y - b.r < 0) { b.y = b.r; b.vy = Math.abs(b.vy); sfx.wall(); }

    if (
      b.vy > 0 &&
      b.y + b.r >= paddle.y &&
      b.y - b.r <= paddle.y + paddle.h &&
      b.x >= paddle.x - paddle.w / 2 - b.r &&
      b.x <= paddle.x + paddle.w / 2 + b.r
    ) {
      paddleBounce(b);
    }

    collideBricks(b);

    if (b.y - b.r > H) { b.dead = true; }
  }

  b.trail.push({ x: b.x, y: b.y });
  if (b.trail.length > 13) b.trail.shift();
}

function update(dt) {
  if (state === "paused") return;

  updateStars(dt);
  updateFx(dt);

  shake = Math.max(0, shake - dt * 34);
  flashRed = Math.max(0, flashRed - dt * 1.1);

  if (state !== "playing" && state !== "serve") return;

  effects.expand = Math.max(0, effects.expand - dt);
  effects.slow = Math.max(0, effects.slow - dt);

  const targetW = PADDLE_BASE + (effects.expand > 0 ? 52 : 0);
  paddle.w += (targetW - paddle.w) * Math.min(1, dt * 9);

  const dir = (keys.right ? 1 : 0) - (keys.left ? 1 : 0);
  if (useMouse) {
    paddle.x += (mouseX - paddle.x) * Math.min(1, dt * 18);
  } else if (dir !== 0) {
    paddle.x += dir * 820 * dt;
  }
  paddle.x = clamp(paddle.x, paddle.w / 2, W - paddle.w / 2);

  for (const b of balls) {
    if (b.stuck) {
      b.x = paddle.x;
      b.y = paddle.y - b.r - 2;
      b.trail.length = 0;
    } else {
      moveBall(b, dt);
    }
  }
  balls = balls.filter(b => !b.dead);

  if (balls.length === 0) {
    lives--;
    updateHUD();
    shake = 30;
    flashRed = 0.7;
    sfx.lose();
    if (lives < 0) {
      state = "gameover";
      showOverlay("GAME OVER", "Final Score: " + score + "<br>Press SPACE or click START to try again.", "RETRY");
    } else {
      newServe();
    }
    return;
  }

  for (const p of powerups) {
    p.y += p.vy * dt;
    p.flash += dt * 6;
  }

  const caught = [];
  for (const p of powerups) {
    if (
      p.x + p.w >= paddle.x - paddle.w / 2 &&
      p.x <= paddle.x + paddle.w / 2 &&
      p.y + p.h >= paddle.y &&
      p.y <= paddle.y + paddle.h
    ) {
      caught.push(p);
      activatePower(p);
    }
  }
  if (caught.length) powerups = powerups.filter(p => !caught.includes(p));
  powerups = powerups.filter(p => p.y < H + 30);

  if (bricks.length && !bricks.some(b => b.maxHp > 0 && !b.dead)) {
    nextLevel();
  }
}

/* ------------------------- draw ------------------------- */
function drawBackground() {
  ctx.fillStyle = "#02020a";
  ctx.fillRect(0, 0, W, H);

  ctx.fillStyle = "rgba(0,232,255,0.045)";
  for (let x = 0; x <= W; x += 40) ctx.fillRect(x, 0, 1, H);
  for (let y = 0; y <= H; y += 40) ctx.fillRect(0, y, W, 1);

  for (const s of stars) {
    ctx.globalAlpha = 0.18 + s.z * 0.6;
    ctx.fillStyle = "#ffffff";
    ctx.fillRect(s.x, s.y, 1 + s.z * 2, 1 + s.z * 2);
  }
  ctx.globalAlpha = 1;
}

function drawBricks() {
  for (const b of bricks) {
    if (b.dead) continue;
    const style = BRICK_STYLE[b.type] || BRICK_STYLE[1];

    ctx.fillStyle = style.fill;
    roundRect(b.x, b.y, b.w, b.h, 4);
    ctx.fill();

    ctx.fillStyle = "rgba(255,255,255,0.16)";
    ctx.fillRect(b.x + 3, b.y + 3, b.w - 6, 3);

    if (b.flash > 0) {
      ctx.fillStyle = "rgba(255,255,255," + (b.flash * 0.75) + ")";
      roundRect(b.x, b.y, b.w, b.h, 4);
      ctx.fill();
    }

    ctx.lineWidth = 2;
    ctx.strokeStyle = style.stroke;
    roundRect(b.x, b.y, b.w, b.h, 4);
    ctx.stroke();

    if (b.type === 4) {
      ctx.save();
      ctx.setLineDash([3, 4]);
      ctx.strokeStyle = "rgba(255,255,255,0.35)";
      ctx.lineWidth = 1;
      roundRect(b.x + 3, b.y + 3, b.w - 6, b.h - 6, 3);
      ctx.stroke();
      ctx.restore();
    } else if (b.maxHp > 1) {
      ctx.fillStyle = "rgba(0,0,0,0.45)";
      ctx.font = "bold 12px monospace";
      ctx.textAlign = "center";
      ctx.textBaseline = "middle";
      ctx.fillText(String(Math.max(1, b.hp)), b.x + b.w / 2, b.y + b.h / 2 + 1);
    }
  }
}

function drawPowerups() {
  const colors = {
    EXP: "#00e8ff", MULT: "#ff2bd6", SLOW: "#ffe234", LIFE: "#8cff6b"
  };
  for (const p of powerups) {
    ctx.save();
    ctx.shadowColor = colors[p.type] || "#fff";
    ctx.shadowBlur = 14;
    ctx.fillStyle = "rgba(0,0,0,0.55)";
    roundRect(p.x, p.y, p.w, p.h, 4);
    ctx.fill();
    ctx.strokeStyle = colors[p.type] || "#fff";
    ctx.lineWidth = 2;
    roundRect(p.x, p.y, p.w, p.h, 4);
    ctx.stroke();
    ctx.restore();

    ctx.fillStyle = colors[p.type] || "#fff";
    ctx.font = "bold 10px monospace";
    ctx.textAlign = "center";
    ctx.textBaseline = "middle";
    ctx.fillText(p.type === "MULT" ? "M" : p.type[0], p.x + p.w / 2, p.y + p.h / 2 + 1);
  }
}

function drawPaddle() {
  const w = paddle.w;
  const x = paddle.x - w / 2;
  ctx.save();
  ctx.shadowColor = "#00e8ff";
  ctx.shadowBlur = 16 + paddle.flash * 24;
  ctx.fillStyle = paddle.flash > 0.3 ? "#ffffff" : "#00e8ff";
  roundRect(x, paddle.y, w, paddle.h, 7);
  ctx.fill();
  ctx.strokeStyle = "rgba(255,255,255,0.85)";
  ctx.lineWidth = 2;
  roundRect(x + 2, paddle.y + 2, w - 4, paddle.h - 4, 5);
  ctx.stroke();
  ctx.restore();
}

function drawBalls() {
  for (const b of balls) {
    for (let i = 0; i < b.trail.length; i++) {
      const t = b.trail[i];
      const a = (i + 1) / b.trail.length;
      ctx.fillStyle = "rgba(0,232,255," + (a * 0.17).toFixed(3) + ")";
      ctx.beginPath();
      ctx.arc(t.x, t.y, b.r * a * 0.92, 0, Math.PI * 2);
      ctx.fill();
    }

    ctx.save();
    ctx.shadowColor = "#eafcff";
    ctx.shadowBlur = 20;
    ctx.fillStyle = "#ffffff";
    ctx.beginPath();
    ctx.arc(b.x, b.y, b.r, 0, Math.PI * 2);
    ctx.fill();
    ctx.restore();
  }
}

function drawParticles() {
  for (const p of particles) {
    ctx.globalAlpha = Math.max(0, p.life / p.maxLife);
    ctx.fillStyle = p.color;
    ctx.fillRect(p.x - p.size / 2, p.y - p.size / 2, p.size, p.size);
  }
  ctx.globalAlpha = 1;
}

function drawFloaters() {
  ctx.textAlign = "center";
  ctx.textBaseline = "middle";
  for (const f of floaters) {
    ctx.globalAlpha = Math.max(0, f.life);
    ctx.fillStyle = f.color;
    ctx.font = "bold " + Math.round(15 * f.scale + 2) + "px monospace";
    ctx.fillText(f.text, f.x, f.y);
  }
  ctx.globalAlpha = 1;
}

function drawOverlayFx() {
  if (flashRed > 0) {
    ctx.fillStyle = "rgba(255,40,80," + (flashRed * 0.45).toFixed(3) + ")";
    ctx.fillRect(0, 0, W, H);
  }

  ctx.fillStyle = "rgba(0,0,0,0.14)";
  for (let y = 0; y < H; y += 3) ctx.fillRect(0, y, W, 1);

  const grad = ctx.createRadialGradient(W / 2, H / 2, H / 3.2, W / 2, H / 2, H / 1.1);
  grad.addColorStop(0, "rgba(0,0,0,0)");
  grad.addColorStop(1, "rgba(0,0,0,0.75)");
  ctx.fillStyle = grad;
  ctx.fillRect(0, 0, W, H);
}

function drawHint() {
  if (state !== "serve") return;
  ctx.save();
  ctx.textAlign = "center";
  ctx.font = "bold 18px monospace";
  ctx.shadowColor = "#00e8ff";
  ctx.shadowBlur = 16;
  ctx.fillStyle = "rgba(255,255,255," + (0.6 + 0.35 * Math.sin(performance.now() / 190)).toFixed(3) + ")";
  ctx.fillText("CLICK OR PRESS SPACE TO LAUNCH", W / 2, H - 110);
  ctx.restore();
}

function draw() {
  drawBackground();

  ctx.save();
  if (shake > 0.2) {
    ctx.translate((Math.random() - 0.5) * shake, (Math.random() - 0.5) * shake);
  }
  drawBricks();
  drawPowerups();
  drawPaddle();
  drawBalls();
  drawParticles();
  drawFloaters();
  ctx.restore();

  drawOverlayFx();
  drawHint();
}

/* ------------------------- input ------------------------- */
window.addEventListener("keydown", (e) => {
  if (["Space", "ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown"].includes(e.code)) e.preventDefault();
  initAudio();

  if (e.code === "ArrowLeft" || e.code === "KeyA") { keys.left = true; useMouse = false; }
  if (e.code === "ArrowRight" || e.code === "KeyD") { keys.right = true; useMouse = false; }

  if (e.code === "Space") {
    if (state === "menu" || state === "gameover" || state === "victory") startGame();
    else if (state === "paused") togglePause();
    else if (state === "serve") launchAll();
  }
  if (e.code === "Enter") {
    if (state === "menu" || state === "gameover" || state === "victory") startGame();
    else if (state === "paused") togglePause();
  }
  if (e.code === "KeyP") {
    if (state === "playing" || state === "serve" || state === "paused") togglePause();
  }
  if (e.code === "KeyM") {
    mute = !mute;
    addFloater(paddle.x, paddle.y - 34, mute ? "SOUND OFF" : "SOUND ON", "#ffffff", 0.8);
  }
});

window.addEventListener("keyup", (e) => {
  if (e.code === "ArrowLeft" || e.code === "KeyA") keys.left = false;
  if (e.code === "ArrowRight" || e.code === "KeyD") keys.right = false;
});

canvas.addEventListener("mousemove", (e) => {
  const rect = canvas.getBoundingClientRect();
  mouseX = ((e.clientX - rect.left) / rect.width) * W;
  useMouse = true;
});

canvas.addEventListener("mousedown", (e) => {
  e.preventDefault();
  initAudio();
  const rect = canvas.getBoundingClientRect();
  mouseX = ((e.clientX - rect.left) / rect.width) * W;
  useMouse = true;

  if (state === "serve") launchAll();
  else if (state === "paused") togglePause();
  else if (state === "menu" || state === "gameover" || state === "victory") startGame();
});

startBtn.addEventListener("click", (e) => {
  e.preventDefault();
  initAudio();
  if (state === "paused") togglePause();
  else startGame();
});

/* ------------------------- main ------------------------- */
for (let i = 0; i < 90; i++) {
  stars.push({ x: Math.random() * W, y: Math.random() * H, z: Math.random() * 0.8 + 0.2 });
}

loadLevel(0);
updateHUD();

let last = performance.now();
function loop(t) {
  const dt = Math.min(0.05, (t - last) / 1000);
  last = t;
  update(dt);
  draw();
  requestAnimationFrame(loop);
}
requestAnimationFrame(loop);
})();
</script>
</body>
</html>

Compare with

Step 5 Preview: other attempts