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.
A rock dove — the everyday street pigeon — b-boying on a water-tower rooftop at dusk, because the pigeon is the only bird with genuine street credentials. The look is screen-print poster: five flat inks, hard-edged shapes, film grain, no outlines, a magenta-and-green iridescent neck patch as the only flourish, and two spectator pigeons on the parapet bobbing their heads to the beat.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>ROCK DOVE · rooftop b-boying</title>
<style>
*{box-sizing:border-box;margin:0}
html,body{height:100%;background:#1b1512;overflow:hidden}
canvas{display:block}
#ui{position:fixed;left:0;right:0;bottom:0;display:flex;align-items:center;justify-content:center;flex-wrap:wrap;gap:10px 16px;padding:10px 14px 12px;background:rgba(22,17,13,.9);border-top:2px solid #e8dcc4;font-family:'Courier New',monospace;user-select:none;z-index:2}
.call{font-family:inherit;font-weight:700;font-size:12px;letter-spacing:1px;background:#e8dcc4;color:#191512;border:2px solid #14100d;padding:7px 10px;cursor:pointer;box-shadow:3px 3px 0 #6b5638;transition:transform .06s,box-shadow .06s}
.call:hover{background:#f6ead0;transform:translate(-1px,-1px);box-shadow:4px 4px 0 #6b5638}
.call:active{transform:translate(2px,2px);box-shadow:0 0 0 #6b5638}
.call.q{outline:2px solid #d98a3d}
kbd{font-family:inherit;font-size:9px;border:1px solid #14100d;background:#cbb994;padding:0 3px;margin-left:6px;border-radius:2px}
.hype{display:flex;align-items:center;gap:8px;color:#8a7a63;font-size:11px;letter-spacing:2px}
#cells{display:flex;gap:4px}
#cells span{width:12px;height:12px;border:2px solid #6b5638}
#cells span.on{background:#e8dcc4;border-color:#e8dcc4}
#hstate{color:#d98a3d;font-weight:700;min-width:74px}
#hstate.ready{animation:bl .5s steps(2) infinite}
@keyframes bl{50%{opacity:.15}}
.tempo{display:flex;align-items:center;gap:8px;color:#8a7a63;font-size:11px;letter-spacing:2px}
.tempo input{accent-color:#e8dcc4;width:110px}
#bpml{color:#e8dcc4;min-width:22px}
#hint{position:fixed;top:12px;right:14px;color:#6b5a45;font:10px 'Courier New',monospace;letter-spacing:1px;z-index:2}
@media (max-width:760px){#hint{display:none}.call{font-size:10px;padding:6px 7px}}
</style>
</head>
<body>
<canvas id="c"></canvas>
<div id="hint">1–5 CALL A MOVE · S SOUND · ← → TEMPO</div>
<div id="ui">
<button class="call" data-move="sixstep">SIX-STEP<kbd>1</kbd></button>
<button class="call" data-move="babyfreeze">BABY FREEZE<kbd>2</kbd></button>
<button class="call" data-move="windmill">WINDMILL<kbd>3</kbd></button>
<button class="call" data-move="backspin">BACKSPIN<kbd>4</kbd></button>
<button class="call" data-move="headspin">HEADSPIN<kbd>5</kbd></button>
<div class="hype">HYPE<span id="cells"><span></span><span></span><span></span><span></span><span></span><span></span></span><span id="hstate"></span></div>
<label class="tempo">TEMPO<input id="bpm" type="range" min="72" max="112" value="92"><span id="bpml">92</span></label>
<button class="call" id="snd">SOUND OFF<kbd>S</kbd></button>
</div>
<script>
'use strict';
// ---------- helpers ----------
const TAU=Math.PI*2;
const clamp=(v,a,b)=>v<a?a:v>b?b:v;
const lerp=(a,b,t)=>a+(b-a)*t;
const sq=v=>v*v;
const ss=(x,a,b)=>{x=clamp((x-a)/(b-a),0,1);return x*x*(3-2*x);};
const rnd=(a,b)=>a+Math.random()*(b-a);
const srand=i=>{const x=Math.sin(i*127.1+311.7)*43758.5453;return x-Math.floor(x);};
const sub=(a,b)=>[a[0]-b[0],a[1]-b[1]],add=(a,b)=>[a[0]+b[0],a[1]+b[1]],mul=(a,s)=>[a[0]*s,a[1]*s];
const norm2=v=>{const l=Math.hypot(v[0],v[1])||1;return[v[0]/l,v[1]/l];};
const lerp2=(a,b,t)=>[lerp(a[0],b[0],t),lerp(a[1],b[1],t)];
// ---------- canvas ----------
const cv=document.getElementById('c'),ctx=cv.getContext('2d');
let dpr=1,cw=0,ch=0,vs=1,vox=0,voy=0;
function resize(){dpr=Math.min(2,window.devicePixelRatio||1);cw=innerWidth;ch=innerHeight;
cv.width=Math.round(cw*dpr);cv.height=Math.round(ch*dpr);cv.style.width=cw+'px';cv.style.height=ch+'px';
vs=Math.min(cw/960,ch/540);vox=(cw-960*vs)/2;voy=(ch-540*vs)/2;}
addEventListener('resize',resize);resize();
// ---------- palette / constants ----------
const C={sky:'#1b1512',bldg:'#251c15',bldg2:'#1f1712',roof:'#33291f',gravel:'#3d3126',gravel2:'#2b2119',
parapet:'#4a2f26',parapetLine:'#3a241d',parapetTop:'#6b4232',pool:'#6b5638',pool2:'#7d6642',
moon:'#f0dfbd',crater:'#e2cda6',star:'#e8dcc4',body:'#9a938a',belly:'#c6bfb1',wing:'#57514a',wingD:'#3b362f',
beak:'#d9a05b',beakD2:'#5f4630',cere:'#e8dcc4',eye:'#f4e3c2',iris:'#d96b3c',green:'#3e7d68',magenta:'#8f4a63',
cream:'#e8dcc4',ink:'#14100d',orange:'#d98a3d',dust:'#b09a72'};
const LEGN={th:'#8a8378',sk:'#c96f3d'},LEGF={th:'#6e675d',sk:'#96582f'};
const FLOOR=452,AX=480;
const dancer={face:1};
let beats=0,tSec=0,bpm=92,blink=false,shakeA=0,sqImp=0,specEx=0,headNodS=0;
// ---------- precomputed scenery ----------
const BLD=[[-30,130,150],[100,85,95],[185,115,190,1],[300,70,120],[370,135,82],[505,90,160],[595,70,105],[665,125,178,2],[790,95,128],[885,115,96]];
const WINS=[];
BLD.forEach((b,bi)=>{const cols=Math.floor(b[1]/16),rows=Math.floor(b[2]/22);
for(let c=0;c<cols;c++)for(let r=0;r<rows;r++){const si=bi*97+c*13+r*7;
if(srand(si)>0.86)WINS.push([b[0]+6+c*15,400-b[2]+8+r*21,srand(si+1)>0.5?1:0.55]);}});
const STARS=[];for(let i=0;i<55;i++)STARS.push([srand(i)*960,20+srand(i+40)*330,0.7+srand(i+80)*1.2,srand(i+120)*6]);
const GRAV=[];for(let i=0;i<110;i++)GRAV.push([srand(i+200)*960,410+srand(i+300)*126,2+srand(i+400)*4,srand(i+500)>0.5]);
const TJIT='ROCK DOVE'.split('').map((c,i)=>({x:(srand(i)-0.5)*2.5,y:(srand(i+30)-0.5)*4,r:(srand(i+60)-0.5)*0.08}));
// ---------- grain tile ----------
const gr=document.createElement('canvas');gr.width=gr.height=140;
{const g=gr.getContext('2d'),im=g.createImageData(140,140);
for(let i=0;i<im.data.length;i+=4){const v=100+Math.random()*90|0;im.data[i]=im.data[i+1]=im.data[i+2]=v;im.data[i+3]=255;}
g.putImageData(im,0,0);}
let grPat=null;
// ---------- audio (all synthesized, locked to the visual beat clock) ----------
let AC=null,master=null,noiseBuf=null,soundOn=false,nextStep=0;
function initAudio(){AC=new (window.AudioContext||window.webkitAudioContext)();
master=AC.createGain();master.gain.value=0.7;
const cp=AC.createDynamicsCompressor();master.connect(cp);cp.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;}
function noise(t,dur,type,freq,q,peak){const s=AC.createBufferSource();s.buffer=noiseBuf;
const f=AC.createBiquadFilter();f.type=type;f.frequency.value=freq;f.Q.value=q;
const g=AC.createGain();g.gain.setValueAtTime(peak,t);g.gain.exponentialRampToValueAtTime(0.001,t+dur);
s.connect(f);f.connect(g);g.connect(master);s.start(t);s.stop(t+dur+0.02);}
function kick(t,f0,f1,peak,dec){const o=AC.createOscillator();o.type='sine';
o.frequency.setValueAtTime(f0,t);o.frequency.exponentialRampToValueAtTime(f1,t+0.1);
const g=AC.createGain();g.gain.setValueAtTime(peak,t);g.gain.exponentialRampToValueAtTime(0.001,t+dec);
o.connect(g);g.connect(master);o.start(t);o.stop(t+dec+0.02);}
function snare(t){noise(t,0.16,'bandpass',1800,0.8,0.5);
const o=AC.createOscillator();o.type='triangle';o.frequency.value=190;
const g=AC.createGain();g.gain.setValueAtTime(0.3,t);g.gain.exponentialRampToValueAtTime(0.001,t+0.09);
o.connect(g);g.connect(master);o.start(t);o.stop(t+0.11);}
function hat(t,open,v){noise(t,open?0.28:0.05,'highpass',7500,0.7,v);}
function bassN(t,f){const o=AC.createOscillator();o.type='triangle';o.frequency.value=f;
const fl=AC.createBiquadFilter();fl.type='lowpass';fl.frequency.value=380;
const g=AC.createGain();g.gain.setValueAtTime(0.42,t);g.gain.exponentialRampToValueAtTime(0.001,t+0.38);
o.connect(fl);fl.connect(g);g.connect(master);o.start(t);o.stop(t+0.4);}
function stabS(t){[220,329.63].forEach(f=>{const o=AC.createOscillator();o.type='square';o.frequency.value=f;
const fl=AC.createBiquadFilter();fl.type='lowpass';fl.frequency.value=1300;
const g=AC.createGain();g.gain.setValueAtTime(0.12,t);g.gain.exponentialRampToValueAtTime(0.001,t+0.2);
o.connect(fl);fl.connect(g);g.connect(master);o.start(t);o.stop(t+0.22);});}
function schedStep(k,t){const s=k%16,bar=Math.floor(k/16)%2;
if(s===0||s===10)kick(t,150,44,0.85,0.3);
if(bar===1&&s===7)kick(t,120,42,0.45,0.2);
if(s===4||s===12)snare(t);
if(s%2===0)hat(t,bar===1&&s===14,s%4===2?0.3:0.18);
if(s===0)bassN(t,bar===0?55:65.41);
if(s===7&&bar===1)bassN(t,49);
if(s===10)bassN(t,bar===0?65.41:43.65);}
function pump(){if(!soundOn||!AC)return;const spb=60/bpm,ahead=0.14;
if(nextStep<beats*4-0.25)nextStep=Math.ceil(beats*4);
let guard=0;
while(guard++<40){const off=(nextStep/4-beats)*spb;
if(off>ahead)break;
if(off>-0.05)schedStep(nextStep,Math.max(AC.currentTime,AC.currentTime+off));
nextStep++;}}
function sfx(kind){if(!soundOn||!AC)return;const t=AC.currentTime;
if(kind==='land')kick(t,110,48,0.5,0.2);
else if(kind==='tick')noise(t,0.035,'highpass',3800,0.7,0.14);
else if(kind==='stab'){stabS(t);kick(t,90,45,0.35,0.18);}}
// ---------- pose system ----------
const PKEYS=['x','y','rot','curl','squash','nod','tilt','tail','headF','wLs','wLe','wRs','wRe',
'lLh','lLk','lLf','lRh','lRk','lRf','spin','spinA','scx','scy','smear','swap'];
const I={};PKEYS.forEach((k,i)=>I[k]=i);
const DEF={x:0,y:FLOOR-40,rot:0.08,curl:0.12,squash:1,nod:0,tilt:0,tail:3.7,headF:0,
wLs:3.3,wLe:2.1,wRs:3.45,wRe:2.25,lLh:2.9,lLk:0.5,lLf:-1.9,lRh:2.95,lRk:0.45,lRf:-1.85,
spin:0,spinA:0,scx:0,scy:0,smear:0,swap:0};
const full=o=>PKEYS.map(k=>o[k]!==undefined?o[k]:DEF[k]);
const STAND_P={y:FLOOR-40,rot:0.08,curl:0.12,nod:0,tail:3.7,headF:0,squash:1,x:0,
wLs:3.3,wLe:2.1,wRs:3.45,wRe:2.25,lLh:2.9,lLk:0.5,lLf:-1.9,lRh:2.95,lRk:0.45,lRf:-1.85};
const CROUCH_P={x:0,y:FLOOR-34,rot:1.1,curl:0.2,squash:1,nod:-0.25,tilt:0,tail:3.9,headF:0,
wLs:2.0,wLe:0.12,wRs:2.12,wRe:0.2,lLh:1.06,lLk:1.54,lLf:-2.13,lRh:1.2,lRk:1.45,lRf:-2.05};
const E={lin:u=>u,out:u=>1-Math.pow(1-u,3),outX:u=>1-Math.pow(1-u,5),inn:u=>u*u*u,
inout:u=>u<0.5?4*u*u*u:1-Math.pow(-2*u+2,3)/2,
back:u=>{const c=1.6;return 1+(c+1)*Math.pow(u-1,3)+c*Math.pow(u-1,2);}};
function mkMove(id,label,base,steps,mod,opt={}){
let acc=0,merged=Object.assign({},DEF,base);const pre=[];
for(const s of steps){const from=full(merged);merged=Object.assign({},merged,s.p);
pre.push({t0:acc,d:s.d,e:s.e,from,to:full(merged)});acc+=s.d;}
return{id,label,beats:acc,pre,mod,floor:!!opt.floor,big:!!opt.big,sz:opt.sz||1};}
function evalMove(m,local){const pre=m.pre;
if(local<=0)return pre[0].from.slice();
for(let i=pre.length-1;i>=0;i--){const s=pre[i];
if(local>=s.t0){const u=Math.min(1,(local-s.t0)/s.d),e=s.e(u),a=new Array(PKEYS.length);
for(let k=0;k<a.length;k++)a[k]=s.from[k]+(s.to[k]-s.from[k])*e;return a;}}
return pre[0].from.slice();}
// ---------- forward kinematics ----------
function fkPoints(p){
const F=dancer.face;
const S=(a,l,o)=>[o[0]+Math.sin(a)*l*F,o[1]-Math.cos(a)*l];
const pelvis=[AX+p[I.x],p[I.y]];
const mid=S(p[I.rot],22,pelvis);
const chestA=p[I.rot]+p[I.curl];
const chest=S(chestA,18,mid);
const hA=chestA+p[I.nod]+p[I.tilt];
const headC=S(hA,13,chest);
headC[0]+=Math.cos(hA)*F*p[I.headF];headC[1]+=Math.sin(hA)*p[I.headF];
const beakD=[Math.cos(hA)*F,Math.sin(hA)];
const perpD=[-Math.sin(hA)*F,Math.cos(hA)];
const wing=(s,e)=>{const sh=S(chestA+Math.PI,4,chest),el=S(p[I.rot]+s,24,sh),tip=S(p[I.rot]+s+e,22,el);return[sh,el,tip];};
const leg=(h,k,f,ox,oy)=>{const hip=[pelvis[0]+ox*F,pelvis[1]+oy],knee=S(p[I.rot]+h,22,hip),
ankle=S(p[I.rot]+h+k,20,knee),footA=p[I.rot]+h+k+f,toe=S(footA,12,ankle);return{hip,knee,ankle,footA,toe};};
const o={pelvis,mid,chest,headC,beakD,perpD,
wL:wing(p[I.wLs],p[I.wLe]),wR:wing(p[I.wRs],p[I.wRe]),
lL:leg(p[I.lLh],p[I.lLk],p[I.lLf],1,-1),lR:leg(p[I.lRh],p[I.lRk],p[I.lRf],-3,2),
tailBase:S(p[I.rot]+Math.PI,5,pelvis)};
if(p[I.spin]){
const c=[p[I.scx],p[I.scy]],ca=Math.cos(p[I.spinA]),sa=Math.sin(p[I.spinA]);
const R=pt=>[c[0]+(pt[0]-c[0])*ca-(pt[1]-c[1])*sa,c[1]+(pt[0]-c[0])*sa+(pt[1]-c[1])*ca];
o.pelvis=R(o.pelvis);o.mid=R(o.mid);o.chest=R(o.chest);o.headC=R(o.headC);
o.wL=o.wL.map(R);o.wR=o.wR.map(R);
for(const k of['lL','lR']){const L=o[k];o[k]={hip:R(L.hip),knee:R(L.knee),ankle:R(L.ankle),footA:L.footA,toe:R(L.toe)};}
o.tailBase=R(o.tailBase);
o.beakD=[o.beakD[0]*ca-o.beakD[1]*sa,o.beakD[0]*sa+o.beakD[1]*ca];
o.perpD=[o.perpD[0]*ca-o.perpD[1]*sa,o.perpD[0]*sa+o.perpD[1]*ca];}
return o;}
function clampPose(p){const fk=fkPoints(p);let mx=-1e9;
mx=Math.max(mx,fk.wL[2][1]+2,fk.wR[2][1]+2,fk.lL.ankle[1]+2,fk.lR.ankle[1]+2,fk.headC[1]+11.5);
const bc=[lerp(fk.pelvis[0],fk.chest[0],0.55),lerp(fk.pelvis[1],fk.chest[1],0.55)];
const sa=Math.atan2(fk.chest[1]-fk.pelvis[1],fk.chest[0]-fk.pelvis[0]);
mx=Math.max(mx,bc[1]+Math.sqrt(900*sq(Math.sin(sa))+441*sq(Math.cos(sa))));
const over=mx-(FLOOR-1);
if(over>0)p[I.y]-=over;}
// ---------- draw primitives ----------
function line(a,b){ctx.beginPath();ctx.moveTo(a[0],a[1]);ctx.lineTo(b[0],b[1]);ctx.stroke();}
function tri(a,b,c){ctx.beginPath();ctx.moveTo(a[0],a[1]);ctx.lineTo(b[0],b[1]);ctx.lineTo(c[0],c[1]);ctx.closePath();ctx.fill();}
function smoothPoly(pts){const n=pts.length;ctx.beginPath();
ctx.moveTo((pts[0][0]+pts[1][0])/2,(pts[0][1]+pts[1][1])/2);
for(let i=1;i<=n;i++){const a=pts[i%n],b=pts[(i+1)%n];
ctx.quadraticCurveTo(a[0],a[1],(a[0]+b[0])/2,(a[1]+b[1])/2);}
ctx.closePath();}
function drawLeg(L,st,F){
ctx.strokeStyle=st.th;ctx.lineWidth=8;line(L.hip,L.knee);
ctx.strokeStyle=st.sk;ctx.lineWidth=4.5;line(L.knee,L.ankle);
ctx.lineWidth=2.8;
for(const o of[[-0.3,10],[0,12.5],[0.28,9.5]]){
const a=L.footA+o[0],dx=Math.sin(a)*F,dy=-Math.cos(a);
line(L.ankle,[L.ankle[0]+dx*o[1],L.ankle[1]+dy*o[1]]);}}
function drawWing(w,near){
const sh=w[0],el=w[1],tip=w[2];
const d1=norm2(sub(el,sh)),d2=norm2(sub(tip,el)),dm=norm2(sub(tip,sh));
const p1=[-d1[1],d1[0]],p2=[-d2[1],d2[0]],pm=[-dm[1],dm[0]];
smoothPoly([add(sh,mul(p1,5.5)),add(el,mul(pm,7)),add(tip,mul(p2,3)),
sub(tip,mul(p2,4)),sub(el,mul(pm,7)),sub(sh,mul(p1,5.5))]);
ctx.fillStyle=near?C.wing:C.wingD;ctx.fill();
if(near){ctx.strokeStyle=C.wingD;ctx.lineWidth=1.4;ctx.globalAlpha=0.7;
line(add(el,mul(pm,3.5)),add(lerp2(el,tip,0.3),mul(p2,2)));
line(add(lerp2(sh,el,0.55),mul(pm,3.5)),add(lerp2(el,tip,0.55),mul(pm,1)));
ctx.globalAlpha=1;}}
function drawTail(fk,p,F){
const tb=fk.tailBase;
for(let i=0;i<4;i++){
const a=p[I.rot]+p[I.tail]+(i-1.5)*0.17,len=26+((i===1||i===2)?5:0);
const dx=Math.sin(a)*F,dy=-Math.cos(a);
const tip=[tb[0]+dx*len,tb[1]+dy*len],px=-dy,py=dx;
ctx.fillStyle=i%2?C.wing:C.wingD;
tri([tb[0]+px*2.6,tb[1]+py*2.6],tip,[tb[0]-px*2.6,tb[1]-py*2.6]);}}
function fan(cx,cy,a,r,al,F){
if(al<0.02)return;
ctx.fillStyle=C.wing;ctx.globalAlpha=al;
ctx.beginPath();ctx.moveTo(cx,cy);
for(let k=0;k<=7;k++){const ang=a-1.1+1.6*k/7;
ctx.lineTo(cx+Math.sin(ang)*F*r,cy-Math.cos(ang)*r);}
ctx.closePath();ctx.fill();ctx.globalAlpha=1;}
function drawSmear(p,fk,F){
const cx=p[I.scx]||fk.pelvis[0],cy=p[I.scy]||fk.pelvis[1];
const aN=p[I.rot]+p[I.lLh],aF=p[I.rot]+p[I.lRh];
fan(cx,cy,aN,44,0.16*p[I.smear],F);
fan(cx,cy,aF,38,0.11*p[I.smear],F);}
function drawDancer(p,alpha,depth){
alpha=alpha===undefined?1:alpha;depth=depth||0;
const F=dancer.face,fk=fkPoints(p);
ctx.save();ctx.globalAlpha*=alpha;
if(p[I.spin]){ctx.translate(p[I.scx],p[I.scy]);ctx.rotate(p[I.spinA]);ctx.translate(-p[I.scx],-p[I.scy]);}
if(depth===0&&!p[I.spin]&&p[I.smear]>0.06)drawSmear(p,fk,F);
if(depth<1&&p[I.spin]&&p[I.smear]>0.25){
const q=p.slice();q[I.spinA]-=0.38;q[I.smear]=0;drawDancer(q,0.16,depth+1);
q[I.spinA]-=0.38;drawDancer(q,0.08,depth+1);}
const sw=p[I.swap]>0.5;
drawLeg(sw?fk.lL:fk.lR,LEGF,F);
drawWing(sw?fk.wL:fk.wR,false);
drawTail(fk,p,F);
// body
const bc=[lerp(fk.pelvis[0],fk.chest[0],0.55),lerp(fk.pelvis[1],fk.chest[1],0.55)];
const spA=Math.atan2(fk.chest[1]-fk.pelvis[1],fk.chest[0]-fk.pelvis[0]);
const spineD=[Math.cos(spA),Math.sin(spA)];
const frontP=[-spineD[1]*F,spineD[0]*F];
const rAlong=30*p[I.squash],rAcross=21*(2-p[I.squash]);
ctx.fillStyle=C.body;ctx.beginPath();ctx.ellipse(bc[0],bc[1],rAlong,rAcross,spA,0,TAU);ctx.fill();
ctx.save();ctx.beginPath();ctx.ellipse(bc[0],bc[1],rAlong,rAcross,spA,0,TAU);ctx.clip();
ctx.fillStyle=C.belly;ctx.beginPath();
ctx.ellipse(bc[0]+frontP[0]*5.5-spineD[0]*2,bc[1]+frontP[1]*5.5-spineD[1]*2,20,14,spA,0,TAU);ctx.fill();
const pc=add(bc,mul(spineD,11));
ctx.globalAlpha=0.95;ctx.fillStyle=C.green;ctx.beginPath();ctx.ellipse(pc[0],pc[1],9,7,spA,0,TAU);ctx.fill();
ctx.fillStyle=C.magenta;ctx.beginPath();ctx.ellipse(pc[0]+frontP[0]*3,pc[1]+frontP[1]*3,5.5,4,spA,0,TAU);ctx.fill();
ctx.globalAlpha=1;ctx.restore();
drawLeg(sw?fk.lR:fk.lL,LEGN,F);
// neck + head
ctx.strokeStyle=C.body;ctx.lineWidth=10;line(fk.chest,fk.headC);
const na=Math.atan2(fk.headC[1]-fk.chest[1],fk.headC[0]-fk.chest[0]);
const np=lerp2(fk.chest,fk.headC,0.45);
ctx.fillStyle=C.green;ctx.beginPath();ctx.ellipse(np[0],np[1],7,5.5,na,0,TAU);ctx.fill();
const np2=lerp2(fk.chest,fk.headC,0.3);
ctx.globalAlpha=0.85;ctx.fillStyle=C.magenta;ctx.beginPath();
ctx.ellipse(np2[0]+fk.perpD[0]*1.5,np2[1]+fk.perpD[1]*1.5,4.5,3.5,na,0,TAU);ctx.fill();ctx.globalAlpha=1;
ctx.fillStyle=C.body;ctx.beginPath();ctx.arc(fk.headC[0],fk.headC[1],11.5,0,TAU);ctx.fill();
const ba=Math.atan2(fk.beakD[1],fk.beakD[0]);
const ce=[fk.headC[0]+fk.beakD[0]*5.5-fk.perpD[0]*4.8,fk.headC[1]+fk.beakD[1]*5.5-fk.perpD[1]*4.8];
ctx.fillStyle=C.cere;ctx.beginPath();ctx.ellipse(ce[0],ce[1],3.4,2.6,ba,0,TAU);ctx.fill();
const b1=[fk.headC[0]+fk.beakD[0]*6-fk.perpD[0]*4.6,fk.headC[1]+fk.beakD[1]*6-fk.perpD[1]*4.6];
const b2=[fk.headC[0]+fk.beakD[0]*6+fk.perpD[0]*4.2,fk.headC[1]+fk.beakD[1]*6+fk.perpD[1]*4.2];
const bt=[fk.headC[0]+fk.beakD[0]*18.5,fk.headC[1]+fk.beakD[1]*18.5];
ctx.fillStyle=C.beak;tri(b1,bt,b2);
const m1=[fk.headC[0]+fk.beakD[0]*11.5-fk.perpD[0]*2.6,fk.headC[1]+fk.beakD[1]*11.5-fk.perpD[1]*2.6];
const m2=[fk.headC[0]+fk.beakD[0]*11.5+fk.perpD[0]*2.4,fk.headC[1]+fk.beakD[1]*11.5+fk.perpD[1]*2.4];
const bt2=[fk.headC[0]+fk.beakD[0]*19.5,fk.headC[1]+fk.beakD[1]*19.5];
ctx.fillStyle=C.beakD2;tri(m1,bt2,m2);
const e=[fk.headC[0]+fk.beakD[0]*2.2-fk.perpD[0]*4.6,fk.headC[1]+fk.beakD[1]*2.2-fk.perpD[1]*4.6];
if(blink){ctx.strokeStyle='#3b362f';ctx.lineWidth=2;
line([e[0]-fk.beakD[0]*3,e[1]-fk.beakD[1]*3],[e[0]+fk.beakD[0]*3,e[1]+fk.beakD[1]*3]);}
else{ctx.fillStyle=C.eye;ctx.beginPath();ctx.arc(e[0],e[1],3.4,0,TAU);ctx.fill();
ctx.fillStyle=C.iris;ctx.beginPath();ctx.arc(e[0]+fk.beakD[0]*0.7,e[1]+fk.beakD[1]*0.7,2.3,0,TAU);ctx.fill();
ctx.fillStyle='#14100d';ctx.beginPath();ctx.arc(e[0]+fk.beakD[0]*1.2,e[1]+fk.beakD[1]*1.2,1.15,0,TAU);ctx.fill();}
drawWing(sw?fk.wR:fk.wL,true);
ctx.restore();}
function drawSpectator(x,y,s,F,ph){
const bob=Math.max(0,Math.sin((beats+ph)*Math.PI))*2.6;
const ex=specEx*(0.75+0.25*Math.sin(tSec*11+ph*3));
ctx.save();ctx.translate(x,y);ctx.scale(F*s,s);
ctx.strokeStyle='#96582f';ctx.lineWidth=2.2;
ctx.beginPath();ctx.moveTo(-2,-8);ctx.lineTo(-2.5,0);ctx.moveTo(3,-8);ctx.lineTo(3.5,0);ctx.stroke();
ctx.fillStyle='#453f38';tri([-5,-13],[-13,-9],[-5,-9]);
ctx.fillStyle='#6e675d';ctx.beginPath();ctx.ellipse(0,-15,7.5,10,0.12,0,TAU);ctx.fill();
const wa=lerp(2.5,0.5,ex),wx=Math.sin(wa)*13,wy=-Math.cos(wa)*13;
ctx.fillStyle='#453f38';ctx.beginPath();ctx.moveTo(1,-20);
ctx.quadraticCurveTo(1+wx*0.6,-17+wy*0.6,1+wx,-18+wy);
ctx.quadraticCurveTo(1+wx*0.5,-22+wy*0.5,1,-19);ctx.closePath();ctx.fill();
const hx=2.5+bob,hy=-27-ex*4;
ctx.fillStyle='#6e675d';ctx.beginPath();ctx.arc(hx,hy,5.5,0,TAU);ctx.fill();
ctx.fillStyle='#b08048';tri([hx+3,hy-2],[hx+10.5,hy+0.5],[hx+3,hy+2]);
ctx.fillStyle=C.cream;ctx.beginPath();ctx.arc(hx+1.5,hy-1.5,1.7,0,TAU);ctx.fill();
ctx.fillStyle='#14100d';ctx.beginPath();ctx.arc(hx+2,hy-1.5,0.9,0,TAU);ctx.fill();
ctx.restore();}
// ---------- particles & stamps ----------
const PARTS=[],STAMPS=[];
function dust(x,y,n){for(let i=0;i<n;i++)PARTS.push({t:'d',x:x+rnd(-8,8),y:y+rnd(-3,3),
vx:rnd(-55,55),vy:rnd(-75,-15),r:rnd(2,4.5),l:1,d:rnd(1.8,2.8)});}
function feathers(x,y,n){const cols=[C.belly,C.body,C.cream];
for(let i=0;i<n;i++)PARTS.push({t:'f',x,y:y-10,vx:rnd(-70,70),vy:rnd(-130,-30),
ro:rnd(0,6),vr:rnd(-4,4),ph:rnd(0,6),l:1,d:rnd(0.35,0.6),c:cols[i%3]});}
function updParts(dt){for(let i=PARTS.length-1;i>=0;i--){const p=PARTS[i];p.l-=p.d*dt;
if(p.l<=0){PARTS.splice(i,1);continue;}
if(p.t==='d'){p.vy+=170*dt;p.x+=p.vx*dt;p.y+=p.vy*dt;
if(p.y>FLOOR+6){p.y=FLOOR+6;p.vy*=-0.25;p.vx*=0.7;}}
else{p.vy=Math.min(p.vy+70*dt,46);p.x+=p.vx*dt+Math.sin(p.l*9+p.ph)*30*dt;
p.y+=p.vy*dt;p.ro+=p.vr*dt;p.vx*=0.98;}}}
function drawParts(){for(const p of PARTS){
if(p.t==='d'){ctx.globalAlpha=p.l*0.4;ctx.fillStyle=C.dust;
ctx.beginPath();ctx.arc(p.x,p.y,p.r*(2-p.l),0,TAU);ctx.fill();}
else{ctx.globalAlpha=Math.min(1,p.l*1.6);ctx.fillStyle=p.c;
ctx.save();ctx.translate(p.x,p.y);ctx.rotate(p.ro);
ctx.beginPath();ctx.ellipse(0,0,5,2.1,0,0,TAU);ctx.fill();ctx.restore();}}
ctx.globalAlpha=1;}
function addStamp(txt,size,col,y){STAMPS.push({txt,size,col,y:y||268,t:0});}
function drawStamps(dt){for(let i=STAMPS.length-1;i>=0;i--){const s=STAMPS[i];s.t+=dt;
if(s.t>1.15){STAMPS.splice(i,1);continue;}
const sc=s.t<0.16?E.back(s.t/0.16):1;
const al=s.t<0.7?1:Math.max(0,1-(s.t-0.7)/0.45);
ctx.save();ctx.translate(480,s.y);ctx.rotate(-0.05);ctx.scale(sc,sc);ctx.globalAlpha=al;
ctx.font=s.size+'px Impact,"Arial Black",sans-serif';ctx.textAlign='center';
ctx.fillStyle=C.ink;ctx.fillText(s.txt,3,3);
ctx.fillStyle=s.col;ctx.fillText(s.txt,0,0);ctx.restore();}
ctx.globalAlpha=1;}
function fx(kind,x,y){
if(kind==='land'){dust(x,y,7);sfx('land');sqImp=0.22;shakeA=Math.max(shakeA,2);}
else if(kind==='tick'){dust(x,y,3);sfx('tick');shakeA=Math.max(shakeA,1.2);}
else if(kind==='stab'){sfx('stab');shakeA=Math.max(shakeA,4);specEx=1;
if(DIR.finishA)feathers(x,y,12);}}
// ---------- the move set ----------
function topMod(l,u,p,st){const b=Math.pow(Math.max(0,Math.sin(l*Math.PI)),1.5);
p[I.nod]+=b*0.45;p[I.headF]+=b*3.5;p[I.rot]+=0.03*Math.sin(l*3.14);p[I.squash]*=1-0.02*b;}
function godMod(l,u,p,st){if(l>=1.9&&!st.land){st.land=1;fx('land',AX,FLOOR-6);}}
function getMod(l,u,p,st){if(l>=1.55&&!st.sh){st.sh=1;feathers(AX,FLOOR-70,5);sfx('tick');}}
function sixMod(l,u,p,st){
p[I.x]+=Math.sin(u*TAU)*9;p[I.rot]+=Math.sin(u*TAU*2)*0.04;
const b=Math.max(0,Math.sin(l*Math.PI));p[I.nod]+=b*0.2;p[I.headF]+=b*2;
if(l>=0.15&&!st.k1){st.k1=1;fx('tick',AX+40,FLOOR-8);}
if(l>=2.15&&!st.k2){st.k2=1;fx('tick',AX+40,FLOOR-8);}
if(l>=4.95&&!st.l2){st.l2=1;fx('land',AX,FLOOR-8);}}
function bfMod(l,u,p,st){
if(l>=0.9&&!st.land){st.land=1;fx('land',AX+p[I.x],FLOOR-8);}
if(l>=1.05&&!st.st){st.st=1;fx('stab',AX,FLOOR-14);}
if(l>1&&l<5.2){p[I.squash]*=1+0.035*Math.sin(l*2.1);p[I.nod]+=0.05*Math.sin(l*1.3);p[I.y]+=Math.sin(l*2.1)*1.2;}
if(l>=5.85&&!st.out){st.out=1;fx('tick',AX,FLOOR-6);}}
function wmMod(l,u,p,st){
if(l<2)return;
if(l<6.5){
const su=ss(l,2,6.4),phi=TAU*3*su;
const tilt=0.62*Math.sin(phi),open=0.55+0.22*Math.sin(phi*2+0.6);
const aN=tilt-open*0.55,aF=tilt+open*0.55+0.12;
p[I.rot]=-1.5+0.3*Math.sin(phi+0.7);
p[I.y]=FLOOR-14+5*Math.abs(Math.sin(phi));
p[I.curl]=0.1;
p[I.lLh]=aN-p[I.rot];p[I.lLk]=0.3;p[I.lLf]=-0.35;
p[I.lRh]=aF-p[I.rot];p[I.lRk]=0.25;p[I.lRf]=-0.3;
p[I.swap]=Math.sin(phi)>0?1:0;
const push=ss(Math.sin(phi+0.6),0.25,0.85),push2=ss(Math.sin(phi-0.6+Math.PI),0.25,0.85);
p[I.wLs]=lerp(3.0,2.25,push);p[I.wLe]=lerp(2.0,0.15,push);
p[I.wRs]=lerp(3.2,2.4,push2);p[I.wRe]=lerp(2.1,0.2,push2);
p[I.nod]=-0.3+0.15*Math.sin(phi);
p[I.smear]=Math.sin(Math.min(1,(l-2)/0.5)*1.5708)*Math.sin(Math.min(1,(6.5-l)/0.5)*1.5708);
const half=Math.floor(phi/Math.PI);
if(st.h===undefined)st.h=-1;
if(half>st.h){st.h=half;fx('tick',AX,FLOOR-6);}
}else{
const fr=ss(l,6.5,7.1);
p[I.rot]=lerp(-1.31,-1.5,fr);p[I.y]=lerp(FLOOR-14,FLOOR-16,fr);p[I.curl]=0.1;p[I.nod]=-0.3;
p[I.lLh]=lerp(-0.37,-0.6,fr)-p[I.rot];p[I.lRh]=lerp(0.49,0.55,fr)-p[I.rot];
p[I.lLk]=lerp(0.3,0.35,fr);p[I.lRk]=lerp(0.25,0.3,fr);p[I.lLf]=-0.35;p[I.lRf]=-0.3;
p[I.wLs]=lerp(3.0,2.6,fr);p[I.wLe]=lerp(2.0,0.5,fr);
p[I.wRs]=lerp(3.2,2.9,fr);p[I.wRe]=lerp(2.1,0.45,fr);
p[I.smear]=0;p[I.swap]=0;
p[I.squash]*=1+0.03*Math.sin(l*3);
if(!st.fz){st.fz=1;fx('stab',AX,FLOOR-10);}}}
function bsMod(l,u,p,st){
if(l>=1.2&&l<5.5){
const su=ss(l,1.2,5.3),sa=-TAU*3*su;
p[I.spin]=1;p[I.spinA]=sa;
const q=p.slice();q[I.spin]=0;const fk=fkPoints(q);
p[I.scx]=lerp(fk.pelvis[0],fk.chest[0],0.55);p[I.scy]=lerp(fk.pelvis[1],fk.chest[1],0.55);
p[I.lLk]=2.3+0.3*Math.sin(sa*2);p[I.lRk]=2.2+0.3*Math.sin(sa*2+1);
p[I.smear]=Math.sin(Math.min(1,(l-1.2)/0.4)*1.5708)*Math.sin(Math.min(1,(5.5-l)/0.4)*1.5708);
const rev=Math.floor(-sa/TAU);
if(st.r===undefined)st.r=-1;
if(rev>st.r){st.r=rev;fx('tick',p[I.scx],FLOOR-6);}
}else{p[I.spin]=0;p[I.smear]=0;}
if(l>=5.5&&!st.land){st.land=1;fx('land',AX+p[I.x],FLOOR-8);}}
function hsMod(l,u,p,st){
if(l>=2&&l<6){
const su=ss(l,2,5.8),phi=TAU*3*su;
const tilt=0.6*Math.sin(phi);
const aN=0.35+tilt,aF=-0.35+tilt;
p[I.rot]=3.08+0.18*Math.sin(phi*2+1);
p[I.lLh]=aN-p[I.rot];p[I.lRh]=aF-p[I.rot];
p[I.lLk]=p[I.lRk]=0.25;p[I.lLf]=p[I.lRf]=-0.4;
p[I.wLe]=0.35+0.1*Math.sin(phi*3);p[I.wRe]=0.3+0.1*Math.sin(phi*3+2);
p[I.nod]=0.1+0.08*Math.sin(phi);
p[I.swap]=Math.cos(phi)>0?1:0;
p[I.smear]=Math.sin(Math.min(1,(l-2)/0.5)*1.5708)*Math.sin(Math.min(1,(6-l)/0.5)*1.5708)*0.9;
const half=Math.floor(phi/Math.PI);
if(st.h===undefined)st.h=-1;
if(half>st.h){st.h=half;fx('tick',AX,FLOOR-4);}
}else if(l<7){p[I.smear]=0;
if(l>=6){p[I.rot]=3.08;p[I.nod]=0.1;
p[I.lLh]=0.55-p[I.rot];p[I.lRh]=-0.55-p[I.rot];
p[I.lLk]=p[I.lRk]=0.3;p[I.lLf]=p[I.lRf]=-0.4;
p[I.squash]*=1+0.02*Math.sin(l*8);
if(!st.fz){st.fz=1;fx('stab',AX,FLOOR-8);}}
}else p[I.smear]=0;
if(l>=7.8&&!st.land){st.land=1;fx('land',AX+p[I.x],FLOOR-10);}}
const MOVES={};
MOVES.toprock=mkMove('toprock','TOPROCK',{},[
{d:1,e:E.out,p:{y:FLOOR-37,squash:0.96,wLs:1.9,wLe:0.7,wRs:3.5,wRe:2.3,lLh:2.75,lLk:0.55,lLf:-1.8}},
{d:1,e:E.inout,p:{y:FLOOR-39,rot:0.14,lLh:2.25,lLk:0.45,lLf:-1.5,lRh:3.5,lRk:0.35,lRf:-2.0,wLs:3.4,wLe:2.2,wRs:1.95,wRe:0.75}},
{d:1,e:E.out,p:{y:FLOOR-41,rot:0.08,lLh:2.9,lLk:0.5,lLf:-1.9,lRh:2.95,lRk:0.45,lRf:-1.85,wLs:2.6,wLe:1.4,wRs:2.7,wRe:1.5}},
{d:1,e:E.inout,p:{y:FLOOR-38,rot:0.05,lLh:3.15,lLk:0.5,lLf:-2.0,lRh:2.55,lRk:0.6,lRf:-1.75,wLs:2.2,wLe:1.2,wRs:2.9,wRe:1.7}},
{d:1,e:E.out,p:{y:FLOOR-37,squash:0.96,wLs:3.5,wLe:2.3,wRs:1.9,wRe:0.7,lRh:2.75,lRk:0.55,lRf:-1.8}},
{d:1,e:E.inout,p:{y:FLOOR-39,rot:0.02,lRh:2.25,lRk:0.45,lRf:-1.5,lLh:3.5,lLk:0.35,lLf:-2.0,wRs:3.4,wRe:2.2,wLs:1.95,wLe:0.75}},
{d:1,e:E.out,p:{y:FLOOR-41,rot:0.08,lLh:2.9,lLk:0.5,lLf:-1.9,lRh:2.95,lRk:0.45,lRf:-1.85,wLs:2.6,wLe:1.4,wRs:2.7,wRe:1.5}},
{d:1,e:E.inout,p:STAND_P}],topMod,{sz:0.75});
MOVES.godown=mkMove('godown','DROP',{},[
{d:1,e:E.inout,p:{y:FLOOR-42,rot:0.65,curl:0.35,nod:0.2,wLs:1.6,wLe:0.6,wRs:3.4,wRe:2.3,lLh:2.4,lLk:0.9,lLf:-1.8,lRh:3.1,lRk:0.7,lRf:-1.9}},
{d:1,e:E.inn,p:CROUCH_P}],godMod,{sz:0.75,floor:true});
MOVES.getup=mkMove('getup','',CROUCH_P,[
{d:1,e:E.back,p:{y:FLOOR-44,rot:0.35,curl:0.2,nod:0.1,wLs:2.6,wLe:1.6,wRs:2.8,wRe:1.7,lLh:2.6,lLk:0.5,lLf:-1.8,lRh:2.7,lRk:0.45,lRf:-1.8}},
{d:1,e:E.out,p:STAND_P}],getMod,{floor:true});
MOVES.sixstep=mkMove('sixstep','SIX-STEP',CROUCH_P,[
{d:1,e:E.outX,p:{y:FLOOR-31,rot:1.0,lLh:0.7,lLk:0.1,lLf:-0.5,lRh:1.35,lRk:1.7,lRf:-2.0,wLs:-0.35,wLe:0.6,wRs:2.0,wRe:0.12,nod:-0.1}},
{d:1,e:E.inout,p:{y:FLOOR-35,rot:1.15,lLh:1.5,lLk:1.25,lLf:-1.95,wLs:2.0,wLe:0.12}},
{d:1,e:E.outX,p:{y:FLOOR-31,rot:1.25,lRh:0.9,lRk:0.12,lRf:-0.6,lLh:1.4,lLk:1.6,lLf:-2.0,wRs:-0.2,wRe:0.6,wLs:2.0,wLe:0.1}},
{d:1,e:E.inout,p:{y:FLOOR-35,rot:1.1,lRh:1.5,lRk:1.3,lRf:-2.0,wRs:2.0,wRe:0.12}},
{d:1,e:E.back,p:{y:FLOOR-40,rot:1.05,squash:0.92,lLh:1.2,lLk:2.0,lLf:-1.6,lRh:1.3,lRk:1.95,lRf:-1.65,wLs:2.05,wLe:0.1,wRs:2.1,wRe:0.12,nod:-0.3}},
{d:1,e:E.out,p:CROUCH_P}],sixMod,{floor:true});
MOVES.babyfreeze=mkMove('babyfreeze','BABY FREEZE',CROUCH_P,[
{d:1,e:E.outX,p:{y:FLOOR-58,rot:2.2,curl:0.5,nod:0.3,tail:3.5,x:0,headF:0,squash:1,
wLs:0.94,wLe:-1.5,wRs:2.8,wRe:1.6,lLh:-1.05,lLk:0.85,lLf:-1.0,lRh:-0.75,lRk:1.0,lRf:-0.9}},
{d:3.5,e:E.lin,p:{}},
{d:0.5,e:E.lin,p:{}},
{d:1,e:E.inout,p:CROUCH_P}],bfMod,{floor:true,big:true});
MOVES.windmill=mkMove('windmill','WINDMILL',CROUCH_P,[
{d:2,e:E.inout,p:{y:FLOOR-12,rot:-1.5,curl:0.1,nod:-0.35,tail:2.95,x:0,headF:0,squash:1,
wLs:3.0,wLe:2.0,wRs:3.2,wRe:2.1,lLh:1.2,lLk:0.3,lLf:-0.4,lRh:1.5,lRk:0.25,lRf:-0.35}},
{d:6,e:E.lin,p:{}}],wmMod,{floor:true,big:true});
MOVES.backspin=mkMove('backspin','BACKSPIN',CROUCH_P,[
{d:1.2,e:E.inn,p:{y:FLOOR-24,rot:-1.15,curl:1.55,nod:0.85,tail:3.4,x:0,headF:0,squash:1,
wLs:2.75,wLe:1.95,wRs:2.95,wRe:2.05,lLh:1.7,lLk:2.3,lLf:-1.7,lRh:2.3,lRk:2.2,lRf:-1.75}},
{d:4.3,e:E.lin,p:{}},
{d:0.5,e:E.out,p:CROUCH_P}],bsMod,{floor:true,big:true});
MOVES.headspin=mkMove('headspin','HEADSPIN',CROUCH_P,[
{d:1,e:E.inn,p:{y:FLOOR-26,rot:1.75,curl:0.3,nod:0.35,wLs:1.85,wLe:0.25,wRs:2.0,wRe:0.3,lLh:1.3,lLk:1.3,lLf:-2.0,lRh:1.5,lRk:1.2,lRf:-1.9}},
{d:1,e:E.inout,p:{y:FLOOR-64,rot:3.08,curl:0.05,nod:0.1,tail:2.9,x:0,headF:0,squash:1,
wLs:2.9,wLe:0.35,wRs:3.15,wRe:0.3,lLh:-2.79,lLk:0.25,lLf:-0.4,lRh:-3.49,lRk:0.25,lRf:-0.4}},
{d:4,e:E.lin,p:{}},
{d:1,e:E.lin,p:{}},
{d:1,e:E.inn,p:CROUCH_P}],hsMod,{floor:true,big:true,sz:1.3});
// ---------- the director (choreographer) ----------
const DIR={cur:null,start:0,st:{},queue:[],plan:null,pi:0,pqSet:null,hype:0,finishP:false,finishA:false,live:null,tCount:0};
const ROUTINES=[
['godown','sixstep','babyfreeze','getup'],
['godown','sixstep','sixstep','getup'],
['godown','babyfreeze','sixstep','getup'],
['godown','backspin','sixstep','getup'],
['godown','windmill','getup'],
['godown','sixstep','headspin','getup'],
['godown','backspin','windmill','getup'],
['godown','headspin','windmill','getup']];
const cells=[...document.querySelectorAll('#cells span')],hstate=document.getElementById('hstate');
function hypeUI(){cells.forEach((c,i)=>c.classList.toggle('on',i<DIR.hype));
hstate.classList.toggle('ready',DIR.finishP);
hstate.textContent=DIR.finishP?'FINISH READY':'';}
function setMove(id,at,fromQueue){const m=MOVES[id];
if(DIR.live)m.pre[0].from=DIR.live.slice();
DIR.cur=m;DIR.start=at;DIR.st={};
const showLb=m.label&&!(id==='toprock'&&++DIR.tCount>1);
if(showLb)addStamp(m.label,Math.round(30*m.sz),m.big?C.orange:C.cream,m.big?235:268);
if(fromQueue){DIR.hype=Math.min(6,DIR.hype+1);
if(DIR.hype>=6)DIR.finishP=true;hypeUI();}}
function next(beats){const last=DIR.cur.id;
if(DIR.finishP){DIR.finishP=false;DIR.hype=0;hypeUI();DIR.finishA=true;
DIR.plan=['godown','headspin','windmill','babyfreeze','getup'];DIR.pi=0;DIR.pqSet=null;
addStamp('FINISH!',46,C.orange,235);}
if(DIR.plan){const id=DIR.plan[DIR.pi++];const fq=DIR.pqSet&&DIR.pqSet.has(id);
if(DIR.pi>=DIR.plan.length){DIR.plan=null;DIR.pqSet=null;DIR.finishA=false;}
setMove(id,beats,fq);return;}
if(DIR.queue.length){const q=DIR.queue.shift();
if(MOVES[q].floor&&last==='toprock'){setMove('godown',beats);DIR.queue.unshift(q);}
else setMove(q,beats,true);return;}
if(last==='getup'){dancer.face=Math.random()<0.5?-1:1;setMove('toprock',beats);return;}
if(last==='toprock'){const expo=Math.max(0.7,2.3-DIR.hype*0.27);
let idx=Math.min(ROUTINES.length-1,Math.floor(Math.pow(Math.random(),expo)*ROUTINES.length));
DIR.plan=ROUTINES[idx].slice();DIR.pi=1;setMove(DIR.plan[0],beats);return;}
if(MOVES[last].floor){setMove('getup',beats);return;}
setMove('toprock',beats);}
function director(beats){const m=DIR.cur,local=beats-DIR.start;
if(DIR.queue.length&&m.id==='toprock'&&local>=2&&!DIR.finishP){
const q=DIR.queue.shift();
DIR.plan=MOVES[q].floor?['godown',q]:[q];DIR.pi=0;DIR.pqSet=new Set([q]);}
if(local>=m.beats)next(beats);}
function request(id){
if(DIR.finishP||DIR.finishA)return;
if(DIR.cur&&DIR.cur.id===id)return;
if(DIR.queue.includes(id)||DIR.queue.length>=2)return;
DIR.plan=null;DIR.pqSet=null;DIR.queue.push(id);
const b=document.querySelector('[data-move="'+id+'"]');
if(b){b.classList.add('q');setTimeout(()=>b.classList.remove('q'),350);}}
// ---------- scene ----------
function drawScene(x0,x1,yT,yB){
ctx.fillStyle=C.sky;ctx.fillRect(x0,yT,x1-x0,yB-yT);
for(const s of STARS){ctx.globalAlpha=0.22+0.3*Math.abs(Math.sin(tSec*0.7+s[3]));
ctx.fillStyle=C.star;ctx.fillRect(s[0],s[1],s[2],s[2]);}
ctx.globalAlpha=1;
ctx.fillStyle=C.moon;ctx.beginPath();ctx.arc(770,105,44,0,TAU);ctx.fill();
ctx.fillStyle=C.crater;
for(const c of[[756,92,7],[783,118,5],[769,110,3.5]]){ctx.beginPath();ctx.arc(c[0],c[1],c[2],0,TAU);ctx.fill();}
for(const b of BLD){ctx.fillStyle=b[3]?C.bldg2:C.bldg;ctx.fillRect(b[0],400-b[2],b[1],b[2]+4);}
ctx.fillStyle='#c98d4a';
for(const w of WINS){ctx.globalAlpha=0.5*w[2];ctx.fillRect(w[0],w[1],3,4.5);}
ctx.globalAlpha=1;
// water tower
ctx.fillStyle=C.bldg2;ctx.fillRect(212,150,58,38);
tri([206,152],[241,128],[276,152]);ctx.fillRect(239,118,3,12);
ctx.strokeStyle=C.bldg2;ctx.lineWidth=3;ctx.beginPath();
ctx.moveTo(216,188);ctx.lineTo(206,212);ctx.moveTo(266,188);ctx.lineTo(276,212);
ctx.moveTo(212,200);ctx.lineTo(270,200);ctx.stroke();
// antenna + beacon
ctx.lineWidth=2.5;ctx.beginPath();
ctx.moveTo(742,222);ctx.lineTo(742,148);ctx.moveTo(730,176);ctx.lineTo(754,176);
ctx.moveTo(734,192);ctx.lineTo(750,192);ctx.stroke();
ctx.globalAlpha=Math.sin(tSec*3.1)>0.4?0.9:0.15;ctx.fillStyle='#e04f3a';
ctx.beginPath();ctx.arc(742,145,2.6,0,TAU);ctx.fill();ctx.globalAlpha=1;
// rooftop clutter behind the parapet
ctx.fillStyle=C.bldg2;ctx.fillRect(618,358,52,28);ctx.fillRect(676,368,30,18);
ctx.strokeStyle='#2a2019';ctx.lineWidth=1.5;ctx.beginPath();
for(let i=0;i<4;i++){ctx.moveTo(622,364+i*5);ctx.lineTo(666,364+i*5);}ctx.stroke();
// parapet
ctx.fillStyle=C.parapet;ctx.fillRect(x0,386,x1-x0,18);
ctx.strokeStyle=C.parapetLine;ctx.lineWidth=1.4;
ctx.beginPath();ctx.moveTo(x0,395);ctx.lineTo(x1,395);ctx.stroke();
ctx.beginPath();
for(let bx=Math.floor(x0/26)*26;bx<x1+26;bx+=26){
ctx.moveTo(bx,387);ctx.lineTo(bx,395);ctx.moveTo(bx+13,395);ctx.lineTo(bx+13,403);}
ctx.stroke();
ctx.strokeStyle=C.parapetTop;ctx.lineWidth=2;ctx.beginPath();ctx.moveTo(x0,386.5);ctx.lineTo(x1,386.5);ctx.stroke();
// roof + gravel
ctx.fillStyle=C.roof;ctx.fillRect(x0,404,x1-x0,yB-404);
for(const g of GRAV){ctx.strokeStyle=g[3]?C.gravel:C.gravel2;ctx.lineWidth=1.6;
ctx.beginPath();ctx.moveTo(g[0],g[1]);ctx.lineTo(g[0]+g[2],g[1]+1);ctx.stroke();}
// light pool
ctx.globalAlpha=0.5;ctx.fillStyle=C.pool;
ctx.beginPath();ctx.ellipse(480,466,235,36,0,0,TAU);ctx.fill();
ctx.globalAlpha=0.45;ctx.fillStyle=C.pool2;
ctx.beginPath();ctx.ellipse(480,464,150,22,0,0,TAU);ctx.fill();ctx.globalAlpha=1;}
function drawTitle(){
ctx.save();ctx.translate(36,60);
ctx.font='44px Impact,"Arial Black",sans-serif';ctx.textAlign='left';
const widths=[...'ROCK DOVE'].map(c=>ctx.measureText(c).width);
for(let pass=0;pass<2;pass++){let x=0;
for(let i=0;i<9;i++){const ch2='ROCK DOVE'[i],j=TJIT[i];
ctx.save();ctx.translate(x+j.x,j.y);ctx.rotate(j.r);
ctx.fillStyle=pass?(i<5?C.cream:C.orange):'#0d0a08';
ctx.fillText(ch2,pass?0:2.5,pass?0:2.5);ctx.restore();x+=widths[i]-1.5;}
if(pass===0)var tw=x;}
ctx.fillStyle=C.orange;ctx.fillRect(tw+12,-26,44,18);
ctx.fillStyle=C.ink;ctx.font='bold 11px "Courier New",monospace';ctx.textAlign='center';
ctx.fillText('LIVE',tw+34,-13);ctx.textAlign='left';
ctx.font='10px "Courier New",monospace';
try{ctx.letterSpacing='2px';}catch(e){}
ctx.fillStyle='#8a7a63';ctx.fillText('B-BOY COLUMBA \u00B7 ROOFTOP SESSION \u00B7 ALL VINYL',2,26);
try{ctx.letterSpacing='0px';}catch(e){}
const act=Math.floor(((beats%4)+4)%4);
for(let i=0;i<4;i++){ctx.beginPath();ctx.arc(4+i*15,40,3,0,TAU);
if(i===act){ctx.fillStyle=C.cream;ctx.fill();}
else{ctx.strokeStyle='#6b5638';ctx.lineWidth=1.2;ctx.stroke();}}
ctx.restore();}
// ---------- render + loop ----------
function render(p,dt){
ctx.setTransform(dpr,0,0,dpr,0,0);
ctx.fillStyle=C.sky;ctx.fillRect(0,0,cw,ch);
ctx.lineCap='round';ctx.lineJoin='round';
ctx.translate(vox,voy);ctx.scale(vs,vs);
const x0=-vox/vs,x1=(cw-vox)/vs,yT=-voy/vs,yB=(ch-voy)/vs;
ctx.translate((Math.random()-0.5)*2*shakeA,(Math.random()-0.5)*2*shakeA);
drawScene(x0,x1,yT,yB);
drawSpectator(150,386,0.85,1,0.3);
drawSpectator(806,386,0.85,-1,0.75);
const fk=fkPoints(p);
const hx=p[I.spin]?p[I.scx]:fk.pelvis[0],hy=p[I.spin]?p[I.scy]:fk.pelvis[1];
const sc=clamp(1.35-(FLOOR-hy)/230,0.3,1);
ctx.globalAlpha=0.35*sc;ctx.fillStyle='#0d0a08';
ctx.beginPath();ctx.ellipse(hx,FLOOR+9,38*sc,6.5*sc,0,0,TAU);ctx.fill();ctx.globalAlpha=1;
drawDancer(p);
drawParts();
drawStamps(dt);
drawTitle();
if(!grPat)grPat=ctx.createPattern(gr,'repeat');
ctx.save();ctx.globalCompositeOperation='overlay';ctx.globalAlpha=0.09;
const gx=(Math.floor(tSec*8)%3-1)*3,gy=(Math.floor(tSec*8)%4-1)*3;
ctx.translate(gx,gy);ctx.fillStyle=grPat;
ctx.fillRect(x0-gx-4,yT-gy-4,(x1-x0)+8,(yB-yT)+8);ctx.restore();}
let last=performance.now();
function frame(now){requestAnimationFrame(frame);
let dt=Math.min(0.05,(now-last)/1000);last=now;
const spb=60/bpm;beats+=dt/spb;tSec+=dt;
blink=(beats%3.4)<0.09;
if(!DIR.cur)setMove('toprock',0);
director(beats);
const m=DIR.cur;
let local=clamp(beats-DIR.start,0,m.beats);
const p=evalMove(m,local);
if(m.mod)m.mod(local,local/m.beats,p,DIR.st);
p[I.squash]*=1+0.015*Math.sin(beats*2.3)-sqImp;
headNodS+=(p[I.nod]-headNodS)*Math.min(1,dt*16);p[I.nod]=headNodS;
clampPose(p);
DIR.live=p;
pump();
render(p,dt);
updParts(dt);
shakeA*=Math.exp(-dt*7);sqImp*=Math.exp(-dt*9);specEx=Math.max(0,specEx-dt*1.1);}
requestAnimationFrame(frame);
// ---------- UI ----------
const bpmEl=document.getElementById('bpm'),bpml=document.getElementById('bpml'),sndBtn=document.getElementById('snd');
bpmEl.addEventListener('input',()=>{bpm=+bpmEl.value;bpml.textContent=bpm;});
sndBtn.addEventListener('click',()=>{
if(!AC)initAudio();AC.resume();
soundOn=!soundOn;if(soundOn)nextStep=Math.ceil(beats*4);
sndBtn.innerHTML='SOUND: '+(soundOn?'ON':'OFF')+' <kbd>S</kbd>';});
document.querySelectorAll('.call[data-move]').forEach(b=>
b.addEventListener('click',()=>request(b.dataset.move)));
addEventListener('keydown',e=>{
if(e.key>='1'&&e.key<='5'){document.querySelectorAll('.call[data-move]')[+e.key-1].click();}
else if(e.key==='s'||e.key==='S'){sndBtn.click();}
else if(e.key==='ArrowRight'){e.preventDefault();bpm=Math.min(112,bpm+2);bpmEl.value=bpm;bpml.textContent=bpm;}
else if(e.key==='ArrowLeft'){e.preventDefault();bpm=Math.max(72,bpm-2);bpmEl.value=bpm;bpml.textContent=bpm;}});
</script>
</body>
</html>