• Updated 22 Oct, 10:10 UTC
  • Next run 23 Oct, 05:17 UTCNext 23 Oct 05:17
  • Last 7 days: 7 new models, 157 runs7 d: 7 models, 157 runs
Network offOpen alone (new tab)

Run details

Model
elorian/glimpse
Route
OpenRouter API, one user message
Served by
Together
Reasoning
provider default, 8,590 reasoning tokens
Limits
60 min · 200,000 output tokens ($0.60 at $3/M)
Date
22 Oct 2026
Time
80 s
Size
23 kB
Tokens
282 in, 18,383 out
List price
$0.055 ($0.6 in / $3 out per M)
Paid
$0.055

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

23 kB 691 lines Raw

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>NEON BREAKOUT</title>
<style>
  * { margin: 0; padding: 0; box-sizing: border-box; }
  html, body {
    height: 100%;
    background: radial-gradient(ellipse at 50% 0%, #1a2040 0%, #0a0d1e 60%, #05060f 100%);
    display: flex;
    align-items: center;
    justify-content: center;
    overflow: hidden;
    font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
  }
  #wrap {
    position: relative;
    width: min(96vw, calc(96vh * 900 / 640));
    aspect-ratio: 900 / 640;
    filter: drop-shadow(0 0 40px rgba(80, 120, 255, 0.25));
  }
  canvas {
    width: 100%;
    height: 100%;
    display: block;
    border-radius: 12px;
    background: #0a0e20;
    cursor: none;
    touch-action: none;
  }
