hypeorcode
Network offOpen alone (new tab)

Run details

Model
reflection/beam-1.0-501b-a23b
Route
OpenRouter API, one user message
Reasoning
provider default, 10,917 reasoning tokens
Limits
60 min · 600,000 output tokens
Date
21 Oct 2026
Time
51 s
Size
27 kB
Tokens
122 in, 20,062 out
List price
$0.020 ($0.3 in / $1 out per M)
Paid
$0.020

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

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Breakout: Neon Bricks</title>
<style>
  * { box-sizing: border-box; }
  html, body {
    margin: 0;
    padding: 0;
    width: 100%;
    height: 100%;
    background: #05070d;
    color: #f5f7ff;
    font-family: "Segoe UI", system-ui, -apple-system, sans-serif;
    overflow: hidden;
  }
  body {
    display: flex;
    align-items: center;
    justify-content: center;
  }
  canvas {
    display: block;
    max-width: 100vw;
    max-height: 100vh;
    width: auto;
    height: auto;
    box-shadow: 0 0 48px rgba(0, 0, 0, 0.75);
    border: 1px solid rgba(255, 255, 255, 0.08);
    border-radius: 8px;
    background: #05070d;
  }
</style>
</head>
<body>
<canvas id="game" width="960" height="640" tabindex="0"></canvas>
<script>
(() => {
  const W = 960;
  const H = 640;
  const COLS = 10;
  const BRICK_W = 80;
  const BRICK_H = 24;
  const GAP_X = 8;
  const GAP_Y = 8;
  const FIELD_W = COLS * BRICK_W + (COLS - 1) * GAP_X;
  const START_X = (W - FIELD_W) / 2;
  const START_Y = 92;
  const MAX_LEVEL = 5;

  const canvas = document.getElementById('game');
  const ctx = canvas.getContext('2d');

  let state = 'start'; // start, playing, paused, levelclear, gameover, victory
  let score = 0;
  let highScore = 0;
  let lives = 3;
  let level = 1;
  let combo = 0;
  let muted = false;
  let mouseActive = true;
  let keyboardActive = false;
  let stuckTimer = 0;
  let levelClearTimer = 0;
  let lastTime = 0;

  const paddle = {
    x: W / 2,
    y: H - 52,
    w: 140,
    h: 18,
    vx: 0,
    targetX: W / 2
  };

  const ball = {
    x: W / 2,
    y: H - 72,
    r: 9,
    vx: 0,
    vy: 0,
    stuck: true,
    squash: 0,
    trail: []
  };

  let bricks = [];
  let particles = [];
  let floats = [];
  let stars = [];
  let shakeX = 0, shakeY = 0, shakeTime = 0, shakeDur = 0.2, shakeMag = 0;
  let audioCtx = null;

  // ---------- utility ----------
  function clamp(v, a, b) { return Math.max(a, Math.min(b, v)); }
  function roundRect(x, y, w, h, r) {
    r = Math.min(r, w / 2, h / 2);
    ctx.beginPath();
    ctx.moveTo(x + r, y);
    ctx.arcTo(x + w, y, x + w, y + h, r);
    ctx.arcTo(x + w, y + h, x, y + h, r);
    ctx.arcTo(x, y + h, x, y, r);
    ctx.arcTo(x, y, x + w, y, r);
    ctx.closePath();
  }

  function tryStorage(fn) {
    try { return fn(); } catch (e) { return null; }
  }

  function loadHighScore() {
    const v = tryStorage(() => localStorage.getItem('breakout_neon_hs'));
    highScore = v ? Number(v) || 0 : 0;
  }

  function saveHighScore() {
    if (score > highScore) {
      highScore = score;
      tryStorage(() => localStorage.setItem('breakout_neon_hs', String(highScore)));
    }
  }

  // ---------- audio ----------
  function ensureAudio() {
    if (!audioCtx) {
      try {
        audioCtx = new (window.AudioContext || window.webkitAudioContext)();
      } catch (e) {
        audioCtx = null;
      }
    }
    if (audioCtx && audioCtx.state === 'suspended') audioCtx.resume();
  }

  function playSound(type, pitch = 1) {
    if (muted || !audioCtx) return;
    const now = audioCtx.currentTime;
    const osc = audioCtx.createOscillator();
    const gain = audioCtx.createGain();
    osc.connect(gain);
    gain.connect(audioCtx.destination);

    let freq = 440, dur = 0.08, vol = 0.07, wave = 'square';
    switch (type) {
      case 'paddle': freq = 300; dur = 0.07; wave = 'triangle'; vol = 0.06; break;
      case 'brick': freq = 520; dur = 0.06; wave = 'square'; vol = 0.05; break;
      case 'wall': freq = 220; dur = 0.05; wave = 'square'; vol = 0.04; break;
      case 'launch': freq = 380; dur = 0.12; wave = 'sawtooth'; vol = 0.06; break;
      case 'lose': freq = 160; dur = 0.35; wave = 'sawtooth'; vol = 0.09; break;
      case 'clear': freq = 660; dur = 0.25; wave = 'triangle'; vol = 0.09; break;
      case 'over': freq = 110; dur = 0.6; wave = 'sawtooth'; vol = 0.1; break;
      case 'ui': freq = 420; dur = 0.05; wave = 'sine'; vol = 0.05; break;
      case 'steel': freq = 180; dur = 0.08; wave = 'square'; vol = 0.05; break;
    }
    osc.type = wave;
    osc.frequency.setValueAtTime(freq * pitch, now);
    if (type === 'clear') {
      osc.frequency.exponentialRampToValueAtTime(freq * pitch * 1.5, now + dur);
    }
    gain.gain.setValueAtTime(vol, now);
    gain.gain.exponentialRampToValueAtTime(0.0001, now + dur);
    osc.start(now);
    osc.stop(now + dur + 0.03);
  }

  // ---------- level generation ----------
  function hpAt(level, r, c, rows) {
    if (level === 1) return 1;
    if (level === 2) return r < 2 ? 2 : 1;
    if (level === 3) {
      const half = r + 1;
      if (Math.abs(c + 0.5 - COLS / 2) > half) return 0;
      return Math.min(3, Math.floor(r / 2) + 1);
    }
    if (level === 4) {
      if (c === 0 || c === COLS - 1) return 3;
      if ((r + c) % 4 === 0) return 0;
      if ((r * c) % 7 === 0) return 2;
      return 1;
    }
    // level 5
    const v = (r * 31 + c * 17 + level * 13) % 9;
    if (v === 0 || v === 8) return 0;
    if (v === 7) return 999;
    return 1 + (v % 3);
  }

  function buildLevel(n) {
    bricks = [];
    const rows = 5 + Math.min(n - 1, 2);
    for (let r = 0; r < rows; r++) {
      for (let c = 0; c < COLS; c++) {
        const hp = hpAt(n, r, c, rows);
        if (hp === 0) continue;
        const hue = (c * 34 + r * 22 + n * 45) % 360;
        let color;
        if (hp >= 999) color = '#5f6b7a';
        else color = `hsl(${hue}, 74%, ${52 + hp * 6}%)`;
        bricks.push({
          x: START_X + c * (BRICK_W + GAP_X),
          y: START_Y + r * (BRICK_H + GAP_Y),
          w: BRICK_W,
          h: BRICK_H,
          hp,
          maxHp: hp,
          alive: true,
          flash: 0,
          color
        });
      }
    }
  }

  function remainingBreakable() {
    let n = 0;
    for (const b of bricks) if (b.alive && b.maxHp < 999) n++;
    return n;
  }

  // ---------- effects ----------
  function spawnParticles(x, y, color, count = 12, speed = 220, life = 0.65, gravity = 320, size = 3) {
    for (let i = 0; i < count; i++) {
      const a = Math.random() * Math.PI * 2;
      const s = Math.random() * speed;
      particles.push({
        x, y,
        vx: Math.cos(a) * s,
        vy: Math.sin(a) * s,
        life,
        maxLife: life,
        color,
        size: Math.random() * size + 1,
        gravity
      });
    }
    if (particles.length > 700) particles.splice(0, particles.length - 700);
  }

  function spawnFloat(x, y, text, color = '#ffffff') {
    floats.push({
      x, y,
      text,
      life: 1.0,
      maxLife: 1.0,
      color,
      vy: -60,
      scale: 0.6
    });
  }

  function triggerShake(mag, dur) {
    shakeMag = Math.max(shakeMag, mag);
    shakeDur = dur;
    shakeTime = dur;
  }

  function updateParticles(dt) {
    for (let i = particles.length - 1; i >= 0; i--) {
      const p = particles[i];
      p.vy += p.gravity * dt;
      p.x += p.vx * dt;
      p.y += p.vy * dt;
      p.vx *= 0.985;
      p.life -= dt;
      if (p.life <= 0) particles.splice(i, 1);
    }
    for (let i = floats.length - 1; i >= 0; i--) {
      const f = floats[i];
      f.y += f.vy * dt;
      f.vy *= 0.96;
      f.life -= dt;
      f.scale = Math.min(1, f.scale + dt * 4);
      if (f.life <= 0) floats.splice(i, 1);
    }
    for (let i = ball.trail.length - 1; i >= 0; i--) {
      ball.trail[i].life -= dt;
      if (ball.trail[i].life <= 0) ball.trail.splice(i, 1);
    }
    if (shakeTime > 0) {
      shakeTime -= dt;
      const t = Math.max(0, shakeTime / shakeDur);
      const m = shakeMag * t;
      shakeX = (Math.random() * 2 - 1) * m;
      shakeY = (Math.random() * 2 - 1) * m;
      if (shakeTime <= 0) {
        shakeX = shakeY = 0;
        shakeMag = 0;
      }
    }
  }

  // ---------- game flow ----------
  function resetBallOnPaddle() {
    ball.stuck = true;
    ball.trail = [];
    ball.x = paddle.x;
    ball.y = paddle.y - ball.r - 2;
    ball.vx = 0;
    ball.vy = 0;
    ball.squash = 0;
    stuckTimer = 0;
  }

  function startGame() {
    score = 0;
    lives = 3;
    level = 1;
    combo = 0;
    buildLevel(1);
    paddle.w = 140;
    paddle.x = W / 2;
    paddle.targetX = W / 2;
    paddle.vx = 0;
    mouseActive = true;
    keyboardActive = false;
    resetBallOnPaddle();
    state = 'playing';
    ensureAudio();
    playSound('ui');
  }

  function launchBall() {
    if (!ball.stuck || state !== 'playing') return;
    ball.stuck = false;
    const speed = 330 + (level - 1) * 38;
    ball.vx = (Math.random() * 0.7 - 0.35) * speed;
    ball.vy = -speed;
    stuckTimer = 0;
    playSound('launch');
  }

  function loseLife() {
    lives--;
    spawnParticles(ball.x, ball.y, '#ff4d6d', 45, 320, 0.85, 360, 4);
    triggerShake(18, 0.45);
    playSound('lose');
    combo = 0;
    if (lives <= 0) {
      state = 'gameover';
      saveHighScore();
      playSound('over');
    } else {
      resetBallOnPaddle();
    }
  }

  function startLevelClear() {
    state = 'levelclear';
    ball.vx = 0;
    ball.vy = 0;
    ball.stuck = false;
    levelClearTimer = 0;
    const bonus = lives * 500 + level * 1000;
    score += bonus;
    saveHighScore();
    spawnFloat(W / 2, H / 2 - 50, `LEVEL CLEAR  +${bonus}`, '#9ef0ff');
    // confetti
    const colors = ['#ff595e', '#ffca3a', '#8ac926', '#1982c4', '#c77dff', '#f15bb5'];
    for (let i = 0; i < 90; i++) {
      spawnParticles(
        Math.random() * W, -20,
        colors[Math.floor(Math.random() * colors.length)],
        1, 120 + Math.random() * 200, 1.2, 120, 4
      );
    }
    triggerShake(12, 0.35);
    playSound('clear');
  }

  function nextLevel() {
    if (state !== 'levelclear') return;
    level++;
    if (level > MAX_LEVEL) {
      state = 'victory';
      saveHighScore();
      playSound('clear');
      return;
    }
    buildLevel(level);
    paddle.w = Math.max(92, 140 - (level - 1) * 10);
    paddle.x = W / 2;
    paddle.targetX = W / 2;
    paddle.vx = 0;
    resetBallOnPaddle();
    state = 'playing';
    playSound('ui');
  }

  // ---------- collisions ----------
  function circleRectHit(b) {
    const cx = clamp(ball.x, b.x, b.x + b.w);
    const cy = clamp(ball.y, b.y, b.y + b.h);
    const dx = ball.x - cx;
    const dy = ball.y - cy;
    return dx * dx + dy * dy <= ball.r * ball.r;
  }

  function reflectBrick(b) {
    const leftOverlap = ball.x + ball.r - b.x;
    const rightOverlap = b.x + b.w - (ball.x - ball.r);
    const topOverlap = ball.y + ball.r - b.y;
    const bottomOverlap = b.y + b.h - (ball.y - ball.r);
    const minOverlapX = Math.min(leftOverlap, rightOverlap);
    const minOverlapY = Math.min(topOverlap, bottomOverlap);

    if (minOverlapX < minOverlapY) {
      ball.vx = -ball.vx;
      if (leftOverlap < rightOverlap) ball.x = b.x - ball.r - 0.1;
      else ball.x = b.x + b.w + ball.r + 0.1;
    } else {
      ball.vy = -ball.vy;
      if (topOverlap < bottomOverlap) ball.y = b.y - ball.r - 0.1;
      else ball.y = b.y + b.h + ball.r + 0.1;
    }
  }

  function handleBrickHit(b) {
    b.flash = 0.18;
    const destroyed = b.maxHp < 999;
    if (destroyed) {
      b.hp -= 1;
      if (b.hp <= 0) b.alive = false;
    }
    combo++;
    let pts = 50 * combo;
    if (destroyed && !b.alive) pts += 100 * b.maxHp;
    score += pts;
    if (score > highScore) highScore = score;

    const cx = b.x + b.w / 2;
    const cy = b.y + b.h / 2;
    spawnParticles(cx, cy, b.color, 16, 260, 0.7, 340, 3);
    spawnFloat(cx, cy, `+${pts}`, '#ffffff');
    if (combo >= 3) spawnFloat(cx, cy - 18, `x${combo} COMBO`, '#ffd166');

    triggerShake(4, 0.12);
    if (b.maxHp >= 999) {
      playSound('steel');
    } else {
      playSound('brick', 1 + Math.min(combo, 12) * 0.045);
    }

    if (remainingBreakable() === 0) startLevelClear();
  }

  // ---------- input ----------
  function setKeyboardControl() {
    if (!keyboardActive) {
      keyboardActive = true;
      mouseActive = false;
      paddle.vx = 0;
    }
  }

  function handlePrimary() {
    ensureAudio();
    if (state === 'start') startGame();
    else if (state === 'playing') {
      if (ball.stuck) launchBall();
    } else if (state === 'levelclear') nextLevel();
    else if (state === 'paused') state = 'playing';
    else if (state === 'gameover' || state === 'victory') startGame();
  }

  window.addEventListener('keydown', (e) => {
    const k = e.key.toLowerCase();
    if (k === 'arrowleft' || k === 'a') { e.preventDefault(); setKeyboardControl(); }
    if (k === 'arrowright' || k === 'd') { e.preventDefault(); setKeyboardControl(); }
    if (k === 'arrowleft' || k === 'a') { /* handled below */ }
    if (['arrowleft', 'a'].includes(k)) { /* noop */ }

    if (k === 'arrowleft' || k === 'a') { /* placeholder */ }
  });

  // cleaner key handling
  const keys = { left: false, right: false };
  window.addEventListener('keydown', (e) => {
    const k = e.key.toLowerCase();
    if (k === 'arrowleft' || k === 'a') {
      keys.left = true;
      setKeyboardControl();
      e.preventDefault();
    }
    if (k === 'arrowright' || k === 'd') {
      keys.right = true;
      setKeyboardControl();
      e.preventDefault();
    }
    if (k === ' ') {
      e.preventDefault();
      handlePrimary();
    }
    if (k === 'p') {
      if (state === 'playing') state = 'paused';
      else if (state === 'paused') state = 'playing';
    }
    if (k === 'm') muted = !muted;
    if (k === 'r') {
      if (state !== 'playing') startGame();
    }
    if (k === 'escape') {
      if (state === 'playing') state = 'paused';
      else if (state === 'paused') state = 'playing';
    }
  });

  window.addEventListener('keyup', (e) => {
    const k = e.key.toLowerCase();
    if (k === 'arrowleft' || k === 'a') keys.left = false;
    if (k === 'arrowright' || k === 'd') keys.right = false;
  });

  function canvasPos(e) {
    const rect = canvas.getBoundingClientRect();
    return (e.clientX - rect.left) * (W / rect.width);
  }

  canvas.addEventListener('mousemove', (e) => {
    paddle.targetX = canvasPos(e);
    if (!mouseActive) {
      mouseActive = true;
      keyboardActive = false;
      paddle.vx = 0;
    }
  });

  canvas.addEventListener('mousedown', (e) => {
    canvas.focus();
    ensureAudio();
    paddle.targetX = canvasPos(e);
    mouseActive = true;
    keyboardActive = false;
    paddle.vx = 0;
    handlePrimary();
  });

  canvas.addEventListener('touchstart', (e) => {
    e.preventDefault();
    canvas.focus();
    ensureAudio();
    const t = e.touches[0];
    const rect = canvas.getBoundingClientRect();
    paddle.targetX = (t.clientX - rect.left) * (W / rect.width);
    mouseActive = true;
    keyboardActive = false;
    paddle.vx = 0;
    handlePrimary();
  }, { passive: false });

  canvas.addEventListener('touchmove', (e) => {
    e.preventDefault();
    const t = e.touches[0];
    const rect = canvas.getBoundingClientRect();
    paddle.targetX = (t.clientX - rect.left) * (W / rect.width);
    mouseActive = true;
    keyboardActive = false;
  }, { passive: false });

  // ---------- stars ----------
  function initStars() {
    stars = [];
    for (let i = 0; i < 110; i++) {
      stars.push({
        x: Math.random() * W,
        y: Math.random() * H,
        z: Math.random() * 0.9 + 0.15,
        phase: Math.random() * Math.PI * 2
      });
    }
  }

  // ---------- update ----------
  function update(dt) {
    if (state === 'playing') {
      // keyboard paddle
      if (keyboardActive) {
        paddle.vx = 0;
        if (keys.left) paddle.vx -= 620;
        if (keys.right) paddle.vx += 620;
      }

      // mouse smoothing
      if (mouseActive) {
        paddle.x += (paddle.targetX - paddle.x) * Math.min(1, dt * 16);
      } else {
        paddle.x += paddle.vx * dt;
      }
      const half = paddle.w / 2;
      paddle.x = clamp(paddle.x, half, W - half);

      if (ball.stuck) {
        ball.x = paddle.x;
        ball.y = paddle.y - ball.r - 2;
        ball.trail = [];
        stuckTimer += dt;
        if (stuckTimer > 4.5) launchBall();
      } else {
        // trail
        ball.trail.push({ x: ball.x, y: ball.y, life: 0.22 });
        if (ball.trail.length > 26) ball.trail.shift();

        ball.x += ball.vx * dt;
        ball.y += ball.vy * dt;
        ball.squash = Math.max(0, ball.squash - dt * 7);

        // walls
        if (ball.x - ball.r < 0) {
          ball.x = ball.r;
          ball.vx = Math.abs(ball.vx);
          spawnParticles(ball.x, ball.y, '#7dd3fc', 6, 100, 0.35, 120, 2);
          playSound('wall');
        }
        if (ball.x + ball.r > W) {
          ball.x = W - ball.r;
          ball.vx = -Math.abs(ball.vx);
          spawnParticles(ball.x, ball.y, '#7dd3fc', 6, 100, 0.35, 120, 2);
          playSound('wall');
        }
        if (ball.y - ball.r < 0) {
          ball.y = ball.r;
          ball.vy = Math.abs(ball.vy);
          spawnParticles(ball.x, ball.y, '#7dd3fc', 6, 100, 0.35, 120, 2);
          playSound('wall');
        }
        if (ball.y + ball.r > H) {
          loseLife();
          return;
        }

        // paddle
        const ph = paddle.h;
        if (ball.vy > 0 &&
            ball.y + ball.r >= paddle.y &&
            ball.y - ball.r <= paddle.y + ph &&
            ball.x >= paddle.x - paddle.w / 2 - ball.r &&
            ball.x <= paddle.x + paddle.w / 2 + ball.r) {
          const hit = clamp((ball.x - paddle.x) / (paddle.w / 2), -1, 1);
          const speed = Math.hypot(ball.vx, ball.vy);
          const maxAngle = 72 * Math.PI / 180;
          const angle = hit * maxAngle;
          ball.vx = speed * Math.sin(angle);
          ball.vy = -Math.abs(speed * Math.cos(angle));
          ball.y = paddle.y - ball.r - 0.1;
          ball.squash = 0.4;
          const minVy = speed * 0.35;
          if (Math.abs(ball.vy) < minVy) {
            ball.vy = -minVy;
            ball.vx = Math.sign(ball.vx || 1) * Math.sqrt(Math.max(0, speed * speed - ball.vy * ball.vy));
          }
          spawnParticles(ball.x, paddle.y, '#7dd3fc', 12, 150, 0.45, 180, 2);
          playSound('paddle');
          if (combo > 2) spawnFloat(ball.x, paddle.y - 24, `x${combo} COMBO`, '#ffd166');
          combo = 0;
        }

        // bricks
        let hitBrick = null;
        for (const b of bricks) {
          if (!b.alive) continue;
          if (circleRectHit(b)) {
            hitBrick = b;
            break;
          }
        }
        if (hitBrick) {
          reflectBrick(hitBrick);
          handleBrickHit(hitBrick);
        }
      }
    } else if (state === 'levelclear') {
      levelClearTimer += dt;
      if (levelClearTimer > 2.4) nextLevel();
    }

    updateParticles(dt);
  }

  // ---------- rendering ----------
  function drawBackground() {
    const grad = ctx.createLinearGradient(0, 0, 0, H);
    grad.addColorStop(0, '#07111f');
    grad.addColorStop(0.45, '#0b1026');
    grad.addColorStop(1, '#02040a');
    ctx.fillStyle = grad;
    ctx.fillRect(0, 0, W, H);

    // nebula
    const t = performance.now() / 1000;
    const g2 = ctx.createRadialGradient(W * 0.7, H * 0.25, 10, W * 0.7, H * 0.25, 260);
    g2.addColorStop(0, 'rgba(125, 90, 255, 0.16)');
    g2.addColorStop(1, 'rgba(125, 90, 255, 0)');
    ctx.fillStyle = g2;
    ctx.fillRect(0, 0, W, H);

    const g3 = ctx.createRadialGradient(W * 0.2, H * 0.75, 10, W * 0.2, H * 0.75, 220);
    g3.addColorStop(0, 'rgba(0, 200, 255, 0.10)');
    g3.addColorStop(1, 'rgba(0, 200, 255, 0)');
    ctx.fillStyle = g3;
    ctx.fillRect(0, 0, W, H);

    for (const s of stars) {
      const tw = 0.35 + 0.65 * (0.5 + 0.5 * Math.sin(t * 1.7 + s.phase));
      ctx.globalAlpha = tw * s.z;
      ctx.fillStyle = '#ffffff';
      ctx.fillRect(s.x, s.y, s.z * 1.6, s.z * 1.6);
    }
    ctx.globalAlpha = 1;
  }

  function drawBricks() {
    for (const b of bricks) {
      if (!b.alive) continue;
      if (b.flash > 0) b.flash -= 0.016;
      // body
      roundRect(b.x, b.y, b.w, b.h, 5);
      ctx.fillStyle = b.color;
      ctx.fill();

      // top highlight
      const hg = ctx.createLinearGradient(b.x, b.y, b.x, b.y + b.h);
      hg.addColorStop(0, 'rgba(255,255,255,0.28)');
      hg.addColorStop(0.45, 'rgba(255,255,255,0.04)');
      hg.addColorStop(1, 'rgba(0,0,0,0.18)');
      ctx.fillStyle = hg;
      roundRect(b.x, b.y, b.w, b.h, 5);
      ctx.fill();

      if (b.maxHp >= 999) {
        ctx.strokeStyle = 'rgba(255,255,255,0.18)';
        ctx.lineWidth = 1;
        for (let i = 6; i < b.w; i += 10) {
          ctx.beginPath();
          ctx.moveTo(b.x + i, b.y + 2);
          ctx.lineTo(b.x + i - 6, b.y + b.h - 2);
          ctx.stroke();
        }
      } else if (b.maxHp > 1) {
        ctx.fillStyle = 'rgba(255,255,255,0.85)';
        ctx.font = 'bold 11px system-ui, sans-serif';
        ctx.textAlign = 'center';
        ctx.textBaseline = 'middle';
        ctx.fillText(String(b.hp), b.x + b.w / 2, b.y + b.h / 2);
        ctx.textBaseline = 'alphabetic';
      }

      if (b.flash > 0) {
        ctx.globalAlpha = Math.min(1, b.flash * 5);
        ctx.fillStyle = '#ffffff';
        roundRect(b.x, b.y, b.w, b.h, 5);
        ctx.fill();
        ctx.globalAlpha = 1;
      }
    }
  }

  function drawPaddle() {
    const half = paddle.w / 2;
    const x = paddle.x - half;
    const y = paddle.y;
    ctx.save();
    ctx.shadowColor = '#4cc9f0';
    ctx.shadowBlur = 20;
    const grad = ctx.createLinearGradient(x, y, x, y + paddle.h);
    grad.addColorStop(0, '#9be7ff');
    grad.addColorStop(0.45, '#4cc9f0');
    grad.addColorStop(1, '#1b7ea6');
    ctx.fillStyle = grad;
    roundRect(x, y, paddle.w, paddle.h, 9);
    ctx.fill();
    ctx.shadowBlur = 0;
    // sheen
    ctx.fillStyle = 'rgba(255,255,255,0.35)';
    roundRect(x + 6, y + 3, paddle.w - 12, 5, 3);
    ctx.fill();
    ctx.restore();
  }

  function drawBall() {
    // trail
    for (let i = 0; i < ball.trail.length; i++) {
      const t = ball.trail[i];
      const a = (i + 1) / ball.trail.length;
      ctx.globalAlpha = a * 0.45;
      ctx.fillStyle = '#7dd3fc';
      ctx.beginPath();
      ctx.arc(t.x, t.y, ball.r * (0.5 + a * 0.5), 0, Math.PI * 2);
      ctx.fill();
    }
    ctx.globalAlpha = 1;

    const sx = 1 + ball.squash * 0.75;
    const sy = 1 - ball.squash * 0.45;
    ctx.save();
    ctx.shadowColor = '#7dd3fc';
    ctx.shadowBlur = 18;
    const grad = ctx.createRadialGradient(ball.x - 2, ball.y - 2, 1, ball.x, ball.y, ball.r * 1.3);
    grad.addColorStop(0, '#ffffff');
    grad.addColorStop(0.35, '#a5f3fc');
    grad.addColorStop(1, '#0ea5e9');
    ctx.fillStyle = grad;
    ctx.beginPath();
    ctx.ellipse(ball.x, ball.y, ball.r * sx, ball.r * sy, 0, 0, Math.PI * 2);
    ctx.fill();
    ctx.restore();

    if (ball.stuck && state === 'playing') {
      ctx.fillStyle = 'rgba(255,255,255,0.75)';
      ctx.font = 'bold 13px system-ui, sans-serif';
      ctx.textAlign = 'center';
      ctx.fillText('SPACE / CLICK TO LAUNCH', ball.x, ball.y - 26);
    }
  }

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

  function drawFloats() {
    for (const f of floats) {
      const a = Math.max(0, f.life / f.maxLife);
      ctx.globalAlpha = a;
      ctx.fillStyle = f.color;
      ctx.font = `bold ${Math.round(16 * f.scale)}px system-ui, sans-serif`;
      ctx.textAlign = 'center';
      ctx.fillText(f.text, f.x, f.y);
    }
    ctx.globalAlpha = 1;
  }

  function drawUI() {
    ctx.fillStyle = 'rgba(0,0,0,0.35)';
    ctx.fillRect(0, 0, W, 58);

    ctx.font = 'bold 22px system-ui, sans-serif';
    ctx.textAlign = 'left';
    ctx.fillStyle = '#ffffff';
    ctx.fillText(`SCORE  ${score.toLocaleString()}`, 24, 36);

    ctx.textAlign = 'center';
    ctx.fillStyle = '#ffd166';
    ctx.fillText(`BEST  ${highScore.toLocaleString()}`, W / 2, 36);

    ctx.textAlign = 'right';
    ctx.fillStyle = '#ffffff';
    ctx.fillText(`LEVEL  ${level}`, W - 150, 36);

    // lives
    ctx.font = '22px system-ui, sans-serif';
    ctx.fillStyle = '#ff4d6d';
    for (let i = 0; i < 3; i++) {
      ctx.globalAlpha = i < lives ? 1 : 0.25;
      ctx.fillText('♥', W - 118 + i * 26, 36);
    }
    ctx.globalAlpha = 1;

    // combo
    if (combo > 1 && state === 'playing') {
      ctx.textAlign = 'center';
      ctx.fillStyle = '#9ef0ff';
      ctx.font = 'bold 16px system-ui, sans-serif';
      ctx.fillText(`COMBO x${combo}`, W / 2, 54);
    }

    ctx.textAlign = 'left';
    ctx.font = '12px system-ui, sans-serif';
    ctx.fillStyle = 'rgba(255,255,255,0.55)';
    ctx.fillText(muted ? 'M: muted' : 'M: mute   P: pause   R: restart', 24, H - 16);
  }

  function drawOverlay() {
    if (state === 'start') {
      panel('NEON BREAKOUT', 'Clear every brick. Catch the ball. Climb the levels.', [
        'Mouse or A / D / Arrow Keys: move paddle',
        'Space or Click: launch ball / start',
        'P or Esc: pause   R: restart   M: mute',
        '',
        `Best score: ${highScore.toLocaleString()}`,
        'Press SPACE or CLICK to begin'
      ]);
    } else if (state === 'paused') {
      panel('PAUSED', '', ['Press P or Esc to resume']);
    } else if (state === 'levelclear') {
      panel(`LEVEL ${level - 1} CLEAR`, 'Get ready for the next wave.', ['Press SPACE or CLICK to continue']);
    } else if (state === 'gameover') {
      panel('GAME OVER', `Final score: ${score.toLocaleString()}`, [
        `Best score: ${highScore.toLocaleString()}`,
        'Press R or SPACE to play again'
      ]);
    } else if (state === 'victory') {
      panel('YOU WIN!', `Final score: ${score.toLocaleString()}`, [
        `Best score: ${highScore.toLocaleString()}`,
        'Press R or SPACE to play again'
      ]);
    }
  }

  function panel(title, subtitle, lines) {
    ctx.fillStyle = 'rgba(0, 0, 0, 0.55)';
    ctx.fillRect(0, 0, W, H);
    ctx.textAlign = 'center';
    ctx.fillStyle = '#ffffff';
    ctx.shadowColor = 'rgba(76, 201, 240, 0.8)';
    ctx.shadowBlur = 24;
    ctx.font = 'bold 58px system-ui, sans-serif';
    ctx.fillText(title, W / 2, H / 2 - 60);
    ctx.shadowBlur = 0;

    if (subtitle) {
      ctx.font = '22px system-ui, sans-serif';
      ctx.fillStyle = '#9ef0ff';
      ctx.fillText(subtitle, W / 2, H / 2 - 18);
    }

    ctx.font = '18px system-ui, sans-serif';
    ctx.fillStyle = 'rgba(255,255,255,0.85)';
    let y = H / 2 + 28;
    for (const line of lines) {
      ctx.fillText(line, W / 2, y);
      y += 28;
    }
  }

  function render() {
    canvas.style.cursor = state === 'playing' ? 'none' : 'default';
    ctx.save();
    ctx.translate(shakeX, shakeY);
    drawBackground();
    drawBricks();
    drawParticles();
    drawPaddle();
    drawBall();
    drawFloats();
    ctx.restore();
    drawUI();
    drawOverlay();
  }

  // ---------- main loop ----------
  function frame(ts) {
    if (!lastTime) lastTime = ts;
    const dt = Math.min((ts - lastTime) / 1000, 1 / 30);
    lastTime = ts;

    if (state === 'playing' || state === 'levelclear') {
      update(dt);
    } else {
      updateParticles(dt);
    }
    render();
    requestAnimationFrame(frame);
  }

  // ---------- init ----------
  loadHighScore();
  initStars();
  buildLevel(1);
  // show start with a prepared board
  paddle.x = W / 2;
  paddle.targetX = W / 2;
  resetBallOnPaddle();
  canvas.focus();
  requestAnimationFrame(frame);
})();
</script>
</body>
</html>

Compare with

Beam 1.0: other attempts