• 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
unbiased/pareto-26.9
Route
OpenRouter API, one user message
Served by
Unbiased
Reasoning
provider default
Limits
60 min · 80,000 output tokens ($0.60 at $7.5/M)
Date
22 Oct 2026
Time
2 min 28 s
Size
24 kB
Tokens
83 in, 8,440 out
List price
$0.064 ($2.5 in / $7.5 out per M)
Paid
$0.064

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

24 kB 705 lines Raw

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="#090c19">
<title>PRISM — Breakout</title>
<style>
  :root { color-scheme: dark; --ink:#eef5ff; --muted:#8e9bb9; --cyan:#67efff; }
  * { box-sizing:border-box; }
  body {
    margin:0; min-height:100vh; min-height:100svh; padding:28px 18px;
    display:flex; align-items:center; justify-content:center;
    background:radial-gradient(ellipse at 20% 10%,#202041 0,transparent 52%),
               radial-gradient(ellipse at 85% 85%,#102d3a 0,transparent 45%),#090c19;
    color:var(--ink); font-family:system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;
  }
  main { width:min(1040px,100%); }
  header { display:flex; align-items:center; justify-content:space-between; gap:14px; margin-bottom:20px; }
  .brand { font-weight:900; font-size:28px; letter-spacing:7px; }
  .brand span { color:var(--cyan); }
  .tag { color:var(--muted); font-size:10px; letter-spacing:3px; margin-top:5px; }
  .buttons { display:flex; gap:8px; }
  button {
    background:#ffffff08; border:1px solid #ffffff20; color:var(--ink);
    border-radius:10px; padding:10px 13px; cursor:pointer; font:600 12px system-ui;
    transition:background .15s,border-color .15s;
  }
  button:hover { background:#ffffff14; border-color:#67efff80; }
  button:focus-visible { outline:2px solid var(--cyan); outline-offset:3px; }
  .stats {
    display:grid; grid-template-columns:1.2fr 1.2fr 1fr 1fr;
    border:1px solid #ffffff13; border-bottom:0; border-radius:16px 16px 0 0;
    background:#0e1424; padding:17px 22px; gap:16px;
  }
  .label { font-size:10px; font-weight:700; color:var(--muted); letter-spacing:2px; }
  .value { font-size:23px; font-weight:750; font-variant-numeric:tabular-nums; margin-top:3px; }
  #score { color:var(--cyan); }
  #lives { color:#ff82af; letter-spacing:5px; }
  .screen {
    position:relative; overflow:hidden; border:1px solid #ffffff1c;
    border-radius:0 0 16px 16px; background:#090e1d;
    box-shadow:0 30px 90px #0006,0 0 55px #67efff05;
  }
  canvas { display:block; width:100%; aspect-ratio:8/5; touch-action:none; cursor:crosshair; }
  footer { display:flex; justify-content:space-between; gap:16px; margin-top:16px; color:var(--muted); font-size:12px; line-height:1.9; }
  kbd { font:11px system-ui; background:#ffffff09; border:1px solid #ffffff18; border-radius:4px; padding:2px 5px; color:#d6e2f7; }
  .accent { color:#67efffaa; }
  .sr { position:absolute; width:1px; height:1px; overflow:hidden; clip-path:inset(50%); }
  @media (max-width:600px) {
    body { padding:16px 10px; }
    header { margin-bottom:14px; }
    .brand { font-size:22px; letter-spacing:4px; }
    .tag { letter-spacing:1.5px; font-size:9px; }
    button { padding:9px 8px; font-size:11px; }
    .stats { padding:12px; gap:8px; }
    .label { font-size:8px; letter-spacing:1px; }
    .value { font-size:18px; }
    #lives { letter-spacing:2px; }
    footer { flex-direction:column; gap:3px; font-size:11px; }
  }
</style>
</head>
<body>
<main>
  <header>
    <div>
      <div class="brand">PR<span>I</span>SM</div>
      <div class="tag">BREAK THE SPECTRUM</div>
    </div>
    <div class="buttons">
      <button id="sound" aria-pressed="false" title="Toggle synthesized sound">Sound: off</button>
      <button id="pause" title="Pause or resume (P)">Pause</button>
      <button id="restart" title="Restart (R)">Restart</button>
    </div>
  </header>
  <section class="stats" aria-label="Game statistics">
    <div><div class="label">SCORE</div><div class="value" id="score">000000</div></div>
    <div><div class="label">PERSONAL BEST</div><div class="value" id="best">000000</div></div>
    <div><div class="label">SECTOR</div><div class="value" id="level">01 / 05</div></div>
    <div><div class="label">LIVES</div><div class="value" id="lives">♥♥♥</div></div>
  </section>
  <div class="screen">
    <canvas id="game" width="960" height="600" tabindex="0"
      aria-label="Prism Breakout. Move with left and right arrows, A and D, mouse, or touch. Space or click to launch. P pauses. R restarts.">
      Your browser needs canvas support to play.
    </canvas>
  </div>
  <footer>
    <div><kbd>←</kbd> <kbd>→</kbd> / <kbd>A</kbd> <kbd>D</kbd> or mouse to move · <kbd>Space</kbd> / click to launch · <kbd>P</kbd> pause</div>
    <div class="accent">5 sectors · Angled paddle shots · Keep the combo alive</div>
  </footer>
  <div id="announce" class="sr" aria-live="polite"></div>
</main>
<script>
(() => {
  'use strict';
  const canvas = document.getElementById('game');
  const ctx = canvas.getContext('2d');
  const $ = id => document.getElementById(id);
  const W = 960, H = 600, TAU = Math.PI * 2;
  const colors = ['#ff79b0','#bc91ff','#7aa6ff','#65e5ff','#6cf0c0','#ffd280'];
  const names = ['FIRST LIGHT','DIAMOND DUST','CROSS CURRENT','NEON FORTRESS','SUPERNOVA'];
  const reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
  const clamp = (v,a,b) => Math.max(a,Math.min(b,v));
  const rand = (a,b) => a + Math.random() * (b-a);
  const keys = new Set();
  let mode = 'ready', pausedFrom = 'playing';
  let level = 0, score = 0, lives = 3, best = 0, combo = 0;
  let bricks = [], particles = [], rings = [], labels = [], trail = [];
  let remaining = 0, shake = 0, flash = 0, elapsed = 0, lastTime = 0;
  let pointerTarget = W/2, control = 'keyboard', launchDirection = 1;
  let readyHeading = 'FIND YOUR ANGLE';
  let soundOn = false, audio = null;
  let scoreShown = -1, bestShown = -1, levelShown = -1, livesShown = -1;
  const paddle = {x:W/2, y:546, w:128, h:14, velocity:0};
  const ball = {x:W/2, y:530, vx:0, vy:0, r:8, speed:390};

  try { best = Math.max(0, Number(localStorage.getItem('prism-breakout-best')) || 0); } catch (_) {}

  function announce(text) { $('announce').textContent = text; }

  function initAudio() {
    if (!soundOn) return;
    try {
      if (!audio) {
        const AC = window.AudioContext || window.webkitAudioContext;
        if (AC) audio = new AC();
      }
      if (audio && audio.state === 'suspended') audio.resume().catch(() => {});
    } catch (_) {}
  }

  function tone(freq, duration=.08, type='sine', volume=.04, delay=0, endFreq=freq) {
    if (!soundOn || !audio || audio.state !== 'running') return;
    const t = audio.currentTime + delay;
    const oscillator = audio.createOscillator();
    const gain = audio.createGain();
    oscillator.type = type;
    oscillator.frequency.setValueAtTime(freq,t);
    oscillator.frequency.exponentialRampToValueAtTime(Math.max(20,endFreq),t+duration);
    gain.gain.setValueAtTime(.0001,t);
    gain.gain.exponentialRampToValueAtTime(volume,t+.006);
    gain.gain.exponentialRampToValueAtTime(.0001,t+duration);
    oscillator.connect(gain);
    gain.connect(audio.destination);
    oscillator.start(t);
    oscillator.stop(t+duration+.015);
  }

  function chord(win) {
    (win ? [392,494,587,784] : [260,196,130]).forEach((n,i) =>
      tone(n,.24,'triangle',.055,i*.085));
  }

  function hud() {
    if (score !== scoreShown) {
      $('score').textContent = String(score).padStart(6,'0');
      scoreShown = score;
    }
    if (best !== bestShown) {
      $('best').textContent = String(best).padStart(6,'0');
      bestShown = best;
    }
    if (level !== levelShown) {
      $('level').textContent = String(level+1).padStart(2,'0')+' / 05';
      levelShown = level;
    }
    if (lives !== livesShown) {
      $('lives').textContent = '♥'.repeat(lives)+'♡'.repeat(3-lives);
      $('lives').setAttribute('aria-label',lives+' lives');
      livesShown = lives;
    }
  }

  function addScore(points) {
    score += points;
    if (score > best) {
      best = score;
      try { localStorage.setItem('prism-breakout-best',String(best)); } catch (_) {}
    }
  }

  function makeLevel() {
    bricks = [];
    const cols = 12, rows = 6, bw = 66, bh = 23, gap = 7;
    const left = (W-(cols*bw+(cols-1)*gap))/2;
    for (let row=0;row<rows;row++) {
      for (let col=0;col<cols;col++) {
        let exists = true, hp = 1;
        if (level === 1) {
          exists = Math.abs(col-5.5)+Math.abs(row-2.5)*1.45 < 7;
          hp = row === 2 || row === 3 ? 2 : 1;
        } else if (level === 2) {
          exists = (col+row)%4 !== 0;
          hp = (col+row)%3 === 0 ? 2 : 1;
        } else if (level === 3) {
          exists = row < 2 || col%3 !== 1 || row === 5;
          hp = row < 2 ? 3 : 2;
        } else if (level === 4) {
          exists = !(row===0 && (col===0 || col===11));
          hp = 1+((row+col)%3);
        }
        if (!exists) continue;
        bricks.push({
          x:left+col*(bw+gap), y:80+row*(bh+gap), w:bw, h:bh,
          hp, maxHp:hp, color:colors[(row+level)%colors.length], hit:0
        });
      }
    }
    remaining = bricks.length;
    paddle.w = 128-level*5;
    paddle.x = W/2;
    pointerTarget = W/2;
    combo = 0;
    resetBall();
  }

  function resetBall() {
    ball.speed = 390+level*35;
    ball.vx = 0; ball.vy = 0;
    ball.x = paddle.x;
    ball.y = paddle.y-ball.r-3;
    trail = [];
    paddle.velocity = 0;
  }

  function restart() {
    level = 0; score = 0; lives = 3; combo = 0;
    particles = []; rings = []; labels = []; shake = 0; flash = 0;
    keys.clear();
    readyHeading = 'FIND YOUR ANGLE';
    mode = 'ready';
    $('pause').textContent = 'Pause';
    makeLevel();
    hud();
    announce('New game. Sector 1. Press Space or click to launch.');
  }

  function launch() {
    if (mode !== 'ready') return;
    mode = 'playing';
    const angle = .31 * launchDirection;
    launchDirection *= -1;
    ball.vx = Math.sin(angle)*ball.speed;
    ball.vy = -Math.cos(angle)*ball.speed;
    burst(ball.x,ball.y,'#67efff',14,120);
    tone(430,.14,'triangle',.06,0,740);
    announce('Sector '+(level+1)+'. '+names[level]+'.');
  }

  function pause() {
    if (mode === 'paused') {
      mode = pausedFrom;
      $('pause').textContent = 'Pause';
      announce('Resumed.');
    } else if (mode === 'playing' || mode === 'ready') {
      pausedFrom = mode;
      mode = 'paused';
      keys.clear();
      $('pause').textContent = 'Resume';
      announce('Paused.');
    }
  }

  function activate() {
    initAudio();
    if (mode === 'paused') pause();
    else if (mode === 'ready') launch();
    else if (mode === 'over' || mode === 'won') { restart(); launch(); }
  }

  function burst(x,y,color,n=18,speed=180) {
    if (reduced) n = Math.ceil(n*.35);
    for (let i=0;i<n;i++) {
      const a=rand(0,TAU), v=rand(speed*.2,speed);
      const life=rand(.25,.65);
      particles.push({x,y,vx:Math.cos(a)*v,vy:Math.sin(a)*v,
        life,maxLife:life,size:rand(1.5,4),color});
    }
    if (particles.length>650) particles.splice(0,particles.length-650);
  }

  function ring(x,y,color,max=55) {
    rings.push({x,y,color,age:0,duration:.4,max});
  }

  function hitBrick(b) {
    b.hp--;
    b.hit = .16;
    combo++;
    const multiplier = Math.min(5,1+Math.floor((combo-1)/4));
    if (b.hp === 0) {
      remaining--;
      const pts = 100*multiplier;
      addScore(pts);
      burst(b.x+b.w/2,b.y+b.h/2,b.color,22,215);
      ring(b.x+b.w/2,b.y+b.h/2,b.color,43);
      if (multiplier > 1) labels.push({
        x:b.x+b.w/2,y:b.y,text:'+'+pts,color:b.color,life:.75,maxLife:.75
      });
      shake = Math.max(shake,3.2);
      ball.speed = Math.min(720,ball.speed+3);
    } else {
      addScore(25*multiplier);
      burst(ball.x,ball.y,b.color,9,120);
      shake = Math.max(shake,1.7);
    }
    tone(290*Math.pow(2,Math.min(combo,20)/15),.085,'triangle',.045);
    const length = Math.hypot(ball.vx,ball.vy);
    if (length) {
      ball.vx *= ball.speed/length;
      ball.vy *= ball.speed/length;
    }
  }

  function completeLevel() {
    addScore(500*(level+1));
    chord(true);
    flash = .26;
    shake = 7;
    for (let i=0;i<6;i++) burst(rand(130,W-130),rand(150,380),colors[i],28,280);
    if (level === 4) {
      mode = 'won';
      announce('All five sectors cleared! Final score '+score+'. Click or press Space to play again.');
    } else {
      level++;
      mode = 'ready';
      readyHeading = 'SECTOR CLEAR';
      makeLevel();
      announce('Sector cleared. Sector '+(level+1)+'. Press Space or click to launch.');
    }
  }

  function loseLife() {
    lives--;
    combo = 0;
    burst(ball.x,H-15,'#ff79b0',32,240);
    shake = 9;
    tone(220,.35,'sawtooth',.035,0,50);
    if (lives <= 0) {
      mode = 'over';
      chord(false);
      announce('Game over. Score '+score+'. Press Space or click to try again.');
    } else {
      mode = 'ready';
      readyHeading = 'STAY IN THE FLOW';
      resetBall();
      announce(lives+' lives left. Press Space or click to launch.');
    }
  }

  // Circle versus rectangle: resolve along the actual contact normal.
  function collideBrick(b) {
    const nx = clamp(ball.x,b.x,b.x+b.w);
    const ny = clamp(ball.y,b.y,b.y+b.h);
    let dx = ball.x-nx, dy = ball.y-ny;
    let dist = Math.hypot(dx,dy);
    if (dist >= ball.r) return false;
    if (dist < .00001) {
      const edges = [
        {d:ball.x-b.x,x:-1,y:0},
        {d:b.x+b.w-ball.x,x:1,y:0},
        {d:ball.y-b.y,x:0,y:-1},
        {d:b.y+b.h-ball.y,x:0,y:1}
      ];
      edges.sort((a,b)=>a.d-b.d);
      dx = edges[0].x; dy = edges[0].y;
      dist = -edges[0].d;
    } else { dx /= dist; dy /= dist; }
    ball.x += dx*(ball.r-dist+.05);
    ball.y += dy*(ball.r-dist+.05);
    const dot = ball.vx*dx+ball.vy*dy;
    if (dot < 0) {
      ball.vx -= 2*dot*dx;
      ball.vy -= 2*dot*dy;
      hitBrick(b);
      return true;
    }
    return false;
  }

  function step(dt) {
    let direction = 0;
    if (keys.has('ArrowLeft') || keys.has('KeyA')) direction--;
    if (keys.has('ArrowRight') || keys.has('KeyD')) direction++;
    const oldX = paddle.x;
    if (control === 'pointer') {
      paddle.x += clamp(pointerTarget-paddle.x,-1500*dt,1500*dt);
    } else {
      paddle.x += direction*700*dt;
    }
    paddle.x = clamp(paddle.x,20+paddle.w/2,W-20-paddle.w/2);
    paddle.velocity = (paddle.x-oldX)/dt;
    if (mode === 'ready') {
      ball.x = paddle.x; ball.y = paddle.y-ball.r-3;
      return;
    }
    if (mode !== 'playing') return;

    const previousY = ball.y;
    ball.x += ball.vx*dt;
    ball.y += ball.vy*dt;
    if (ball.x-ball.r < 16 && ball.vx < 0) {
      ball.x = 16+ball.r; ball.vx = Math.abs(ball.vx);
      burst(17,ball.y,'#67efff',5,80); tone(180,.045);
    }
    if (ball.x+ball.r > W-16 && ball.vx > 0) {
      ball.x = W-16-ball.r; ball.vx = -Math.abs(ball.vx);
      burst(W-17,ball.y,'#67efff',5,80); tone(180,.045);
    }
    if (ball.y-ball.r < 16 && ball.vy < 0) {
      ball.y = 16+ball.r; ball.vy = Math.abs(ball.vy);
      tone(220,.045);
    }

    if (ball.vy > 0 &&
        previousY+ball.r <= paddle.y+4 &&
        ball.y+ball.r >= paddle.y &&
        ball.x+ball.r >= paddle.x-paddle.w/2 &&
        ball.x-ball.r <= paddle.x+paddle.w/2) {
      const offset = clamp((ball.x-paddle.x)/(paddle.w/2),-1,1);
      const angle = clamp(offset*1.08+paddle.velocity*.00010,-1.13,1.13);
      ball.vx = Math.sin(angle)*ball.speed;
      ball.vy = -Math.cos(angle)*ball.speed;
      ball.y = paddle.y-ball.r-.1;
      combo = 0;
      burst(ball.x,paddle.y,'#67efff',12,150);
      ring(ball.x,paddle.y,'#67efff',30);
      tone(280+Math.abs(offset)*170,.11,'sine',.065,0,180);
    }

    for (const b of bricks) {
      if (b.hp > 0 && collideBrick(b)) {
        // Avoid trajectories that get stuck nearly horizontal.
        if (Math.abs(ball.vy)<ball.speed*.22) {
          ball.vy = (ball.vy<0?-1:1)*ball.speed*.22;
          ball.vx = (ball.vx<0?-1:1)*Math.sqrt(ball.speed**2-ball.vy**2);
        }
        break;
      }
    }
    if (remaining === 0) { completeLevel(); return; }
    if (ball.y-ball.r > H) loseLife();
  }

  function update(dt) {
    if (mode === 'paused') return;
    elapsed += dt;
    for (const b of bricks) b.hit = Math.max(0,b.hit-dt);
    shake = Math.max(0,shake-dt*23);
    flash = Math.max(0,flash-dt);
    for (const p of particles) {
      p.life -= dt; p.x += p.vx*dt; p.y += p.vy*dt;
      p.vy += 210*dt;
      p.vx *= Math.exp(-dt*2);
    }
    particles = particles.filter(p=>p.life>0);
    for (const r of rings) r.age += dt;
    rings = rings.filter(r=>r.age<r.duration);
    for (const l of labels) { l.life -= dt; l.y -= dt*42; }
    labels = labels.filter(l=>l.life>0);

    if (mode === 'playing' || mode === 'ready') {
      const count = Math.max(1,Math.ceil(dt/(1/180)));
      for (let i=0;i<count;i++) {
        step(dt/count);
        if (mode !== 'ready' && mode !== 'playing') break;
      }
    }
    if (mode === 'playing') {
      trail.unshift({x:ball.x,y:ball.y});
      if (trail.length>15) trail.pop();
    } else if (trail.length) trail.pop();
    hud();
  }

  function rounded(x,y,w,h,r,fill) {
    ctx.fillStyle = fill;
    ctx.beginPath();
    ctx.roundRect(x,y,w,h,r);
    ctx.fill();
  }

  function text(str,x,y,size,color='#eef5ff',weight=600) {
    ctx.font = weight+' '+size+'px system-ui, sans-serif';
    ctx.textAlign = 'center';
    ctx.textBaseline = 'middle';
    ctx.fillStyle = color;
    ctx.fillText(str,x,y);
  }

  function drawOverlay() {
    if (mode === 'playing') return;
    ctx.fillStyle = mode === 'ready' ? '#090e1d55' : '#090e1dc9';
    ctx.fillRect(0,0,W,H);
    const boxY = 287;
    rounded(200,boxY,560,192,20,'#10182bf5');
    ctx.strokeStyle = '#9adfff32';
    ctx.lineWidth = 1;
    ctx.beginPath(); ctx.roundRect(200,boxY,560,192,20); ctx.stroke();
    let eyebrow,heading,sub,action;
    if (mode === 'ready') {
      eyebrow = 'SECTOR '+String(level+1).padStart(2,'0')+'  /  '+names[level];
      heading = readyHeading;
      sub = level === 0 && lives === 3
        ? 'Clear the bricks. Catch the light. Chase your best.'
        : (level >= 1 ? 'Armored bricks take extra hits. You’ve got this.' : 'One good bounce changes everything.');
      action = 'SPACE / CLICK TO LAUNCH';
    } else if (mode === 'paused') {
      eyebrow = 'TAKE A BREATHER';
      heading = 'ON HOLD';
      sub = 'Your next perfect shot can wait.';
      action = 'P / SPACE / CLICK TO RESUME';
    } else if (mode === 'won') {
      eyebrow = 'ALL 5 SECTORS CLEARED';
      heading = 'SPECTRUM COMPLETE';
      sub = 'Final score '+score.toLocaleString()+'  ·  Beautifully broken.';
      action = 'SPACE / CLICK TO PLAY AGAIN';
    } else {
      eyebrow = 'THE LIGHT FADES. YOU DON’T.';
      heading = 'ONE MORE RUN?';
      sub = 'Final score '+score.toLocaleString()+'  ·  Best '+best.toLocaleString();
      action = 'SPACE / CLICK TO RESTART';
    }
    text(eyebrow,W/2,boxY+30,11,'#67efff',750);
    text(heading,W/2,boxY+70,32,'#f3f7ff',850);
    text(sub,W/2,boxY+109,14,'#a6b6d0',450);
    const pulse = reduced ? 1 : .8+.2*Math.sin(elapsed*3);
    ctx.globalAlpha = pulse;
    rounded(W/2-165,boxY+138,330,34,8,'#67efff18');
    text(action,W/2,boxY+155,12,'#8af3ff',750);
    ctx.globalAlpha = 1;
  }

  function draw() {
    ctx.clearRect(0,0,W,H);
    ctx.fillStyle = '#090e1d';
    ctx.fillRect(0,0,W,H);
    ctx.save();
    if (!reduced && shake>0 && mode !== 'paused')
      ctx.translate(rand(-shake,shake),rand(-shake,shake));

    // A quiet grid and a low horizon glow keep the action readable.
    ctx.strokeStyle = '#839cdb09'; ctx.lineWidth = 1;
    ctx.beginPath();
    for (let x=0;x<=W;x+=40) { ctx.moveTo(x,0); ctx.lineTo(x,H); }
    for (let y=0;y<=H;y+=40) { ctx.moveTo(0,y); ctx.lineTo(W,y); }
    ctx.stroke();
    const glow = ctx.createRadialGradient(W/2,555,10,W/2,555,430);
    glow.addColorStop(0,'#17435835'); glow.addColorStop(1,'#17435800');
    ctx.fillStyle=glow; ctx.fillRect(0,0,W,H);

    ctx.strokeStyle = '#6bdbff28'; ctx.lineWidth = 2;
    ctx.beginPath(); ctx.moveTo(16,520); ctx.lineTo(16,16);
    ctx.lineTo(W-16,16); ctx.lineTo(W-16,520); ctx.stroke();
    text(names[level],W/2,43,11,'#627695',650);
    ctx.strokeStyle = '#ff79b025'; ctx.setLineDash([4,9]);
    ctx.beginPath(); ctx.moveTo(24,H-15); ctx.lineTo(W-24,H-15); ctx.stroke();
    ctx.setLineDash([]);

    for (const b of bricks) {
      if (b.hp<=0) continue;
      ctx.shadowColor = b.color; ctx.shadowBlur = reduced ? 0 : 9;
      rounded(b.x,b.y,b.w,b.h,5,b.hit>0?'#ffffff':b.color);
      ctx.shadowBlur = 0;
      rounded(b.x+2,b.y+2,b.w-4,3,2,'#ffffff50');
      rounded(b.x+2,b.y+b.h-5,b.w-4,3,2,'#00000022');
      if (b.hp>1) {
        for (let j=0;j<b.hp;j++) {
          rounded(b.x+b.w/2+(j-(b.hp-1)/2)*9-2,b.y+b.h/2-2,4,4,1,'#17203980');
        }
      }
    }

    for (const r of rings) {
      const t=r.age/r.duration;
      ctx.globalAlpha=(1-t)*.6; ctx.strokeStyle=r.color;
      ctx.lineWidth=2*(1-t)+.5;
      ctx.beginPath(); ctx.arc(r.x,r.y,4+t*r.max,0,TAU); ctx.stroke();
    }
    ctx.globalAlpha=1;

    if (!reduced) {
      for (let i=trail.length-1;i>=0;i--) {
        ctx.globalAlpha=(1-i/trail.length)*.28;
        ctx.fillStyle='#82efff';
        ctx.beginPath(); ctx.arc(trail[i].x,trail[i].y,ball.r*(1-i/(trail.length+2)),0,TAU); ctx.fill();
      }
      ctx.globalAlpha=1;
    }

    ctx.shadowColor='#67efff'; ctx.shadowBlur=reduced?0:22;
    const pg=ctx.createLinearGradient(paddle.x-paddle.w/2,0,paddle.x+paddle.w/2,0);
    pg.addColorStop(0,'#61b8ff'); pg.addColorStop(.5,'#b7fbff'); pg.addColorStop(1,'#61b8ff');
    rounded(paddle.x-paddle.w/2,paddle.y,paddle.w,paddle.h,7,pg);
    ctx.shadowBlur=0;
    rounded(paddle.x-14,paddle.y+5,28,3,2,'#246c8b80');
    rounded(paddle.x-paddle.w/2+8,paddle.y+2,paddle.w-16,2,1,'#ffffffbb');

    if (mode !== 'over' && mode !== 'won') {
      ctx.shadowColor='#a8f5ff'; ctx.shadowBlur=reduced?0:19;
      ctx.fillStyle='#f5ffff';
      ctx.beginPath(); ctx.arc(ball.x,ball.y,ball.r,0,TAU); ctx.fill();
      ctx.shadowBlur=0;
    }

    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;
    for (const l of labels) {
      ctx.globalAlpha=l.life/l.maxLife;
      text(l.text,l.x,l.y,15,l.color,800);
    }
    ctx.globalAlpha=1;
    if (mode === 'playing' && combo>=4) {
      const mult=Math.min(5,1+Math.floor((combo-1)/4));
      text(mult+'×  COMBO',W-105,306,18,'#ffd280',800);
      text(combo+' consecutive hits',W-105,329,11,'#8e9bb9',500);
    }
    ctx.restore();
    if (!reduced && flash>0) {
      ctx.fillStyle='rgba(174,245,255,'+(flash*.5)+')';
      ctx.fillRect(0,0,W,H);
    }
    drawOverlay();
  }

  function resize() {
    const rect=canvas.getBoundingClientRect();
    const dpr=Math.min(window.devicePixelRatio||1,2);
    canvas.width=Math.round(rect.width*dpr);
    canvas.height=Math.round(rect.height*dpr);
    ctx.setTransform(canvas.width/W,0,0,canvas.height/H,0,0);
    draw();
  }

  function pointerPosition(e) {
    const rect=canvas.getBoundingClientRect();
    pointerTarget=clamp((e.clientX-rect.left)/rect.width*W,0,W);
    control='pointer';
  }
  canvas.addEventListener('pointermove',pointerPosition);
  canvas.addEventListener('pointerdown',e=>{
    if (e.button !== 0) return;
    pointerPosition(e);
    if (mode === 'ready') {
      paddle.x=clamp(pointerTarget,20+paddle.w/2,W-20-paddle.w/2);
      ball.x=paddle.x;
    }
    canvas.focus({preventScroll:true});
    try { canvas.setPointerCapture(e.pointerId); } catch (_) {}
    activate();
  });
  document.addEventListener('keydown',e=>{
    const handled=['ArrowLeft','ArrowRight','KeyA','KeyD','Space','KeyP','KeyR','Escape'];
    if (!handled.includes(e.code)) return;
    if (e.target instanceof HTMLButtonElement && e.code==='Space') return;
    e.preventDefault();
    if (['ArrowLeft','ArrowRight','KeyA','KeyD'].includes(e.code)) {
      keys.add(e.code); control='keyboard'; return;
    }
    if (e.repeat) return;
    initAudio();
    if (e.code==='Space') activate();
    if (e.code==='KeyP' || e.code==='Escape') pause();
    if (e.code==='KeyR') restart();
  });
  document.addEventListener('keyup',e=>keys.delete(e.code));
  window.addEventListener('blur',()=>{
    keys.clear();
    if (mode==='playing') pause();
  });
  document.addEventListener('visibilitychange',()=>{
    if (document.hidden && mode==='playing') pause();
  });
  $('pause').addEventListener('click',()=>{initAudio();pause();});
  $('restart').addEventListener('click',()=>{initAudio();restart();});
  $('sound').addEventListener('click',()=>{
    soundOn=!soundOn;
    $('sound').textContent='Sound: '+(soundOn?'on':'off');
    $('sound').setAttribute('aria-pressed',String(soundOn));
    initAudio();
    if (soundOn) tone(660,.12,'sine',.04);
  });
  window.addEventListener('resize',resize);

  function frame(time) {
    const dt=Math.min((time-lastTime)/1000 || 1/60,.035);
    lastTime=time;
    update(dt);
    draw();
    requestAnimationFrame(frame);
  }

  restart();
  resize();
  requestAnimationFrame(frame);
})();
</script>
</body>
</html>

Compare with

Pareto 26.9: other attempts