</style>
</head>
<body>
<div id="wrap"><canvas id="game" width="900" height="640"></canvas></div>
<script>
(function () {
"use strict";

const canvas = document.getElementById("game");
const ctx = canvas.getContext("2d");
const W = 900, H = 640;
const F = "system-ui, -apple-system, 'Segoe UI', sans-serif";

/* ------------------------------------------------------------------ audio */
let actx = null, muted = false;
function audio() {
  if (!actx) { try { actx = new (window.AudioContext || window.webkitAudioContext)(); } catch (e) {} }
  if (actx && actx.state === "suspended") actx.resume();
  return actx;
}
function tone(freq, dur, type, vol, slide) {
  const ac = audio();
  if (!ac || muted) return;
  const t = ac.currentTime;
  const o = ac.createOscillator(), g = ac.createGain();
  o.type = type || "square";
  o.frequency.setValueAtTime(freq, t);
  if (slide) o.frequency.exponentialRampToValueAtTime(Math.max(40, freq + slide), t + dur);
  g.gain.setValueAtTime(vol || 0.15, t);
  g.gain.exponentialRampToValueAtTime(0.0001, t + dur);
  o.connect(g); g.connect(ac.destination);
  o.start(t); o.stop(t + dur + 0.02);
}
function seq(notes) {
  const ac = audio();
  if (!ac || muted) return;
  const t0 = ac.currentTime;
  notes.forEach((n, i) => {
    const t = t0 + i * 0.11;
    const o = ac.createOscillator(), g = ac.createGain();
    o.type = "triangle";
    o.frequency.setValueAtTime(n, t);
    g.gain.setValueAtTime(0.14, t);
    g.gain.exponentialRampToValueAtTime(0.0001, t + 0.22);
    o.connect(g); g.connect(ac.destination);
    o.start(t); o.stop(t + 0.25);
  });
}
const sfx = {
  paddle: () => tone(300, 0.08, "square", 0.12, 140),
  wall:   () => tone(180, 0.05, "triangle", 0.08, -40),
  brick:  (c) => tone(480 + Math.min(c, 12) * 26, 0.09, "square", 0.13, 120),
  tough:  () => tone(140, 0.1, "sawtooth", 0.14, -60),
  power:  () => seq([520, 660, 830]),
  life:   () => tone(300, 0.5, "sawtooth", 0.16, -220),
  clear:  () => seq([523, 659, 784, 1047]),
  over:   () => seq([400, 320, 250, 180]),
  win:    () => seq([523, 659, 784, 1047, 784, 1047, 1319])
};

/* ------------------------------------------------------------------ levels */
const LEVELS = [
  ["11111111111","22222222222","33333333333","44444444444","55555555555"],
  [".....1.....","....121....","...12321...","..1234321..",".123454321.","12345554321"],
  ["2.2.2.2.2.2","33333333333","2.2.2.2.2.2","44444444444","S.S.S.S.S.S"],
  ["55555555555","54444444445","54333333345","5433SSSS345","54333333345","54444444445","55555555555"],
  (function(){ const r=[]; for (let y=0;y<7;y++){ let row="";
    for (let x=0;x<11;x++){ const v=(x+y)%6;
      row += v===0 ? "S" : String(((x+2*y)%5)+1); }
    r.push(row); } return r; })(),
  ["55555555555","4SS44444SS4","444S555S444","3333S5S3333","22222S22222","222S2S2S222"]
];
const COLORS = { "1":"#ff5d8f","2":"#38e8ff","3":"#ffe14d","4":"#7dff6a","5":"#b98cff","S":"#9db8cc" };
const COLS = 11, BRICK_TOP = 92, BRICK_H = 26, GAP = 5;
const BRICK_W = (W - 160) / COLS;

/* ------------------------------------------------------------------ state */
let state = "menu";            // menu | playing | clear | over | win
let levelIdx = 0, score = 0, lives = 3, combo = 0, best = 0;
try { best = parseInt(localStorage.getItem("nb.best")) || 0; } catch (e) {}

const paddle = { x: W/2, y: H-52, w: 120, h: 14, target: W/2, squash: 0 };
let balls = [], bricks = [], particles = [], popups = [], powerups = [];
const effects = { expand: 0, slow: 0 };

let shakeT = 0, shakeMag = 0, flashT = 0, flashCol = "#fff", introT = 0, clearT = 0, tGlobal = 0;
let comboPulse = 0;

const keys = {};
let mouseInside = false;

function baseSpeed() { return Math.min(380 + levelIdx * 24, 520); }

/* ------------------------------------------------------------------ build */
function buildLevel() {
  bricks = [];
  const rows = LEVELS[levelIdx];
  rows.forEach((row, r) => {
    for (let c = 0; c < COLS; c++) {
      const ch = row[c];
      if (ch === "." || !ch) continue;
      const tough = ch === "S";
      bricks.push({
        x: 80 + c * BRICK_W,
        y: BRICK_TOP + r * (BRICK_H + GAP),
        w: BRICK_W - GAP, h: BRICK_H,
        color: COLORS[ch],
        hp: tough ? 2 : 1, maxHp: tough ? 2 : 1,
        shake: 0
      });
    }
  });
  powerups = []; particles = []; popups = [];
  balls = [makeStuckBall()];
  introT = 1.4;
}
function makeStuckBall() {
  return { x: paddle.x, y: paddle.y - 20, r: 9, dx: 0, dy: -1, speed: baseSpeed(), stuck: true, trail: [] };
}
function launch(b) {
  const a = (Math.random() - 0.5) * 0.6;
  b.dx = Math.sin(a); b.dy = -Math.cos(a); b.stuck = false;
}
function startGame() {
  score = 0; lives = 3; combo = 0; levelIdx = 0;
  effects.expand = 0; effects.slow = 0;
  paddle.w = 120;
  buildLevel();
  state = "playing";
}

/* ------------------------------------------------------------------ juice */
function shake(mag, dur) { shakeMag = Math.max(shakeMag, mag); shakeT = Math.max(shakeT, dur); }
function flash(col, dur) { flashCol = col; flashT = dur; }
function popup(x, y, text, col) { popups.push({ x, y, text, col, life: 0.9, max: 0.9 }); }
function burst(x, y, col, n) {
  for (let i = 0; i < n; i++) {
    const a = Math.random() * Math.PI * 2, sp = 60 + Math.random() * 260;
    particles.push({
      x, y, vx: Math.cos(a) * sp, vy: Math.sin(a) * sp - 40,
      life: 0.5 + Math.random() * 0.5, max: 1,
      size: 2 + Math.random() * 4, col, g: 420
    });
  }
}
function sparks(x, y, col, n) {
  for (let i = 0; i < n; i++) {
    const a = Math.random() * Math.PI * 2, sp = 40 + Math.random() * 120;
    particles.push({
      x, y, vx: Math.cos(a) * sp, vy: Math.sin(a) * sp,
      life: 0.25 + Math.random() * 0.25, max: 0.5,
      size: 1.5 + Math.random() * 2, col, g: 0
    });
  }
}

/* ------------------------------------------------------------------ power-ups */
const PU = [
  { id: "E", col: "#7dff6a", name: "EXPAND" },
  { id: "M", col: "#38e8ff", name: "MULTIBALL" },
  { id: "S", col: "#ffe14d", name: "SLOW-MO" }
];
function maybeDrop(x, y) {
  if (Math.random() > 0.16) return;
  const t = PU[Math.floor(Math.random() * PU.length)];
  powerups.push({ x, y, w: 30, h: 18, vy: 150, type: t, t: 0 });
}
function applyPU(p) {
  if (p.type.id === "E") effects.expand = 10;
  if (p.type.id === "S") effects.slow = 8;
  if (p.type.id === "M") {
    const src = balls.filter(b => !b.stuck);
    const srcs = src.length ? src : [balls[0]];
    srcs.slice(0, 2).forEach(b => {
      for (const da of [-0.5, 0.5]) {
        const ca = Math.cos(da), sa = Math.sin(da);
        const nx = b.dx * ca - b.dy * sa, ny = b.dx * sa + b.dy * ca;
        balls.push({ x: b.x, y: b.y, r: b.r, dx: nx, dy: ny, speed: b.speed, stuck: false, trail: [] });
      }
    });
  }
  popup(p.x, p.y - 16, p.type.name, p.type.col);
  burst(p.x, p.y, p.type.col, 16);
  sfx.power();
}

/* ------------------------------------------------------------------ input */
function ptr(e) {
  const r = canvas.getBoundingClientRect();
  paddle.target = Math.max(paddle.w/2, Math.min(W - paddle.w/2, (e.clientX - r.left) * (W / r.width)));
  mouseInside = true;
}
canvas.addEventListener("pointermove", ptr);
canvas.addEventListener("pointerdown", (e) => {
  audio(); ptr(e);
  if (state === "menu" || state === "over" || state === "win") { startGame(); return; }
  if (state === "playing") {
    const b = balls.find(b => b.stuck);
    if (b) { launch(b); sfx.paddle(); }
  }
});
canvas.addEventListener("pointerleave", () => mouseInside = false);

window.addEventListener("keydown", (e) => {
  audio();
  if (["Space","ArrowLeft","ArrowRight","ArrowUp","ArrowDown"].includes(e.code)) e.preventDefault();
  keys[e.code] = true;
  if (e.code === "Space") {
    if (state === "menu" || state === "over" || state === "win") startGame();
    else if (state === "playing") {
      const b = balls.find(b => b.stuck);
      if (b) { launch(b); sfx.paddle(); }
    } else if (state === "paused") state = "playing";
  }
  if ((e.code === "KeyP" || e.code === "Escape") && (state === "playing" || state === "paused"))
    state = state === "playing" ? "paused" : "playing";
  if (e.code === "KeyM") muted = !muted;
  if (e.code === "KeyR" && (state === "over" || state === "win")) startGame();
});
window.addEventListener("keyup", (e) => keys[e.code] = false);
window.addEventListener("blur", () => { if (state === "playing") state = "paused"; });

/* ------------------------------------------------------------------ update */
function update(dt) {
  tGlobal += dt;
  if (introT > 0) introT -= dt;
  if (shakeT > 0) shakeT -= dt;
  if (flashT > 0) flashT -= dt;
  if (comboPulse > 0) comboPulse -= dt;

  // particles & popups run in most states
  if (state === "playing" || state === "clear" || state === "menu") {
    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 += p.g * dt;
      p.x += p.vx * dt; p.y += p.vy * 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);
    }
  }

  if (state === "menu") return;

  if (state === "clear") {
    clearT += dt;
    if (clearT > 1.7) {
      levelIdx++;
      if (levelIdx >= LEVELS.length) {
        state = "win";
        saveBest();
        sfx.win();
      } else { buildLevel(); state = "playing"; }
    }
    return;
  }

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

  /* timers */
  for (const k in effects) if (effects[k] > 0) effects[k] = Math.max(0, effects[k] - dt);

  /* paddle */
  const kb = ((keys["ArrowRight"] || keys["KeyD"] ? 1 : 0) - (keys["ArrowLeft"] || keys["KeyA"] ? 1 : 0));
  if (kb) paddle.target = Math.max(0, Math.min(W, paddle.target + kb * 560 * dt));
  paddle.target = Math.max(paddle.w/2, Math.min(W - paddle.w/2, paddle.target));
  paddle.x += (paddle.target - paddle.x) * Math.min(1, dt * 18);
  paddle.w += ((effects.expand > 0 ? 180 : 120) - paddle.w) * Math.min(1, dt * 10);
  if (paddle.squash > 0) paddle.squash = Math.max(0, paddle.squash - dt * 6);

  const slowF = effects.slow > 0 ? 0.55 : 1;

  /* balls */
  for (let bi = balls.length - 1; bi >= 0; bi--) {
    const b = balls[bi];
    if (b.stuck) { b.x = paddle.x; b.y = paddle.y - 20 - b.r; b.trail.length = 0; continue; }

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

    b.x += b.dx * b.speed * slowF * dt;
    b.y += b.dy * b.speed * slowF * dt;

    /* walls */
    if (b.x - b.r < 0)      { b.x = b.r; b.dx = Math.abs(b.dx); onWall(b); }
    if (b.x + b.r > W)      { b.x = W - b.r; b.dx = -Math.abs(b.dx); onWall(b); }
    if (b.y - b.r < 0)      { b.y = b.r; b.dy = Math.abs(b.dy); onWall(b); }
    if (b.y - b.r > H + 20) { balls.splice(bi, 1); continue; }

    /* paddle */
    if (b.dy > 0 &&
        b.x > paddle.x - paddle.w/2 - b.r && b.x < paddle.x + paddle.w/2 + b.r &&
        b.y + b.r > paddle.y - paddle.h/2 && b.y - b.r < paddle.y + paddle.h/2) {
      const hit = Math.max(-1, Math.min(1, (b.x - paddle.x) / (paddle.w / 2)));
      const ang = hit * Math.PI / 3.1;
      b.dx = Math.sin(ang); b.dy = -Math.cos(ang);
      b.speed = Math.min(b.speed + 4, 620);
      b.y = paddle.y - paddle.h/2 - b.r - 0.5;
      combo = 0;
      paddle.squash = 1;
      sparks(b.x, paddle.y - 6, "#38e8ff", 5);
      sfx.paddle();
    }

    /* bricks */
    for (let i = bricks.length - 1; i >= 0; i--) {
      const br = bricks[i];
      const cx = Math.max(br.x, Math.min(b.x, br.x + br.w));
      const cy = Math.max(br.y, Math.min(b.y, br.y + br.h));
      const dx = b.x - cx, dy = b.y - cy;
      if (dx*dx + dy*dy > b.r*b.r) continue;

      const penX = (b.r + br.w/2) - Math.abs(b.x - (br.x + br.w/2));
      const penY = (b.r + br.h/2) - Math.abs(b.y - (br.y + br.h/2));
      if (penX < penY) { b.dx = -b.dx; b.x += (b.dx > 0 ? penX : -penX); }
      else             { b.dy = -b.dy; b.y += (b.dy > 0 ? penY : -penY); }

      br.hp--;
      if (br.hp > 0) {
        br.shake = 0.18;
        sparks(cx, cy, "#dfe9f5", 6);
        sfx.tough();
      } else {
        bricks.splice(i, 1);
        combo++;
        comboPulse = 0.25;
        const mult = Math.min(1 + combo, 10);
        const pts = (br.maxHp > 1 ? 100 : 50) * mult;
        score += pts;
        const bx = br.x + br.w/2, by = br.y + br.h/2;
        burst(bx, by, br.color, 16);
        burst(bx, by, "#ffffff", 4);
        popup(bx, by - 8, "+" + pts, br.color);
        maybeDrop(bx, by);
        shake(br.maxHp > 1 ? 6 : 3, 0.12);
        sfx.brick(combo);
        if (bricks.length === 0) {
          const bonus = 250 * (levelIdx + 1);
          score += bonus;
          popup(W/2, H/2 - 40, "LEVEL CLEAR +" + bonus, "#38e8ff");
          state = "clear"; clearT = 0;
          flash("#38e8ff", 0.35);
          sfx.clear();
          saveBest();
        }
      }
      break;
    }
  }

  /* power-ups */
  for (let i = powerups.length - 1; i >= 0; i--) {
    const p = powerups[i];
    p.t += dt;
    p.y += p.vy * dt;
    if (p.y > H + 20) { powerups.splice(i, 1); continue; }
    if (p.y + p.h/2 > paddle.y - paddle.h/2 && p.y - p.h/2 < paddle.y + paddle.h/2 &&
        p.x + p.w/2 > paddle.x - paddle.w/2 && p.x - p.w/2 < paddle.x + paddle.w/2) {
      applyPU(p);
      powerups.splice(i, 1);
    }
  }

  /* life lost */
  if (balls.length === 0) {
    lives--; combo = 0;
    flash("#ff4d6a", 0.4);
    shake(10, 0.35);
    sfx.life();
    if (lives <= 0) { state = "over"; saveBest(); sfx.over(); }
    else balls = [makeStuckBall()];
  }
}
function onWall(b) { sparks(b.x, b.y, "#8ea6ff", 3); sfx.wall(); }
function saveBest() {
  if (score > best) { best = score; try { localStorage.setItem("nb.best", best); } catch (e) {} }
}

