• Updated 2 Nov, 13:50 UTC
  • Next run 3 Nov, 05:17 UTCNext 3 Nov 05:17
  • Last 7 days: 3 new models · 113 runs7 d: 3 models · 113 runs
Playable Breakout

GLM 5.3 FlashX LegalZ.ai

Network offOpen alone (new tab)

Run details

Model
z-ai/glm-5.3-flashx-legal
Route
OpenRouter API, one user message
Served by
Z.AI
Effort
max, OpenRouter's default for this model
Limits
60 min · 131,072 output tokens (the model's limit)
Date
2 Nov 2026
Time
11 min 20 s
Size
33 kB
Tokens
88 in, 83,992 out (71,077 of them thinking)
List price
$0.11 at today's price ($0.37 in / $1.25 out per M)
Paid
$0.11

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

33 kB 901 lines Raw

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1,user-scalable=no">
<meta name="theme-color" content="#F3EDE0">
<title>MISPRINT · a breakout in ink &amp; paper</title>
<style>
  *{margin:0;padding:0;box-sizing:border-box}
  html,body{height:100%;overflow:hidden}
  body{background:#F3EDE0;user-select:none;-webkit-user-select:none}
  #c{display:block;width:100vw;height:100vh;touch-action:none}
  #grain{position:fixed;inset:0;pointer-events:none;opacity:.09;mix-blend-mode:multiply}
</style>
</head>
<body>
<canvas id="c"></canvas>
<div id="grain" aria-hidden="true"></div>
<script>
'use strict';

/* ---------- roundRect fallback ---------- */
if(!CanvasRenderingContext2D.prototype.roundRect){
  CanvasRenderingContext2D.prototype.roundRect=function(x,y,w,h,r){
    r=Math.min(r,Math.abs(w)/2,Math.abs(h)/2);
    this.moveTo(x+r,y);
    this.arcTo(x+w,y,x+w,y+h,r);
    this.arcTo(x+w,y+h,x,y+h,r);
    this.arcTo(x,y+h,x,y,r);
    this.arcTo(x,y,x+w,y,r);
    this.closePath();
    return this;
  };
}

/* ---------- paper grain (generated, no image files) ---------- */
(function(){
  const n=document.createElement('canvas'); n.width=n.height=140;
  const g=n.getContext('2d'), id=g.createImageData(140,140);
  for(let i=0;i<id.data.length;i+=4){
    const v=185+(Math.random()*70|0);
    id.data[i]=v; id.data[i+1]=v; id.data[i+2]=v; id.data[i+3]=255;
  }
  g.putImageData(id,0,0);
  document.getElementById('grain').style.backgroundImage='url('+n.toDataURL()+')';
})();

/* ---------- constants ---------- */
const FW=540, FH=720, HUD=56;
const COLS=11, BGAP=5, BH=20, BWALL=12;
const BW=(FW-BWALL*2-(COLS-1)*BGAP)/COLS;
const BTOP=HUD+36;
const PADDLE_Y=FH-46;
const INK='#181410', PAPER='#F3EDE0', PAPER2='#F9F4E8';
const ACCENT='#E8481F', MUSTARD='#E9A820', TEAL='#1E7A6F', BLUE='#2E5EA8';
const ROWC=[ACCENT,MUSTARD,TEAL,BLUE];
const PAL=[ACCENT,MUSTARD,TEAL,BLUE,INK];
const DISP='Futura,"Avenir Next","Century Gothic","Trebuchet MS",sans-serif';
const MONO='Menlo,Consolas,"DejaVu Sans Mono","Courier New",monospace';
const POWC={wide:ACCENT,multi:TEAL,slow:BLUE,pts:MUSTARD,life:ACCENT};
const POWL={wide:'W',multi:'M',slow:'S',pts:'500',life:'+'};
const LBL={wide:'WIDE',multi:'TRIPLE',slow:'SLOW',pts:'+500',life:'+1 LIFE'};
const LCOL={wide:ACCENT,multi:TEAL,slow:BLUE,pts:'#B47E10',life:ACCENT};

const LEVELS=[
 {name:'INDEX',rows:[
  'aaaaaaaaaaa','bbbbbbbbbbb','ccccccccccc','ddddddddddd','bbbbbbbbbbb','aaaaaaaaaaa']},
 {name:'FREQUENCY',rows:[
  'a.a.a.a.a.a','.b.b.b.b.b.','aTa.a.a.aTa','.b.b.b.b.b.','c.c.c.c.c.c','.d.d.d.d.d.']},
 {name:'SIGNAL',rows:[
  '.....b.....','....bbb....','...bbbbb...','..bbTdTbb..','.bbbdbdbbb.',
  '..bbTdTbb..','...bbbbb...','....bbb....','.....b.....']},
 {name:'STATIC',rows:[
  'a.b.T.d.c.b','.d.c.a.T.b.','b.d.b.a.d.c','.c.a.d.b.T.','d.b.c.a.c.a','.T.d.b.c.d.']},
 {name:'MONOLITH',rows:[
  '.....a.....','....aTa....','...aSSSa...','..aTSSSTa..','.aSSSSSSSa.','aTTTTTTTTTa']},
 {name:'BARCODE',rows:[
  'a.a.a.a.a.a','aaaaaaaaaaa','b.b.b.b.b.b','bbbbbbbbbbb','c.c.c.c.c.c','ccccccccccc']},
 {name:'THE PRESS',rows:[
  'TTTTTTTTTTT','.a.b.c.d.a.','T.T.T.T.T.T','.d.c.b.a.d.','S.S.S.S.S.S','.b.a.d.c.b.']},
];

/* ---------- state ---------- */
let W=0,H=0,dpr=1,scl=1,ox=0,oy=0;
let state='title',paused=false,muted=false;
let time=0,timescale=1,slowmoT=0,freezeT=0;
let trauma=0,flash=0,scorePunch=0;
let score=0,lives=3,level=0,combo=0,mult=1;
let hi=0,hiStart=0,hiBeaten=false;
try{ hi=parseInt(localStorage.getItem('misprint_hi'),10)||0; }catch(err){}
let bricks=[],balls=[],pows=[],parts=[],texts=[];
let remaining=0;
let mouseX=null,lastInput='key';
let introT=0,introHit=false,readyT=0;
let clearT=0,bonused=false,lastBonus=0;
let overT=0,overHit=false,winT=0,winHit=false,winAcc=0;
let wideUntil=0,slowUntil=0;
const keys={left:false,right:false};
const paddle={x:FW/2,w:92,wt:92,vx:0,sq:0,tilt:0};
const MUTE={x:FW-86,y:FH-30,w:76,h:20};

/* ---------- tiny helpers ---------- */
const clamp=(v,a,b)=>v<a?a:v>b?b:v;
const rand=(a,b)=>a+Math.random()*(b-a);
const pick=a=>a[(Math.random()*a.length)|0];
const pad=n=>String(Math.max(0,n|0)).padStart(6,'0');
function rr(x,y,w,h,r){ ctx.beginPath(); ctx.roundRect(x,y,w,h,r); }
function line(x1,y1,x2,y2,w,col){
  ctx.strokeStyle=col||INK; ctx.lineWidth=w||2;
  ctx.beginPath(); ctx.moveTo(x1,y1); ctx.lineTo(x2,y2); ctx.stroke();
}
function text(str,x,y,size,font,col,align,ls,alpha,weight){
  ctx.save();
  ctx.globalAlpha=alpha==null?1:alpha;
  ctx.fillStyle=col;
  ctx.font=(weight||'700')+' '+size+'px '+font;
  ctx.textAlign=align||'center';
  ctx.textBaseline='middle';
  try{ ctx.letterSpacing=(ls||0)+'px'; }catch(err){}
  ctx.fillText(str,x,y);
  ctx.restore();
}
function misPrint(str,x,y,size){ // misregistration: the whole identity
  ctx.save();
  ctx.font='800 '+size+'px '+DISP;
  ctx.textAlign='center'; ctx.textBaseline='middle';
  try{ ctx.letterSpacing='2px'; }catch(err){}
  const o=2.4+Math.sin(time*2.1)*1.1;
  ctx.fillStyle=TEAL;   ctx.fillText(str,x+o,y-o*0.65);
  ctx.fillStyle=ACCENT; ctx.fillText(str,x-o,y+o*0.65);
  ctx.fillStyle=INK;    ctx.fillText(str,x,y);
  ctx.restore();
}
function vtext(str,x,y,size,col,dir){
  ctx.save(); ctx.translate(x,y); ctx.rotate(dir<0?-Math.PI/2:Math.PI/2);
  text(str,0,0,size,DISP,col,'center',3,1,'700');
  ctx.restore();
}
function dim(a){ ctx.fillStyle='rgba(243,237,224,'+a+')'; ctx.fillRect(0,0,FW,FH); }

/* ---------- audio (all synthesized) ---------- */
let actx=null,master=null;
function initAudio(){
  if(actx){ if(actx.state==='suspended') actx.resume(); return; }
  try{
    actx=new (window.AudioContext||window.webkitAudioContext)();
    master=actx.createGain(); master.gain.value=0.4; master.connect(actx.destination);
  }catch(err){}
}
function beep(freq,dur,type,vol,slideTo,when){
  if(!actx||muted) return;
  const t=actx.currentTime+(when||0);
  const o=actx.createOscillator(), g=actx.createGain();
  o.type=type; o.frequency.setValueAtTime(freq,t);
  if(slideTo) o.frequency.exponentialRampToValueAtTime(Math.max(20,slideTo),t+dur);
  g.gain.setValueAtTime(0.0001,t);
  g.gain.exponentialRampToValueAtTime(vol,t+0.008);
  g.gain.exponentialRampToValueAtTime(0.0001,t+dur);
  o.connect(g); g.connect(master); o.start(t); o.stop(t+dur+0.05);
}
function hiss(dur,vol,when){
  if(!actx||muted) return;
  const t=actx.currentTime+(when||0), len=Math.max(1,actx.sampleRate*dur|0);
  const buf=actx.createBuffer(1,len,actx.sampleRate), d=buf.getChannelData(0);
  for(let i=0;i<len;i++) d[i]=(Math.random()*2-1)*(1-i/len);
  const s=actx.createBufferSource(); s.buffer=buf;
  const f=actx.createBiquadFilter(); f.type='highpass'; f.frequency.value=900;
  const g=actx.createGain(); g.gain.value=vol;
  s.connect(f); f.connect(g); g.connect(master); s.start(t);
}
const sfx={
  pop(c){ const f=280*Math.pow(2,Math.min(c,14)/12); beep(f,0.1,'square',0.32); hiss(0.05,0.1); },
  thud(){ beep(150,0.07,'triangle',0.38,90); },
  steel(){ beep(92,0.05,'square',0.22); beep(184,0.035,'square',0.12); },
  paddle(){ beep(105,0.12,'sine',0.5,65); hiss(0.03,0.06); },
  wall(){ beep(640,0.035,'triangle',0.1); },
  pow(){ beep(523,0.07,'square',0.22); beep(784,0.1,'square',0.22,0,0.07); },
  life(){ beep(659,0.09,'sine',0.28); beep(880,0.12,'sine',0.28,0,0.08); },
  lose(){ beep(300,0.5,'sawtooth',0.3,55); },
  launch(){ beep(180,0.16,'square',0.22,460); },
  clear(){ [523,659,784,1046].forEach((f,i)=>beep(f,0.13,'square',0.26,0,i*0.09)); },
  over(){ [392,330,262,196].forEach((f,i)=>beep(f,0.2,'triangle',0.28,0,i*0.15)); },
  start(){ beep(392,0.09,'square',0.22); beep(523,0.11,'square',0.22,0,0.08); },
  stamp(){ beep(70,0.16,'sine',0.6,38); hiss(0.05,0.18); },
};
function toggleMute(){ muted=!muted; if(!muted) beep(660,0.06,'square',0.18); }

/* ---------- entities & fx ---------- */
const makeBall=(x,y,vx,vy)=>({x,y,vx,vy,r:6,trail:[],sq:0,stuck:false});
function addText(x,y,txt,col,size,mono){
  texts.push({x,y,txt,t:0,life:0.85,col,size,mono});
  if(texts.length>28) texts.shift();
}
function addScore(n,x,y,col){
  score+=n; scorePunch=1;
  if(score>hi) hi=score;
  if(x!=null) addText(x,y,'+'+n,col||INK,12,true);
}
function addPart(p){ parts.push(p); if(parts.length>380) parts.shift(); }
function fleckBurst(x,y,col,n,sp){
  for(let i=0;i<n;i++){
    const a=rand(0,Math.PI*2), s=rand(sp*0.4,sp);
    addPart({type:'fleck',x,y,vx:Math.cos(a)*s,vy:Math.sin(a)*s-60,t:0,life:rand(0.3,0.6),col,sz:rand(2.5,5),rot:rand(0,6)});
  }
}
function burstBrick(k){
  const cx=k.x+BW/2, cy=k.y+BH/2;
  for(let i=0;i<9;i++){
    const a=rand(0,Math.PI*2), s=rand(70,330);
    addPart({type:'fleck',x:cx,y:cy,vx:Math.cos(a)*s,vy:Math.sin(a)*s-90,t:0,life:rand(0.35,0.75),col:Math.random()<0.7?k.col:INK,sz:rand(3,7),rot:rand(0,6)});
  }
  addPart({type:'ring',x:cx,y:cy,t:0,life:0.32,col:k.col,r0:8,r1:36});
}
function burstConf(x,y,n){
  for(let i=0;i<n;i++){
    const a=rand(0,Math.PI*2), s=rand(90,380);
    addPart({type:'conf',x,y,vx:Math.cos(a)*s,vy:Math.sin(a)*s-120,t:0,life:rand(1.2,2.2),col:pick(PAL),sz:rand(4,7),rot:rand(0,6),ph:rand(0,6)});
  }
}
function spawnConfetti(n){
  for(let i=0;i<n;i++)
    addPart({type:'conf',x:rand(30,FW-30),y:-12,vx:rand(-40,40),vy:rand(70,190),t:0,life:rand(1.8,3),col:pick(PAL),sz:rand(4,7),rot:rand(0,6),ph:rand(0,6)});
}

/* ---------- game flow ---------- */
function saveHi(){ try{ localStorage.setItem('misprint_hi',String(hi)); }catch(err){} }
addEventListener('beforeunload',saveHi);

function loadLevel(i){
  bricks=[]; remaining=0; pows=[]; texts=[]; parts=[];
  const rows=LEVELS[i].rows;
  for(let r=0;r<rows.length;r++){
    for(let c=0;c<COLS;c++){
      const ch=rows[r][c];
      if(!ch||ch==='.') continue;
      const b={x:BWALL+c*(BW+BGAP),y:BTOP+r*(BH+BGAP),hp:1,steel:false,tough:false,col:ROWC[0],dead:false};
      if(ch==='S') b.steel=true;
      else if(ch==='T'){ b.tough=true; b.hp=2; b.col=INK; }
      else b.col=ROWC['abcd'.indexOf(ch)]||ROWC[0];
      bricks.push(b);
      if(!b.steel) remaining++;
    }
  }
  balls=[makeBall(paddle.x,PADDLE_Y-13,0,0)];
  balls[0].stuck=true;
  paddle.wt=92; wideUntil=0; slowUntil=0;
  combo=0; mult=1;
}
function startGame(){
  score=0; lives=3; level=0; hiStart=hi; hiBeaten=false;
  loadLevel(0);
  introT=0; introHit=false; state='intro'; paused=false;
  sfx.start();
}
function nextLevel(){
  level++;
  if(level>=LEVELS.length){
    state='win'; winT=0; winHit=false; winAcc=0;
    hiBeaten=score>hiStart;
    hi=Math.max(hi,score); saveHi(); sfx.clear();
  }else{
    loadLevel(level);
    introT=0; introHit=false; state='intro'; sfx.start();
  }
}
function toTitle(){ paused=false; state='title'; initAttract(); }
function launch(){
  let b=null;
  for(const x of balls) if(x.stuck){ b=x; break; }
  if(!b) return;
  b.stuck=false;
  const sp=320+level*14;
  const a=clamp((paddle.vx||0)*0.0007,-0.5,0.5)+rand(-0.12,0.12);
  b.vx=Math.sin(a)*sp; b.vy=-Math.cos(a)*sp;
  paddle.sq=1; state='play'; sfx.launch();
}
function action(){
  if(paused){ paused=false; return; }
  if(state==='title') startGame();
  else if(state==='intro') introT=2;
  else if(state==='ready') launch();
  else if(state==='clear'){ if(clearT>0.8) clearT=2.45; }
  else if(state==='over'&&overT>0.6) startGame();
  else if(state==='win'&&winT>0.8) toTitle();
}
function togglePause(){ if(state==='play'||state==='ready') paused=!paused; }

function loseLife(){
  lives--; combo=0; mult=1;
  pows.length=0; wideUntil=0; slowUntil=0; paddle.wt=92;
  trauma=0.7; flash=0.45; slowmoT=0.28;
  sfx.lose();
  addText(paddle.x,PADDLE_Y-30,'BALL LOST',ACCENT,14,false);
  if(lives<=0){
    state='over'; overT=0; overHit=false;
    hiBeaten=score>hiStart;
    hi=Math.max(hi,score); saveHi();
    sfx.over();
    if(hiBeaten) burstConf(FW/2,FH/2,60);
  }else{
    balls=[makeBall(paddle.x,PADDLE_Y-13,0,0)];
    balls[0].stuck=true;
    state='ready'; readyT=0;
  }
}
function onClear(){
  slowmoT=0.55;
  freezeT=Math.max(freezeT,0.1);
  trauma=Math.min(1,trauma+0.45);
  state='clear'; clearT=0; bonused=false;
}

/* ---------- gameplay ---------- */
function maybeDrop(k){
  if(Math.random()>=0.14) return;
  const r=Math.random();
  const type=r<0.30?'wide':r<0.52?'multi':r<0.74?'slow':r<0.90?'pts':'life';
  pows.push({x:k.x+BW/2,y:k.y+BH/2,type,t:0,ph:rand(0,6)});
}
function applyPow(type){
  if(type==='wide') wideUntil=time+12;
  else if(type==='slow') slowUntil=time+6;
  else if(type==='life'){
    if(lives<5) lives++;
    else { score+=250; scorePunch=1; if(score>hi) hi=score; }
  }
  else if(type==='pts') addScore(500,paddle.x,PADDLE_Y-40,MUSTARD);
  else if(type==='multi'){
    const snap=balls.filter(b=>!b.stuck);
    for(const b of snap){
      const sp=Math.hypot(b.vx,b.vy)||300, a=Math.atan2(b.vy,b.vx);
      for(const da of [-0.5,0.5]){
        if(balls.length>=6) break;
        balls.push(makeBall(b.x,b.y,Math.cos(a+da)*sp,Math.sin(a+da)*sp));
      }
    }
  }
}
function hitBrick(k){
  if(k.steel){
    sfx.steel(); trauma=Math.min(1,trauma+0.05);
    fleckBurst(k.x+BW/2,k.y+BH/2,'#7B7264',3,150);
    return;
  }
  k.hp--;
  if(k.hp>0){
    sfx.thud(); trauma=Math.min(1,trauma+0.06);
    freezeT=Math.min(0.05,freezeT+0.025);
    fleckBurst(k.x+BW/2,k.y+BH/2,k.col,4,170);
    return;
  }
  k.dead=true; remaining--;
  combo++;
  const nm=Math.min(8,1+((combo/4)|0));
  if(nm>mult){ mult=nm; addText(k.x+BW/2,k.y-10,'×'+mult,ACCENT,15,false); }
  addScore((k.tough?120:50)*mult,k.x+BW/2,k.y+BH/2);
  burstBrick(k);
  freezeT=Math.min(0.09,freezeT+0.04);
  trauma=Math.min(1,trauma+0.11);
  sfx.pop(combo);
  maybeDrop(k);
  if(remaining<=0) onClear();
}
function jitter(b){
  const a=(Math.random()-0.5)*0.04, c=Math.cos(a), s=Math.sin(a);
  const vx=b.vx; b.vx=b.vx*c-b.vy*s; b.vy=vx*s+b.vy*c;
}
function brickCollide(b,px,py){
  for(const k of bricks){
    if(k.dead) continue;
    const nx=clamp(b.x,k.x,k.x+BW), ny=clamp(b.y,k.y,k.y+BH);
    const dx=b.x-nx, dy=b.y-ny;
    if(dx*dx+dy*dy>b.r*b.r) continue;
    let h=false,v=false;
    if(px<=k.x||px>=k.x+BW) h=true;
    if(py<=k.y||py>=k.y+BH) v=true;
    if(!h&&!v) v=true;
    if(h) b.vx=-b.vx;
    if(v) b.vy=-b.vy;
    jitter(b);
    b.x=px; b.y=py;
    b.sq=1;
    hitBrick(k);
    return true;
  }
  return false;
}
function paddleHit(b){
  b.y=PADDLE_Y-b.r-0.5;
  const rel=clamp((b.x-paddle.x)/(paddle.w/2+b.r),-1,1);
  const ang=rel*1.05;
  const sp=Math.min(Math.hypot(b.vx,b.vy)+7,620);
  b.vx=Math.sin(ang)*sp; b.vy=-Math.cos(ang)*sp;
  b.sq=1; paddle.sq=1;
  if(state!=='title'){
    combo=0; mult=1;
    sfx.paddle();
    trauma=Math.min(1,trauma+0.05);
  }
  fleckBurst(b.x,PADDLE_Y,ACCENT,4,160);
}
function wallFx(x,y){
  fleckBurst(x,y,INK,3,130);
  sfx.wall();
  trauma=Math.min(1,trauma+0.012);
}
function stickBalls(){
  for(const b of balls) if(b.stuck){ b.x=paddle.x; b.y=PADDLE_Y-7-b.r; b.trail.length=0; }
}

/* ---------- updates ---------- */
function updatePaddle(dt){
  const dir=(keys.right?1:0)-(keys.left?1:0);
  if(lastInput==='mouse'&&mouseX!=null){
    const t=1-Math.exp(-16*dt);
    const nx=paddle.x+(mouseX-paddle.x)*t;
    paddle.vx=dt>0?(nx-paddle.x)/dt:0;
    paddle.x=nx;
  }else{
    if(dir) paddle.vx=clamp(paddle.vx+dir*2800*dt,-840,840);
    else paddle.vx*=Math.exp(-9*dt);
    paddle.x+=paddle.vx*dt;
  }
  const hw=paddle.w/2+4;
  paddle.x=clamp(paddle.x,hw,FW-hw);
  paddle.wt=time<wideUntil?150:92;
  paddle.w+=(paddle.wt-paddle.w)*(1-Math.exp(-9*dt));
  paddle.tilt=clamp(paddle.vx*0.00011,-0.085,0.085);
}
function updateBalls(dt){
  const f=time<slowUntil?0.62:1;
  for(let i=balls.length-1;i>=0;i--){
    const b=balls[i];
    if(b.stuck){ b.x=paddle.x; b.y=PADDLE_Y-7-b.r; b.trail.length=0; continue; }
    const sp=Math.hypot(b.vx,b.vy)||1;
    const steps=Math.max(1,Math.ceil(sp*f*dt/(b.r*0.8)));
    const sdt=dt*f/steps;
    let lost=false;
    for(let s=0;s<steps;s++){
      const px=b.x, py=b.y;
      b.x+=b.vx*sdt; b.y+=b.vy*sdt;
      if(b.x<b.r){ b.x=b.r; b.vx=Math.abs(b.vx); wallFx(b.x,b.y); b.sq=1; }
      else if(b.x>FW-b.r){ b.x=FW-b.r; b.vx=-Math.abs(b.vx); wallFx(b.x,b.y); b.sq=1; }
      if(b.y<b.r){ b.y=b.r; b.vy=Math.abs(b.vy); wallFx(b.x,b.y); b.sq=1; }
      if(b.vy>0&&b.y+b.r>=PADDLE_Y&&b.y-b.r<=PADDLE_Y+14&&Math.abs(b.x-paddle.x)<=paddle.w/2+b.r) paddleHit(b);
      brickCollide(b,px,py);
      if(b.y-b.r>FH+10){ lost=true; break; }
    }
    const s2=Math.hypot(b.vx,b.vy);
    if(s2>0&&Math.abs(b.vy)<s2*0.18){
      const sign=b.vy<0?-1:(b.vy>0?1:-1);
      b.vy=sign*s2*0.18;
      b.vx=(b.vx<0?-1:1)*Math.sqrt(Math.max(0,s2*s2-b.vy*b.vy));
    }
    b.sq=Math.max(0,(b.sq||0)-dt*5);
    b.trail.push({x:b.x,y:b.y});
    if(b.trail.length>11) b.trail.shift();
    if(lost){
      balls.splice(i,1);
      if(state==='play'&&balls.length===0) loseLife();
    }
  }
}
function updatePows(dt){
  for(let i=pows.length-1;i>=0;i--){
    const p=pows[i];
    p.t+=dt; p.y+=130*dt; p.x+=Math.sin(p.t*3+p.ph)*14*dt;
    if(p.y>FH+16){ pows.splice(i,1); continue; }
    if(p.y+11>=PADDLE_Y-2&&p.y-11<=PADDLE_Y+16&&Math.abs(p.x-paddle.x)<=paddle.w/2+13){
      applyPow(p.type);
      addText(p.x,PADDLE_Y-22,LBL[p.type],LCOL[p.type],13,false);
      addPart({type:'ring',x:paddle.x,y:PADDLE_Y+6,t:0,life:0.35,col:LCOL[p.type],r0:8,r1:34});
      if(p.type==='life') sfx.life(); else sfx.pow();
      pows.splice(i,1);
    }
  }
}
function updateParts(dt){
  for(let i=parts.length-1;i>=0;i--){
    const p=parts[i];
    p.t+=dt;
    if(p.t>=p.life){ parts.splice(i,1); continue; }
    if(p.type==='fleck'){
      p.vy+=850*dt; p.vx*=Math.exp(-1.6*dt);
      p.x+=p.vx*dt; p.y+=p.vy*dt; p.rot+=6*dt;
    }else if(p.type==='conf'){
      p.vy+=55*dt;
      p.x+=p.vx*dt+Math.sin((p.t+p.ph)*3)*22*dt;
      p.y+=p.vy*dt; p.rot+=2.6*dt;
    }
  }
}
function updateTexts(dt){
  for(let i=texts.length-1;i>=0;i--){
    const t=texts[i]; t.t+=dt; t.y-=28*dt;
    if(t.t>=t.life) texts.splice(i,1);
  }
}
function initAttract(){
  bricks=[]; pows=[]; parts=[]; texts=[]; balls=[];
  paddle.x=FW/2; paddle.w=paddle.wt=92; paddle.vx=0;
  for(let i=0;i<3;i++){
    const a=rand(0,Math.PI*2), sp=rand(230,310);
    balls.push({x:rand(80,FW-80),y:rand(90,320),vx:Math.cos(a)*sp,vy:Math.sin(a)*sp,r:6,trail:[],sq:0,stuck:false});
  }
}
function updateAttract(dt){
  paddle.x=FW/2+Math.sin(time*0.8)*150;
  paddle.w=92; paddle.wt=92;
  updateBalls(dt);
  let guard=0;
  while(balls.length<3&&guard++<5){
    const a=Math.PI/2+rand(-0.7,0.7), sp=rand(230,310);
    balls.push({x:rand(70,FW-70),y:rand(70,150),vx:Math.cos(a)*sp,vy:Math.sin(a)*sp,r:6,trail:[],sq:0,stuck:false});
  }
}
function update(dt,rdt){
  time+=dt;
  switch(state){
    case 'title': updateAttract(dt); break;
    case 'intro':
      updatePaddle(dt); stickBalls();
      introT+=rdt;
      if(!introHit&&introT>0.16){ introHit=true; sfx.stamp(); trauma=Math.min(1,trauma+0.3); }
      if(introT>1.35){ state='ready'; readyT=0; }
      break;
    case 'ready':
      updatePaddle(dt); stickBalls(); readyT+=rdt;
      break;
    case 'play':
      updatePaddle(dt); updateBalls(dt); updatePows(dt);
      break;
    case 'clear':
      updatePaddle(dt); updateBalls(dt); updatePows(dt);
      clearT+=rdt;
      if(!bonused&&clearT>0.55){
        bonused=true;
        lastBonus=400+level*200+lives*150;
        score+=lastBonus; scorePunch=1;
        if(score>hi) hi=score;
        addText(FW/2,180,'BONUS +'+lastBonus,ACCENT,16,false);
        sfx.clear(); burstConf(FW/2,FH/2,46); saveHi();
      }
      if(clearT>2.45) nextLevel();
      break;
    case 'over':
      overT+=rdt;
      if(!overHit&&overT>0.16){ overHit=true; sfx.stamp(); trauma=Math.min(1,trauma+0.35); }
      if(hiBeaten&&overT<1.4&&Math.random()<0.18) spawnConfetti(1);
      break;
    case 'win':
      winT+=rdt; updatePaddle(dt); updateBalls(dt);
      if(!winHit&&winT>0.16){ winHit=true; sfx.stamp(); trauma=0.4; }
      winAcc+=rdt;
      while(winAcc>0.05){ winAcc-=0.05; spawnConfetti(1); }
      break;
  }
  updateParts(dt); updateTexts(dt);
}

/* ---------- drawing ---------- */
function drawField(){
  ctx.fillStyle=INK; rr(8,10,FW,FH,6); ctx.fill();
  ctx.fillStyle=PAPER2; rr(0,0,FW,FH,6); ctx.fill();
  ctx.lineWidth=3; ctx.strokeStyle=INK; rr(1.5,1.5,FW-3,FH-3,5); ctx.stroke();
  ctx.lineWidth=1; ctx.strokeStyle='rgba(24,20,16,0.22)'; rr(6,6,FW-12,FH-12,4); ctx.stroke();
}
function drawBricks(){
  ctx.fillStyle=INK;
  for(const k of bricks) if(!k.dead){ rr(k.x+3,k.y+4,BW,BH,3); ctx.fill(); }
  for(const k of bricks){
    if(k.dead) continue;
    if(k.steel){
      ctx.fillStyle='#4E463C'; rr(k.x,k.y,BW,BH,3); ctx.fill();
      ctx.save(); rr(k.x,k.y,BW,BH,3); ctx.clip();
      ctx.strokeStyle='rgba(243,237,224,0.16)'; ctx.lineWidth=1.2;
      ctx.beginPath();
      for(let i=-BH;i<BW;i+=5){ ctx.moveTo(k.x+i,k.y+BH); ctx.lineTo(k.x+i+BH,k.y); }
      ctx.stroke(); ctx.restore();
      ctx.fillStyle='rgba(243,237,224,0.45)';
      const rv=[[4,4],[BW-4,4],[4,BH-4],[BW-4,BH-4]];
      for(const d of rv){ ctx.beginPath(); ctx.arc(k.x+d[0],k.y+d[1],1.4,0,Math.PI*2); ctx.fill(); }
    }else{
      ctx.fillStyle=k.col; rr(k.x,k.y,BW,BH,3); ctx.fill();
      if(k.tough){
        if(k.hp>=2){
          ctx.fillStyle='rgba(243,237,224,0.55)';
          ctx.fillRect(k.x+5,k.y+BH/2-1,BW-10,2);
        }else{
          ctx.strokeStyle='rgba(243,237,224,0.8)'; ctx.lineWidth=1.4;
          ctx.beginPath();
          ctx.moveTo(k.x+BW*0.28,k.y+2);
          ctx.lineTo(k.x+BW*0.46,k.y+BH*0.42);
          ctx.lineTo(k.x+BW*0.3,k.y+BH*0.68);
          ctx.lineTo(k.x+BW*0.52,k.y+BH-2);
          ctx.stroke();
        }
      }
    }
  }
}
function drawPows(){
  for(const p of pows){
    ctx.save();
    ctx.translate(p.x,p.y);
    ctx.rotate(Math.sin(p.t*2.2+p.ph)*0.12);
    ctx.fillStyle=INK; rr(-8.5,-7.5,22,22,6); ctx.fill();
    ctx.fillStyle=POWC[p.type]; rr(-11,-11,22,22,6); ctx.fill();
    text(POWL[p.type],0,1,p.type==='pts'?8:12,DISP,p.type==='pts'?INK:PAPER2,'center',0,1,'800');
    ctx.restore();
  }
}
function drawBall(b){
  const n=b.trail.length;
  for(let i=0;i<n;i++){
    const p=b.trail[i], a=(i+1)/n;
    ctx.globalAlpha=a*0.16; ctx.fillStyle=INK;
    ctx.beginPath(); ctx.arc(p.x,p.y,b.r*a,0,Math.PI*2); ctx.fill();
  }
  ctx.globalAlpha=1;
  const r=b.r*(1+(b.sq||0)*0.35);
  const off=1.4+Math.min(combo,10)*0.22; // misregistration grows with combo
  ctx.fillStyle=ACCENT;
  ctx.beginPath(); ctx.arc(b.x-off,b.y-off*0.7,r,0,Math.PI*2); ctx.fill();
  ctx.fillStyle=INK;
  ctx.beginPath(); ctx.arc(b.x,b.y,r,0,Math.PI*2); ctx.fill();
  ctx.fillStyle=PAPER2;
  ctx.beginPath(); ctx.arc(b.x-2,b.y-2,1.6,0,Math.PI*2); ctx.fill();
}
function drawPaddle(){
  const w=paddle.w, h=14;
  ctx.save();
  ctx.translate(paddle.x,PADDLE_Y+h/2);
  ctx.rotate(paddle.tilt||0);
  const s=paddle.sq||0;
  ctx.scale(1+s*0.28,1-s*0.38);
  ctx.fillStyle=INK; rr(-w/2+3,-h/2+4,w,h,7); ctx.fill();
  ctx.fillStyle=ACCENT; rr(-w/2-2,-h/2-1.5,w,h,7); ctx.fill();
  ctx.fillStyle=INK; rr(-w/2,-h/2,w,h,7); ctx.fill();
  ctx.fillStyle=ACCENT;
  if(w>110) rr(-16,-h/2+3,32,h-6,3); else rr(-10,-h/2+3,20,h-6,3);
  ctx.fill();
  ctx.restore();
}
function drawParts(){
  for(const p of parts){
    const a=1-p.t/p.life;
    if(p.type==='ring'){
      const r=p.r0+(p.r1-p.r0)*(p.t/p.life);
      ctx.save(); ctx.globalAlpha=a*0.9; ctx.strokeStyle=p.col; ctx.lineWidth=2.5;
      ctx.beginPath(); ctx.arc(p.x,p.y,r,0,Math.PI*2); ctx.stroke(); ctx.restore();
    }else{
      ctx.save(); ctx.globalAlpha=a; ctx.translate(p.x,p.y); ctx.rotate(p.rot);
      ctx.fillStyle=p.col; ctx.fillRect(-p.sz/2,-p.sz/2,p.sz,p.sz); ctx.restore();
    }
  }
}
function drawTexts(){
  for(const t of texts){
    const a=1-t.t/t.life;
    text(t.txt,t.x,t.y,t.size,t.mono?MONO:DISP,t.col,'center',1,a,'800');
  }
}
function drawHud(){
  line(14,HUD,FW-14,HUD,2,INK);
  ctx.save();
  ctx.translate(16,18);
  const k=1+scorePunch*0.16; ctx.scale(k,k);
  text('SCORE',0,0,8,DISP,INK,'left',2,0.55,'700');
  text(pad(score),0,24,23,MONO,INK,'left',1,1,'700');
  if(mult>1) text('×'+mult,112,24,14,DISP,ACCENT,'left',0,1,'800');
  ctx.restore();
  text('PLATE '+String(level+1).padStart(2,'0')+' / '+String(LEVELS.length).padStart(2,'0'),FW/2,18,9,DISP,INK,'center',2,0.55,'700');
  text(LEVELS[level].name,FW/2,36,13,DISP,INK,'center',4,1,'800');
  text('HI '+pad(hi),FW-16,15,9,MONO,INK,'right',1,0.6,'700');
  const lx0=FW-18;
  for(let i=0;i<lives;i++){
    const lx=lx0-i*26;
    ctx.fillStyle=INK; rr(lx-18,30,18,6,3); ctx.fill();
    ctx.fillStyle=ACCENT; rr(lx-9,31.5,6,3,1.5); ctx.fill();
  }
  text('LIVES',lx0-Math.max(lives,1)*26-10,33,8,DISP,INK,'right',1,0.5,'700');
}
function drawChip(){
  MUTE.x=FW-86; MUTE.y=FH-30;
  ctx.save(); ctx.globalAlpha=0.45;
  ctx.strokeStyle=INK; ctx.lineWidth=1.5;
  rr(MUTE.x,MUTE.y,76,20,10); ctx.stroke();
  text(muted?'SOUND OFF':'SOUND ON',MUTE.x+38,MUTE.y+10.5,8,DISP,INK,'center',1,1,'700');
  ctx.restore();
}
function drawStamp(main,sub,t,col){
  const p=Math.min(1,t/0.16), e=1-Math.pow(1-p,3);
  const sc=1+(1-e)*1.9, al=Math.min(1,t/0.08);
  ctx.save();
  ctx.translate(FW/2,FH/2-16); ctx.rotate(-0.055); ctx.scale(sc,sc);
  ctx.globalAlpha=al;
  const w=376, h=sub?126:92;
  ctx.fillStyle='rgba(249,244,232,0.94)'; rr(-w/2,-h/2,w,h,10); ctx.fill();
  ctx.lineWidth=5; ctx.strokeStyle=col; rr(-w/2,-h/2,w,h,10); ctx.stroke();
  ctx.lineWidth=1.5; rr(-w/2+7,-h/2+7,w-14,h-14,6); ctx.stroke();
  text(main,0,sub?-10:0,30,DISP,col,'center',2,1,'800');
  if(sub) text(sub,0,26,12,MONO,INK,'center',1,0.85,'700');
  ctx.restore();
}
function drawReady(){
  const b=balls[0]; if(!b) return;
  ctx.save();
  ctx.strokeStyle='rgba(24,20,16,0.5)'; ctx.lineWidth=2;
  ctx.setLineDash([5,7]); ctx.lineDashOffset=-time*46;
  ctx.beginPath(); ctx.moveTo(b.x,b.y-12); ctx.lineTo(b.x,b.y-86); ctx.stroke();
  ctx.restore();
  if(Math.sin(time*5.2)>-0.15)
    text('SPACE OR CLICK TO LAUNCH',FW/2,FH-118,13,DISP,INK,'center',2,0.95,'800');
  if(level===0&&readyT<5)
    text('MOVE WITH MOUSE OR ARROW KEYS',FW/2,FH-94,10,DISP,INK,'center',2,0.55,'700');
}
function drawTitle(){
  const pw=400, ph=410, px=(FW-pw)/2, py=(FH-ph)/2;
  ctx.fillStyle=INK; rr(px+7,py+9,pw,ph,8); ctx.fill();
  ctx.fillStyle=PAPER2; rr(px,py,pw,ph,8); ctx.fill();
  ctx.lineWidth=3; ctx.strokeStyle=INK; rr(px,py,pw,ph,8); ctx.stroke();
  const cols=[ACCENT,MUSTARD,TEAL,BLUE];
  for(let i=0;i<4;i++){
    const sx=FW/2-62+i*32, sy=py+26;
    ctx.save(); ctx.translate(sx+12,sy+7); ctx.rotate((i-1.5)*0.05);
    ctx.fillStyle=INK; rr(-14,-10,24,14,3); ctx.fill();
    ctx.fillStyle=cols[i]; rr(-12,-7,24,14,3); ctx.fill();
    ctx.restore();
  }
  ctx.strokeStyle=INK; ctx.lineWidth=1.5;
  rr(FW/2-96,py+56,192,22,11); ctx.stroke();
  text('SEVEN-PLATE EDITION',FW/2,py+67.5,8,DISP,INK,'center',2.5,0.8,'700');
  misPrint('MISPRINT',FW/2,py+148,54);
  line(px+44,py+182,px+pw-44,py+182,2,INK);
  text('A BREAKOUT IN INK & PAPER',FW/2,py+202,11,DISP,INK,'center',3,0.75,'700');
  text('BEST  '+pad(hi),FW/2,py+226,12,MONO,INK,'center',1,0.85,'700');
  if(Math.sin(time*4.6)>-0.2)
    text('CLICK OR PRESS SPACE',FW/2,py+260,14,DISP,ACCENT,'center',2,1,'800');
  const rows=[['MOVE','MOUSE / ARROWS'],['LAUNCH','SPACE'],['PAUSE','P'],['SOUND','M']];
  rows.forEach((r,i)=>{
    const yy=py+294+i*21;
    text(r[0],px+70,yy,9,MONO,'rgba(24,20,16,0.5)','left',1,1,'700');
    text(r[1],px+150,yy,10,MONO,INK,'left',1,1,'700');
  });
  text('PRINTED LOCALLY — NO NETWORK REQUIRED',FW/2,py+ph-20,8,DISP,INK,'center',2,0.4,'700');
}
function drawOverlay(){
  if(state==='title'){ drawTitle(); return; }
  if(state==='intro')
    drawStamp('PLATE '+String(level+1).padStart(2,'0'),LEVELS[level].name,introT,ACCENT);
  else if(state==='ready') drawReady();
  else if(state==='clear'&&clearT>0.5)
    drawStamp('PLATE CLEAR','BONUS +'+lastBonus,clearT-0.5,ACCENT);
  else if(state==='over'){
    dim(0.78);
    drawStamp('GAME OVER',hiBeaten?'NEW HI-SCORE — '+pad(score):'SCORE '+pad(score),overT,ACCENT);
    if(overT>0.7&&Math.sin(time*5)>-0.1)
      text('SPACE TO RETRY',FW/2,FH/2+118,13,DISP,INK,'center',2,0.9,'800');
  }else if(state==='win'){
    dim(0.55);
    drawStamp('EDITION COMPLETE','ALL 7 PLATES PRINTED — '+pad(score),winT,ACCENT);
    if(winT>0.8&&Math.sin(time*5)>-0.1)
      text('SPACE FOR TITLE',FW/2,FH/2+118,13,DISP,INK,'center',2,0.9,'800');
  }
  if(paused){ dim(0.82); drawStamp('PAUSED','P TO RESUME · R TO QUIT',0.2,INK); }
}
function drawWorld(){
  if(state!=='title'){
    ctx.save();
    ctx.font='900 200px '+DISP;
    ctx.textAlign='center'; ctx.textBaseline='middle';
    ctx.strokeStyle='rgba(24,20,16,0.055)'; ctx.lineWidth=2;
    ctx.strokeText(String(level+1),FW/2,410);
    ctx.restore();
    drawBricks();
  }
  drawPows();
  for(const b of balls) drawBall(b);
  drawPaddle();
  drawParts();
  if(state!=='title') drawHud();
  drawTexts();
  drawChip();
  drawOverlay();
}
function drawMarks(){
  const fx=ox, fy=oy, fw=FW*scl, fh=FH*scl;
  ctx.strokeStyle='rgba(24,20,16,0.6)'; ctx.lineWidth=1.5;
  const cs=[[fx,fy,-1,-1],[fx+fw,fy,1,-1],[fx,fy+fh,-1,1],[fx+fw,fy+fh,1,1]];
  for(let i=0;i<4;i++){
    const cx=cs[i][0]+cs[i][2]*13, cy=cs[i][1]+cs[i][3]*13;
    if(cx<10||cx>W-10||cy<10||cy>H-10) continue;
    ctx.beginPath(); ctx.arc(cx,cy,5,0,Math.PI*2); ctx.stroke();
    ctx.beginPath();
    ctx.moveTo(cx-9,cy); ctx.lineTo(cx+9,cy);
    ctx.moveTo(cx,cy-9); ctx.lineTo(cx,cy+9);
    ctx.stroke();
  }
  const mL=fx-8, mR=W-(fx+fw)-8, mB=H-(fy+fh)-6;
  if(mL>76) vtext('MISPRINT — INK & PAPER BREAKOUT',fx-16,fy+fh/2,10,'rgba(24,20,16,0.5)',-1);
  if(mR>76){
    vtext('SEVEN-PLATE EDITION · HI '+pad(hi),fx+fw+16,fy+fh/2,10,'rgba(24,20,16,0.5)',1);
    if(mR>100){
      const n=LEVELS.length, bh=14, gap=6, tot=n*bh+(n-1)*gap;
      const cxp=fx+fw+36;
      let yy=fy+fh/2-tot/2;
      for(let i=0;i<n;i++){
        if(i<level){ ctx.fillStyle=INK; rr(cxp-5,yy,10,bh,3); ctx.fill(); }
        else if(i===level){ ctx.fillStyle=ACCENT; rr(cxp-5,yy,10,bh,3); ctx.fill(); }
        else { ctx.strokeStyle='rgba(24,20,16,0.4)'; ctx.lineWidth=1.5; rr(cxp-5,yy,10,bh,3); ctx.stroke(); }
        yy+=bh+gap;
      }
    }
  }
  if(mB>30)
    text('MOUSE OR ARROWS MOVE · SPACE LAUNCH · P PAUSE · M SOUND',W/2,fy+fh+16,9,MONO,'rgba(24,20,16,0.55)','center',1,1,'700');
}
function render(){
  ctx.setTransform(dpr,0,0,dpr,0,0);
  ctx.fillStyle=PAPER; ctx.fillRect(0,0,W,H);
  drawMarks();
  const sh=trauma*trauma;
  const shx=(Math.random()*2-1)*sh*13, shy=(Math.random()*2-1)*sh*13;
  const rot=(Math.random()*2-1)*sh*0.014;
  ctx.save();
  ctx.translate(ox+FW*scl/2,oy+FH*scl/2);
  ctx.rotate(rot); ctx.scale(scl,scl);
  ctx.translate(-FW/2+shx,-FH/2+shy);
  drawField();
  ctx.save(); rr(3,3,FW-6,FH-6,4); ctx.clip();
  drawWorld();
  ctx.restore();
  ctx.restore();
  if(flash>0){
    ctx.globalAlpha=Math.min(0.5,flash);
    ctx.fillStyle=ACCENT;
    ctx.fillRect(ox,oy,FW*scl,FH*scl);
    ctx.globalAlpha=1;
  }
  canvas.style.cursor=(!paused&&(state==='play'||state==='ready'))?'none':'default';
}

/* ---------- input ---------- */
const canvas=document.getElementById('c'), ctx=canvas.getContext('2d');
addEventListener('keydown',e=>{
  if(e.code==='ArrowLeft'||e.code==='KeyA'){ keys.left=true; lastInput='key'; e.preventDefault(); }
  else if(e.code==='ArrowRight'||e.code==='KeyD'){ keys.right=true; lastInput='key'; e.preventDefault(); }
  else if(e.code==='Space'||e.code==='Enter'){ e.preventDefault(); if(!e.repeat){ initAudio(); action(); } }
  else if(e.code==='KeyP'||e.code==='Escape') togglePause();
  else if(e.code==='KeyM') toggleMute();
  else if(e.code==='KeyR'){ if(state==='play'||state==='ready'||paused) toTitle(); }
});
addEventListener('keyup',e=>{
  if(e.code==='ArrowLeft'||e.code==='KeyA') keys.left=false;
  if(e.code==='ArrowRight'||e.code==='KeyD') keys.right=false;
});
addEventListener('pointermove',e=>{
  mouseX=clamp((e.clientX-ox)/scl,-30,FW+30);
  lastInput='mouse';
});
addEventListener('pointerdown',e=>{
  initAudio();
  const fx=(e.clientX-ox)/scl, fy=(e.clientY-oy)/scl;
  if(fx>=MUTE.x&&fx<=MUTE.x+76&&fy>=MUTE.y&&fy<=MUTE.y+20){ toggleMute(); return; }
  if(e.button===0) action();
});
addEventListener('blur',()=>{ if(state==='play'||state==='ready') paused=true; });

/* ---------- boot & loop ---------- */
function resize(){
  dpr=Math.min(window.devicePixelRatio||1,2);
  W=window.innerWidth; H=window.innerHeight;
  canvas.width=Math.round(W*dpr); canvas.height=Math.round(H*dpr);
  canvas.style.width=W+'px'; canvas.style.height=H+'px';
  scl=Math.min((W-14)/FW,(H-14)/FH);
  ox=(W-FW*scl)/2; oy=(H-FH*scl)/2;
}
resize();
addEventListener('resize',resize);
initAttract();

let last=performance.now();
function frame(now){
  requestAnimationFrame(frame);
  const rdt=Math.min(0.05,(now-last)/1000)||0.016; last=now;
  if(slowmoT>0){ slowmoT-=rdt; timescale+=(0.18-timescale)*Math.min(1,rdt*14); }
  else timescale+=(1-timescale)*Math.min(1,rdt*8);
  let dt=rdt*timescale;
  if(freezeT>0){ freezeT-=rdt; dt=0; } // hit-stop
  trauma=Math.max(0,trauma-rdt*1.5);
  flash=Math.max(0,flash-rdt*2.4);
  scorePunch=Math.max(0,scorePunch-rdt*3.5);
  paddle.sq=Math.max(0,paddle.sq-rdt*4.5);
  if(!paused) update(dt,rdt);
  render();
}
requestAnimationFrame(frame);
</script>
</body>
</html>

Compare with

GLM 5.3 FlashX Legal: other attempts