• 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.10
Route
OpenRouter API, one user message
Served by
Unbiased
Reasoning
provider default
Limits
60 min · 131,072 output tokens
Date
22 Oct 2026
Time
2 min 30 s
Size
23 kB
Tokens
100 in, 8,649 out
List price
$0.028 ($0.8 in / $3.2 out per M)
Paid
$0.028

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 515 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="#0a0d18">
<title>PRISM — Breakout</title>
<style>
  :root { color-scheme: dark; --ink:#eaf0ff; --muted:#8d9ab7; --mint:#6fffd5; }
  * { box-sizing:border-box; }
  body {
    margin:0; min-height:100vh; background:#090c16; color:var(--ink);
    font-family:system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;
    display:flex; align-items:center; justify-content:center; padding:24px;
    background-image:radial-gradient(ellipse at 15% 0%,#192543 0%,transparent 55%),
                     radial-gradient(ellipse at 90% 100%,#211735 0%,transparent 50%);
  }
  main { width:min(100%,1000px); }
  header { display:flex; align-items:center; justify-content:space-between; gap:16px; margin-bottom:20px; }
  .brand { font-size:28px; font-weight:900; letter-spacing:7px; line-height:1; }
  .brand span { color:var(--mint); }
  .tag { color:var(--muted); font-size:10px; letter-spacing:3px; margin-top:9px; }
  .stats { display:flex; gap:28px; text-align:right; }
  .label { font-size:10px; color:var(--muted); letter-spacing:2px; margin-bottom:4px; }
  .value { font-size:23px; font-weight:750; font-variant-numeric:tabular-nums; }
  #lives { color:#ff7ba8; letter-spacing:3px; }
  .arena {
    position:relative; border:1px solid #303b56; border-radius:18px; overflow:hidden;
    background:#0c1120; box-shadow:0 25px 90px #0007,0 0 50px #67ffd507;
  }
  canvas { width:100%; aspect-ratio:3/2; display:block; touch-action:none; cursor:crosshair; outline:none; }
  canvas:focus-visible { outline:2px solid var(--mint); outline-offset:-4px; }
  .toolbar { display:flex; align-items:center; justify-content:space-between; gap:14px; margin-top:16px; flex-wrap:wrap; }
  .help { color:var(--muted); font-size:12px; line-height:1.9; }
  kbd { font:11px system-ui; color:#dbe7ff; background:#192236; padding:3px 6px; border-radius:4px; border:1px solid #303a51; }
  .buttons { display:flex; gap:8px; }
  button {
    color:#dce6fa; background:#161f32; border:1px solid #34415b; padding:10px 13px;
    font:600 11px system-ui; letter-spacing:.5px; border-radius:8px; cursor:pointer;
  }
  button:hover { border-color:var(--mint); color:var(--mint); }
  button:focus-visible { outline:2px solid var(--mint); outline-offset:3px; }
  .footer { margin-top:14px; display:flex; justify-content:space-between; gap:16px; color:#71809f; font-size:10px; letter-spacing:1px; }
  #status { color:#a0b2d0; }
  @media(max-width:600px) {
    body { padding:12px; align-items:flex-start; padding-top:28px; }
    header { gap:10px; }
    .brand { font-size:22px; letter-spacing:4px; }
    .tag { font-size:8px; letter-spacing:1.5px; }
    .stats { gap:12px; }
    .value { font-size:17px; }
    .label { font-size:8px; letter-spacing:1px; }
    #lives { letter-spacing:0; }
    .arena { border-radius:10px; }
    .footer { font-size:8px; }
  }
</style>
</head>
<body>
<main>
  <header>
    <div>
      <div class="brand">PR<span>I</span>SM</div>
      <div class="tag">BREAKOUT / FIVE SECTORS</div>
    </div>
    <div class="stats">
      <div><div class="label">SCORE</div><div class="value" id="score">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>
    </div>
  </header>
  <div class="arena">
    <canvas id="game" width="960" height="640" tabindex="0"
      aria-label="Prism Breakout. Move with the mouse, touch, or arrow keys. Click or press Space to launch. P pauses. R restarts."></canvas>
  </div>
  <div class="toolbar">
    <div class="help">
      <kbd>←</kbd> <kbd>→</kbd> / <kbd>A</kbd> <kbd>D</kbd> or mouse to move
      &nbsp;·&nbsp; <kbd>Space</kbd> / click to launch &nbsp;·&nbsp; <kbd>P</kbd> pause
    </div>
    <div class="buttons">
      <button id="pause" type="button">Pause</button>
      <button id="sound" type="button" aria-pressed="false">Sound: off</button>
      <button id="restart" type="button">Restart</button>
    </div>
  </div>
  <div class="footer">
    <span>W = WIDE PADDLE &nbsp; S = SLOW BALL &nbsp; + = EXTRA LIFE</span>
    <span id="status" aria-live="polite">Ready when you are.</span>
  </div>
</main>
<script>
(() => {
  "use strict";
  const canvas = document.getElementById("game");
  const ctx = canvas.getContext("2d");
  const scoreEl = document.getElementById("score");
  const levelEl = document.getElementById("level");
  const livesEl = document.getElementById("lives");
  const statusEl = document.getElementById("status");
  const pauseBtn = document.getElementById("pause");
  const soundBtn = document.getElementById("sound");
  const W = 960, H = 640;
  const reduced = matchMedia("(prefers-reduced-motion: reduce)").matches;
  const colors = ["#ff719f","#ffab73","#ffe28a","#71ffd4","#70caff","#ac95ff"];
  const names = ["FIRST LIGHT","CROSS SIGNAL","DIAMOND SKY","THE GATE","PRISM CORE"];
  const keys = new Set();
  let state = "title", beforePause = "ready", level = 0, score = 0, lives = 3;
  let bricks = [], particles = [], drops = [], popups = [], trail = [];
  let remaining = 0, combo = 0, shake = 0, flash = 0, clearTime = 0;
  let wideTime = 0, slowTime = 0, elapsed = 0;
  let mouseMode = true, targetX = W / 2, lastFrame = 0;
  let soundOn = false, audio = null;
  const paddle = { x:W/2, y:590, w:124, h:14 };
  const ball = { x:W/2, y:575, r:8, vx:0, vy:0, speed:465 };
  const stars = Array.from({length:65}, (_,i) => ({
    x:(i*173.73+43)%W, y:(i*97.31+19)%H, size:i%3===0?1.5:.8, phase:i*1.7
  }));

  function fit() {
    const r = canvas.getBoundingClientRect();
    const dpr = Math.min(devicePixelRatio || 1, 2);
    canvas.width = Math.round(r.width*dpr);
    canvas.height = Math.round(r.height*dpr);
  }
  window.addEventListener("resize",fit);
  fit();
  const clamp = (n,a,b) => Math.max(a,Math.min(b,n));
  const rand = (a,b) => a+Math.random()*(b-a);

  function hud() {
    scoreEl.textContent = String(score).padStart(6,"0");
    levelEl.textContent = String(level+1).padStart(2,"0")+" / 05";
    livesEl.textContent = "♥".repeat(lives) + "♡".repeat(Math.max(0,3-lives));
    pauseBtn.textContent = state==="paused" ? "Resume" : "Pause";
  }
  function say(s) { statusEl.textContent=s; }
  function unlockAudio() {
    if (!soundOn) return;
    try {
      if (!audio) audio = new (window.AudioContext || window.webkitAudioContext)();
      if (audio.state==="suspended") audio.resume().catch(()=>{});
    } catch (_) { soundOn=false; soundBtn.textContent="Sound: unavailable"; }
  }
  function tone(freq,duration=.07,type="sine",volume=.035,slide=0) {
    if (!soundOn || !audio || audio.state!=="running") return;
    const t=audio.currentTime, o=audio.createOscillator(), g=audio.createGain();
    o.type=type; o.frequency.setValueAtTime(freq,t);
    if(slide) o.frequency.exponentialRampToValueAtTime(Math.max(30,freq+slide),t+duration);
    g.gain.setValueAtTime(volume,t);
    g.gain.exponentialRampToValueAtTime(.0001,t+duration);
    o.connect(g); g.connect(audio.destination); o.start(t); o.stop(t+duration);
  }
  function burst(x,y,color,n=15,force=1) {
    if(reduced) n=Math.ceil(n/3);
    for(let i=0;i<n;i++) {
      const angle=rand(0,Math.PI*2), speed=rand(35,210)*force;
      const life=rand(.25,.65);
      particles.push({x,y,vx:Math.cos(angle)*speed,vy:Math.sin(angle)*speed,
        color,life,max:life,size:rand(2,5)});
    }
    if(particles.length>600) particles.splice(0,particles.length-600);
  }
  function popup(text,x,y,color="#eaf0ff") {
    popups.push({text,x,y,color,life:1,max:1});
  }
  function makeLevel() {
    bricks=[]; drops=[]; trail=[]; combo=0; wideTime=0; slowTime=0;
    paddle.w=124; paddle.x=W/2; targetX=paddle.x;
    const cols=12, rows=6, bw=65, bh=23, gap=8, startX=(W-(cols*(bw+gap)-gap))/2;
    for(let row=0;row<rows;row++) {
      for(let col=0;col<cols;col++) {
        let include=true, hp=1;
        if(level===0) include = !(row===5 && (col<2 || col>9));
        if(level===1) { include=(row+col)%3!==0; hp=row<2?2:1; }
        if(level===2) { include=Math.abs(col-5.5)+Math.abs(row-2.5)*1.35<6.2; hp=row===2||row===3?2:1; }
        if(level===3) { include=col<3||col>8||row<2||row===5; hp=row<2||col===0||col===11?2:1; }
        if(level===4) { include=!(row===5&&(col===0||col===11)); hp=row<2?3:row<4?2:1; }
        if(include) bricks.push({x:startX+col*(bw+gap), y:94+row*(bh+gap),
          w:bw,h:bh,hp,max:hp,color:colors[row],hit:0});
      }
    }
    remaining=bricks.length;
    ball.speed=465+level*36;
    ball.vx=0; ball.vy=0;
    ball.x=paddle.x; ball.y=paddle.y-ball.r-3;
    state="ready"; say(names[level]+" — click or press Space to launch."); hud();
  }
  function newGame() {
    level=0; score=0; lives=3; particles=[]; popups=[]; shake=0; flash=0;
    makeLevel();
  }
  function launch() {
    if(state!=="ready") return;
    state="playing";
    ball.vx=ball.speed*.34*(Math.random()<.5?-1:1);
    ball.vy=-Math.sqrt(ball.speed*ball.speed-ball.vx*ball.vx);
    say("Keep it alive. Catch the falling upgrades.");
    tone(420,.13,"triangle",.045,340);
  }
  function action() {
    unlockAudio();
    if(state==="title" || state==="gameover" || state==="win") { newGame(); launch(); }
    else if(state==="ready") launch();
    else if(state==="paused") togglePause();
  }
  function togglePause() {
    if(state==="paused") { state=beforePause; say(state==="ready"?"Click or Space to launch.":"Back in the flow."); }
    else if(state==="playing" || state==="ready") { beforePause=state; state="paused"; say("Paused. Take your time."); }
    hud();
  }
  function loseLife() {
    lives--; combo=0; wideTime=0; slowTime=0; paddle.w=124; drops=[]; trail=[];
    shake=12; flash=.22;
    burst(ball.x,H-15,"#ff719f",40,1.3);
    tone(170,.35,"sawtooth",.04,-130);
    if(lives<=0) {
      state="gameover"; say("Run ended. Click or Space to try again.");
    } else {
      state="ready"; ball.x=paddle.x; ball.y=paddle.y-ball.r-3;
      ball.speed=465+level*36;
      say("One more shot — click or Space to launch.");
    }
    hud();
  }
  function hitBrick(b) {
    b.hp--; b.hit=.14;
    tone(320+(5-Math.round((b.y-94)/31))*60+Math.min(combo,16)*22,.075,"triangle",.035,70);
    if(b.hp===0) {
      remaining--; combo++;
      const mult=Math.min(5,1+Math.floor(combo/4));
      const points=100*mult;
      score+=points;
      burst(b.x+b.w/2,b.y+b.h/2,b.color,19);
      popup("+"+points,b.x+b.w/2,b.y,b.color);
      shake=Math.max(shake,3);
      if(Math.random()<.15) {
        const n=Math.random(), type=n<.47?"W":n<.9?"S":"+";
        drops.push({x:b.x+b.w/2,y:b.y+b.h/2,type,phase:rand(0,6)});
      }
      if(remaining===0) {
        score+=500*(level+1); state="cleared"; clearTime=1.9; drops=[];
        flash=.3; shake=8; popup("SECTOR BONUS +"+500*(level+1),W/2,330,"#71ffd4");
        tone(660,.5,"sine",.05,660);
        say("Sector clear! Bonus awarded.");
      }
    } else {
      score+=10; burst(ball.x,ball.y,b.color,7,.6); shake=Math.max(shake,1.5);
    }
    hud();
  }
  function collect(d) {
    if(d.type==="W") { wideTime=13; popup("WIDE PADDLE",paddle.x,paddle.y-30,"#71ffd4"); }
    if(d.type==="S") { slowTime=10; popup("SLOW MOTION",paddle.x,paddle.y-30,"#70caff"); }
    if(d.type==="+") {
      if(lives<5) { lives++; popup("EXTRA LIFE",paddle.x,paddle.y-30,"#ff719f"); }
      else { score+=500; popup("+500",paddle.x,paddle.y-30,"#ff719f"); }
    }
    burst(d.x,d.y,"#ffffff",20); tone(740,.2,"sine",.05,500); hud();
  }
  function movePaddle(dt) {
    const dir=(keys.has("ArrowRight")||keys.has("d")?1:0)-(keys.has("ArrowLeft")||keys.has("a")?1:0);
    if(dir) { paddle.x+=dir*740*dt; targetX=paddle.x; }
    else if(mouseMode) paddle.x+=(targetX-paddle.x)*(1-Math.exp(-26*dt));
    paddle.x=clamp(paddle.x,paddle.w/2+12,W-paddle.w/2-12);
  }
  function simulateBall(dt) {
    const factor=slowTime>0?.73:1;
    const steps=Math.max(1,Math.ceil(ball.speed*factor*dt/5));
    const step=dt*factor/steps;
    for(let s=0;s<steps;s++) {
      ball.x+=ball.vx*step; ball.y+=ball.vy*step;
      if(ball.x<ball.r+10) { ball.x=ball.r+10; ball.vx=Math.abs(ball.vx); tone(230,.035); }
      if(ball.x>W-ball.r-10) { ball.x=W-ball.r-10; ball.vx=-Math.abs(ball.vx); tone(230,.035); }
      if(ball.y<ball.r+10) { ball.y=ball.r+10; ball.vy=Math.abs(ball.vy); tone(260,.035); }
      if(ball.vy>0 && ball.y+ball.r>=paddle.y && ball.y-ball.r<=paddle.y+paddle.h &&
          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=offset*1.12;
        ball.vx=ball.speed*Math.sin(angle);
        if(Math.abs(ball.vx)<55) ball.vx=55*(offset<0?-1:1);
        ball.vy=-Math.sqrt(ball.speed*ball.speed-ball.vx*ball.vx);
        ball.y=paddle.y-ball.r-.1; combo=0;
        burst(ball.x,paddle.y,"#71ffd4",8,.5);
        tone(280,.09,"triangle",.04,150);
      }
      for(const b of bricks) {
        if(b.hp<=0) continue;
        const nearX=clamp(ball.x,b.x,b.x+b.w), nearY=clamp(ball.y,b.y,b.y+b.h);
        let nx=ball.x-nearX, ny=ball.y-nearY;
        const dist2=nx*nx+ny*ny;
        if(dist2>ball.r*ball.r) continue;
        let dist=Math.sqrt(dist2);
        if(dist>0) { nx/=dist; ny/=dist; }
        else {
          const edges=[Math.abs(ball.x-b.x),Math.abs(ball.x-b.x-b.w),Math.abs(ball.y-b.y),Math.abs(ball.y-b.y-b.h)];
          const side=edges.indexOf(Math.min(...edges));
          nx=side===0?-1:side===1?1:0; ny=side===2?-1:side===3?1:0;
          dist=-edges[side];
        }
        const dot=ball.vx*nx+ball.vy*ny;
        ball.x+=nx*(ball.r-dist+.2); ball.y+=ny*(ball.r-dist+.2);
        if(dot<0) {
          ball.vx-=2*dot*nx; ball.vy-=2*dot*ny;
          // Keep every trajectory moving vertically enough to reach the paddle.
          const sign=ball.vy<0?-1:1;
          if(Math.abs(ball.vy)<ball.speed*.24) {
            ball.vy=sign*ball.speed*.24;
            ball.vx=(ball.vx<0?-1:1)*Math.sqrt(ball.speed**2-ball.vy**2);
          }
          hitBrick(b);
        }
        break;
      }
      if(state!=="playing") break;
      if(ball.y>H+ball.r) { loseLife(); break; }
    }
  }
  function update(dt) {
    if(state==="paused") return;
    elapsed+=dt;
    shake=Math.max(0,shake-dt*24); flash=Math.max(0,flash-dt);
    for(const b of bricks) b.hit=Math.max(0,b.hit-dt);
    for(const p of particles) {
      p.life-=dt; p.x+=p.vx*dt; p.y+=p.vy*dt; p.vy+=160*dt; p.vx*=Math.exp(-2*dt);
    }
    particles=particles.filter(p=>p.life>0);
    for(const p of popups) { p.life-=dt; p.y-=33*dt; }
    popups=popups.filter(p=>p.life>0);
    for(const t of trail) t.life-=dt;
    trail=trail.filter(t=>t.life>0);
    if(state==="cleared") {
      clearTime-=dt;
      if(clearTime<=0) {
        if(level===4) { state="win"; say("All five sectors cleared. You are the prism master!"); }
        else { level++; makeLevel(); }
      }
      return;
    }
    if(state!=="ready" && state!=="playing") return;
    wideTime=Math.max(0,wideTime-dt); slowTime=Math.max(0,slowTime-dt);
    paddle.w+=( (wideTime>0?188:124)-paddle.w)*Math.min(1,dt*12);
    movePaddle(dt);
    if(state==="ready") { ball.x=paddle.x; ball.y=paddle.y-ball.r-3; return; }
    simulateBall(dt);
    if(state!=="playing") return;
    if(!reduced) trail.push({x:ball.x,y:ball.y,life:.18});
    for(const d of drops) {
      d.y+=128*dt;
      if(d.y+12>=paddle.y && d.y-12<=paddle.y+paddle.h && Math.abs(d.x-paddle.x)<paddle.w/2+12) {
        collect(d); d.y=H+50;
      }
    }
    drops=drops.filter(d=>d.y<H+25);
  }
  function roundRect(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,align="center",weight=600) {
    ctx.fillStyle=color; ctx.font=weight+" "+size+"px system-ui, sans-serif";
    ctx.textAlign=align; ctx.textBaseline="middle"; ctx.fillText(str,x,y);
  }
  function drawOverlay(title,sub,button,kicker) {
    ctx.fillStyle="rgba(6,10,21,.79)"; ctx.fillRect(0,0,W,H);
    const g=ctx.createRadialGradient(W/2,290,0,W/2,290,360);
    g.addColorStop(0,"#6fffd510"); g.addColorStop(1,"#6fffd500");
    ctx.fillStyle=g; ctx.fillRect(0,0,W,H);
    text(kicker,W/2,205,13,"#71ffd4","center",750);
    text(title,W/2,269,60,"#eff5ff","center",850);
    text(sub,W/2,331,18,"#aab9d3","center",400);
    roundRect(W/2-145,379,290,52,12,"#71ffd4");
    text(button,W/2,405,17,"#0a2021","center",800);
    text("CLICK ANYWHERE OR PRESS SPACE",W/2,461,11,"#8596b6","center",600);
  }
  function render() {
    ctx.setTransform(canvas.width/W,0,0,canvas.height/H,0,0);
    ctx.clearRect(0,0,W,H);
    ctx.fillStyle="#0c1120"; ctx.fillRect(0,0,W,H);
    const bg=ctx.createLinearGradient(0,0,0,H);
    bg.addColorStop(0,"#18233c55"); bg.addColorStop(1,"#09101d00");
    ctx.fillStyle=bg; ctx.fillRect(0,0,W,H);
    ctx.strokeStyle="#b0c7ff06"; 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();
    for(const s of stars) {
      ctx.globalAlpha=.18+.13*Math.sin(elapsed*.6+s.phase);
      ctx.fillStyle="#bacdff"; ctx.fillRect(s.x,s.y,s.size,s.size);
    }
    ctx.globalAlpha=1;
    ctx.save();
    if(!reduced && shake>0) ctx.translate(rand(-shake,shake),rand(-shake,shake));
    text("SECTOR "+String(level+1).padStart(2,"0")+"  /  "+names[level],35,34,12,"#8e9fbd","left",650);
    text(remaining+" BRICKS",W-35,34,12,"#8e9fbd","right",650);
    if(combo>=4 && state==="playing") text("×"+Math.min(5,1+Math.floor(combo/4))+" COMBO",W/2,35,16,"#ffe28a","center",800);
    ctx.strokeStyle="#35425f"; ctx.lineWidth=2;
    ctx.beginPath(); ctx.moveTo(10,60); ctx.lineTo(10,H-55); ctx.moveTo(W-10,60); ctx.lineTo(W-10,H-55); ctx.stroke();
    for(const b of bricks) {
      if(b.hp<=0) continue;
      roundRect(b.x,b.y+3,b.w,b.h,5,"#0005");
      roundRect(b.x,b.y,b.w,b.h,5,b.hit>0?"#ffffff":b.color);
      roundRect(b.x+2,b.y+2,b.w-4,3,2,"#ffffff55");
      ctx.fillStyle="#091526";
      if(b.hp>1) {
        for(let i=0;i<b.hp;i++) roundRect(b.x+b.w/2-(b.hp*7-3)/2+i*7,b.y+b.h/2-2,4,4,1,"#13243b99");
      }
      if(b.max>1 && b.hp===1) {
        ctx.strokeStyle="#16203255"; ctx.lineWidth=1.5;
        ctx.beginPath(); ctx.moveTo(b.x+b.w*.55,b.y+3); ctx.lineTo(b.x+b.w*.43,b.y+11);
        ctx.lineTo(b.x+b.w*.58,b.y+15); ctx.lineTo(b.x+b.w*.49,b.y+b.h-2); ctx.stroke();
      }
    }
    for(const d of drops) {
      const color=d.type==="W"?"#71ffd4":d.type==="S"?"#70caff":"#ff719f";
      ctx.save(); ctx.translate(d.x,d.y); ctx.rotate(Math.sin(elapsed*4+d.phase)*.1);
      ctx.shadowBlur=18; ctx.shadowColor=color;
      roundRect(-14,-14,28,28,7,color); ctx.shadowBlur=0;
      text(d.type,0,1,17,"#102037","center",850); ctx.restore();
    }
    for(const t of trail) {
      ctx.globalAlpha=t.life/.18*.35; ctx.fillStyle=slowTime>0?"#70caff":"#71ffd4";
      ctx.beginPath(); ctx.arc(t.x,t.y,ball.r*t.life/.18,0,Math.PI*2); ctx.fill();
    }
    ctx.globalAlpha=1;
    if(state!=="gameover" && state!=="win" && state!=="cleared") {
      ctx.shadowBlur=20; ctx.shadowColor=slowTime>0?"#70caff":"#71ffd4";
      ctx.fillStyle="#f5fffe"; ctx.beginPath(); ctx.arc(ball.x,ball.y,ball.r,0,Math.PI*2); ctx.fill(); ctx.shadowBlur=0;
    }
    ctx.shadowBlur=wideTime>0?24:14; ctx.shadowColor="#71ffd4";
    roundRect(paddle.x-paddle.w/2,paddle.y,paddle.w,paddle.h,7,"#71ffd4"); ctx.shadowBlur=0;
    roundRect(paddle.x-paddle.w/2+8,paddle.y+2,paddle.w-16,3,2,"#d8fff5");
    roundRect(paddle.x-paddle.w/2+12,paddle.y+5,5,5,2,"#158b83");
    roundRect(paddle.x+paddle.w/2-17,paddle.y+5,5,5,2,"#158b83");
    let badgeX=32;
    if(wideTime>0) { text("WIDE "+Math.ceil(wideTime)+"s",badgeX,H-17,11,"#71ffd4","left"); badgeX+=110; }
    if(slowTime>0) text("SLOW "+Math.ceil(slowTime)+"s",badgeX,H-17,11,"#70caff","left");
    if(state==="playing" && wideTime<=0 && slowTime<=0) text("AIM WITH THE EDGES OF YOUR PADDLE",W/2,H-17,10,"#566987");
    for(const p of particles) {
      ctx.globalAlpha=Math.max(0,p.life/p.max); ctx.fillStyle=p.color;
      ctx.fillRect(p.x-p.size/2,p.y-p.size/2,p.size,p.size);
    }
    for(const p of popups) { ctx.globalAlpha=Math.min(1,p.life*2); text(p.text,p.x,p.y,14,p.color,"center",800); }
    ctx.globalAlpha=1;
    ctx.restore();
    if(flash>0 && !reduced) { ctx.fillStyle="rgba(160,230,255,"+flash*.25+")"; ctx.fillRect(0,0,W,H); }
    if(state==="ready") {
      text(names[level],W/2,399,30,"#eff5ff","center",800);
      text("Click or press Space to launch",W/2,441,17,"#aab9d3","center",450);
      text("Move the paddle to choose your starting position",W/2,471,12,"#7184a6","center",400);
    }
    if(state==="cleared") {
      text("SECTOR CLEAR",W/2,388,45,"#71ffd4","center",850);
      text(level===4?"The whole spectrum. Yours.":"Next sector incoming…",W/2,440,18,"#c3d0e7","center",450);
    }
    if(state==="title") drawOverlay("CHASE THE LIGHT","Five sectors. One paddle. Make every bounce count.","LET'S PLAY","P R I S M  /  B R E A K O U T");
    if(state==="paused") drawOverlay("ON HOLD","Your next great bounce can wait.","RESUME","P A U S E D");
    if(state==="gameover") drawOverlay("ONE MORE RUN?","Final score: "+score.toLocaleString()+"  ·  Sector "+(level+1)+" of 5","PLAY AGAIN","O U T  O F  L I G H T");
    if(state==="win") drawOverlay("SPECTRUM COMPLETE","All five sectors cleared. Final score: "+score.toLocaleString(),"PLAY AGAIN","Y O U  A R E  T H E  P R I S M");
  }
  function pointerPosition(e) {
    const r=canvas.getBoundingClientRect();
    targetX=clamp((e.clientX-r.left)/r.width*W,0,W);
    mouseMode=true;
  }
  canvas.addEventListener("pointermove",pointerPosition);
  canvas.addEventListener("pointerdown",e=>{
    if(e.button!==0) return;
    e.preventDefault(); canvas.focus({preventScroll:true}); pointerPosition(e);
    if(e.pointerType!=="mouse") canvas.setPointerCapture(e.pointerId);
    if(state==="ready") {
      paddle.x=clamp(targetX,paddle.w/2+12,W-paddle.w/2-12);
      ball.x=paddle.x;
    }
    action();
  });
  window.addEventListener("keydown",e=>{
    const key=e.key.length===1?e.key.toLowerCase():e.key;
    if(["ArrowLeft","ArrowRight"," ","a","d","p","r","Escape"].includes(key)) {
      if(e.target instanceof HTMLButtonElement && key===" ") return;
      e.preventDefault();
    }
    if(key==="ArrowLeft"||key==="ArrowRight"||key==="a"||key==="d") {
      keys.add(key); mouseMode=false;
    }
    if(e.repeat) return;
    if(key===" ") action();
    if(key==="p"||key==="Escape") togglePause();
    if(key==="r") { unlockAudio(); newGame(); }
  });
  window.addEventListener("keyup",e=>keys.delete(e.key.length===1?e.key.toLowerCase():e.key));
  function autoPause() {
    keys.clear();
    if(state==="playing"||state==="ready") togglePause();
  }
  window.addEventListener("blur",autoPause);
  document.addEventListener("visibilitychange",()=>{ if(document.hidden) autoPause(); });
  pauseBtn.addEventListener("click",()=>{ unlockAudio(); togglePause(); });
  document.getElementById("restart").addEventListener("click",()=>{ unlockAudio(); newGame(); canvas.focus({preventScroll:true}); });
  soundBtn.addEventListener("click",()=>{
    soundOn=!soundOn; unlockAudio();
    soundBtn.textContent="Sound: "+(soundOn?"on":"off");
    soundBtn.setAttribute("aria-pressed",String(soundOn));
    if(soundOn) tone(700,.13,"sine",.035,200);
  });
  makeLevel(); state="title"; say("Ready when you are.");
  function frame(now) {
    const dt=lastFrame?Math.min((now-lastFrame)/1000,.035):0;
    lastFrame=now; update(dt); render(); requestAnimationFrame(frame);
  }
  requestAnimationFrame(frame);
})();
</script>
</body>
</html>

Compare with

Pareto 26.10: other attempts