/* ------------------------------------------------------------------ draw */
function rr(x, y, w, h, r) {
  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 drawBackground() {
  const g = ctx.createLinearGradient(0, 0, 0, H);
  g.addColorStop(0, "#131a3a"); g.addColorStop(0.6, "#0a0e20"); g.addColorStop(1, "#070a18");
  ctx.fillStyle = g; ctx.fillRect(0, 0, W, H);
  ctx.strokeStyle = "rgba(120,160,255,0.05)"; ctx.lineWidth = 1;
  for (let x = 0; x < W; x += 45) { ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, H); ctx.stroke(); }
  for (let y = 0; y < H; y += 45) { ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(W, y); ctx.stroke(); }
}

function drawBrick(b) {
  const sx = b.shake > 0 ? (Math.random() - 0.5) * 6 * b.shake / 0.18 : 0;
  ctx.save();
  ctx.shadowColor = b.color; ctx.shadowBlur = 14;
  ctx.fillStyle = b.color;
  rr(b.x + sx, b.y, b.w, b.h, 5); ctx.fill();
  ctx.shadowBlur = 0;
  ctx.fillStyle = "rgba(255,255,255,0.18)";
  rr(b.x + sx + 2, b.y + 2, b.w - 4, b.h * 0.4, 3); ctx.fill();
  if (b.hp < b.maxHp) {
    ctx.strokeStyle = "rgba(10,15,30,0.7)"; ctx.lineWidth = 2;
    ctx.beginPath();
    ctx.moveTo(b.x + sx + b.w * 0.3, b.y + 3);
    ctx.lineTo(b.x + sx + b.w * 0.5, b.y + b.h * 0.55);
    ctx.lineTo(b.x + sx + b.w * 0.42, b.y + b.h - 3);
    ctx.moveTo(b.x + sx + b.w * 0.5, b.y + b.h * 0.55);
    ctx.lineTo(b.x + sx + b.w * 0.7, b.y + 5);
    ctx.stroke();
  }
  ctx.restore();
  if (b.shake > 0) b.shake -= 1/60;
}

