• Updated 14 Nov, 12:11 UTC
  • Next run 15 Nov, 05:17 UTCNext 15 Nov 05:17
  • Last 7 days: 9 new models · 71 runs7 d: 9 models · 71 runs
Playable Breakout

ERNIE-Light-DT-1.0Baidu

Network offOpen alone (new tab)

Run details

Model
baidu/ernie-light-dt-1.0-intl
Route
OpenRouter API, one user message
Served by
Baidu
Effort
max, OpenRouter's default for this model
Limits
60 min · 131,072 output tokens (the model's limit)
Date
14 Nov 2026
Time
12 min 3 s
Size
38 kB
Tokens
88 in, 79,624 out (65,649 of them thinking)
List price
$0.31 at today's price ($1.13 in / $3.94 out per M)
Paid
$0.31

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

38 kB 1,052 lines Raw

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="color-scheme" content="dark">
<title>BALLISTIC — a brick-breaking arcade</title>
<style>
  *{margin:0;padding:0;box-sizing:border-box}
  html,body{height:100%;overflow:hidden}
  body{
    background:#0c0a09;color:#efe6d4;
    font-family:ui-monospace,"SF Mono","Cascadia Mono",Menlo,Consolas,"Liberation Mono",monospace;
    display:flex;align-items:center;justify-content:center;
    user-select:none;-webkit-user-select:none;-webkit-tap-highlight-color:transparent;
  }
  .cab{display:flex;flex-direction:column;gap:11px;padding:14px;max-width:100vw}
  header{position:relative;display:flex;align-items:flex-end;justify-content:space-between;gap:16px;
    border-bottom:1px solid #2b241c;padding-bottom:10px;align-self:stretch}
  .brand{min-width:0}
  h1{font-size:16px;font-weight:700;letter-spacing:.46em;white-space:nowrap}
  .sub{font-size:9px;letter-spacing:.24em;color:#8a7f6c;margin-top:5px;text-transform:uppercase;
    white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
  .board{display:flex;gap:20px;flex:none}
  .cell{display:flex;flex-direction:column;align-items:flex-end;gap:4px}
  .lbl{font-size:8px;letter-spacing:.3em;color:#8a7f6c}
  .val{display:block;font-size:17px;font-weight:700;letter-spacing:.04em;font-variant-numeric:tabular-nums;line-height:1}
  #comboCell{visibility:hidden}
  #comboCell.show{visibility:visible}
  #comboCell .val{color:#ff5c29}
  #comboCell.pulse .val{animation:comboPop .18s ease-out}
  @keyframes comboPop{from{transform:scale(1.45)}to{transform:scale(1)}}
  #best.newbest{color:#ff5c29}
  .lives{display:flex;gap:5px;align-items:center;height:17px}
  .lives i{width:8px;height:8px;border-radius:50%;border:1px solid #5d5546;display:block}
  .lives i.on{background:#efe6d4;border-color:#efe6d4}
  #prog{position:absolute;left:0;bottom:-1px;height:2px;width:0%;background:#ff5c29;transition:width .35s ease}
  #stage{position:relative;align-self:center;background:#141110;border:1px solid #2b241c;
    border-radius:8px;overflow:hidden;cursor:none;touch-action:none}
  #game{display:block}
  footer{display:flex;align-items:center;justify-content:space-between;gap:12px;color:#5d5546;align-self:stretch}
  .hintTxt{font-size:9px;letter-spacing:.2em;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
  .btns{display:flex;gap:8px;flex:none}
  button{font-family:inherit;font-size:9px;letter-spacing:.22em;color:#8a7f6c;background:none;
    border:1px solid #2b241c;border-radius:4px;padding:6px 11px;cursor:pointer}
  button:hover{color:#efe6d4;border-color:#5d5546}
  button:active{transform:translateY(1px)}
  @media (max-width:620px){
    h1{font-size:12px;letter-spacing:.3em}
    .val{font-size:13px}
    .board{gap:12px}
    .sub{font-size:8px}
  }
</style>
</head>
<body>
<main class="cab">
  <header>
    <div class="brand">
      <h1>BALLISTIC</h1>
      <div class="sub" id="levelName">PRESS SPACE OR CLICK TO START</div>
    </div>
    <div class="board">
      <div class="cell"><span class="lbl">SCORE</span><span class="val" id="score">000000</span></div>
      <div class="cell"><span class="lbl">BEST</span><span class="val" id="best">000000</span></div>
      <div class="cell" id="comboCell"><span class="lbl">COMBO</span><span class="val" id="comboVal">×1</span></div>
      <div class="cell"><span class="lbl">LIVES</span><span class="lives" id="lives"></span></div>
    </div>
    <div id="prog"></div>
  </header>
  <div id="stage">
    <canvas id="game" width="800" height="600" role="img" aria-label="Brick arcade play field"></canvas>
  </div>
  <footer>
    <div class="hintTxt">MOUSE / ← → — MOVE&nbsp;&nbsp;·&nbsp;&nbsp;SPACE — LAUNCH&nbsp;&nbsp;·&nbsp;&nbsp;P — PAUSE&nbsp;&nbsp;·&nbsp;&nbsp;M — SOUND&nbsp;&nbsp;·&nbsp;&nbsp;R — RESTART</div>
    <div class="btns">
      <button id="pauseBtn" type="button">PAUSE</button>
      <button id="soundBtn" type="button">SOUND ON</button>
    </div>
  </footer>
</main>

<script>
'use strict';
/* ============================ helpers ============================ */
const W=800, H=600;
const FONT='ui-monospace, "SF Mono", "Cascadia Mono", Menlo, Consolas, "Liberation Mono", monospace';
const RM=matchMedia('(prefers-reduced-motion: reduce)').matches;
const TAU=Math.PI*2;
const clamp=(v,a,b)=>v<a?v:(v>b?b:v);
const lerp=(a,b,t)=>a+(b-a)*t;
const rnd=(a,b)=>b===undefined?Math.random()*a:a+Math.random()*(b-a);
const easeOutBack=t=>{const c=1.70158;t-=1;return 1+(c+1)*t*t*t+c*t;};
const easeOutCubic=t=>1-Math.pow(1-t,3);
function shade(hex,f){ // darken a hex colour toward the court background
  const n=parseInt(hex.slice(1),16), t=[0x14,0x11,0x10];
  return 'rgb('+Math.round(lerp((n>>16)&255,t[0],f))+','+
    Math.round(lerp((n>>8)&255,t[1],f))+','+Math.round(lerp(n&255,t[2],f))+')';
}
function rr(c,x,y,w,h,r){ // rounded rect path
  r=Math.min(r,w/2,h/2);
  c.beginPath();c.moveTo(x+r,y);
  c.arcTo(x+w,y,x+w,y+h,r);c.arcTo(x+w,y+h,x,y+h,r);
  c.arcTo(x,y+h,x,y,r);c.arcTo(x,y,x+w,y,r);
  c.closePath();
}
function fmt(n){return String(Math.max(0,Math.floor(n))).padStart(6,'0');}
// letter-spaced canvas text with per-letter animation
function trackedText(c,text,cx,cy,size,track,fn,color){
  c.save();
  c.font='700 '+size+'px '+FONT;
  c.fillStyle=color;c.textAlign='center';c.textBaseline='middle';
  const cw=c.measureText('0').width;
  let x=cx-(text.length*cw+(text.length-1)*track)/2+cw/2;
  for(let i=0;i<text.length;i++){
    const st=fn?fn(i):{a:1,dy:0};
    c.globalAlpha=Math.max(0,Math.min(1,st.a===undefined?1:st.a));
    c.fillText(text[i],x,cy+(st.dy||0));
    x+=cw+track;
  }
  c.restore();
}

/* ============================ level data ============================ */
const LEVELS=[
 {name:'KEYSTONE', pal:['#ffd23f','#ff9a2b','#ff5c29'],
  rowPal:['#ffd23f','#ffb03a','#ff5c29','#ffb03a','#ffd23f'],
  rows:['..111111..','.11111111.','1111111111','1111111111','.11111111.']},
 {name:'CHECKER', pal:['#ffd23f','#ff9a2b','#ff5c29'],
  rows:['1212121212','2121212121','1212121212','2121212121','1212121212']},
 {name:'ZIGGURAT', pal:['#ffb03a','#ffd23f','#ff5c29'],
  rows:['....33....','...2112...','..211112..','.21111112.','3111111113']},
 {name:'THE GATES', pal:['#ffb03a','#3fbf9f','#ff5c29'],
  rows:['2.1.22.1.2','2.1.22.1.2','2.1.22.1.2','2.1.22.1.2','2.1.22.1.2','2.1.22.1.2']},
 {name:'THE VAULT', pal:['#e8dcc2','#ffb03a','#ff5c29'],
  rows:['11..22..11','1.222222.1','.22333322.','.23333332.','.22333322.','1.222222.1','11..22..11']},
 {name:'FORTRESS', pal:['#ffb03a','#3fbf9f','#ff5c29'],
  rows:['X22222222X','X2......2X','X2.3333.2X','X2.3..3.2X','X2.3333.2X','X2......2X','X22222222X']},
 {name:'SUPERNOVA', pal:['#ffd23f','#ff9a2b','#ff3b2e'],
  rows:['X22222222X','2111111112','2113333112','2133333312','2111111112','X22222222X']},
];

/* ============================ audio (all synthesized) ============================ */
const Snd=(()=>{
  let ac=null,master=null,noiseBuf=null,muted=false;
  function init(){
    if(ac){if(ac.state==='suspended')ac.resume();return;}
    try{
      const AC=window.AudioContext||window.webkitAudioContext;
      if(!AC)return;
      ac=new AC();
      master=ac.createGain();master.gain.value=0.35;master.connect(ac.destination);
      const len=Math.floor(ac.sampleRate*0.4);
      noiseBuf=ac.createBuffer(1,len,ac.sampleRate);
      const d=noiseBuf.getChannelData(0);
      for(let i=0;i<len;i++)d[i]=Math.random()*2-1;
    }catch(e){ac=null;}
  }
  function tone(f0,f1,dur,type,vol,delay){
    if(!ac||muted)return;
    try{
      const t=ac.currentTime+(delay||0);
      const o=ac.createOscillator(),g=ac.createGain();
      o.type=type;o.frequency.setValueAtTime(f0,t);
      if(f1&&f1!==f0)o.frequency.exponentialRampToValueAtTime(Math.max(20,f1),t+dur);
      g.gain.setValueAtTime(vol,t);
      g.gain.exponentialRampToValueAtTime(0.0001,t+dur);
      o.connect(g);g.connect(master);o.start(t);o.stop(t+dur+0.03);
    }catch(e){}
  }
  function noise(dur,vol,f0,f1){
    if(!ac||muted||!noiseBuf)return;
    try{
      const t=ac.currentTime;
      const s=ac.createBufferSource();s.buffer=noiseBuf;s.loop=true;
      const f=ac.createBiquadFilter();f.type='lowpass';
      f.frequency.setValueAtTime(f0,t);
      f.frequency.exponentialRampToValueAtTime(Math.max(40,f1),t+dur);
      const g=ac.createGain();
      g.gain.setValueAtTime(vol,t);
      g.gain.exponentialRampToValueAtTime(0.0001,t+dur);
      s.connect(f);f.connect(g);g.connect(master);s.start(t);s.stop(t+dur+0.03);
    }catch(e){}
  }
  return{
    init,
    get muted(){return muted;},
    setMuted(v){muted=!!v;},
    toggle(){muted=!muted;try{localStorage.setItem('bb.mute',muted?'1':'0');}catch(e){}return muted;},
    wall(){tone(190,120,0.06,'triangle',0.15);},
    paddle(){tone(240,430,0.07,'square',0.10);},
    launch(){tone(300,540,0.09,'triangle',0.12);},
    brickHit(c){tone(300+Math.min(600,c*26),240,0.05,'square',0.09);},
    brickBreak(c){noise(0.16,0.22,3200,180);tone(500+Math.min(900,c*45),160,0.12,'square',0.12);},
    steel(){tone(1500,900,0.05,'sine',0.10);tone(2300,1800,0.03,'sine',0.05);},
    pow(){tone(660,660,0.07,'square',0.09);tone(990,990,0.09,'square',0.09,0.08);},
    lost(){tone(300,60,0.5,'sawtooth',0.16);noise(0.3,0.12,900,120);},
    clear(){[523,659,784,1046].forEach((f,i)=>tone(f,f,0.12,'square',0.10,i*0.09));},
    win(){[392,494,587,784,988,1175].forEach((f,i)=>tone(f,f,0.14,'square',0.10,i*0.10));},
    gameOver(){[330,262,208,165].forEach((f,i)=>tone(f,f*0.98,0.2,'sawtooth',0.11,i*0.13));},
  };
})();

/* ============================ DOM refs & layout ============================ */
const $=id=>document.getElementById(id);
const canvas=$('game'), ctx=canvas.getContext('2d');
const elScore=$('score'),elBest=$('best'),elComboCell=$('comboCell'),elComboVal=$('comboVal'),
      elLives=$('lives'),elLevel=$('levelName'),elProg=$('prog'),
      elSound=$('soundBtn'),elPause=$('pauseBtn'),stageEl=$('stage');
const headerEl=document.querySelector('header'),footerEl=document.querySelector('footer');

function layout(){
  const headH=headerEl.offsetHeight,footH=footerEl.offsetHeight;
  const availW=Math.max(200,window.innerWidth-28-14);
  const availH=Math.max(200,window.innerHeight-headH-footH-14*2-11*2-2);
  const s=Math.max(0.25,Math.min(availW/W,availH/H));
  const cw=Math.max(200,Math.floor(W*s)),ch=Math.max(150,Math.floor(H*s));
  const dpr=Math.min(window.devicePixelRatio||1,2);
  canvas.style.width=cw+'px';canvas.style.height=ch+'px';
  canvas.width=Math.round(cw*dpr);canvas.height=Math.round(ch*dpr);
}
addEventListener('resize',layout);
addEventListener('orientationchange',()=>setTimeout(layout,120));

/* ============================ game state ============================ */
const S={
  state:'title',time:0,stateT:0,paused:false,
  freeze:0,shake:0,slowmo:1,slowTimer:0,slowEase:1,slowUntil:0,wideUntil:0,
  level:0,score:0,dispScore:0,best:0,lives:3,combo:0,comboFlash:0,
  bottomFlash:0,perfect:true,newBest:false,total:1,remaining:0,aim:-Math.PI/2,
};
const paddle={x:W/2,tx:W/2,vx:0,w:96,tw:96,h:14,y:H-44,squash:1,squashV:0};
const balls=[],bricks=[],parts=[],pops=[],pows=[],waves=[];
let demo=[],banner=null;
const keys={left:false,right:false};

const POWTYPES=[
  {k:'W',w:0.34,col:'#3fbf9f',label:'WIDE PADDLE'},
  {k:'M',w:0.27,col:'#ff5c29',label:'MULTI BALL'},
  {k:'S',w:0.26,col:'#ffb03a',label:'SLOW BALL'},
  {k:'+',w:0.13,col:'#efe6d4',label:'EXTRA LIFE'},
];

// ambient reactive dot field
const dots=[];
for(let y=30;y<H-14;y+=40)for(let x=30;x<W-14;x+=40)dots.push({x,y});

// pre-rendered vignette
const vig=document.createElement('canvas');vig.width=W;vig.height=H;
{
  const v=vig.getContext('2d');
  const g=v.createRadialGradient(W/2,H/2,H*0.42,W/2,H/2,H*0.95);
  g.addColorStop(0,'rgba(0,0,0,0)');g.addColorStop(1,'rgba(0,0,0,0.45)');
  v.fillStyle=g;v.fillRect(0,0,W,H);
}

/* ============================ HUD ============================ */
function updateLives(){
  let h='';
  const n=Math.max(3,S.lives);
  for(let i=0;i<n;i++)h+='<i class="'+(i<S.lives?'on':'')+'"></i>';
  elLives.innerHTML=h;
}
function updateCombo(){
  if(S.combo>=2){
    elComboVal.textContent='×'+S.combo;
    elComboCell.classList.add('show');
    elComboCell.classList.remove('pulse');void elComboCell.offsetWidth;
    elComboCell.classList.add('pulse');
  }else{
    elComboCell.classList.remove('show','pulse');
    elComboVal.textContent='×1';
  }
}
function setSoundLabel(){elSound.textContent=Snd.muted?'SOUND OFF':'SOUND ON';}
function lightPos(){
  if(S.state==='title'&&demo.length)return demo[0];
  return balls[0]||null;
}

/* ============================ level building ============================ */
function brickBaseCol(br){
  const L=LEVELS[S.level];
  return L.rowPal?L.rowPal[br.row]:L.pal[br.maxHp-1];
}
function buildLevel(n){
  bricks.length=0;pows.length=0;parts.length=0;pops.length=0;waves.length=0;balls.length=0;
  const L=LEVELS[n],bw=70,bh=26,gx=6,gy=8;
  const ox=(W-(bw*10+gx*9))/2,oy=72;
  let destructible=0;
  L.rows.forEach((row,r)=>{
    for(let c=0;c<10;c++){
      const ch=row[c];
      if(ch==='.')continue;
      const steel=ch==='X';
      if(!steel)destructible++;
      bricks.push({
        x:ox+c*(bw+gx),y:oy+r*(bh+gy),w:bw,h:bh,
        hp:steel?Infinity:+ch,maxHp:steel?Infinity:+ch,steel,alive:true,
        flash:0,cracks:[],t:0,delay:r*0.07+c*0.018,row:r,col:c
      });
    }
  });
  S.total=destructible;S.remaining=destructible;
}
function resetPaddle(){
  paddle.w=96;paddle.tw=96;paddle.x=W/2;paddle.tx=W/2;
  paddle.vx=0;paddle.squash=1;paddle.squashV=0;
  S.wideUntil=0;
}
function resetDemo(){
  demo=[
    {x:180,y:160,vx:265,vy:-210,trail:[]},
    {x:620,y:420,vx:-230,vy:245,trail:[]},
  ];
}
function newBall(){
  const sp=340+S.level*16;
  return{x:paddle.x,y:paddle.y-paddle.h/2-10,vx:0,vy:0,speed:sp,base:sp,r:7,stuck:true,trail:[]};
}
function startLevel(n){
  S.level=n;S.perfect=true;S.combo=0;updateCombo();
  buildLevel(n);resetPaddle();
  S.slowUntil=0;S.slowEase=1;S.slowTimer=0;S.slowmo=1;
  S.state='intro';S.stateT=0;
  banner={
    lines:[
      {text:'LEVEL '+(n+1)+' OF '+LEVELS.length,size:13,dim:true},
      {text:LEVELS[n].name,size:44,big:true}
    ],
    hold:1.0,fade:0.3,scrim:true,rule:true,t:0
  };
  elLevel.textContent='LEVEL '+(n+1)+' / '+LEVELS.length+' — '+LEVELS[n].name;
  elProg.style.width='0%';
}
function startRun(){
  S.score=0;S.dispScore=0;S.lives=3;S.paused=false;S.newBest=false;
  elScore.textContent=fmt(0);
  elBest.classList.remove('newbest');elBest.textContent=fmt(S.best);
  elPause.textContent='PAUSE';
  updateLives();startLevel(0);
}
function toTitle(){
  S.state='title';S.stateT=0;S.paused=false;
  S.score=0;S.dispScore=0;S.lives=3;S.combo=0;
  banner=null;balls.length=0;pows.length=0;parts.length=0;pops.length=0;waves.length=0;
  resetPaddle();resetDemo();
  updateLives();updateCombo();
  elScore.textContent=fmt(0);
  elBest.classList.remove('newbest');elBest.textContent=fmt(S.best);
  elLevel.textContent='PRESS SPACE OR CLICK TO START';
  elProg.style.width='0%';elPause.textContent='PAUSE';
}
function toServe(){
  S.state='serve';S.stateT=0;S.aim=-Math.PI/2;
  balls.length=0;balls.push(newBall());
}
function launch(){
  const b=balls[0];
  if(!b||!b.stuck)return;
  b.stuck=false;
  const sp=b.speed*S.slowEase;
  b.vx=Math.cos(S.aim)*sp;b.vy=Math.sin(S.aim)*sp;
  S.state='play';S.stateT=0;
  Snd.launch();
}
function saveBest(){
  S.newBest=S.score>S.best;
  if(S.newBest)S.best=S.score;
  try{localStorage.setItem('bb.best',String(S.best));}catch(e){}
  elBest.textContent=fmt(S.best);
  elBest.classList.remove('newbest');
}

/* ============================ physics & gameplay ============================ */
function addShake(v){S.shake=Math.min(1,S.shake+v*(RM?0.4:1));}
function spawnParticles(x,y,n,col,sp0,sp1){
  if(RM)n=Math.ceil(n/2);
  for(let i=0;i<n;i++){
    const a=rnd(TAU),s=rnd(sp0,sp1);
    parts.push({x,y,vx:Math.cos(a)*s,vy:Math.sin(a)*s-60,
      s:rnd(3,9),h:rnd(3,9),rot:rnd(TAU),vr:rnd(-6,6),life:0,ttl:rnd(0.45,0.9),col});
  }
}
function burstBall(b){spawnParticles(b.x,Math.min(b.y,H-8),12,'#efe6d4',140,340);}
function makeCrack(w,h){
  const pts=[],x0=rnd(w*0.2,w*0.8),x1=rnd(w*0.2,w*0.8);
  for(let i=0;i<=4;i++)pts.push([clamp(lerp(x0,x1,i/4)+(i>0&&i<4?rnd(-5,5):0),2,w-2),h*i/4]);
  return pts;
}
function addPop(x,y,main,sub,col,size,ttl){
  pops.push({x:clamp(x,54,W-54),y,t:0,ttl:ttl||0.9,main,sub,col:col||'#efe6d4',size:size||13});
}
function dropPow(br){
  let r=Math.random(),acc=0,type=POWTYPES[0];
  for(const t of POWTYPES){acc+=t.w;if(r<=acc){type=t;break;}}
  pows.push({x:br.x+br.w/2,y:br.y+br.h/2,vy:135,ph:rnd(TAU),type});
}
function splitBalls(){
  const cur=balls.slice();
  for(const b of cur){
    if(b.stuck||balls.length>=6)continue;
    const sp=b.speed,ang=Math.atan2(b.vy,b.vx);
    for(const da of[-0.45,0.45]){
      if(balls.length>=6)break;
      balls.push({x:b.x,y:b.y,vx:Math.cos(ang+da)*sp*S.slowEase,vy:Math.sin(ang+da)*sp*S.slowEase,
        speed:sp,base:b.base,r:7,stuck:false,trail:[]});
    }
  }
}
function catchPow(t,x,y){
  S.score+=50;
  addPop(x,y-14,t.label,null,t.col,12,1.0);
  Snd.pow();
  if(t.k==='W'){paddle.tw=150;S.wideUntil=S.time+12;}
  else if(t.k==='S'){S.slowUntil=S.time+6;}
  else if(t.k==='M'){splitBalls();}
  else if(t.k==='+'){S.lives=Math.min(5,S.lives+1);updateLives();}
}

function collideWalls(b){
  let hit=false;
  if(b.x<b.r){b.x=b.r;b.vx=Math.abs(b.vx);hit=true;}
  else if(b.x>W-b.r){b.x=W-b.r;b.vx=-Math.abs(b.vx);hit=true;}
  if(b.y<b.r){b.y=b.r;b.vy=Math.abs(b.vy);hit=true;}
  if(hit){
    addShake(0.05);Snd.wall();
    if(waves.length<8)waves.push({x:b.x,y:b.y,t:0});
  }
}
function collidePaddle(b){
  if(b.vy<=0)return;
  const half=paddle.w/2,ph=paddle.h;
  const cx=clamp(b.x,paddle.x-half,paddle.x+half);
  const cy=clamp(b.y,paddle.y-ph/2,paddle.y+ph/2);
  const dx=b.x-cx,dy=b.y-cy;
  if(dx*dx+dy*dy>b.r*b.r)return;
  b.y=paddle.y-ph/2-b.r-0.5;
  const off=clamp((b.x-paddle.x)/half,-1,1);
  const ang=-Math.PI/2+off*1.05;           // up to ~60° from vertical, based on hit position
  b.speed=Math.min(b.speed*1.028,b.base*1.6); // subtle speed-up per paddle hit
  const sp=b.speed*S.slowEase;
  b.vx=Math.cos(ang)*sp+clamp(paddle.vx*0.10,-80,80);
  b.vy=Math.sin(ang)*sp;
  if(b.vy>-40)b.vy=-40;                    // never let it fly horizontally
  paddle.squashV=-5.5;
  addShake(0.05);Snd.paddle();
  S.combo=0;updateCombo();
}
function hitBrick(br,b){
  br.flash=1;
  if(br.steel){
    addShake(0.07);Snd.steel();
    spawnParticles(b.x,b.y,4,'#8a8272',80,220);
    return;
  }
  br.hp--;
  if(br.hp<=0){destroyBrick(br);}
  else{
    br.cracks.push(makeCrack(br.w,br.h));
    addShake(0.06);
    S.freeze=Math.max(S.freeze,0.02*(RM?0.5:1));
    Snd.brickHit(S.combo);
  }
}
function collideBricks(b){
  for(const br of bricks){
    if(!br.alive)continue;
    if(b.x+b.r<br.x||b.x-b.r>br.x+br.w||b.y+b.r<br.y||b.y-b.r>br.y+br.h)continue;
    const cx=clamp(b.x,br.x,br.x+br.w),cy=clamp(b.y,br.y,br.y+br.h);
    const dx=b.x-cx,dy=b.y-cy,d2=dx*dx+dy*dy;
    if(d2>b.r*b.r)continue;
    let nx,ny,pen;
    if(d2>1e-6){
      const d=Math.sqrt(d2);nx=dx/d;ny=dy/d;pen=b.r-d;
    }else{ // ball center inside brick: resolve along smallest axis
      const l=b.x-br.x,r=br.x+br.w-b.x,t=b.y-br.y,bt=br.y+br.h-b.y;
      const m=Math.min(l,r,t,bt);
      if(m===l){nx=-1;ny=0;pen=l+b.r;}
      else if(m===r){nx=1;ny=0;pen=r+b.r;}
      else if(m===t){nx=0;ny=-1;pen=t+b.r;}
      else{nx=0;ny=1;pen=bt+b.r;}
    }
    b.x+=nx*pen;b.y+=ny*pen;
    const dot=b.vx*nx+b.vy*ny;
    if(dot<0){b.vx-=2*dot*nx;b.vy-=2*dot*ny;}
    hitBrick(br,b);
    break;
  }
}
function destroyBrick(br){
  br.alive=false;
  S.combo++;S.comboFlash=1;
  const base=40+20*(br.maxHp-1);
  const pts=base*S.combo;
  S.score+=pts;
  const px=br.x+br.w/2,py=br.y+br.h/2;
  spawnParticles(px,py,16+br.maxHp*4,brickBaseCol(br),150,430);
  waves.push({x:px,y:py,t:0});
  addPop(px,br.y-4,'+'+pts,S.combo>=4?'×'+S.combo:null,'#efe6d4');
  if(S.combo%5===0)addPop(W/2,H*0.30,'COMBO ×'+S.combo,null,'#ff5c29',16,1.1);
  addShake(0.10+Math.min(0.16,S.combo*0.012));
  S.freeze=Math.max(S.freeze,(0.045+Math.min(0.05,S.combo*0.004))*(RM?0.5:1));
  Snd.brickBreak(S.combo);
  updateCombo();
  if(Math.random()<0.16)dropPow(br);
  S.remaining--;
  elProg.style.width=((S.total-S.remaining)/S.total*100)+'%';
  if(S.remaining<=0)levelCleared();
}
function levelCleared(){
  S.state='clear';S.stateT=0;
  S.slowTimer=0.55;                       // brief slow-motion savor
  S.freeze=Math.max(S.freeze,0.16);
  for(const b of balls)burstBall(b);
  balls.length=0;
  const bonus=500+S.lives*250+(S.perfect?500:0);
  S.score+=bonus;
  banner={
    lines:[
      {text:LEVELS[S.level].name+' — CLEAR',size:40,big:true},
      {text:'BONUS +'+bonus,size:15,dim:true}
    ].concat(S.perfect?[{text:'PERFECT — NO LIVES LOST',size:12,accent:true}]:[]),
    hold:1.6,fade:0.4,scrim:true,rule:true,t:0
  };
  addShake(0.22);
  Snd.clear();
  elLevel.textContent='LEVEL '+(S.level+1)+' — CLEAR!';
}
function nextLevel(){
  if(S.level+1<LEVELS.length)startLevel(S.level+1);
  else winGame();
}
function winGame(){
  S.state='win';S.stateT=0;
  saveBest();
  banner={
    lines:[
      {text:'ALL CLEAR',size:52,big:true},
      {text:LEVELS.length+' LEVELS · CHAMPION',size:13,dim:true},
      {text:'FINAL SCORE '+fmt(S.score),size:15,dim:true}
    ],
    hold:1e9,fade:1e9,scrim:true,t:0
  };
  elLevel.textContent='ALL CLEAR — CHAMPION';
  Snd.win();
}
function lifeLost(){
  S.combo=0;updateCombo();
  S.bottomFlash=1;
  addShake(0.55);
  S.freeze=Math.max(S.freeze,0.12*(RM?0.5:1));
  Snd.lost();
  S.perfect=false;
  S.lives--;
  updateLives();
  if(S.lives<=0){gameOver();}
  else{
    resetPaddle();S.slowUntil=0;
    S.state='serve';S.stateT=0;S.aim=-Math.PI/2;
    balls.length=0;balls.push(newBall());
  }
}
function gameOver(){
  S.state='gameover';S.stateT=0;
  saveBest();
  banner={
    lines:[
      {text:'GAME OVER',size:50,big:true},
      {text:'SCORE '+fmt(S.score),size:15,dim:true},
      {text:S.newBest?'NEW BEST':'BEST '+fmt(S.best),size:13,accent:S.newBest}
    ],
    hold:1e9,fade:1e9,scrim:true,t:0
  };
  elLevel.textContent='GAME OVER';
  Snd.gameOver();
  addShake(0.3);
}

/* ============================ per-frame updates ============================ */
function updatePaddle(dt){
  const dir=(keys.right?1:0)-(keys.left?1:0);
  if(dir)paddle.tx+=dir*640*dt;
  const half=paddle.w/2+12;
  paddle.tx=clamp(paddle.tx,half,W-half);
  const px=paddle.x;
  paddle.x=lerp(paddle.x,paddle.tx,1-Math.exp(-dt*22));
  paddle.x=clamp(paddle.x,half,W-half);
  paddle.vx=(paddle.x-px)/Math.max(dt,1e-4);
  paddle.w=lerp(paddle.w,paddle.tw,1-Math.exp(-dt*10));
  paddle.squashV+=(1-paddle.squash)*260*dt;
  paddle.squashV*=Math.exp(-13*dt);
  paddle.squash=clamp(paddle.squash+paddle.squashV*dt,0.5,1.55);
}
function updateDemo(dt){
  for(const b of demo){
    b.x+=b.vx*dt;b.y+=b.vy*dt;
    let hit=false;
    if(b.x<12){b.x=12;b.vx=Math.abs(b.vx);hit=true;}
    if(b.x>W-12){b.x=W-12;b.vx=-Math.abs(b.vx);hit=true;}
    if(b.y<12){b.y=12;b.vy=Math.abs(b.vy);hit=true;}
    if(b.y>H-12){b.y=H-12;b.vy=-Math.abs(b.vy);hit=true;}
    if(hit&&waves.length<5)waves.push({x:b.x,y:b.y,t:0});
    b.trail.push({x:b.x,y:b.y,age:0,life:0.14});
    for(const t of b.trail)t.age+=dt;
    b.trail=b.trail.filter(t=>t.age<t.life);
  }
}
function stepBall(b,dt){
  if(b.stuck){b.x=paddle.x;b.y=paddle.y-paddle.h/2-b.r-3;return;}
  const target=b.speed*S.slowEase;
  const cur=Math.hypot(b.vx,b.vy)||1;
  const k=target/cur;b.vx*=k;b.vy*=k;
  const steps=Math.max(1,Math.ceil(target*dt/5)); // sub-steps prevent tunnelling
  const sdt=dt/steps;
  for(let i=0;i<steps;i++){
    b.x+=b.vx*sdt;b.y+=b.vy*sdt;
    collideWalls(b);
    collidePaddle(b);
    collideBricks(b);
  }
  const tl=(0.12+Math.min(0.16,S.combo*0.02))*(RM?0.55:1); // trail grows with combo
  b.trail.push({x:b.x,y:b.y,age:0,life:tl});
  for(const t of b.trail)t.age+=dt;
  b.trail=b.trail.filter(t=>t.age<t.life);
}
function updateBalls(dt){
  const keep=[];
  let lostAny=false;
  for(const b of balls){
    stepBall(b,dt);
    if(S.state!=='play')return; // level cleared / life lost mid-loop
    if(b.y>H+26){lostAny=true;burstBall(b);}
    else keep.push(b);
  }
  balls.length=0;
  for(const k of keep)balls.push(k);
  if(lostAny&&balls.length===0)lifeLost();
}
function updatePows(dt){
  for(const p of pows){
    p.y+=p.vy*dt;p.ph+=dt*3.2;
    if(p.y>H+24){p.dead=true;}
    else if(Math.abs(p.x-paddle.x)<paddle.w/2+14&&Math.abs(p.y-paddle.y)<paddle.h/2+15){
      p.dead=true;catchPow(p.type,p.x,p.y);
    }
  }
  for(let i=pows.length-1;i>=0;i--)if(pows[i].dead)pows.splice(i,1);
}
function updateParts(dt){
  for(const p of parts){
    p.life+=dt;p.vy+=880*dt;
    p.x+=p.vx*dt;p.y+=p.vy*dt;p.rot+=p.vr*dt;
  }
  for(let i=parts.length-1;i>=0;i--)if(parts[i].life>=parts[i].ttl)parts.splice(i,1);
  if(parts.length>450)parts.splice(0,parts.length-450);
}
function updateWaves(dt){
  for(const w of waves)w.t+=dt/0.55;
  for(let i=waves.length-1;i>=0;i--)if(waves[i].t>=1)waves.splice(i,1);
}
function updatePops(dt){
  for(const p of pops)p.t+=dt;
  for(let i=pops.length-1;i>=0;i--)if(pops[i].t>=pops[i].ttl)pops.splice(i,1);
}

function update(sdt,rdt){
  S.stateT+=rdt;
  if(banner)banner.t+=rdt;
  updatePaddle(sdt);
  if(S.time>S.wideUntil&&paddle.tw>96)paddle.tw=96;

  switch(S.state){
    case 'title':updateDemo(rdt);break;
    case 'intro':{
      for(const br of bricks)if(br.alive&&S.stateT>br.delay)br.t=Math.min(1,br.t+rdt/0.32);
      if(S.stateT>1.55)toServe();
      break;
    }
    case 'serve':{
      const b=balls[0];
      if(b){b.x=paddle.x;b.y=paddle.y-paddle.h/2-b.r-3;}
      S.aim=-Math.PI/2+Math.sin(S.stateT*2.3)*0.55; // oscillating launch angle: time it
      updatePows(rdt);
      break;
    }
    case 'play':updateBalls(sdt);updatePows(sdt);break;
    case 'clear':updatePows(sdt);if(S.stateT>2.35)nextLevel();break;
  }
  updateParts(sdt);
  updateWaves(rdt);
  updatePops(rdt);
  for(const br of bricks)if(br.flash>0)br.flash=Math.max(0,br.flash-rdt*7);
  S.comboFlash=Math.max(0,S.comboFlash-rdt*2.5);
  S.bottomFlash=Math.max(0,S.bottomFlash-rdt*1.4);

  // animated score counter
  if(S.dispScore!==S.score){
    S.dispScore+=(S.score-S.dispScore)*Math.min(1,rdt*9);
    if(Math.abs(S.score-S.dispScore)<1)S.dispScore=S.score;
    elScore.textContent=fmt(S.dispScore);
    if(S.score>S.best){elBest.textContent=fmt(S.dispScore);elBest.classList.add('newbest');}
  }
}

/* ============================ rendering ============================ */
function drawDots(){
  const lp=lightPos();
  ctx.fillStyle='#efe6d4';
  for(const d of dots){
    let a=0.05;
    if(lp){
      const dd=Math.hypot(d.x-lp.x,d.y-lp.y);
      if(dd<130)a+=0.10*(1-dd/130);           // the field glows around the ball
    }
    for(const w of waves){                     // ...and ripples from every impact
      const dist=Math.hypot(d.x-w.x,d.y-w.y);
      const df=Math.abs(dist-(8+w.t*150));
      if(df<26)a+=0.38*(1-w.t)*(1-df/26);
    }
    ctx.globalAlpha=Math.min(0.65,a);
    ctx.fillRect(d.x-1,d.y-1,2,2);
  }
  ctx.globalAlpha=1;
}
function drawWaves(){
  ctx.lineWidth=1.5;ctx.strokeStyle='#ff5c29';
  for(const w of waves){
    ctx.globalAlpha=(1-w.t)*0.38;
    ctx.beginPath();ctx.arc(w.x,w.y,6+w.t*150,0,TAU);ctx.stroke();
  }
  ctx.globalAlpha=1;
}
function drawBricks(){
  for(const br of bricks){
    if(!br.alive)continue;
    let y=br.y,al=1;
    if(br.t<1){ // cascade-in with overshoot
      const E=easeOutBack(br.t);
      y=br.y-46*(1-E);al=Math.min(1,br.t*1.4);
    }
    ctx.globalAlpha=al;
    if(br.steel){
      rr(ctx,br.x,y,br.w,br.h,3);ctx.fillStyle='#332f29';ctx.fill();
      ctx.save();
      rr(ctx,br.x,y,br.w,br.h,3);ctx.clip();
      ctx.strokeStyle='rgba(238,230,212,0.12)';ctx.lineWidth=1;
      ctx.beginPath();
      for(let d=-br.h;d<br.w+br.h;d+=9){ctx.moveTo(br.x+d,y+br.h+1);ctx.lineTo(br.x+d+br.h,y-1);}
      ctx.stroke();ctx.restore();
      ctx.strokeStyle='rgba(0,0,0,0.5)';ctx.lineWidth=1;
      rr(ctx,br.x+0.5,y+0.5,br.w-1,br.h-1,3);ctx.stroke();
    }else{
      let col=brickBaseCol(br);
      if(br.hp<br.maxHp)col=shade(col,0.12+0.4*(br.maxHp-br.hp)/br.maxHp); // damage darkens
      rr(ctx,br.x,y,br.w,br.h,3);ctx.fillStyle=col;ctx.fill();
      ctx.fillStyle='rgba(0,0,0,0.22)';ctx.fillRect(br.x+1,y+br.h-3,br.w-2,3);
      ctx.fillStyle='rgba(255,255,255,0.10)';ctx.fillRect(br.x+1,y+1,br.w-2,2);
      if(br.cracks.length){
        ctx.strokeStyle='rgba(10,8,7,0.5)';ctx.lineWidth=1.1;
        for(const c of br.cracks){
          ctx.beginPath();ctx.moveTo(br.x+c[0][0],y+c[0][1]);
          for(let i=1;i<c.length;i++)ctx.lineTo(br.x+c[i][0],y+c[i][1]);
          ctx.stroke();
        }
      }
    }
    if(br.flash>0){ // white hit flash (visible during hit-stop)
      rr(ctx,br.x,y,br.w,br.h,3);
      ctx.fillStyle='rgba(255,246,225,'+(br.flash*0.85).toFixed(3)+')';ctx.fill();
    }
    ctx.globalAlpha=1;
  }
}
function drawPows(){
  for(const p of pows){
    ctx.save();
    ctx.translate(p.x,p.y);ctx.rotate(Math.sin(p.ph)*0.18);
    rr(ctx,-15,-9,30,18,9);ctx.fillStyle=p.type.col;ctx.fill();
    ctx.strokeStyle='rgba(12,10,9,0.4)';ctx.lineWidth=1;ctx.stroke();
    ctx.fillStyle='#141110';ctx.font='700 11px '+FONT;
    ctx.textAlign='center';ctx.textBaseline='middle';
    ctx.fillText(p.type.k==='+'?'+':p.type.k,0,0.5);
    ctx.restore();
  }
}
function drawParts(){
  for(const p of parts){
    const k=1-p.life/p.ttl;
    if(k<=0)continue;
    ctx.save();
    ctx.translate(p.x,p.y);ctx.rotate(p.rot);ctx.globalAlpha=k;
    ctx.fillStyle=p.col;
    const s=p.s*k+0.5;
    ctx.fillRect(-s/2,-p.h*k/2,s,p.h*k);
    ctx.restore();
  }
}
function drawBallList(list,r){
  for(const b of list){
    for(const tr of b.trail){
      const k=1-tr.age/tr.life;
      if(k<=0)continue;
      ctx.globalAlpha=k*k*0.32;ctx.fillStyle='#ff5c29';
      ctx.beginPath();ctx.arc(tr.x,tr.y,r*k*0.95+0.4,0,TAU);ctx.fill();
    }
    ctx.globalAlpha=1;ctx.fillStyle='#efe6d4';
    ctx.beginPath();ctx.arc(b.x,b.y,r,0,TAU);ctx.fill();
    ctx.strokeStyle='rgba(12,10,9,0.45)';ctx.lineWidth=1;ctx.stroke();
  }
}
function drawPaddle(){
  const sq=paddle.squash,lean=clamp(paddle.vx*0.00022,-0.1,0.1);
  ctx.save();
  ctx.translate(paddle.x,paddle.y);ctx.rotate(lean);
  ctx.scale(1+(1-sq)*0.7,sq);                // squash & stretch spring
  const w=paddle.w,h=paddle.h;
  rr(ctx,-w/2,-h/2,w,h,7);ctx.fillStyle='#efe6d4';ctx.fill();
  ctx.fillStyle='rgba(12,10,9,0.20)';ctx.fillRect(-w/2+3,h/2-4,w-6,3);
  ctx.fillStyle='#ff5c29';rr(ctx,-7,-2.5,14,5,2.5);ctx.fill();
  if(S.time<S.wideUntil){                    // teal caps while WIDE is active
    ctx.fillStyle='#3fbf9f';
    rr(ctx,-w/2,-h/2,9,h,4);ctx.fill();
    rr(ctx,w/2-9,-h/2,9,h,4);ctx.fill();
  }
  ctx.restore();
}
function drawPops(){
  ctx.textAlign='center';ctx.textBaseline='middle';
  for(const p of pops){
    const k=p.t/p.ttl;
    ctx.globalAlpha=1-k;
    const y=p.y-28*easeOutCubic(k);
    const sc=0.5+0.5*easeOutBack(Math.min(1,p.t*5));
    ctx.save();ctx.translate(p.x,y);ctx.scale(sc,sc);
    ctx.font='700 '+p.size+'px '+FONT;ctx.fillStyle=p.col;
    ctx.fillText(p.main,0,0);
    if(p.sub){ctx.font='700 10px '+FONT;ctx.fillStyle='#ff5c29';ctx.fillText(p.sub,0,13);}
    ctx.restore();
  }
  ctx.globalAlpha=1;
}
function drawBanner(){
  const b=banner;if(!b)return;
  let a=1;
  if(b.t>b.hold)a=Math.max(0,1-(b.t-b.hold)/b.fade);
  if(b.scrim){ctx.fillStyle='rgba(9,8,7,0.5)';ctx.fillRect(0,0,W,H);}
  ctx.save();ctx.globalAlpha=a;
  let y=H*0.33,ruleY=0;
  b.lines.forEach((ln,li)=>{
    const cy=y+ln.size/2;
    if(ln.big)ruleY=cy+ln.size/2+14;
    trackedText(ctx,ln.text,W/2,cy,ln.size,Math.max(2,ln.size*0.22),i=>{
      const tt=clamp((b.t-0.05-li*0.09-i*0.028)/0.3,0,1);
      return {a:tt*a,dy:(1-easeOutBack(tt))*-26};
    },ln.accent?'#ff5c29':(ln.dim?'#8a7f6c':'#efe6d4'));
    y+=ln.size+18;
    if(ln.big)y+=10;
  });
  if(b.rule){
    const rw=210*easeOutCubic(clamp((b.t-0.4)/0.45,0,1));
    ctx.fillStyle='#ff5c29';ctx.fillRect(W/2-rw/2,ruleY,rw,2);
  }
  ctx.restore();
}
function drawHint(text,y){
  const a=0.35+0.4*Math.abs(Math.sin(S.time*2.4));
  ctx.textAlign='center';ctx.textBaseline='middle';
  ctx.font='700 13px '+FONT;
  ctx.fillStyle='rgba(238,230,212,'+a.toFixed(3)+')';
  ctx.fillText(text,W/2,y);
}
function drawServeAim(){
  const b=balls[0];if(!b)return;
  for(let k=0;k<4;k++){
    const d=20+k*15;
    ctx.globalAlpha=(0.55-k*0.11)*(0.7+0.3*Math.abs(Math.sin(S.time*3)));
    ctx.fillStyle='#efe6d4';
    ctx.beginPath();
    ctx.arc(b.x+Math.cos(S.aim)*d,b.y+Math.sin(S.aim)*d,2.4-k*0.35,0,TAU);
    ctx.fill();
  }
  ctx.globalAlpha=1;
  drawHint('SPACE / CLICK — LAUNCH',H*0.66);
}
function drawTitle(){
  const t=S.stateT;
  const cy=H*0.40;
  trackedText(ctx,'BALLISTIC',W/2,cy,62,14,i=>{
    const tt=clamp((t-0.15-i*0.05)/0.4,0,1);
    return {a:tt,dy:(1-easeOutBack(tt))*-30};
  },'#efe6d4');
  const rw=240*easeOutCubic(clamp((t-0.55)/0.45,0,1));
  ctx.fillStyle='#ff5c29';ctx.fillRect(W/2-rw/2,cy+34,rw,3);
  if(S.best>0){
    ctx.font='700 12px '+FONT;ctx.textAlign='center';ctx.textBaseline='middle';
    ctx.fillStyle='#5d5546';ctx.fillText('HIGH SCORE '+fmt(S.best),W/2,cy-76);
  }
  trackedText(ctx,'A BRICK-BREAKING ARCADE · SEVEN LEVELS',W/2,cy+62,13,3,
    i=>({a:clamp(t-0.8-i*0.02,0,1),dy:0}),'#8a7f6c');
  drawHint('PRESS SPACE OR CLICK TO START',H*0.78);
  ctx.font='700 10px '+FONT;ctx.textAlign='center';ctx.textBaseline='middle';
  ctx.fillStyle='#4d463b';
  ctx.fillText('MOUSE OR ARROW KEYS TO MOVE THE PADDLE',W/2,H*0.86);
}
function drawPause(){
  ctx.fillStyle='rgba(9,8,7,0.6)';ctx.fillRect(0,0,W,H);
  trackedText(ctx,'PAUSED',W/2,H*0.42,40,12,()=>({a:1,dy:0}),'#efe6d4');
  ctx.font='700 12px '+FONT;ctx.textAlign='center';ctx.textBaseline='middle';
  ctx.fillStyle='#8a7f6c';
  ctx.fillText('P OR CLICK — RESUME',W/2,H*0.42+46);
}
function drawCorners(){
  ctx.strokeStyle='rgba(255,92,41,0.4)';ctx.lineWidth=2;
  const m=10,L=16;
  for(const[px,py,dx,dy]of[[m,m,1,1],[W-m,m,-1,1],[m,H-m,1,-1],[W-m,H-m,-1,-1]]){
    ctx.beginPath();ctx.moveTo(px+dx*L,py);ctx.lineTo(px,py);ctx.lineTo(px,py+dy*L);ctx.stroke();
  }
}

function render(){
  const sx=canvas.width/W,sy=canvas.height/H;
  ctx.setTransform(sx,0,0,sy,0,0);
  // camera shake (trauma² model)
  const sh=S.shake*S.shake;
  if(sh>0.00025)ctx.translate((Math.random()*2-1)*sh*17,(Math.random()*2-1)*sh*12);
  ctx.fillStyle='#141110';ctx.fillRect(-40,-40,W+80,H+80);

  drawDots();drawWaves();
  const L=lightPos();
  if(L){ // warm light carried by the ball
    const g=ctx.createRadialGradient(L.x,L.y,0,L.x,L.y,120);
    g.addColorStop(0,'rgba(255,210,150,0.06)');g.addColorStop(1,'rgba(255,210,150,0)');
    ctx.fillStyle=g;ctx.fillRect(L.x-120,L.y-120,240,240);
  }
  drawBricks();drawPows();drawParts();
  drawBallList(balls,7);
  if(S.state==='title')drawBallList(demo,6);
  if(S.state!=='title'){
    drawPaddle();
    ctx.strokeStyle='rgba(255,92,41,0.14)';ctx.lineWidth=1;
    ctx.beginPath();ctx.moveTo(14,H-30);ctx.lineTo(W-14,H-30);ctx.stroke();
    if(S.bottomFlash>0){
      ctx.fillStyle='rgba(255,70,30,'+(S.bottomFlash*0.22).toFixed(3)+')';
      ctx.fillRect(0,H-80,W,80);
      ctx.fillStyle='rgba(255,70,30,'+(S.bottomFlash*0.5).toFixed(3)+')';
      ctx.fillRect(0,H-4,W,4);
    }
  }
  // court frame — pulses with the combo
  const ba=0.14+Math.min(0.45,S.comboFlash*0.55)+(S.combo>=8?0.10+0.07*Math.sin(S.time*9):0);
  ctx.strokeStyle='rgba(238,230,212,'+ba.toFixed(3)+')';ctx.lineWidth=1.5;
  ctx.strokeRect(0.75,0.75,W-1.5,H-1.5);
  drawCorners();

  drawPops();
  ctx.drawImage(vig,0,0,W,H);

  if(S.state==='title'){drawTitle();}
  else{
    if(banner&&(S.state==='intro'||S.state==='clear'||S.state==='gameover'||S.state==='win'))drawBanner();
    if(S.state==='serve')drawServeAim();
    if(S.state==='gameover')drawHint('SPACE OR CLICK — PLAY AGAIN',H*0.62);
    if(S.state==='win')drawHint('SPACE OR CLICK — TITLE',H*0.62);
  }
  if(S.paused)drawPause();
}

/* ============================ main loop ============================ */
let last=performance.now();
function frame(now){
  requestAnimationFrame(frame);
  let dt=(now-last)/1000;last=now;
  if(!(dt>0)||dt>0.05)dt=0.0167;
  S.time+=dt;
  // time-scale easing (clear-slowmo + slow-ball power-up)
  const st=S.slowTimer>0?0.3:1;
  S.slowTimer=Math.max(0,S.slowTimer-dt);
  S.slowmo+=(st-S.slowmo)*Math.min(1,dt*5);
  const sf=S.slowUntil>S.time?0.62:1;
  S.slowEase+=(sf-S.slowEase)*Math.min(1,dt*4);
  S.shake*=Math.exp(-dt*4.2);
  if(S.freeze>0){S.freeze-=dt;}          // hit-stop: the world holds its breath
  else if(!S.paused){update(dt*S.slowmo,dt);}
  render();
}

/* ============================ input ============================ */
function primary(){
  Snd.init();
  if(S.paused){togglePause();return;}
  switch(S.state){
    case 'title':startRun();break;
    case 'serve':launch();break;
    case 'clear':if(S.stateT>0.8)nextLevel();break;
    case 'gameover':if(S.stateT>0.6)startRun();break;
    case 'win':if(S.stateT>0.6)toTitle();break;
  }
}
function togglePause(){
  if(!(S.state==='play'||S.state==='serve'))return;
  S.paused=!S.paused;
  elPause.textContent=S.paused?'RESUME':'PAUSE';
}
function autoPause(){
  keys.left=keys.right=false;
  if((S.state==='play'||S.state==='serve')&&!S.paused)togglePause();
}
window.addEventListener('keydown',e=>{
  const k=e.key;
  if(k==='ArrowLeft'||k==='ArrowRight'||k==='ArrowUp'||k==='ArrowDown'||k===' ')e.preventDefault();
  if(k==='ArrowLeft'||k==='a'||k==='A')keys.left=true;
  else if(k==='ArrowRight'||k==='d'||k==='D')keys.right=true;
  else if(k===' '||k==='Spacebar')primary();
  else if(k==='p'||k==='P')togglePause();
  else if(k==='m'||k==='M'){Snd.init();Snd.toggle();setSoundLabel();}
  else if(k==='r'||k==='R'){if(S.state!=='title')startRun();}
  Snd.init();
});
window.addEventListener('keyup',e=>{
  const k=e.key;
  if(k==='ArrowLeft'||k==='a'||k==='A')keys.left=false;
  if(k==='ArrowRight'||k==='d'||k==='D')keys.right=false;
});
stageEl.addEventListener('pointermove',e=>{
  const r=canvas.getBoundingClientRect();
  paddle.tx=(e.clientX-r.left)/r.width*W;
});
stageEl.addEventListener('pointerdown',e=>{
  Snd.init();
  const r=canvas.getBoundingClientRect();
  paddle.tx=(e.clientX-r.left)/r.width*W;
  primary();
});
window.addEventListener('pointerdown',()=>Snd.init(),{passive:true});
window.addEventListener('blur',autoPause);
document.addEventListener('visibilitychange',()=>{if(document.hidden)autoPause();});
elSound.addEventListener('click',()=>{Snd.init();Snd.toggle();setSoundLabel();elSound.blur();});
elPause.addEventListener('click',()=>{togglePause();elPause.blur();});

/* ============================ boot ============================ */
try{S.best=parseInt(localStorage.getItem('bb.best'))||0;}catch(e){}
try{if(localStorage.getItem('bb.mute')==='1')Snd.setMuted(true);}catch(e){}
setSoundLabel();
elBest.textContent=fmt(S.best);
updateLives();
layout();
toTitle();
requestAnimationFrame(frame);
</script>
</body>
</html>

Compare with

ERNIE-Light-DT-1.0: other attempts