27:18Gemini 4 Hydrogen: Cheaper Tokens, More Expensive Answers?
Nov 13, 2026 · YouTube
18:40ERNIE 5.5 Flash Preview vs GLM 5.3 Flash: Baidu's new model is very impressive!
Nov 11, 2026 · YouTube
36:22Fable 5.2 robbing me blind, plus: Nex AGI's N2.6 vs GLM 5.3 Flash
Nov 11, 2026 · YouTube
14:03Gemini 4 Carbon: Can it handle my challenges? (First look with one-shot coding tests)
Nov 10, 2026 · YouTube
47:51Ranking major U.S. Open-Weights Models. (Where does Ripple 65B fit in?)
Nov 6, 2026 · YouTube
10:57"Crouton Alpha": the latest super-efficient coding model by Meta?
Nov 3, 2026 · YouTube
38:58Testing Particle-1: a post-trained DeepSeek V4.1 Flash specializing in coding and efficiency
Oct 28, 2026 · YouTube
22:14GPT-6.1 Terra Pro vs Terra: How Much Better Is Pro?
Oct 27, 2026 · YouTube
29:55Kimi K3.1 Pro Looked Incredible. Then I Kept Testing.
Oct 26, 2026 · YouTube
31:28Is this company reselling GPT Sol 6.1 at a loss (for non-agentic/text-only)?🤔 Is this sustainable?
Oct 20, 2026 · YouTube
17:58Nemotron 3.5 Super VL vs Opus 5.5: Testing NVIDIA’s new coding juggernaut
Oct 15, 2026 · YouTube
16:04Testing a *massive* new open-weights model 🤯: Muse Spark Open (vs. Nemotron, Opus 5.5, and more)
Oct 15, 2026 · YouTube
16:43Gemini 4 Argon: Great 🐈 Animation, but a broken escalator. (Coding challenge vs Opus 5.5, Sol 6.1)
Oct 13, 2026 · YouTube
16:37Meta's new "Muse Wonder" AI is impressive... but can it compete with Anthropic's models?
Oct 12, 2026 · YouTube
11:12WOW... Haiku 5.5 is very impressive. RIP OpenAI?
Oct 7, 2026 · YouTube
22:47Mistral Large 4 Is Cheap. But Is It Good Enough to Compete?
Oct 6, 2026 · YouTube
22:02Is Pareto 26.10 Preview Just Routing to GPT Sol? I Tested It Against Sol 6.1 Pro
Oct 1, 2026 · YouTube
11:22I Tested Sonnet 5.5 Against Opus 5.5. Is It Actually Cheaper?
Sep 28, 2026 · YouTube
30:00Did Elon Musk's team "train the gay away"... and make Grok 4.7 worse?
Sep 26, 2026 · YouTube
19:21I Tested the Stealth "Space Bunny Alpha" Model Before It Went Offline
Sep 23, 2026 · YouTube
10:58So... how much better is Claude Opus 5.5 vs. GPT-6 Sol, really?
Sep 23, 2026 · YouTube
9:38Claude Opus 5.5... the BEST new model for coding!! 🤩
Sep 22, 2026 · YouTube
29:00Grok 4.7: improved, but GLM 5.3 FlashX beat it anyway (benchmark against Gemini, Sol, etc.)
Sep 22, 2026 · YouTube
You make short animated pieces for the web.
Build me a single HTML page: a bird, breakdancing.
data: URIs are fine.localStorage, sessionStorage, cookies or indexedDB — sandboxed frame, they throw.html block.Two sentences: what the bird is, and the look you went for.
The single fenced html block.
Three bullets.
He's a street pigeon in a backwards snapback and a thin gold chain — a scrappy b-boy with a slate-blue body, dark wing bars, an iridescent neck and orange feet. I drew him procedurally on canvas in an outlined, screen-printed street style: flat inks, film grain, and a night rooftop set with a cardboard dance floor, a boombox on a milk crate, graffiti tags and a parapet full of head-bobbing pigeon spectators.
<!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>B-BOY COO · rooftop session</title>
<style>
:root{--ink:#f2ede4;--acc:#ff6b2b;--bg:#151820;--line:rgba(242,237,228,.25)}
*{box-sizing:border-box;margin:0;padding:0}
html,body{height:100%;overflow:hidden;background:var(--bg)}
body{font-family:ui-monospace,"Cascadia Mono","Courier New",monospace;color:var(--ink);-webkit-user-select:none;user-select:none}
#cv{position:fixed;inset:0;display:block;cursor:pointer;touch-action:manipulation}
#ui{position:fixed;inset:0;pointer-events:none;display:flex;flex-direction:column;justify-content:space-between;padding:18px 20px}
header{display:flex;align-items:flex-start;gap:14px;flex-wrap:wrap}
h1{font-family:"Arial Black","Avenir Next",Impact,sans-serif;font-size:clamp(30px,5.4vw,58px);line-height:.88;letter-spacing:-.02em;transform:skewX(-4deg);text-shadow:3px 3px 0 rgba(0,0,0,.55)}
h1 em{font-style:normal;color:var(--acc)}
.sub{margin-top:8px;font-size:10px;letter-spacing:.28em;opacity:.75}
.tape{border:1.5px dashed var(--line);font-size:9px;letter-spacing:.3em;padding:5px 9px;transform:rotate(-3deg);background:rgba(21,24,32,.55);margin-top:6px}
#ctrl{margin-left:auto;display:flex;flex-direction:column;align-items:flex-end;gap:8px;pointer-events:auto}
.row{display:flex;align-items:center;gap:8px}
.btn{font:inherit;font-weight:700;font-size:10px;letter-spacing:.18em;color:var(--ink);background:rgba(21,24,32,.72);border:2px solid var(--line);padding:7px 10px;cursor:pointer;text-transform:uppercase}
.btn:hover{border-color:var(--ink)}
.btn:active{transform:translateY(1px)}
.btn[aria-pressed="true"]{border-color:var(--acc);color:var(--acc)}
.btn:focus-visible{outline:2px solid var(--acc);outline-offset:2px}
#bpm{width:130px;accent-color:var(--acc);cursor:pointer}
.lbl{font-size:9px;letter-spacing:.22em;opacity:.7}
#dots{display:flex;gap:5px}
#dots i{width:7px;height:7px;border:1.5px solid var(--line);border-radius:50%}
#dots i.on{background:var(--acc);border-color:var(--acc)}
#bottom{display:flex;align-items:flex-end;gap:12px}
#movesWrap{flex:1;display:flex;flex-direction:column;align-items:center;gap:7px}
#moves{pointer-events:auto;display:flex;flex-wrap:wrap;gap:8px;justify-content:center}
.chip{font-size:11px;padding:9px 12px}
.chip kbd{font:inherit;opacity:.55;border:1px solid var(--line);border-radius:3px;padding:0 4px;margin-right:7px}
#next{font-size:9px;letter-spacing:.22em;opacity:.75}
#hint{font-size:9px;letter-spacing:.18em;opacity:.5;line-height:1.9;text-align:right;white-space:nowrap}
@media(max-width:760px){#hint{display:none}#ctrl{align-items:flex-start}}
</style>
</head>
<body>
<canvas id="cv"></canvas>
<div id="ui">
<header>
<div>
<h1>B-BOY<br><em>COO</em></h1>
<div class="sub">ROOFTOP SESSION · ALL CITY</div>
</div>
<div class="tape">LIVE · SQAB CREW · EST. 84</div>
<div id="ctrl">
<div class="row"><span class="lbl">BPM</span><input id="bpm" type="range" min="70" max="132" value="96" aria-label="tempo"><span class="lbl" id="bpmv">96</span></div>
<div class="row">
<button class="btn" id="snd" aria-pressed="false">Sound off</button>
<button class="btn" id="aut" aria-pressed="true">Auto</button>
<button class="btn" id="hrn">Airhorn</button>
</div>
<div id="dots"><i class="on"></i><i></i><i></i><i></i></div>
</div>
</header>
<div id="bottom">
<div style="flex:1"></div>
<div id="movesWrap">
<div id="moves">
<button class="btn chip" data-m="toprock"><kbd>1</kbd>Toprock</button>
<button class="btn chip" data-m="footwork"><kbd>2</kbd>Footwork</button>
<button class="btn chip" data-m="freeze"><kbd>3</kbd>Freeze</button>
<button class="btn chip" data-m="backspin"><kbd>4</kbd>Backspin</button>
<button class="btn chip" data-m="headspin"><kbd>5</kbd>Headspin</button>
<button class="btn chip" data-m="hop"><kbd>6</kbd>Hop</button>
</div>
<div id="next">NEXT ▸ auto</div>
</div>
<div id="hint">1-6 CALL MOVES<br>A AUTO · S SOUND · H HORN<br>TAP FLOOR = HOP</div>
</div>
</div>
<script>
'use strict';
/* ---------------- utils ---------------- */
const TAU=Math.PI*2, PI=Math.PI;
const clamp=(v,a,b)=>v<a?a:v>b?b:v;
const lerp=(a,b,t)=>a+(b-a)*t;
const seg=(b,a,c)=>clamp((b-a)/(c-a),0,1);
const easeIO=t=>t<.5?4*t*t*t:1-Math.pow(-2*t+2,3)/2;
const eOut=t=>1-Math.pow(1-t,3);
const eIn=t=>t*t*t;
const backOut=(t,s=1.9)=>{const u=t-1;return u*u*((s+1)*u+s)+1;};
const sm=t=>t*t*(3-2*t);
const dip=ph=>Math.pow(Math.abs(Math.sin(PI*ph)),1.4);
const hitEnv=ph=>Math.exp(-Math.max(0,ph)*5);
function mulberry(seed){return function(){seed|=0;seed=seed+0x6D2B79F5|0;let t=Math.imul(seed^seed>>>15,1|seed);t=t+Math.imul(t^t>>>7,61|t)^t;return((t^t>>>14)>>>0)/4294967296;};}
const RM=matchMedia('(prefers-reduced-motion: reduce)').matches;
/* ---------------- canvas & layout ---------------- */
const cv=document.getElementById('cv'), cx=cv.getContext('2d');
let W=0,H=0,DPR=1,SC=1,Wu=0,Hu=0,GY=0,AX=0,capY=0,wallTop=0,wallBot=0;
let stars=[],blds=[],cracks=[],vg=null,grainPat=null;
function resize(){
DPR=Math.min(2,window.devicePixelRatio||1);
W=window.innerWidth; H=window.innerHeight;
cv.width=Math.round(W*DPR); cv.height=Math.round(H*DPR);
SC=clamp(W/1150,0.72,1.2); Wu=W/SC; Hu=H/SC;
GY=Hu*0.74; AX=Wu*0.44;
capY=GY-182; wallTop=GY-176; wallBot=GY-124;
buildScene();
vg=cx.createRadialGradient(Wu/2,Hu*0.42,Hu*0.35,Wu/2,Hu*0.6,Hu*0.95);
vg.addColorStop(0,'rgba(0,0,0,0)'); vg.addColorStop(1,'rgba(4,6,10,0.55)');
}
function buildScene(){
const r=mulberry(7); stars=[]; blds=[]; cracks=[];
for(let i=0;i<90;i++) stars.push({x:r()*Wu,y:r()*(capY-60),r:0.7+r()*1.2,tw:0.5+r()*2.2,ph:r()*TAU,b:0.35+r()*0.6});
let x=-20;
while(x<Wu+20){
const bw=40+r()*95, bh=34+r()*112, b={x,w:bw,h:bh,wins:[]};
const cols=Math.min(8,Math.floor(bw/13)), rows=Math.min(9,Math.floor(bh/15));
for(let i=0;i<cols;i++)for(let j=0;j<rows;j++) if(r()<0.2) b.wins.push({x:5+i*(bw-8)/cols,y:6+j*(bh-10)/rows,a:0.25+r()*0.55});
blds.push(b); x+=bw+6+r()*30;
}
for(let i=0;i<6;i++) cracks.push({x:AX+(r()-0.5)*520,y:wallBot+30+r()*(Hu-wallBot-60),a:r()*TAU,l:14+r()*22});
}
window.addEventListener('resize',resize);
/* ---------------- master clock ---------------- */
let bpm=96, t0=performance.now()/1000;
const perfNow=()=>performance.now()/1000;
const beatNow=()=>(perfNow()-t0)*bpm/60;
const beatToPerf=b=>t0+b*60/bpm;
function setBpm(n){ const b=beatNow(); bpm=n; t0=perfNow()-b*60/n; }
/* ---------------- synthesized boom-bap ---------------- */
let AC=null,master=null,noiseBuf=null,crackleGain=null,audioOff=0,stepCount=0,nextStepTime=0,soundOn=false;
const KICK =[1,0,0,0,0,0,1,0,0,0,1,0,0,0,0,0];
const SNARE=[0,0,0,0,1,0,0,0,0,0,0,0,1,0,0,0];
const BASS =[55,0,0,0,0,0,55,0,65.4,0,0,49,0,0,73.4,0];
function ensureAudio(){
if(AC) return true;
try{ AC=new (window.AudioContext||window.webkitAudioContext)(); }catch(e){ return false; }
master=AC.createGain(); master.gain.value=0.6;
const comp=AC.createDynamicsCompressor(); master.connect(comp); comp.connect(AC.destination);
noiseBuf=AC.createBuffer(1,AC.sampleRate,AC.sampleRate);
const d=noiseBuf.getChannelData(0); for(let i=0;i<d.length;i++) d[i]=Math.random()*2-1;
const src=AC.createBufferSource(); src.buffer=noiseBuf; src.loop=true;
const bp=AC.createBiquadFilter(); bp.type='bandpass'; bp.frequency.value=3600; bp.Q.value=0.5;
crackleGain=AC.createGain(); crackleGain.gain.value=soundOn?0.012:0;
src.connect(bp); bp.connect(crackleGain); crackleGain.connect(master); src.start();
setInterval(schedule,30);
return true;
}
const stepDur=()=>60/bpm/4;
function schedule(){
if(!AC||!soundOn) return;
audioOff=AC.currentTime-perfNow();
const want=Math.ceil(beatNow()*4-0.001);
if(want>stepCount){ stepCount=want; nextStepTime=beatToPerf(stepCount/4)+audioOff; }
if(nextStepTime<AC.currentTime) nextStepTime=AC.currentTime+0.02;
while(nextStepTime<AC.currentTime+0.16){
const s=stepCount%16, sw=(s%4===2)?0.13*stepDur():0, t=nextStepTime+sw;
if(KICK[s]) kick(t); if(SNARE[s]) snare(t); if(s%2===0) hat(t,(s%4===0)?1:0.5);
if(BASS[s]) bassN(t,BASS[s]);
stepCount++; nextStepTime+=stepDur();
}
}
function env(g,t,v,dec){ g.gain.setValueAtTime(v,t); g.gain.exponentialRampToValueAtTime(0.001,t+dec); }
function kick(t){ const o=AC.createOscillator(),g=AC.createGain(); o.type='sine';
o.frequency.setValueAtTime(135,t); o.frequency.exponentialRampToValueAtTime(43,t+0.11);
env(g,t,0.95,0.24); o.connect(g); g.connect(master); o.start(t); o.stop(t+0.26); }
function snare(t){ const s=AC.createBufferSource(); s.buffer=noiseBuf;
const f=AC.createBiquadFilter(); f.type='highpass'; f.frequency.value=1500;
const g=AC.createGain(); env(g,t,0.42,0.16); s.connect(f); f.connect(g); g.connect(master); s.start(t); s.stop(t+0.18);
const o=AC.createOscillator(),g2=AC.createGain(); o.type='triangle'; o.frequency.value=185;
env(g2,t,0.22,0.09); o.connect(g2); g2.connect(master); o.start(t); o.stop(t+0.1); }
function hat(t,v){ const s=AC.createBufferSource(); s.buffer=noiseBuf;
const f=AC.createBiquadFilter(); f.type='highpass'; f.frequency.value=6800;
const g=AC.createGain(); env(g,t,0.14*v,0.05); s.connect(f); f.connect(g); g.connect(master); s.start(t); s.stop(t+0.07); }
function bassN(t,fr){ const o=AC.createOscillator(),g=AC.createGain(),f=AC.createBiquadFilter();
o.type='square'; o.frequency.value=fr; f.type='lowpass';
f.frequency.setValueAtTime(750,t); f.frequency.exponentialRampToValueAtTime(240,t+0.22);
env(g,t,0.2,0.3); o.connect(f); f.connect(g); g.connect(master); o.start(t); o.stop(t+0.32); }
function horn(){ if(!ensureAudio())return; AC.resume();
const t=AC.currentTime+0.02, g=AC.createGain(), f=AC.createBiquadFilter();
f.type='lowpass'; f.frequency.value=2400;
g.gain.setValueAtTime(0.0001,t); g.gain.exponentialRampToValueAtTime(0.3,t+0.03);
g.gain.setValueAtTime(0.3,t+0.55); g.gain.exponentialRampToValueAtTime(0.001,t+0.9);
const lfo=AC.createOscillator(), lg=AC.createGain(); lfo.frequency.value=7; lg.gain.value=9; lfo.connect(lg);
for(const det of [0,7]){ const o=AC.createOscillator(); o.type='sawtooth';
o.frequency.setValueAtTime(392+det,t); o.frequency.exponentialRampToValueAtTime(466+det,t+0.06);
lg.connect(o.frequency); o.connect(f); o.start(t); o.stop(t+0.95); }
lfo.start(t); lfo.stop(t+0.95); f.connect(g); g.connect(master); }
/* ---------------- particles & impacts ---------------- */
const parts=[]; let shakeA=0, flashA=0, crowdFlapT=-9;
function addShake(a){ if(!RM) shakeA=Math.max(shakeA,a); }
function push(p){ parts.push(p); if(parts.length>150) parts.splice(0,parts.length-150); }
function dust(x,y,n,pw=1){ for(let i=0;i<n;i++) push({t:0,x:x+(Math.random()-0.5)*18,y:y+Math.random()*3,
vx:(Math.random()-0.5)*70*pw,vy:-(8+Math.random()*42)*pw,r:1.6+Math.random()*3.4,l:0,ml:0.45+Math.random()*0.45}); }
function feathers(x,y,n){ for(let i=0;i<n;i++) push({t:1,x,y,vx:(Math.random()-0.5)*130,vy:-20-Math.random()*70,
r:1.8+Math.random()*2.2,l:0,ml:0.9+Math.random()*0.8,ph:Math.random()*TAU}); }
function stepParts(dt){ for(let i=parts.length-1;i>=0;i--){ const p=parts[i]; p.l+=dt;
if(p.l>=p.ml){ parts.splice(i,1); continue; }
if(p.t===0){ p.vx*=Math.exp(-dt*3); p.vy*=Math.exp(-dt*3); p.r+=7*dt; }
else { p.vy+=55*dt; p.ph+=dt*6; p.x+=Math.sin(p.ph)*22*dt; }
p.x+=p.vx*dt; p.y+=p.vy*dt; } }
/* ---------------- pose & IK ---------------- */
function newPose(){ return {px:0,py:-30,rot:0,sq:1,neck:0.55,headA:0,tailA:0,tailF:0.35,hype:0,hp:0,hpx:5,hpy:-23,
fl:{x:-8,y:30},fr:{x:8,y:30},lb:-1,rb:-1,
wl:{x:-14,y:9},wr:{x:-12,y:10},wb:1,fanL:0.12,fanR:0.12}; }
const NUMK=['px','py','rot','sq','neck','headA','tailA','tailF','hype','hp','hpx','hpy','lb','rb','wb','fanL','fanR'];
function lerpPose(a,b,t,o){ for(const k of NUMK) o[k]=lerp(a[k],b[k],t);
for(const k of ['fl','fr','wl','wr']){ o[k].x=lerp(a[k].x,b[k].x,t); o[k].y=lerp(a[k].y,b[k].y,t); } return o; }
function copyPose(src,dst){ return lerpPose(src,src,0,dst); }
function w2l(P,wx,wy){ const dx=wx-(AX+P.px), dy=wy-(GY+P.py);
const c=Math.cos(-P.rot), s=Math.sin(-P.rot); return {x:dx*c-dy*s, y:dx*s+dy*c}; }
function ik(hx,hy,tx,ty,a,b,bend){ let dx=tx-hx,dy=ty-hy,d=Math.hypot(dx,dy);
d=clamp(d,Math.abs(a-b)+0.6,a+b-0.6);
const ang=Math.atan2(dy,dx), A=Math.acos(clamp((a*a+d*d-b*b)/(2*a*d),-1,1)), ba=ang+bend*A;
return {kx:hx+Math.cos(ba)*a, ky:hy+Math.sin(ba)*a, fx:hx+Math.cos(ang)*d, fy:hy+Math.sin(ang)*d}; }
function baseStand(P){ P.px=0;P.py=-30;P.rot=0;P.sq=1;P.neck=0.55;P.headA=0;P.tailA=0;P.tailF=0.35;P.hype=0;P.hp=0;
P.fl.x=-8;P.fl.y=30;P.fr.x=8;P.fr.y=30;P.lb=-1;P.rb=-1;
P.wl.x=-14;P.wl.y=9;P.wr.x=-12;P.wr.y=10;P.wb=1;P.fanL=0.12;P.fanR=0.12; }
/* ---------------- move library (beats drive everything) ---------------- */
function groove(P,b){ const ph=b-Math.floor(b), d=dip(ph), h=hitEnv(ph);
P.py=-30+3.4*d; P.px=Math.sin(b*PI/2)*2.5; P.rot=0.02*d;
P.neck=0.5+0.3*h; P.headA=0.2*h+0.04*Math.sin(b*1.7);
P.tailA=Math.sin(b*PI/2+1)*0.1; P.tailF=0.35;
const gy2=-P.py, c2=b%2, side=Math.floor(b/2)%2, e=Math.exp(-c2*2.8);
P.fl.y=gy2; P.fr.y=gy2;
const ft= side===0?P.fr:P.fl;
ft.y=gy2-7*e; ft.x=(side===0?8:-8)+3*e;
P.fanL=P.fanR=0.12+0.2*Math.max(0,Math.sin(PI*(ph-0.5)));
}
const MOVES={};
MOVES.toprock={dur:8, fn(P,b){ baseStand(P); groove(P,b); }};
MOVES.entrance={dur:6, fn(P,b,pb){ baseStand(P);
if(b<1.1){ const u=eIn(seg(b,0,1.1)); P.py=lerp(-300,-30,u);
P.rot=Math.sin(b*9)*0.06*(1-u); P.neck=0.8; P.headA=-0.15; P.sq=1.05;
P.wl.x=-16;P.wl.y=-16; P.wr.x=15;P.wr.y=-15; P.fanL=P.fanR=0.6+0.35*Math.sin(b*22);
P.fl.x=-5;P.fl.y=24; P.fr.x=6;P.fr.y=26; P.tailA=0.3; P.tailF=0.7;
} else {
if(pb!=null&&pb<1.1&&b>=1.1){ dust(AX,GY,15,1.3); addShake(7); S.wobY.v+=90; S.sq.v-=6; crowdFlapT=perfNow(); }
const u1=easeIO(seg(b,1.1,2.1)); P.py=lerp(-24,-30,u1);
P.fl.y=P.fr.y=-P.py;
P.headA= b<3? Math.sin(seg(b,1.4,2.9)*TAU)*0.5 : 0;
if(b>=3&&b<4){ P.wr.x=2;P.wr.y=-20;P.fanR=0.05; P.headA=0.3; } // cap tap
else if(b>=4) groove(P,b-4);
}
}};
MOVES.footwork={dur:8, init(st){ st.lx=-10; st.rx=12; },
fn(P,b,pb,st){ baseStand(P);
P.px=0; P.py=-15+2.2*dip(b-Math.floor(b)); P.rot=-0.07;
P.wl=w2l(P,AX-16,GY); P.wr=w2l(P,AX-5,GY); // braced wingtips stay glued to the floor
P.fanL=P.fanR=0.08; P.neck=0.6; P.headA=0.15+0.15*hitEnv(b-Math.floor(b));
const seq=[[0,'L',24],[1,'R',12],[2,'L',-6],[3,'R',26],[4,'L',8],[5,'R',-4]];
let mv=null;
for(const q of seq){ if(b>=q[0]&&b<q[0]+1){ const from=q[1]==='L'?st.lx:st.rx;
mv=[q[1], lerp(from,q[2],easeIO(b-q[0])), Math.sin(PI*(b-q[0]))*9]; } }
if(mv){ if(mv[0]==='L') st.lx=mv[1]; else st.rx=mv[1];
if(pb!=null){ const bi=Math.floor(b); if(bi<=6&&Math.floor(pb)<bi) dust(AX+(mv[0]==='L'?st.lx:st.rx),GY,4,0.8); } }
if(b>=6) P.headA-=0.35*easeIO(seg(b,6,7));
const gy2=-P.py;
P.fl.x=st.lx-P.px; P.fl.y=gy2-(mv&&mv[0]==='L'?mv[2]:0);
P.fr.x=st.rx-P.px; P.fr.y=gy2-(mv&&mv[0]==='R'?mv[2]:0);
}};
MOVES.freeze={dur:6, fn(P,b,pb){ baseStand(P);
if(b<1){ const u=easeIO(seg(b,0,1)); P.py=lerp(-30,-21,u); P.rot=-0.12*u; P.headA=0.25*u;
P.fl.y=P.fr.y=-P.py; P.wl.x=-17;P.wl.y=2; P.wr.x=15;P.wr.y=2;
} else if(b<1.5){ const e=backOut(seg(b,1,1.5));
P.py=lerp(-21,-22,e); P.rot=lerp(-0.12,0.95,e);
P.fl.x=lerp(-8,10,e); P.fl.y=lerp(30,-6,e);
P.fr.x=lerp(8,5,e); P.fr.y=lerp(30,-10,e);
P.wr={x:lerp(15,24.3,e),y:lerp(2,3.85,e)};
P.wl.x=lerp(-17,-8,e); P.wl.y=lerp(2,-4,e); P.fanL=P.fanR=0.05;
P.tailA=-1.15*e; P.tailF=0.85; P.neck=lerp(0.55,0.7,e); P.headA=lerp(0.25,0.5,e); P.hype=0.5;
if(pb!=null&&pb<1.45&&b>=1.45){ dust(AX+11,GY,7,0.9); addShake(2.5); S.wobY.v+=25; crowdFlapT=perfNow(); }
} else if(b<5){ const n=Math.random()-0.5;
P.py=-22+Math.sin(b*2.1)*0.8+n*0.5; P.rot=0.95+n*0.016;
P.fl.x=10+Math.sin(b*3.1)*1.5; P.fl.y=-6; P.fr.x=5; P.fr.y=-10;
P.wr=w2l(P,AX+11,GY); P.wl.x=-8;P.wl.y=-4; P.fanL=P.fanR=0.05;
P.tailA=-1.15; P.tailF=0.85; P.neck=0.7; P.headA=0.5-0.15*seg(b,1.5,5); P.hype=0.6;
} else { const e=backOut(seg(b,5,5.9));
P.py=lerp(-22,-30,e); P.rot=lerp(0.95,0,e);
P.fl.x=lerp(10,-8,e); P.fl.y=lerp(-6,30,e); P.fr.x=lerp(5,8,e); P.fr.y=lerp(-10,30,e);
P.wl.x=lerp(-8,-14,e); P.wl.y=lerp(-4,9,e); P.wr.x=lerp(24.3,-12,e); P.wr.y=lerp(3.85,10,e);
P.tailA=lerp(-1.15,0,e); P.tailF=lerp(0.85,0.35,e); P.headA=lerp(0.35,0,e); P.hype=0;
}
}};
MOVES.backspin={dur:8, fn(P,b,pb){ baseStand(P);
if(b<1){ const u=easeIO(seg(b,0,1)); P.py=lerp(-30,-17,u);
P.rot=-0.25*Math.sin(u*PI); P.headA=0.2*Math.sin(u*PI); P.neck=lerp(0.55,0.2,u);
P.fl.y=P.fr.y=-P.py;
P.wl.x=lerp(-14,-4,u);P.wl.y=lerp(9,-5,u);P.wr.x=lerp(-12,4,u);P.wr.y=lerp(10,-5,u);
} else if(b<7){ const s=seg(b,1,7), f=sm(s), rot=-TAU*2*f;
P.py=-17; P.px=Math.sin(f*TAU*2)*2; P.rot=rot; P.neck=0.2; P.headA=-0.8; P.hype=1; P.sq=1.04;
P.fl=w2l(P,AX-13,GY-46); P.fr=w2l(P,AX+13,GY-46); P.lb=1; P.rb=-1;
P.wl.x=-4;P.wl.y=-5; P.wr.x=4;P.wr.y=-5; P.fanL=P.fanR=0.05; P.tailF=0.5;
if(pb!=null&&pb<1&&b>=1){ dust(AX,GY,10,1.4); feathers(AX,GY-24,6); addShake(3); }
if(pb!=null&&pb>=1&&b<7){ const f0=sm(seg(pb,1,7));
if(Math.floor(f*8)>Math.floor(f0*8)) feathers(AX+P.px,GY-18,2); }
} else { if(pb!=null&&pb<7&&b>=7){ addShake(8); dust(AX,GY,16,1.6); S.wobA.v-=55; S.wobY.v+=60; S.sq.v-=7; crowdFlapT=perfNow(); }
const e=backOut(seg(b,7,7.85));
P.py=lerp(-17,-30,e); P.rot=0; P.neck=0.55; P.headA=0; P.hype=0;
P.fl.x=lerp(-13,-8,e); P.fl.y=lerp(-29,30,e); P.fr.x=lerp(13,8,e); P.fr.y=lerp(-29,30,e);
P.lb=-1; P.rb=-1;
P.wl.x=lerp(-4,-14,e); P.wl.y=lerp(-5,9,e); P.wr.x=lerp(4,-12,e); P.wr.y=lerp(-5,10,e);
P.fanL=P.fanR=lerp(0.05,0.12,e); P.tailF=lerp(0.5,0.35,e);
}
}};
MOVES.headspin={dur:8, fn(P,b,pb){ baseStand(P);
const N0={x:AX+8,y:GY-19}, nb={x:5,y:-13}, H0={x:AX+8,y:GY-8};
const orbit=r=>{ const c=Math.cos(r),s=Math.sin(r);
return {x:N0.x-(nb.x*c-nb.y*s), y:N0.y-(nb.x*s+nb.y*c)}; };
if(b<1){ const u=easeIO(seg(b,0,1)), r=PI*u, o=orbit(r);
P.rot=r; P.px=lerp(0,o.x-AX,u); P.py=lerp(-30,o.y-GY,u);
P.fl.x=lerp(-8,-4,u); P.fl.y=lerp(30,10,u); P.fr.x=lerp(8,6,u); P.fr.y=lerp(30,8,u);
P.wl.x=lerp(-14,-4,u); P.wl.y=lerp(9,-5,u); P.wr.x=lerp(-12,4,u); P.wr.y=lerp(10,-5,u);
P.neck=0.4; P.hp=u;
if(u>0){ const hl=w2l(P,H0.x,H0.y); P.hpx=lerp(5,hl.x,u); P.hpy=lerp(-23,hl.y,u); }
if(pb!=null&&pb<1&&b>=1){ dust(H0.x,GY,9,1.1); addShake(3); }
} else if(b<5){ const s=seg(b,1,5), f=sm(s), r=PI+TAU*f;
const o=orbit(r); P.rot=r; P.px=o.x-AX; P.py=o.y-GY;
P.neck=lerp(0.4,0.15,Math.min(1,s*4)); P.hp=1; P.hype=sm(Math.min(1,s*3));
const hl=w2l(P,H0.x,H0.y); P.hpx=hl.x; P.hpy=hl.y; // pin the head to the floor
const u1=easeIO(seg(s,0,0.35)), u2=easeIO(seg(s,0.35,0.7)), u3=easeIO(seg(s,0.7,1));
P.fr.x=lerp(lerp(6,12,u1),lerp(3,4,u3),u2); P.fr.y=lerp(lerp(8,16,u1),lerp(7,8,u3),u2);
P.fl.x=lerp(lerp(-4,-12,u1),lerp(-2,-4,u3),u2); P.fl.y=lerp(lerp(10,16,u1),lerp(22,8,u3),u2);
P.lb=1;P.rb=-1;
P.wl.x=-4;P.wl.y=-5;P.wr.x=4;P.wr.y=-5;P.fanL=P.fanR=0.05;
if(pb!=null){ const f0=sm(seg(pb,1,5)); if(Math.floor(f*4)>Math.floor(f0*4)) dust(H0.x,GY,3,0.7); }
} else if(b<6){ const r=PI+TAU+Math.sin(b*9)*0.05, o=orbit(r);
P.rot=r; P.px=o.x-AX; P.py=o.y-GY; P.hype=1; P.neck=0.15; P.hp=1;
const hl=w2l(P,H0.x,H0.y); P.hpx=hl.x; P.hpy=hl.y;
P.fl.x=-12;P.fl.y=16;P.fr.x=12;P.fr.y=16;P.lb=1;P.rb=-1;
P.wl.x=-4;P.wl.y=-5;P.wr.x=4;P.wr.y=-5;P.fanL=P.fanR=0.05;
} else if(b<7){ const u=eOut(seg(b,6,7)), r=lerp(PI+TAU,TAU*2,u), o=orbit(r);
P.rot=r; P.px=lerp(o.x-AX,0,u); P.py=lerp(o.y-GY,-30,u);
P.hp=1-u; P.hype=0.4*(1-u); P.neck=0.4;
const hl=w2l(P,H0.x,H0.y); P.hpx=hl.x; P.hpy=hl.y;
P.fl.x=lerp(-12,-8,u); P.fl.y=lerp(16,30,u); P.fr.x=lerp(12,8,u); P.fr.y=lerp(16,30,u);
P.lb=-1;P.rb=-1;
P.wl.x=lerp(-4,-14,u);P.wl.y=lerp(-5,9,u);P.wr.x=lerp(4,-12,u);P.wr.y=lerp(-5,10,u);
} else { if(pb!=null&&pb<7&&b>=7){ addShake(8); dust(AX,GY,15,1.5); S.wobY.v+=100; S.sq.v-=8; crowdFlapT=perfNow(); }
const u=easeIO(seg(b,7,7.7));
P.py=-30; P.rot=0;
P.wl.x=lerp(-14,-20,u);P.wl.y=lerp(9,-22,u);P.wr.x=lerp(-12,20,u);P.wr.y=lerp(10,-22,u);
P.fanL=P.fanR=0.15+0.75*u; P.neck=0.75; P.headA=-0.08; P.tailF=0.7; P.tailA=0.15;
}
}};
MOVES.hop={dur:2, fn(P,b,pb){ baseStand(P);
if(b<0.5){ const u=easeIO(seg(b,0,0.5)); P.py=lerp(-30,-23,u); P.sq=1-0.1*u; P.headA=0.15*u;
P.fl.y=P.fr.y=-P.py;
P.wl.x=lerp(-14,-18,u);P.wl.y=lerp(9,0,u);P.wr.x=lerp(-12,16,u);P.wr.y=lerp(10,1,u);
} else if(b<1.35){ const u=seg(b,0.5,1.35), arc=Math.sin(PI*u);
P.py=lerp(-23,-64,arc); P.rot=-0.06; P.sq=lerp(0.9,1.05,arc);
P.headA=lerp(0.15,-0.1,arc); P.tailF=0.6; P.tailA=0.2;
const tu=Math.min(1,u*3);
P.fl.x=lerp(-8,-4,tu); P.fl.y=lerp(30,14,tu); P.fr.x=lerp(8,6,tu); P.fr.y=lerp(30,15,tu);
const flap=Math.sin(PI*clamp(u*1.25,0,1));
P.wl.x=lerp(-18,10,flap);P.wl.y=lerp(0,-26,flap);
P.wr.x=lerp(16,14,flap);P.wr.y=lerp(1,-24,flap);
P.fanL=P.fanR=0.3+0.6*flap;
if(pb!=null&&pb<0.5&&b>=0.5) dust(AX,GY,5,0.8);
} else { if(pb!=null&&pb<1.35&&b>=1.35){ dust(AX,GY,11,1.2); addShake(4); S.wobY.v+=70; S.sq.v-=5; }
const e=backOut(seg(b,1.35,1.9));
P.py=lerp(-23,-30,e); P.sq=1;
P.fl.x=lerp(-4,-8,e);P.fl.y=lerp(14,30,e);P.fr.x=lerp(6,8,e);P.fr.y=lerp(15,30,e);
P.wl.x=lerp(10,-14,e);P.wl.y=lerp(-26,9,e);P.wr.x=lerp(14,-12,e);P.wr.y=lerp(-24,10,e);
P.fanL=P.fanR=lerp(0.9,0.12,e);
}
}};
/* ---------------- choreographer ---------------- */
let clip={name:'entrance',start:0,dur:6,st:{}};
let fromPose=newPose(), curPose=newPose(), RP=newPose(), FP=newPose(), prevB=0;
fromPose.py=-300;
const queue=[]; let auto=true, lastMove='entrance';
function canEnd(b){ if(clip.name==='entrance') return b>=clip.dur;
if(queue.length) return b>=((clip.name==='toprock')?2:clip.dur);
if(!auto) return b>=((clip.name==='toprock')?4:clip.dur);
return b>=clip.dur; }
function pickMove(last){ const big=(last==='backspin'||last==='headspin'||last==='freeze');
let pool= big? ['toprock','toprock','freeze','hop','footwork']
: ['toprock','footwork','footwork','freeze','backspin','backspin','headspin','hop','hop','toprock'];
pool=pool.filter(k=>k!==last||k==='toprock');
return pool[(Math.random()*pool.length)|0]; }
function selectNext(){ copyPose(RP,fromPose);
let nm= queue.length? queue.shift() : (auto? pickMove(lastMove):'toprock');
if(!MOVES[nm]) nm='toprock';
lastMove=nm;
clip={name:nm,start:beatNow(),dur:MOVES[nm].dur||8,st:{}};
if(MOVES[nm].init) MOVES[nm].init(clip.st);
prevB=beatNow()-clip.start;
updateNextUI(); }
/* ---------------- springs (follow-through & settle) ---------------- */
function mkSpring(x){ return {x:x||0,v:0,t:0}; }
const S={headA:mkSpring(0),neck:mkSpring(0.55),tailA:mkSpring(0),wobY:mkSpring(0),wobA:mkSpring(0),sq:mkSpring(0),chain:mkSpring(0)};
function upSpring(s,dt,w,z){ const a=w*w*(s.t-s.x)-2*z*w*s.v; s.v+=a*dt; s.x+=s.v*dt; }
let rotVel=0, lastFPRot=0, camX=0, camY=0;
function stepSim(dt){
S.headA.t=RP.headA; upSpring(S.headA,dt,16,0.5);
S.neck.t=RP.neck; upSpring(S.neck,dt,12,0.55);
S.tailA.t=RP.tailA; upSpring(S.tailA,dt,10,0.45);
upSpring(S.wobY,dt,9,0.28); upSpring(S.wobA,dt,8,0.3); upSpring(S.sq,dt,11,0.4);
S.chain.t=clamp(-rotVel*0.05,-0.7,0.7); upSpring(S.chain,dt,7,0.35);
copyPose(RP,FP);
FP.py+=S.wobY.x; FP.rot+=S.wobA.x; FP.sq=clamp(FP.sq+S.sq.x,0.55,1.3);
FP.headA=S.headA.x; FP.neck=S.neck.x; FP.tailA=S.tailA.x;
const rv=(FP.rot-lastFPRot)/Math.max(dt,1e-4); lastFPRot=FP.rot;
rotVel=rotVel*0.85+rv*0.15;
const k=1-Math.exp(-dt*3);
camX+=(clamp(RP.px*0.35,-24,24)-camX)*k;
camY+=(clamp((RP.py+30)*0.22,-16,20)-camY)*k;
}
/* ---------------- drawing helpers ---------------- */
function line(x1,y1,x2,y2){ cx.beginPath(); cx.moveTo(x1,y1); cx.lineTo(x2,y2); cx.stroke(); }
function rr(x,y,w,h,r){ cx.beginPath(); cx.moveTo(x+r,y); cx.arcTo(x+w,y,x+w,y+h,r);
cx.arcTo(x+w,y+h,x,y+h,r); cx.arcTo(x,y+h,x,y,r); cx.arcTo(x,y,x+w,y,r); cx.closePath(); }
function feather(x,y,ang,len,w,col,band){ cx.save(); cx.translate(x,y); cx.rotate(ang);
cx.fillStyle=col; cx.beginPath(); cx.moveTo(0,-w);
cx.quadraticCurveTo(len*0.55,-w*0.8,len,-w*0.18);
cx.quadraticCurveTo(len+w*0.6,0,len,w*0.18);
cx.quadraticCurveTo(len*0.55,w*0.8,0,w); cx.closePath(); cx.fill();
if(band){ cx.fillStyle='rgba(35,38,45,0.85)'; cx.beginPath();
cx.moveTo(len*0.72,-w*0.5); cx.quadraticCurveTo(len+w*0.6,0,len*0.72,w*0.5);
cx.quadraticCurveTo(len*0.86,0,len*0.72,-w*0.5); cx.closePath(); cx.fill(); }
cx.restore(); }
const blinking=t=> (t%4.3)<0.13 || ((t+2.1)%5.7)<0.12;
/* ---------------- the pigeon ---------------- */
function drawBird(P,t){ const bx=AX+P.px, by=GY+P.py;
const h=clamp((-P.py-30)/240,0,0.75);
cx.fillStyle=`rgba(8,9,11,${0.32*(1-h)})`;
cx.beginPath(); cx.ellipse(bx,GY+5,46*(1-h*0.55),7*(1-h*0.4),0,0,TAU); cx.fill();
cx.save(); cx.translate(bx,by);
cx.scale(1+(1-P.sq)*0.7,P.sq); cx.rotate(P.rot);
drawWing(P,'L',true); drawLeg(P,'L',true); drawTail(P);
bodyPath(); cx.fillStyle='#99a3ae'; cx.fill();
cx.lineWidth=2; cx.strokeStyle='#23262d'; cx.stroke();
cx.save(); bodyPath(); cx.clip();
cx.fillStyle='#b7bfc8'; cx.beginPath(); cx.ellipse(4,10,10,7,0,0,TAU); cx.fill();
cx.fillStyle='rgba(236,240,244,0.75)'; cx.beginPath(); cx.ellipse(-11,3,4.5,6,-0.3,0,TAU); cx.fill();
cx.restore();
drawLeg(P,'R',false); drawWing(P,'R',false); drawHead(P,t);
cx.restore();
}
function bodyPath(){ cx.beginPath();
cx.moveTo(9,-16);
cx.bezierCurveTo(15,-13,18,-6,17,1);
cx.bezierCurveTo(16,9,9,15,1,16);
cx.bezierCurveTo(-8,17,-15,12,-16,5);
cx.bezierCurveTo(-17,-1,-13,-6,-9,-10);
cx.bezierCurveTo(-6,-14,-2,-16,3,-16);
cx.closePath(); }
function drawLeg(P,side,far){ const hip= side==='L'?{x:-7,y:6}:{x:7,y:6};
const ft= side==='L'?P.fl:P.fr; const bend= side==='L'?P.lb:P.rb;
const r=ik(hip.x,hip.y,ft.x,ft.y,15,15,bend);
cx.lineCap='round';
cx.strokeStyle= far?'#767f8a':'#8b95a1'; cx.lineWidth=6.5; line(hip.x,hip.y,r.kx,r.ky);
cx.strokeStyle= far?'#a8552f':'#d9825a'; cx.lineWidth=3; line(r.kx,r.ky,r.fx,r.fy);
const fd=Math.atan2(r.fy-r.ky,r.fx-r.kx)-PI/2;
cx.lineWidth=2.2;
for(const o of [-0.38,0,0.38]) line(r.fx,r.fy,r.fx+Math.cos(fd+o)*6.5,r.fy+Math.sin(fd+o)*6.5);
line(r.fx,r.fy,r.fx-Math.cos(fd)*3.5,r.fy-Math.sin(fd)*3.5); }
function drawWing(P,side,far){ const sh= side==='L'?{x:-2,y:-10}:{x:3,y:-11};
const tg= side==='L'?P.wl:P.wr; const fan= side==='L'?P.fanL:P.fanR;
const r=ik(sh.x,sh.y,tg.x,tg.y,13,17,P.wb);
cx.lineCap='round';
cx.strokeStyle= far?'#79828e':'#a6afba'; cx.lineWidth=7; line(sh.x,sh.y,r.kx,r.ky);
cx.strokeStyle= far?'#6d7682':'#99a3ae'; cx.lineWidth=5.5; line(r.kx,r.ky,r.fx,r.fy);
const base=Math.atan2(r.fy-r.ky,r.fx-r.kx);
for(let i=0;i<5;i++){ const off=(i-2)*(0.09+0.3*fan);
feather(r.fx,r.fy,base+off,13+i*1.7,3.4-Math.abs(i-2)*0.35, far?'#666f7b':'#8f99a5', fan<0.4); }
cx.fillStyle= far?'#6d7682':'#99a3ae'; cx.beginPath(); cx.arc(r.fx,r.fy,3,0,TAU); cx.fill(); }
function drawTail(P){ for(let i=0;i<5;i++){ const off=(i-2)*(0.11+0.2*P.tailF);
feather(-14,2,PI+P.tailA+off,22+(2-Math.abs(i-2))*3,4.2, i===2?'#5a626e':'#525a65', true); } }
function drawHead(P,t){ const nbx=5,nby=-13, nl=lerp(7,15,P.neck);
const sx=nbx+Math.sin(P.headA)*nl, sy=nby-Math.cos(P.headA)*nl;
const hx=lerp(sx,P.hpx,P.hp), hy=lerp(sy,P.hpy,P.hp);
const sw=clamp(S.chain.x,-0.7,0.7);
cx.strokeStyle='#e3b23c'; cx.lineWidth=1.6;
cx.beginPath(); cx.moveTo(3,-9); cx.quadraticCurveTo(6,-6,6+sw*4,-3); cx.stroke();
cx.save(); cx.translate(6+sw*5,-3+Math.abs(sw)*2); cx.rotate(sw);
cx.fillStyle='#e3b23c'; cx.fillRect(-2.2,-1.5,4.4,5.5);
cx.strokeStyle='#8a6a1c'; cx.lineWidth=0.8; cx.strokeRect(-2.2,-1.5,4.4,5.5);
cx.restore();
cx.strokeStyle='#7d8694'; cx.lineCap='round'; cx.lineWidth=8.5; line(nbx,nby,hx,hy);
cx.strokeStyle='rgba(63,125,107,0.5)'; cx.lineWidth=3;
line(nbx+1.5,nby+1,nbx+1.5+(hx-nbx)*0.4,nby+1+(hy-nby)*0.4);
cx.strokeStyle='rgba(122,90,160,0.4)';
line(nbx-1,nby+2,nbx-1+(hx-nbx)*0.35,nby+2+(hy-nby)*0.35);
cx.save(); cx.translate(hx,hy); cx.rotate(P.headA*0.9);
cx.fillStyle='#7d8694'; cx.beginPath(); cx.arc(0,0,7,0,TAU); cx.fill();
cx.lineWidth=2; cx.strokeStyle='#23262d'; cx.stroke();
cx.fillStyle='#393d45';
cx.beginPath(); cx.moveTo(5,-2.6); cx.lineTo(12.5,-0.8); cx.lineTo(5,0.6); cx.closePath(); cx.fill();
cx.beginPath(); cx.moveTo(5,0.9); cx.lineTo(10.5,1.5); cx.lineTo(5,2.7); cx.closePath(); cx.fill();
cx.fillStyle='#e9e4d8'; cx.beginPath(); cx.ellipse(4.4,-2.8,2.1,1.3,0.35,0,TAU); cx.fill();
cx.fillStyle='#e06a1c'; cx.beginPath(); cx.arc(1.6,-3.4,2.9+0.5*P.hype,0,TAU); cx.fill();
cx.fillStyle='#1a1c22'; cx.beginPath(); cx.arc(2.1,-3.4,Math.max(0.7,1.5-0.5*P.hype),0,TAU); cx.fill();
cx.fillStyle='rgba(255,255,255,0.85)'; cx.beginPath(); cx.arc(0.9,-4.3,0.7,0,TAU); cx.fill();
if(blinking(t)){ cx.strokeStyle='#5d6672'; cx.lineWidth=2.4; line(-0.9,-3.4,4.3,-3.4); }
cx.fillStyle='#2e323a'; cx.beginPath(); cx.arc(0,-1,7.6,PI,TAU); cx.closePath(); cx.fill();
cx.lineWidth=1.6; cx.strokeStyle='#23262d'; cx.stroke();
cx.save(); cx.translate(-7,-2); cx.rotate(-0.1);
cx.fillStyle='#ff6b2b'; rr(-8.6,-1.6,9,3.2,1.5); cx.fill();
cx.strokeStyle='#23262d'; cx.lineWidth=1.4; cx.stroke();
cx.restore();
cx.fillStyle='#ff6b2b'; cx.beginPath(); cx.arc(0,-8.4,1.7,0,TAU); cx.fill();
cx.restore();
}
/* ---------------- scene ---------------- */
function drawSky(t){ cx.fillStyle='#14171e'; cx.fillRect(0,0,Wu,Hu);
for(const s of stars){ const a=(0.3+0.7*(0.5+0.5*Math.sin(t*s.tw+s.ph)))*s.b;
cx.fillStyle=`rgba(233,228,214,${a})`; cx.fillRect(s.x,s.y,s.r,s.r); }
cx.fillStyle='#e8e2d2'; cx.beginPath(); cx.arc(Wu*0.8,capY-170,42,0,TAU); cx.fill();
cx.fillStyle='#d3ccb8';
cx.beginPath(); cx.arc(Wu*0.8-14,capY-180,7,0,TAU); cx.fill();
cx.beginPath(); cx.arc(Wu*0.8+10,capY-158,5,0,TAU); cx.fill();
cx.beginPath(); cx.arc(Wu*0.8+16,capY-186,3.5,0,TAU); cx.fill();
const px=((t*26)%(Wu+300))-150;
cx.fillStyle=(t%1.4<0.12)?'#ff5c4a':'rgba(255,255,255,0.22)';
cx.fillRect(px,capY-215,3,3); }
function drawSkyline(){ cx.fillStyle='#1c212a';
for(const b of blds){ cx.fillRect(b.x,capY-b.h,b.w,b.h+2);
for(const w of b.wins){ cx.fillStyle=`rgba(232,179,76,${w.a})`; cx.fillRect(b.x+w.x,capY-b.h+w.y,2.2,3.2); }
cx.fillStyle='#1c212a'; }
const tx=Wu*0.66, ty=capY-40;
cx.lineWidth=3; cx.strokeStyle='#1c212a';
line(tx-10,capY,tx-7,ty); line(tx+10,capY,tx+7,ty);
cx.fillRect(tx-16,ty-26,32,28);
cx.beginPath(); cx.moveTo(tx-16,ty-26); cx.lineTo(tx,ty-40); cx.lineTo(tx+16,ty-26); cx.closePath(); cx.fill();
line(tx,ty-40,tx,ty-47); }
function drawTags(){ cx.save(); cx.translate(Wu*0.3,wallTop+34); cx.rotate(-0.05);
cx.font='900 46px "Arial Black",sans-serif'; cx.textBaseline='alphabetic'; cx.lineJoin='round';
cx.lineWidth=8; cx.strokeStyle='rgba(16,12,10,0.85)'; cx.strokeText('COO',0,0);
cx.fillStyle='#ff6b2b'; cx.fillText('COO',0,0);
cx.strokeStyle='#ff6b2b'; cx.lineWidth=3;
cx.beginPath(); cx.moveTo(-8,10); cx.quadraticCurveTo(34,20,78,8); cx.stroke();
cx.fillStyle='#ff6b2b'; cx.fillRect(12,4,3,13); cx.fillRect(56,2,3,17);
cx.strokeStyle='rgba(242,237,228,0.8)'; cx.lineWidth=2.5;
cx.beginPath(); cx.moveTo(-14,-58); cx.lineTo(-10,-70); cx.lineTo(-4,-60); cx.lineTo(2,-72); cx.lineTo(8,-60); cx.lineTo(14,-68); cx.lineTo(16,-56); cx.stroke();
cx.restore();
cx.save(); cx.translate(Wu*0.62,wallTop+26); cx.rotate(0.04);
cx.font='900 15px "Arial Black",sans-serif'; cx.fillStyle='rgba(242,237,228,0.6)';
cx.fillText('SQB · ALL CITY',0,0); cx.restore(); }
function drawWall(){ cx.fillStyle='#463731'; cx.fillRect(0,wallTop,Wu,wallBot-wallTop);
cx.strokeStyle='rgba(24,18,15,0.4)'; cx.lineWidth=1;
let row=0;
for(let y=wallTop+5;y<wallBot;y+=9,row++){ line(0,y,Wu,y);
const off=row%2?13:0; cx.beginPath();
for(let x=off;x<Wu;x+=26){ cx.moveTo(x,y); cx.lineTo(x,Math.min(y+9,wallBot)); }
cx.stroke(); }
cx.fillStyle='#59524b'; cx.fillRect(0,capY,Wu,7);
cx.fillStyle='rgba(242,237,228,0.12)'; cx.fillRect(0,capY,Wu,1.5);
drawTags(); }
function drawCrowd(t,beat){ const xs=[AX-298,AX-344,AX+308];
for(let i=0;i<xs.length;i++){ const x=clamp(xs[i],40,Wu-40);
const sc=0.85+((i*37)%10)/40, bob=dip(((beat+i*0.83)%1+1)%1), y=capY;
cx.save(); cx.translate(x,y); cx.scale(sc,sc);
cx.strokeStyle='#2a2e36'; cx.lineWidth=1.5; line(-2.5,0,-2.5,-4); line(2.5,0,2.5,-4);
cx.fillStyle= i===1?'#4a505c':'#3f4550';
cx.beginPath(); cx.ellipse(0,-9,9,6.5,0,0,TAU); cx.fill();
cx.beginPath(); cx.moveTo(-8,-10); cx.lineTo(-13,-7); cx.lineTo(-8,-6); cx.closePath(); cx.fill();
const hx=5.2, hyy=-14+bob*3.2;
cx.beginPath(); cx.arc(hx,hyy,4.4,0,TAU); cx.fill();
cx.fillStyle='#d9825a'; cx.beginPath(); cx.moveTo(hx+4,hyy-1); cx.lineTo(hx+7.5,hyy); cx.lineTo(hx+4,hyy+1); cx.closePath(); cx.fill();
cx.fillStyle='#e06a1c'; cx.fillRect(hx+1.4,hyy-2,1.6,1.6);
if(perfNow()-crowdFlapT<0.55&&i!==2){ cx.strokeStyle='rgba(242,237,228,0.7)'; cx.lineWidth=2;
cx.beginPath(); cx.arc(0,-14,11,PI*1.15,PI*1.85); cx.stroke(); }
cx.restore(); } }
function drawFloor(){ cx.fillStyle='#26231f'; cx.fillRect(0,wallBot,Wu,Hu-wallBot);
cx.strokeStyle='rgba(0,0,0,0.18)'; cx.lineWidth=1;
line(0,wallBot+34,Wu,wallBot+34); line(0,wallBot+92,Wu,wallBot+92);
cx.strokeStyle='rgba(12,10,9,0.35)'; cx.lineWidth=1.2;
for(const c of cracks){ cx.beginPath(); cx.moveTo(c.x,c.y);
cx.lineTo(c.x+Math.cos(c.a)*c.l,c.y+Math.sin(c.a)*c.l*0.3);
cx.lineTo(c.x+Math.cos(c.a)*c.l*1.8,c.y+Math.sin(c.a+0.6)*c.l*0.55); cx.stroke(); } }
function drawCardboard(){ cx.save(); cx.translate(AX,GY+8); cx.rotate(-0.02);
cx.fillStyle='#8a6b44'; cx.fillRect(-118,-2,236,12);
cx.fillStyle='#c39a63'; rr(-118,-12,236,13,3); cx.fill();
cx.fillStyle='rgba(255,255,255,0.12)'; cx.fillRect(-118,-12,236,3);
cx.fillStyle='rgba(236,231,220,0.5)'; cx.fillRect(-62,-12,15,13); cx.fillRect(50,-12,12,13);
cx.strokeStyle='rgba(74,56,32,0.45)'; cx.lineWidth=1.5;
cx.beginPath(); cx.moveTo(-90,2); cx.lineTo(-40,-2); cx.stroke();
cx.beginPath(); cx.moveTo(30,4); cx.lineTo(88,0); cx.stroke();
cx.restore(); }
function drawBoombox(beat){ const bx=AX+185, gy=GY;
const ph=beat-Math.floor(beat), kick=Math.exp(-ph*6)*(soundOn?1:0.35);
cx.fillStyle='#4e5747'; cx.fillRect(bx-20,gy-24,40,24);
cx.strokeStyle='rgba(15,17,13,0.55)'; cx.lineWidth=1.5; cx.strokeRect(bx-20,gy-24,40,24);
line(bx-20,gy-12,bx+20,gy-12); line(bx,gy-24,bx,gy);
cx.fillStyle='#262a31'; rr(bx-34,gy-58,68,34,4); cx.fill();
cx.strokeStyle='#15171c'; cx.lineWidth=2; cx.stroke();
cx.strokeStyle='#3a404a'; cx.lineWidth=3;
cx.beginPath(); cx.arc(bx,gy-56,13,PI,0); cx.stroke();
const r=8.5+2.4*kick;
for(const sx of [bx-17,bx+17]){ cx.fillStyle='#111318'; cx.beginPath(); cx.arc(sx,gy-41,r,0,TAU); cx.fill();
cx.strokeStyle='#4a5160'; cx.lineWidth=1.2; cx.beginPath(); cx.arc(sx,gy-41,r*0.55,0,TAU); cx.stroke(); }
cx.fillStyle='#31363f'; cx.fillRect(bx-9,gy-53,18,11);
cx.strokeStyle='#6b7382'; cx.lineWidth=1;
const ra=beat*PI;
for(const rx of [bx-4,bx+4]){ cx.beginPath(); cx.arc(rx,gy-47.5,2.6,0,TAU); cx.stroke();
line(rx,gy-47.5,rx+Math.cos(ra)*2.6,gy-47.5+Math.sin(ra)*2.6); }
if(soundOn){ for(let i=0;i<3;i++){ const a=(0.3-i*0.08)*kick; if(a<0.02) continue;
cx.strokeStyle=`rgba(242,237,228,${a})`; cx.lineWidth=1.6;
cx.beginPath(); cx.arc(bx+40,gy-42,8+i*7+6*kick,-0.7,0.7); cx.stroke(); } } }
function drawParts(){ for(const p of parts){ const a=1-p.l/p.ml;
if(p.t===0){ cx.fillStyle=`rgba(190,180,165,${0.32*a})`; cx.beginPath(); cx.arc(p.x,p.y,p.r,0,TAU); cx.fill(); }
else { cx.save(); cx.translate(p.x,p.y); cx.rotate(p.ph); cx.fillStyle=`rgba(154,163,174,${0.8*a})`;
cx.fillRect(-p.r,-p.r*0.4,p.r*2,p.r*0.8); cx.restore(); } } }
function draw(t,beat,dt){
cx.setTransform(DPR*SC,0,0,DPR*SC,0,0);
cx.clearRect(0,0,Wu,Hu);
const shx=RM?0:(Math.random()-0.5)*2*shakeA, shy=RM?0:(Math.random()-0.5)*2*shakeA;
shakeA*=Math.exp(-dt*6);
cx.save(); cx.translate(shx+camX,shy+camY);
drawSky(t); drawSkyline(); drawWall(); drawCrowd(t,beat); drawFloor(); drawCardboard(); drawBoombox(beat);
drawBird(FP,t);
drawParts();
cx.restore();
if(grainPat){ cx.save(); if(!RM) cx.translate(-((Math.random()*140)|0),-((Math.random()*140)|0));
cx.fillStyle=grainPat; cx.fillRect(0,0,Wu+140,Hu+140); cx.restore(); }
cx.fillStyle=vg; cx.fillRect(0,0,Wu,Hu);
if(flashA>0.01){ cx.fillStyle=`rgba(255,244,230,${flashA})`; cx.fillRect(0,0,Wu,Hu); flashA*=Math.exp(-dt*5); }
}
/* ---------------- main loop ---------------- */
let lastT=perfNow(), lastDot=-1;
function frame(){ requestAnimationFrame(frame);
const t=perfNow(); const dt=clamp(t-lastT,0.001,0.05); lastT=t;
const beat=beatNow();
let b=beat-clip.start, guard=0;
while(canEnd(b)&&guard++<5){ selectNext(); b=beat-clip.start; }
MOVES[clip.name].fn(curPose,b,prevB,clip.st); prevB=b;
lerpPose(fromPose,curPose,easeIO(clamp(b/0.8,0,1)),RP);
stepSim(dt); stepParts(dt);
draw(t,beat,dt);
const di=Math.floor(beat)%4;
if(di!==lastDot){ lastDot=di; dotEls.forEach((el,i)=>el.classList.toggle('on',i===di)); }
}
/* ---------------- controls ---------------- */
const bpmEl=document.getElementById('bpm'), bpmv=document.getElementById('bpmv'),
sndEl=document.getElementById('snd'), autEl=document.getElementById('aut'),
hrnEl=document.getElementById('hrn'), nextEl=document.getElementById('next'),
dotEls=[...document.querySelectorAll('#dots i')];
function updateNextUI(){ nextEl.textContent='NEXT ▸ '+(queue.length?queue.join(' · '):(auto?'auto — improv set':'your call')); }
function queueMove(m){ if(!MOVES[m]) return; if(queue.length>=4) queue.shift(); queue.push(m); updateNextUI(); }
document.querySelectorAll('.chip').forEach(el=>el.addEventListener('click',()=>queueMove(el.dataset.m)));
sndEl.addEventListener('click',()=>{ soundOn=!soundOn;
if(soundOn){ if(ensureAudio()){ AC.resume(); crackleGain.gain.value=0.012; } }
else if(crackleGain) crackleGain.gain.value=0;
sndEl.textContent= soundOn?'Sound on':'Sound off'; sndEl.setAttribute('aria-pressed',String(soundOn)); });
autEl.addEventListener('click',()=>{ auto=!auto;
autEl.textContent=auto?'Auto':'Manual'; autEl.setAttribute('aria-pressed',String(auto)); updateNextUI(); });
hrnEl.addEventListener('click',()=>{ horn(); queue.unshift('freeze'); flashA=RM?0:0.3; crowdFlapT=perfNow(); updateNextUI(); });
bpmEl.addEventListener('input',()=>{ setBpm(+bpmEl.value); bpmv.textContent=bpmEl.value; });
cv.addEventListener('pointerdown',()=>queueMove('hop'));
window.addEventListener('keydown',e=>{ if(e.metaKey||e.ctrlKey||e.altKey) return;
const map={'1':'toprock','2':'footwork','3':'freeze','4':'backspin','5':'headspin','6':'hop'};
if(map[e.key]) queueMove(map[e.key]);
else if(e.key==='a'||e.key==='A') autEl.click();
else if(e.key==='s'||e.key==='S') sndEl.click();
else if(e.key==='h'||e.key==='H') hrnEl.click();
else if(e.key===' '){ e.preventDefault(); queueMove('hop'); } });
/* ---------------- go ---------------- */
resize();
grainPat=(()=>{ const g=document.createElement('canvas'); g.width=g.height=140;
const gc=g.getContext('2d'), im=gc.createImageData(140,140);
for(let i=0;i<im.data.length;i+=4){ const v=(Math.random()*255)|0;
im.data[i]=im.data[i+1]=im.data[i+2]=v; im.data[i+3]=16; }
gc.putImageData(im,0,0); return cx.createPattern(g,'repeat'); })();
cx.lineCap='round'; cx.lineJoin='round';
updateNextUI();
requestAnimationFrame(frame);
</script>
</body>
</html>