function draw() {
  ctx.clearRect(0, 0, W, H);
  drawBackground();

  ctx.save();
  if (shakeT > 0) ctx.translate((Math.random() - 0.5) * shakeMag, (Math.random() - 0.5) * shakeMag);

  /* bricks */
  for (const b of bricks) drawBrick(b);

  /* power-ups */
  for (const p of powerups) {
    ctx.save();
    ctx.translate(p.x, p.y + Math.sin(p.t * 8) * 2);
    ctx.shadowColor = p.type.col; ctx.shadowBlur = 16;
    ctx.fillStyle = p.type.col;
    rr(-p.w/2, -p.h/2, p.w, p.h, p.h/2); ctx.fill();
    ctx.shadowBlur = 0;
    ctx.fillStyle = "#0a0e20";
    ctx.font = "bold 13px " + F;
    ctx.textAlign = "center"; ctx.textBaseline = "middle";
    ctx.fillText(p.type.id, 0, 1);
    ctx.restore();
  }

  /* balls */
  for (const b of balls) {
    for (let i = 0; i < b.trail.length; i++) {
      const t = b.trail[i], a = i / b.trail.length;
      ctx.globalAlpha = a * 0.35;
      ctx.fillStyle = "#9fd8ff";
      ctx.beginPath(); ctx.arc(t.x, t.y, b.r * a * 0.9, 0, Math.PI * 2); ctx.fill();
    }
    ctx.globalAlpha = 1;
    ctx.save();
    ctx.shadowColor = "#6ecbff"; ctx.shadowBlur = 18;
    ctx.fillStyle = "#ffffff";
    ctx.beginPath(); ctx.arc(b.x, b.y, b.r, 0, Math.PI * 2); ctx.fill();
    ctx.shadowBlur = 0;
    ctx.fillStyle = "rgba(255,255,255,0.6)";
    ctx.beginPath(); ctx.arc(b.x - 3, b.y - 3, b.r * 0.35, 0, Math.PI * 2); ctx.fill();
    ctx.restore();
  }

  /* paddle */
  ctx.save();
  ctx.translate(paddle.x, paddle.y);
  const sq = paddle.squash * 0.25;
  ctx.scale(1 + sq, 1 - sq);
  ctx.shadowColor = "#38e8ff"; ctx.shadowBlur = 16;
  const pg = ctx.createLinearGradient(0, -paddle.h/2, 0, paddle.h/2);
  pg.addColorStop(0, "#aef0ff"); pg.addColorStop(0.5, "#38e8ff"); pg.addColorStop(1, "#1e7fbf");
  ctx.fillStyle = pg;
  rr(-paddle.w/2, -paddle.h/2, paddle.w, paddle.h, paddle.h/2); ctx.fill();
  ctx.restore();

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

  /* popups */
  for (const p of popups) {
    const a = Math.min(1, p.life / p.max);
    ctx.globalAlpha = a;
    ctx.font = "bold 16px " + F;
    ctx.textAlign = "center";
    ctx.fillStyle = p.col;
    ctx.shadowColor = p.col; ctx.shadowBlur = 8;
    ctx.fillText(p.text, p.x, p.y);
    ctx.shadowBlur = 0;
  }
  ctx.globalAlpha = 1;

  /* floor glow line */
  const fg = ctx.createLinearGradient(0, H - 8, 0, H);
  fg.addColorStop(0, "rgba(255,80,120,0)"); fg.addColorStop(1, "rgba(255,80,120,0.35)");
  ctx.fillStyle = fg; ctx.fillRect(0, H - 8, W, 8);

  ctx.restore(); /* shake */

  /* flash */
  if (flashT > 0) {
    ctx.globalAlpha = Math.min(0.35, flashT);
    ctx.fillStyle = flashCol; ctx.fillRect(0, 0, W, H);
    ctx.globalAlpha = 1;
  }

  drawHUD();
  drawOverlay();
}

function drawHUD() {
  ctx.textBaseline = "alphabetic";
  /* score */
  ctx.textAlign = "left";
  ctx.font = "bold 26px " + F;
  ctx.fillStyle = "#ffffff";
  ctx.fillText("SCORE " + score, 20, 38);
  ctx.font = "12px " + F;
  ctx.fillStyle = "rgba(160,180,220,0.8)";
  ctx.fillText("BEST " + Math.max(best, score), 22, 56);

  /* level */
  ctx.textAlign = "center";
  ctx.font = "bold 15px " + F;
  ctx.fillStyle = "#8ea6ff";
  ctx.fillText("LEVEL " + Math.min(levelIdx + 1, LEVELS.length) + " / " + LEVELS.length, W/2, 34);

  /* combo */
  if (combo > 1) {
    const s = 1 + (comboPulse > 0 ? comboPulse * 1.6 : 0);
    ctx.save();
    ctx.translate(W/2, 62); ctx.scale(s, s);
    ctx.font = "bold 17px " + F;
    ctx.fillStyle = "#ffe14d";
    ctx.shadowColor = "#ffe14d"; ctx.shadowBlur = 10;
    ctx.fillText("COMBO ×" + Math.min(1 + combo, 10), 0, 0);
    ctx.restore();
  }

  /* lives */
  ctx.textAlign = "right";
  ctx.font = "22px " + F;
  ctx.fillStyle = "#ff5d8f";
  ctx.shadowColor = "#ff5d8f"; ctx.shadowBlur = 8;
  let hs = "";
  for (let i = 0; i < lives; i++) hs += "\u2665 ";
  ctx.fillText(hs, W - 20, 38);
  ctx.shadowBlur = 0;

  /* active effects */
  ctx.textAlign = "right";
  ctx.font = "11px " + F;
  let ey = 58;
  if (effects.expand > 0) { ctx.fillStyle = "#7dff6a"; ctx.fillText("EXPAND " + effects.expand.toFixed(1) + "s", W - 20, ey); ey += 15; }
  if (effects.slow > 0)   { ctx.fillStyle = "#ffe14d"; ctx.fillText("SLOW-MO " + effects.slow.toFixed(1) + "s", W - 20, ey); }

  /* sound hint */
  ctx.textAlign = "left";
  ctx.font = "11px " + F;
  ctx.fillStyle = "rgba(160,180,220,0.45)";
  ctx.fillText(muted ? "SOUND: OFF  (M)" : "SOUND: ON   (M)", 20, H - 12);

  /* level intro */
  if (introT > 0 && state === "playing") {
    const a = Math.min(1, introT / 0.5);
    ctx.globalAlpha = a;
    ctx.textAlign = "center";
    ctx.font = "bold 54px " + F;
    ctx.fillStyle = "#38e8ff";
    ctx.shadowColor = "#38e8ff"; ctx.shadowBlur = 24;
    ctx.fillText("LEVEL " + (levelIdx + 1), W/2, H/2 - 60);
    ctx.shadowBlur = 0;
    ctx.globalAlpha = 1;
  }
}

function centerText(txt, y, size, col, glow) {
  ctx.textAlign = "center";
  ctx.font = "bold " + size + "px " + F;
  if (glow) { ctx.shadowColor = glow; ctx.shadowBlur = 22; }
  ctx.fillStyle = col;
  ctx.fillText(txt, W/2, y);
  ctx.shadowBlur = 0;
}

function drawOverlay() {
  if (state === "clear") {
    centerText("LEVEL " + (levelIdx + 1) + " CLEARED", H/2, 46, "#38e8ff", "#38e8ff");
    return;
  }
  if (state === "paused") {
    ctx.fillStyle = "rgba(5,8,20,0.7)"; ctx.fillRect(0, 0, W, H);
    centerText("PAUSED", H/2 - 20, 52, "#ffffff", "#8ea6ff");
    ctx.font = "16px " + F; ctx.fillStyle = "rgba(200,215,255,0.8)"; ctx.textAlign = "center";
    ctx.fillText("press P / Esc or Space to resume", W/2, H/2 + 22);
    return;
  }
  if (state !== "menu" && state !== "over" && state !== "win") return;

  ctx.fillStyle = "rgba(5,8,20,0.78)"; ctx.fillRect(0, 0, W, H);

  if (state === "menu") {
    const hue = (tGlobal * 40) % 360;
    centerText("NEON BREAKOUT", H/2 - 110, 64, "#ffffff", "hsl(" + hue + ",100%,70%)");
    ctx.font = "16px " + F; ctx.textAlign = "center";
    ctx.fillStyle = "rgba(200,215,255,0.85)";
    ctx.fillText("Move with MOUSE or \u2190 \u2192 / A D keys", W/2, H/2 - 30);
    ctx.fillText("Launch with CLICK or SPACE \u00b7 Pause with P \u00b7 Mute with M", W/2, H/2 - 2);
    ctx.fillText("Break every brick \u00b7 Chain hits for combo multipliers \u00b7 Catch power-ups", W/2, H/2 + 26);
    if (best > 0) {
      ctx.fillStyle = "#ffe14d"; ctx.font = "bold 15px " + F;
      ctx.fillText("BEST " + best, W/2, H/2 + 66);
    }
  }
  if (state === "over") {
    centerText("GAME OVER", H/2 - 80, 58, "#ff5d8f", "#ff5d8f");
    centerText("SCORE " + score, H/2 - 20, 28, "#ffffff");
    centerText("BEST " + Math.max(best, score), H/2 + 16, 16, "#ffe14d");
  }
  if (state === "win") {
    centerText("YOU WIN!", H/2 - 80, 64, "#7dff6a", "#7dff6a");
    centerText("ALL 6 LEVELS CLEARED", H/2 - 30, 20, "#ffffff");
    centerText("SCORE " + score, H/2 + 8, 28, "#ffffff");
    centerText("BEST " + Math.max(best, score), H/2 + 44, 16, "#ffe14d");
  }

  const blink = 0.5 + 0.5 * Math.sin(tGlobal * 5);
  ctx.globalAlpha = blink;
  ctx.font = "bold 20px " + F; ctx.textAlign = "center";
  ctx.fillStyle = "#38e8ff";
  ctx.fillText(state === "menu" ? "PRESS SPACE OR CLICK TO START" : "PRESS SPACE / R OR CLICK TO PLAY AGAIN", W/2, H/2 + 120);
  ctx.globalAlpha = 1;
}

/* ------------------------------------------------------------------ loop */
let last = performance.now();
function frame(now) {
  const dt = Math.min(0.033, (now - last) / 1000);
  last = now;
  update(dt);
  draw();
  requestAnimationFrame(frame);
}
requestAnimationFrame(frame);

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

Compare with

Glimpse: other attempts