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.
Vinny, a street pigeon with a slate-blue coat, cream belly, a flash of teal on the neck and loud orange feet, drawn thick-outlined and flat against a Saul Bass–style dusk of teal bands, one big cream sun and a cut-paper skyline. He dances on a scuffed refrigerator box because that's what b-boys dance on.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta name="theme-color" content="#17130e">
<title>BREAKIN' BIRD — Vinny the Pigeon, live from the gutter</title>
<style>
:root{
--ink:#17130e; --paper:#ffe9b8; --orange:#ff8b2f; --red:#e5482e;
--teal:#2fae9b; --gold:#f5b73c; --deck:#201a12;
}
*{box-sizing:border-box}
html,body{height:100%;margin:0}
body{
background:var(--ink); overflow:hidden; color:var(--paper);
font-family:ui-monospace,Menlo,Consolas,"Courier New",monospace;
}
canvas{position:fixed;inset:0;width:100%;height:100%;display:block;cursor:crosshair}
/* ---- flyer lockup ---- */
.brand{position:fixed;top:14px;left:20px;z-index:5;transform:rotate(-3deg);pointer-events:none;user-select:none}
.brand h1{
margin:0;font-family:Impact,Haettenschweiler,"Franklin Gothic Bold",'Arial Black',sans-serif;
font-weight:900;text-transform:uppercase;letter-spacing:1px;
font-size:clamp(42px,6.5vw,88px);line-height:.84;color:var(--paper);
text-shadow:4px 4px 0 var(--red),8px 8px 0 rgba(0,0,0,.55);
}
.brand .sub{
display:inline-block;margin-top:10px;padding:4px 9px;background:var(--ink);
border:2px solid var(--paper);color:var(--gold);font-size:clamp(9px,1.1vw,12px);
letter-spacing:2px;transform:rotate(1.4deg);
}
.badge{
position:fixed;top:16px;right:18px;width:112px;height:112px;z-index:5;
pointer-events:none;animation:spin 14s linear infinite;
filter:drop-shadow(3px 4px 0 rgba(0,0,0,.45));
}
@keyframes spin{to{transform:rotate(360deg)}}
/* ---- boombox deck ---- */
.deck{
position:fixed;left:0;right:0;bottom:0;z-index:6;display:flex;flex-wrap:wrap;
gap:10px 16px;align-items:center;padding:22px 16px 12px;
background-color:var(--deck);border-top:4px solid var(--gold);
background-image:repeating-linear-gradient(-45deg,var(--gold) 0 10px,transparent 10px 20px);
background-size:100% 8px;background-repeat:no-repeat;background-position:0 4px;
}
.btn{
font:inherit;font-weight:700;text-transform:uppercase;letter-spacing:1.5px;font-size:12px;
background:var(--paper);color:var(--ink);border:3px solid #000;padding:9px 13px;cursor:pointer;
box-shadow:4px 4px 0 var(--red);transition:transform .08s,box-shadow .08s,background .12s;
}
.btn:hover{transform:translate(-2px,-2px);box-shadow:6px 6px 0 var(--red)}
.btn:active{transform:translate(2px,2px);box-shadow:1px 1px 0 var(--red)}
.btn:focus-visible{outline:3px dashed var(--gold);outline-offset:3px}
.btn kbd{background:var(--ink);color:var(--paper);padding:1px 5px;margin-right:6px;font-family:inherit;font-size:10px}
.btn.beat{background:var(--red);color:var(--paper)}
.btn.beat.on{background:var(--teal);color:var(--ink)}
.btn.hype{background:var(--gold)}
.btn.on{background:var(--teal)}
.btn.queued{outline:3px dashed var(--gold);outline-offset:2px}
.moves{display:flex;gap:8px;flex-wrap:wrap}
.fader{display:flex;flex-direction:column;gap:5px;min-width:180px}
.fader label{font-size:10px;letter-spacing:2px;color:var(--gold);text-transform:uppercase;display:flex;align-items:center;gap:8px}
#dot{width:9px;height:9px;background:var(--orange);display:inline-block;border:1px solid #000}
input[type=range]{-webkit-appearance:none;appearance:none;height:24px;background:transparent;cursor:pointer;width:100%}
input[type=range]::-webkit-slider-runnable-track{
height:8px;border:2px solid var(--paper);background:
repeating-linear-gradient(90deg,var(--paper) 0 2px,transparent 2px 13px),#3a3226;
}
input[type=range]::-webkit-slider-thumb{
-webkit-appearance:none;width:20px;height:24px;margin-top:-10px;
background:var(--orange);border:3px solid #000;box-shadow:2px 2px 0 rgba(0,0,0,.7);
}
input[type=range]::-moz-range-track{height:8px;border:2px solid var(--paper);background:#3a3226}
input[type=range]::-moz-range-thumb{width:16px;height:20px;border-radius:0;background:var(--orange);border:3px solid #000}
.hypebox{display:flex;align-items:center;gap:12px;margin-left:auto}
#meter{display:flex;gap:3px;padding:6px;background:#141009;border:3px solid #000}
#meter i{width:7px;height:20px;background:#3a3226}
#meter i.lit{background:var(--gold)}
#meter i.lit:nth-child(n+6){background:var(--orange)}
#meter i.lit:nth-child(n+10){background:var(--red)}
.tips{flex-basis:100%;margin:2px 0 0;font-size:10px;letter-spacing:1.5px;text-transform:uppercase;color:#9a8c6c}
@media (max-width:760px){.badge{display:none}.hypebox{margin-left:0}}
@media (prefers-reduced-motion:reduce){.badge{animation:none}}
</style>
</head>
<body>
<canvas id="stage" role="img" aria-label="A pigeon breakdancing on a cardboard mat at dusk"></canvas>
<header class="brand">
<h1>Breakin'<br>Bird</h1>
<span class="sub">VINNY THE PIGEON · LIVE FROM THE GUTTER · ONE CARDBOARD, NO CHOREO</span>
</header>
<svg class="badge" viewBox="0 0 120 120" aria-hidden="true">
<circle cx="60" cy="60" r="57" fill="#e5482e" stroke="#17130e" stroke-width="5"/>
<circle cx="60" cy="60" r="29" fill="none" stroke="#17130e" stroke-width="2" stroke-dasharray="4 5"/>
<defs><path id="tp" d="M60 60 m -43 0 a43 43 0 1 1 86 0 a43 43 0 1 1 -86 0"/></defs>
<text font-family="ui-monospace,Menlo,monospace" font-size="10" letter-spacing="2" font-weight="700" fill="#ffe9b8">
<textPath href="#tp">FRESH FEATHERS ★ NO NEST ★ BREAK ZONE ★</textPath>
</text>
<text x="60" y="58" text-anchor="middle" font-family="Impact,'Arial Black',sans-serif" font-size="21" fill="#ffe9b8">VINNY</text>
<text x="60" y="73" text-anchor="middle" font-family="ui-monospace,monospace" font-size="8.5" letter-spacing="2" font-weight="700" fill="#17130e">LIVE · 45 RPM</text>
</svg>
<nav class="deck" aria-label="dance controls">
<button class="btn beat" id="beatBtn">▶ Drop the beat</button>
<div class="fader">
<label for="bpmRange">Tempo <b id="bpmVal">104</b> BPM <i id="dot"></i></label>
<input type="range" id="bpmRange" min="88" max="132" step="1" value="104">
</div>
<div class="moves">
<button class="btn" data-move="toprock"><kbd>1</kbd>Toprock</button>
<button class="btn" data-move="sixstep"><kbd>2</kbd>Six-step</button>
<button class="btn" data-move="windmill"><kbd>3</kbd>Windmill</button>
<button class="btn" data-move="freeze"><kbd>4</kbd>Freeze</button>
</div>
<div class="hypebox">
<button class="btn hype" id="hypeBtn">Hype ⚡</button>
<div id="meter" title="crowd hype" aria-hidden="true"></div>
</div>
<p class="tips">click the bird to hype · 1–4 call a move · space = hype · B = beat · ↑↓ tempo</p>
</nav>
<script>
'use strict';
/* ================= helpers ================= */
const rand=(a,b)=>a+Math.random()*(b-a);
const choice=a=>a[Math.floor(Math.random()*a.length)];
const clamp=(v,a,b)=>v<a?a:v>b?b:v;
const E={
lin:u=>u,
io:u=>u*u*(3-2*u),
i:u=>u*u,
o:u=>1-(1-u)*(1-u),
sine:u=>.5-.5*Math.cos(u*Math.PI),
back:u=>{u-=1;return 1+u*u*(2.7*u+1.7);},
settle:u=>1-Math.exp(-5*u)*Math.cos(9*u)
};
const INK='#17130e', RM=matchMedia('(prefers-reduced-motion: reduce)').matches;
/* ================= canvas ================= */
const cv=document.getElementById('stage'), ctx=cv.getContext('2d');
let W=0,H=0,S=1,CX=0,GY=0,HZ=0,vign=null,buildings=[];
const grain=document.createElement('canvas'); grain.width=grain.height=180;
{ const g=grain.getContext('2d');
for(let i=0;i<2300;i++){
g.fillStyle=Math.random()<.5?'rgba(0,0,0,'+Math.random()*.5+')':'rgba(255,235,190,'+Math.random()*.4+')';
g.fillRect(Math.random()*180,Math.random()*180,1.4,1.4);
}
}
let grainPat=null;
function resize(){
const dpr=Math.min(2,window.devicePixelRatio||1);
W=innerWidth;H=innerHeight;
cv.width=W*dpr;cv.height=H*dpr;ctx.setTransform(dpr,0,0,dpr,0,0);
S=clamp(Math.min(W/1050,(H-140)/600),.5,1.6);
CX=W*.45; GY=H-170; HZ=Math.max(90,GY-230*S);
if(!grainPat)grainPat=ctx.createPattern(grain,'repeat');
vign=ctx.createRadialGradient(W/2,H*.45,H*.3,W/2,H*.5,Math.max(W,H)*.75);
vign.addColorStop(0,'rgba(0,0,0,0)');vign.addColorStop(1,'rgba(15,8,4,.32)');
buildings=[];let x=-30;
while(x<W+60){
const wd=(34+Math.random()*70)*Math.max(.7,S), ht=(50+Math.random()*150)*S, wins=[];
const cols=Math.floor(wd/16),rows=Math.floor(ht/24);
for(let r=0;r<rows;r++)for(let c=0;c<cols;c++)
if(Math.random()<.26)wins.push([x+6+c*14,HZ-ht+9+r*22]);
buildings.push({x,wd,ht,wins,ant:ht>140*S&&Math.random()<.5});
x+=wd+2+Math.random()*10;
}
}
addEventListener('resize',resize);resize();
/* ================= the rig ================= */
function pose(o){
const p={x:0,h:46,rot:0,sq:0,tail:.15,crest:0,beak:0,bow:9,ba:-.06,
f0x:9,f0y:46,f1x:-7,f1y:46, w0x:9,w0y:22,w1x:-5,w1y:23, fl0:.25,fl1:.22,
hdx:34,hdy:-44};
if(o)for(const k in o)p[k]=o[k];
return p;
}
function lerpPose(a,b,u){const o={};for(const k in b){o[k]=(a[k]===undefined)?b[k]:a[k]+(b[k]-a[k])*u;}return o;}
function ik(sx,sy,tx,ty,l1,l2,bend){
let dx=tx-sx,dy=ty-sy,d=Math.hypot(dx,dy);
d=clamp(d,3,l1+l2-.01);
const a=Math.atan2(dy,dx),
c=clamp((l1*l1+d*d-l2*l2)/(2*l1*d),-1,1),
ang=a+bend*Math.acos(c);
return [sx+Math.cos(ang)*l1,sy+Math.sin(ang)*l1];
}
/* ================= move library =================
Limb targets live in the ROOT frame (world-aligned, y down, ground at y=h),
so a planted hand/foot stays planted no matter how the body rotates. */
const MOVES={};
function mv(id,beats,keys,extra){MOVES[id]=Object.assign({id,beats,keys},extra);}
mv('toprockA',4,[
{b:0,p:pose({tail:.2}),e:'io'},
{b:.5,p:pose({h:43.5,x:1,hdx:33,hdy:-41,f0x:12,f1x:-9,fl0:.35,w0x:12,w0y:20,tail:.25}),e:'io'},
{b:1,p:pose({h:46,x:2,f0x:17,f0y:38,hdx:36,hdy:-46,crest:.5,beak:.2,fl0:.6,w0x:17,w0y:13,tail:.35}),e:'o',fx:{word:['POP!','UH!']}},
{b:1.5,p:pose({h:43,x:1,f0x:10,f0y:46,hdx:33,hdy:-42,fl0:.3,w0x:10,w0y:22}),e:'io'},
{b:2,p:pose({h:46,x:-2,f1x:-15,f1y:35,hdx:31,hdy:-45,crest:.4,fl1:.6,w1x:-13,w1y:13,tail:.4}),e:'o'},
{b:2.5,p:pose({h:43,x:-1,f1x:-8,f1y:46,hdx:33,hdy:-42,fl1:.3,w1x:-6,w1y:23}),e:'io'},
{b:3,p:pose({h:49,f0y:42,f1y:42,hdx:35,hdy:-48,tail:.45,fl0:.5,fl1:.5,w0x:15,w0y:12,w1x:-11,w1y:13}),e:'o',fx:{dust:1}},
{b:3.5,p:pose({h:42.5,f0y:46,f1y:46,f0x:8,f1x:-6,hdx:33,hdy:-41,fl0:.25,fl1:.25,w0x:10,w0y:22,w1x:-5,w1y:23,tail:.2}),e:'i'},
{b:4,p:pose({tail:.2}),e:'io'}
]);
mv('toprockB',4,[
{b:0,p:pose({h:44}),e:'io'},
{b:.5,p:pose({h:42,hdx:30,hdy:-40}),e:'io'},
{b:1,p:pose({h:45,w0x:36,w0y:-6,fl0:1,hdx:38,hdy:-42,ba:-.15,crest:.6,f0x:13,tail:.3}),e:'o',fx:{word:['FRESH!','NICE!']}},
{b:1.75,p:pose({h:45,w0x:30,w0y:-2,fl0:.8,hdx:36,hdy:-43}),e:'io'},
{b:2.5,p:pose({h:41,w0x:14,w0y:16,w1x:-14,w1y:12,fl0:.5,fl1:.6,hdx:32,hdy:-39,f0x:12,f1x:-10,tail:.35}),e:'i'},
{b:3,p:pose({h:47,w0x:20,w0y:6,w1x:-18,w1y:4,fl0:.8,fl1:.8,hdx:36,hdy:-47,crest:.7,beak:.4,f0y:43,f1y:43,tail:.5}),e:'o',fx:{dust:1}},
{b:3.5,p:pose({h:42.5,w0x:11,w0y:20,w1x:-6,w1y:22,fl0:.3,fl1:.25,hdx:33,hdy:-42,f0y:46,f1y:46}),e:'i'},
{b:4,p:pose(),e:'io'}
]);
mv('toprockC',2,[
{b:0,p:pose({h:43,hdx:32,hdy:-41}),e:'io'},
{b:.4,p:pose({h:54,f0x:20,f0y:30,f1x:-2,f1y:36,hdx:36,hdy:-50,crest:.7,beak:.3,tail:.55,fl0:.7,fl1:.6,w0x:18,w0y:8,w1x:-16,w1y:10}),e:'o'},
{b:.75,p:pose({h:41,f0x:12,f0y:46,f1x:-6,f1y:46,sq:.45,hdx:31,hdy:-39,fl0:.2,fl1:.2,tail:.2}),e:'i',fx:{dust:2}},
{b:1.2,p:pose({h:46,sq:0,hdx:34,hdy:-44}),e:'io'},
{b:1.4,p:pose({h:43}),e:'io'},
{b:1.75,p:pose({h:53,f1x:-18,f1y:28,f0x:4,f0y:34,hdx:30,hdy:-49,tail:.5,fl0:.6,fl1:.7,w0x:16,w0y:9,w1x:-17,w1y:8}),e:'o'},
{b:2,p:pose({h:42,sq:.4,f0x:9,f0y:46,f1x:-7,f1y:46,hdx:32,hdy:-40}),e:'i',fx:{dust:1}}
]);
mv('drop',2,[
{b:0,p:pose({h:42,rot:.1,hdx:31,hdy:-40,tail:.3}),e:'io'},
{b:.35,p:pose({h:34,rot:.55,f1x:-30,f1y:26,f0x:14,f0y:38,w0x:24,w0y:24,w1x:-2,w1y:26,hdx:38,hdy:-28,tail:.5,fl0:.6}),e:'i'},
{b:.75,p:pose({h:25,rot:.9,w0x:28,w0y:25,w1x:15,w1y:26,f0x:8,f0y:24,f1x:-24,f1y:22,hdx:32,hdy:-16,sq:.3,tail:.6,fl0:.85,fl1:.5}),e:'o',
fx:{dust:3,shake:4,word:['DOWN!','HIT THE FLOOR!',"LET'S GO!"]}},
{b:1.2,p:pose({h:24,rot:.95,sq:.1,w0x:28,w0y:24,w1x:15,w1y:25,f0x:8,f0y:24,f1x:-24,f1y:22,hdx:32,hdy:-16,tail:.5,fl0:.7,fl1:.5}),e:'io'},
{b:2,p:pose({h:24,rot:.93,w0x:28,w0y:24,w1x:15,w1y:25,f0x:8,f0y:24,f1x:-24,f1y:22,hdx:32,hdy:-16,tail:.5,fl0:.7,fl1:.5}),e:'io'}
]);
(function(){ /* six-step: weight rocks side to side while feet tie themselves in knots */
const S0={h:24,rot:.95,w0x:28,w0y:24,w1x:15,w1y:25,fl0:.7,fl1:.6,hdx:30,hdy:-14,tail:.45,sq:0,crest:.2,beak:.1,bow:9,ba:-.06,x:0};
const st=[
[0,6,25,-20,23,.95,24],[.5,-2,26,-20,23,.9,23],[1,-14,26,-12,25,1,23.5],
[1.5,-24,24,-10,26,.92,22.5],[2,-30,18,-8,26,.98,23],[2.5,-26,12,-14,25,1.02,22.5],
[3,-14,11,-22,22,.95,23],[3.5,-4,14,-27,19,.9,22.5],[4,-2,22,-29,15,.97,23],
[4.5,-10,25,-24,12,.93,22.5],[5,-18,25,-16,16,.96,23],[5.5,-24,23,-8,20,.93,23.5],
[6,-8,25,-20,24,.95,24]];
const keys=st.map((s,i)=>({b:s[0],
p:pose(Object.assign({},S0,{f0x:s[1],f0y:s[2],f1x:s[3],f1y:s[4],rot:s[5],h:s[6]})),
e:i?'o':'io', fx:i===0?{dust:2,word:['DOWNROCK!','SIX-STEP!']}:null}));
keys.push({b:6.5,p:pose(Object.assign({},S0,{f0x:-8,f0y:25,f1x:-20,f1y:24})),e:'io'});
mv('sixstep',6.5,keys);
})();
(function(){ /* windmill: legs churn on a half-beat grid, two revolutions, kick out to a V */
const WB={h:35,rot:2.74,hdx:-26,hdy:18,w0x:-22,w0y:36,w1x:20,w1y:36,fl0:.85,fl1:.85,tail:.75,sq:0,crest:.3,beak:.2,bow:6,ba:.1,x:0};
const C=12,R=25, keys=[
{b:0,p:pose({h:32,rot:1.1,hdx:26,hdy:-22,f0x:14,f0y:34,f1x:-16,f1y:32,w0x:22,w0y:30,w1x:-10,w1y:30,fl0:.4,fl1:.4,tail:.5,sq:.1,crest:.2,beak:.1,bow:8,ba:-.05}),e:'io'},
{b:.5,p:pose(Object.assign({},WB,{h:36,f0x:16,f0y:-4,f1x:-14,f1y:-2})),e:'o',fx:{dust:3,shake:4,word:['WINDMILL!','SPIN!'],cheer:1}}];
for(let i=0;i<=8;i++){
const th=i*Math.PI/2, alt=i%2===1;
keys.push({b:1+i*.5,p:pose(Object.assign({},WB,{
f0x:Math.cos(th)*R, f0y:-C+Math.sin(th)*R,
f1x:-Math.cos(th)*R,f1y:-C-Math.sin(th)*R,
h:alt?33.5:35, rot:alt?2.66:2.74})), e:'io',
fx:(i===1||i===5)?{dust:1}:null});
}
keys.push({b:5.5,p:pose(Object.assign({},WB,{h:34,rot:2.6,f0x:-12,f0y:-30,f1x:14,f1y:-34,tail:.95,crest:1})),e:'o',
fx:{word:['HOLD IT!','STILL!'],cheer:1}});
keys.push({b:6.3,p:pose(Object.assign({},WB,{h:34,rot:2.6,f0x:-12,f0y:-30,f1x:14,f1y:-34,tail:.95,crest:1})),e:'io'});
mv('windmill',6.5,keys);
})();
mv('freezeA',4.5,[ /* one-wing baby freeze */
{b:0,p:pose({h:38,rot:.6,w0x:24,w0y:34,f0x:8,f0y:38,f1x:-12,f1y:36,hdx:34,hdy:-30,tail:.4}),e:'io'},
{b:.4,p:pose({h:30,rot:1.5,w0x:20,w0y:28,f0x:0,f0y:14,f1x:6,f1y:16,hdx:22,hdy:-12,tail:.6}),e:'i'},
{b:.9,p:pose({h:50,rot:2.45,w0x:14,w0y:50,w1x:-16,w1y:-6,fl1:.9,fl0:.5,f0x:-14,f0y:-26,f1x:10,f1y:-34,hdx:-16,hdy:34,crest:1,beak:.5,tail:.85,sq:-.05}),e:'back',
fx:{word:['HOLD IT!','STILL!','FROZEN!'],cheer:1,shake:2}},
{b:1.2,p:pose({h:49.5,rot:2.5,w0x:14,w0y:50,w1x:-16,w1y:-6,fl1:.9,fl0:.5,f0x:-15,f0y:-27,f1x:9,f1y:-35,hdx:-16,hdy:34,crest:1,beak:.5,tail:.85}),e:'io'},
{b:3.6,p:pose({h:48,rot:2.2,w0x:14,w0y:48,f0x:-10,f0y:-18,f1x:5,f1y:-24,hdx:-12,hdy:30,tail:.6}),e:'i'},
{b:4.1,p:pose({h:34,rot:1.2,f0x:12,f0y:30,f1x:-8,f1y:28,w0x:20,w0y:26,hdx:20,hdy:-8,sq:.25,tail:.4}),e:'i'},
{b:4.5,p:pose({h:31,rot:.5,f0x:10,f0y:31,f1x:-10,f1y:31,sq:.35,hdx:29,hdy:-24,tail:.3,crest:.1}),e:'o',fx:{dust:2}}
],{tremble:[1.2,3.4,.015]});
mv('freezeB',4.5,[ /* the splits on one wingtip */
{b:0,p:pose({h:36,rot:.5,w0x:22,w0y:30,f0x:10,f0y:36,f1x:-8,f1y:36,hdx:33,hdy:-32,tail:.4}),e:'io'},
{b:.5,p:pose({h:34,rot:1.35,w0x:20,w0y:34,w1x:-12,w1y:8,fl1:.8,f0x:34,f0y:-2,f1x:-36,f1y:4,hdx:36,hdy:-8,crest:1,beak:.4,tail:.8,sq:-.05,bow:10}),e:'back',
fx:{word:['SPLIT!','STACKED!'],cheer:1,shake:2}},
{b:.9,p:pose({h:33.5,rot:1.32,w0x:20,w0y:34,w1x:-12,w1y:8,fl1:.8,f0x:35,f0y:-3,f1x:-37,f1y:3,hdx:36,hdy:-8,crest:1,beak:.4,tail:.8,bow:10}),e:'io'},
{b:3.7,p:pose({h:30,rot:.8,f0x:18,f0y:20,f1x:-16,f1y:18,w0x:18,w0y:24,hdx:28,hdy:-16,tail:.5}),e:'i'},
{b:4.2,p:pose({h:32,rot:.35,f0x:10,f0y:32,f1x:-10,f1y:32,sq:.3,hdx:30,hdy:-24,tail:.3}),e:'o'},
{b:4.5,p:pose({h:36,rot:.1,sq:.05,hdx:33,hdy:-32}),e:'io',fx:{dust:1}}
],{tremble:[.9,3.3,.012]});
mv('getup',2,[
{b:0,p:pose({h:30,rot:.55,f0x:10,f0y:30,f1x:-10,f1y:30,hdx:28,hdy:-26,sq:.2,tail:.25}),e:'io'},
{b:.5,p:pose({h:40,rot:.15,f0x:9,f0y:42,f1x:-7,f1y:42,hdx:32,hdy:-38,sq:0}),e:'o'},
{b:1,p:pose({h:47,rot:-.04,f0x:9,f0y:47,f1x:-7,f1y:47,hdx:35,hdy:-46,crest:.4,tail:.35}),e:'settle'},
{b:1.3,p:pose({h:45.5,rot:.02,f0x:9,f0y:46,f1x:-7,f1y:46,hdx:34,hdy:-44}),e:'io',fx:{dust:1}},
{b:1.6,p:pose({h:45.5,sq:.18,hdx:33,hdy:-42,crest:.1}),e:'io',fx:{feathers:2}},
{b:2,p:pose({sq:0,hdx:34,hdy:-44}),e:'io'}
]);
mv('breath',2,[
{b:0,p:pose({h:45.5}),e:'io'},
{b:1,p:pose({h:47,hdx:35,hdy:-45.5,tail:.3,fl0:.3,w0x:11,w0y:21}),e:'sine'},
{b:2,p:pose({h:45.5,hdx:34,hdy:-44,tail:.2}),e:'io'}
]);
function evalMove(m,lb){
const ks=m.keys;
if(lb<=ks[0].b)return pose(ks[0].p);
let i=0;
while(i<ks.length-2&&lb>=ks[i+1].b)i++;
const a=ks[i],c=ks[i+1],
u=c.b>a.b?clamp((lb-a.b)/(c.b-a.b),0,1):1;
return lerpPose(a.p,c.p,(E[c.e]||E.io)(u));
}
/* ================= choreographer ================= */
let bpm=104,songBeat=0,lastT=performance.now()/1000;
let cur,queue=[],pending=null,lastEndPose=pose(),prevLb=-1,currentPose=pose();
let lastTop='',lastFloor='',freezeFlip=false;
let hype=0,excite=0,shake=0,lastAuto=-99,introDone=false;
function inst(id){return {def:MOVES[id],id,startBeat:0,entry:null};}
function pickTop(){
const o=['toprockA','toprockB','toprockC'].filter(t=>t!==lastTop);
return lastTop=choice(o);
}
function pickFloor(){
let pick;const r=Math.random(),wWind=.22+hype*.4;
if(r<.38)pick='sixstep';else if(r<.38+.3)pick=freezeFlip?'freezeB':'freezeA';else pick='windmill';
if(pick===lastFloor)pick=pick==='sixstep'?'windmill':'sixstep';
if(pick.startsWith('freeze'))freezeFlip=!freezeFlip;
return lastFloor=pick;
}
function appendCycle(){
queue.push(inst(pickTop()));
if(Math.random()<.65)queue.push(inst(pickTop()));
const f=pickFloor();
if(f==='sixstep'||f==='windmill')queue.push(inst('drop'));
queue.push(inst(f),inst('getup'));
if(Math.random()<.8)queue.push(inst('breath'));
}
function planPending(){
const id=pending;pending=null;
const floor=['sixstep','windmill','freezeA','freezeB'],low=lastEndPose.h<34;
if(floor.includes(id)){
if(!low&&(id==='sixstep'||id==='windmill'))queue.push(inst('drop'));
queue.push(inst(id),inst('getup'),inst('breath'));
}else{
if(low)queue.push(inst('getup'));
queue.push(inst(id));
}
}
function refill(){while(queue.length<4){pending?planPending():appendCycle();}}
function callMove(m,user){
if(m==='freeze'){freezeFlip=!freezeFlip;m=freezeFlip?'freezeA':'freezeB';}
if(m==='toprock')m=pickTop();
pending=m;
queue.length=Math.min(queue.length,1); /* bird answers within one move */
refill();
if(user)addWord(['GOT IT!','ON IT!','WATCH THIS!'],birdSX(),birdSY()-110*S,20,'#ffe9b8');
}
cur=inst('toprockA');cur.startBeat=0;cur.entry=pose();
queue=[inst('toprockB')];refill();
function updateChoreo(now){
let lb=songBeat-cur.startBeat,changed=false,guard=0;
while(lb>=cur.def.beats&&guard++<10){
lastEndPose=evalMove(cur,cur.def.beats);
const endAt=cur.startBeat+cur.def.beats;
if(queue.length<2)refill();
cur=queue.shift();cur.startBeat=endAt;cur.entry=lastEndPose;
lb=songBeat-cur.startBeat;changed=true;refill();
}
if(lb>=cur.def.beats){cur.startBeat=songBeat;lb=0;}
if(changed)prevLb=-.001;
if(lb<2.5)for(const k of cur.def.keys)
if(k.fx&&prevLb<k.b&&lb>=k.b)fireFx(k.fx);
prevLb=lb;
let p=evalMove(cur,lb);
if(lb<.3&&cur.entry)p=lerpPose(cur.entry,p,E.o(clamp(lb/.3,0,1)));
const tr=cur.def.tremble;
if(tr&&lb>tr[0]&&lb<tr[1]){
const k=Math.sin((lb-tr[0])/(tr[1]-tr[0])*Math.PI);
p.rot+=Math.sin(now*42)*tr[2]*k; p.hdx+=Math.sin(now*37)*1.2*k;
}
currentPose=p;
}
/* ================= fx ================= */
const dust=[],feathers=[],words=[],flyers=[],visKicks=[];
function birdSX(){return CX+currentPose.x*S;}
function birdSY(){return GY-currentPose.h*S;}
function spawnDust(wx,n){for(let i=0;i<n;i++)dust.push({x:wx+rand(-10,10),y:-2,vx:rand(-45,45),vy:rand(-75,-15),r:rand(2.5,6),t:0,life:rand(.35,.6)});}
function spawnFeathers(n){for(let i=0;i<n;i++)feathers.push({x:currentPose.x+rand(-14,14),y:-currentPose.h-20,vx:rand(-12,12),vy:rand(6,18),rot:rand(0,6),vr:rand(-3,3),t:0,life:1.7});}
function addWord(txts,x,y,size,col){words.push({txt:choice(txts),x:x+rand(-16,16),y,vy:30,rot:rand(-.14,.14),t:0,life:.8,size,col:col||choice(['#ffe9b8','#f5b73c','#ff8b2f'])});}
function fireFx(f){
if(f.dust)spawnDust(currentPose.x,f.dust);
if(f.shake&&!RM)shake=Math.max(shake,f.shake);
if(f.word&&Math.random()<.75)addWord(f.word,birdSX(),birdSY()-130*S,30);
if(f.cheer){excite=1;hype=Math.min(1,hype+.06);if(audioOn)cheer(actx.currentTime+.02,.45);}
if(f.feathers)spawnFeathers(f.feathers);
}
function hypePump(k){
hype=Math.min(1,hype+.12*k+.03);excite=Math.min(1,excite+.5*k);
addWord(['UH!','YEAH!','HYPE!','OOOH!'],birdSX(),birdSY()-120*S,k>.7?26:20);
if(audioOn&&k>.7)cheer(actx.currentTime+.02,.3);
}
/* ================= audio (all synthesized) ================= */
let actx=null,master=null,noiseBuf=null,audioOn=false,nextStepBeat=0;
const KICK=new Set([0,10,16,20,22]),SNARE=new Set([4,12,28]),
GHOST=new Set([15,31]),OHAT=new Set([14,30]),
BASS=[[0,1.6,33],[6,.7,33],[8,1.6,36],[14,1.3,40],[16,1.6,31],[22,.7,31],[24,1.5,33],[28,1.4,36]];
const midi=m=>440*Math.pow(2,(m-69)/12);
function initAudio(){
actx=new (window.AudioContext||window.webkitAudioContext)();
master=actx.createGain();master.gain.value=.85;
const comp=actx.createDynamicsCompressor();
master.connect(comp);comp.connect(actx.destination);
noiseBuf=actx.createBuffer(1,actx.sampleRate,actx.sampleRate);
const d=noiseBuf.getChannelData(0);
for(let i=0;i<d.length;i++)d[i]=Math.random()*2-1;
}
function timeOfBeat(b){return actx.currentTime+(b-songBeat)*60/bpm;}
function kick(t){
const o=actx.createOscillator(),g=actx.createGain();
o.frequency.setValueAtTime(150,t);o.frequency.exponentialRampToValueAtTime(44,t+.11);
g.gain.setValueAtTime(1,t);g.gain.exponentialRampToValueAtTime(.001,t+.22);
o.connect(g);g.connect(master);o.start(t);o.stop(t+.24);
}
function snare(t,v){
const s=actx.createBufferSource();s.buffer=noiseBuf;
const f=actx.createBiquadFilter();f.type='bandpass';f.frequency.value=1800;f.Q.value=.9;
const g=actx.createGain();g.gain.setValueAtTime(v*.9,t);g.gain.exponentialRampToValueAtTime(.001,t+.16);
s.connect(f);f.connect(g);g.connect(master);s.start(t);s.stop(t+.18);
const o=actx.createOscillator(),g2=actx.createGain();o.type='triangle';o.frequency.value=185;
g2.gain.setValueAtTime(v*.5,t);g2.gain.exponentialRampToValueAtTime(.001,t+.07);
o.connect(g2);g2.connect(master);o.start(t);o.stop(t+.08);
}
function hat(t,v,dur){
const s=actx.createBufferSource();s.buffer=noiseBuf;
const f=actx.createBiquadFilter();f.type='highpass';f.frequency.value=7000;
const g=actx.createGain();g.gain.setValueAtTime(v,t);g.gain.exponentialRampToValueAtTime(.001,t+dur);
s.connect(f);f.connect(g);g.connect(master);s.start(t);s.stop(t+dur+.02);
}
function bass(t,dBeats,m){
const d=dBeats*60/bpm,o=actx.createOscillator(),f=actx.createBiquadFilter(),g=actx.createGain();
o.type='sawtooth';o.frequency.value=midi(m);
f.type='lowpass';f.frequency.setValueAtTime(420,t);f.frequency.exponentialRampToValueAtTime(180,t+d);
g.gain.setValueAtTime(0,t);g.gain.linearRampToValueAtTime(.42,t+.02);
g.gain.setValueAtTime(.42,t+d*.7);g.gain.linearRampToValueAtTime(.001,t+d);
o.connect(f);f.connect(g);g.connect(master);o.start(t);o.stop(t+d+.05);
}
function cheer(t,v){
const s=actx.createBufferSource();s.buffer=noiseBuf;
const f=actx.createBiquadFilter();f.type='bandpass';f.Q.value=1.2;
f.frequency.setValueAtTime(420,t);f.frequency.linearRampToValueAtTime(1600,t+.25);
const g=actx.createGain();g.gain.setValueAtTime(0,t);
g.gain.linearRampToValueAtTime(v,t+.1);g.gain.linearRampToValueAtTime(0,t+.65);
s.connect(f);f.connect(g);g.connect(master);s.start(t);s.stop(t+.7);
}
function scheduleStep(sb){
const s=Math.round(sb*4)%32,t=timeOfBeat(sb);
if(KICK.has(s)){kick(t);visKicks.push({t:performance.now()/1000+(t-actx.currentTime)});}
if(SNARE.has(s))snare(t,.9);
if(GHOST.has(s))snare(t,.28);
if(s%2===0&&!OHAT.has(s))hat(t,.16,.04);
if(OHAT.has(s))hat(t,.2,.14);
if(hype>.5&&s%4===3)hat(t,.08,.03);
for(const n of BASS)if(n[0]===s)bass(t,n[1],n[2]);
}
function pumpAudio(){
if(!audioOn||!actx)return;
const horizon=actx.currentTime+.16;
while(timeOfBeat(nextStepBeat)<horizon){scheduleStep(nextStepBeat);nextStepBeat+=.25;}
}
const beatBtn=document.getElementById('beatBtn');
function toggleBeat(){
if(!actx)initAudio();
audioOn=!audioOn;
if(audioOn){actx.resume();nextStepBeat=Math.ceil(songBeat*4)/4;}
beatBtn.innerHTML=audioOn?'■ Cut the beat':'▶ Drop the beat';
beatBtn.classList.toggle('on',audioOn);
}
/* ================= drawing ================= */
function drawLeg(g,hx,hy,tx,ty,far){
const knee=ik(hx,hy,tx,ty,20,22,-1),col=far?'#d97f22':'#ff9b2f';
g.lineCap='round';g.lineJoin='round';
g.strokeStyle=INK;g.lineWidth=11;
g.beginPath();g.moveTo(hx,hy);g.lineTo(knee[0],knee[1]);g.lineTo(tx,ty);g.stroke();
g.strokeStyle=col;g.lineWidth=7;g.stroke();
const sa=Math.atan2(ty-knee[1],tx-knee[0]),fa=sa+(0-sa)*.62;
g.beginPath();
g.moveTo(tx,ty);g.lineTo(tx+Math.cos(fa)*11,ty+Math.sin(fa)*11);
g.moveTo(tx,ty);g.lineTo(tx+Math.cos(fa+2.6)*5,ty+Math.sin(fa+2.6)*5);
g.strokeStyle=INK;g.lineWidth=9;g.stroke();
g.strokeStyle=col;g.lineWidth=5.5;g.stroke();
}
function drawWing(g,sx,sy,tx,ty,fan,far){
const el=ik(sx,sy,tx,ty,20,22,-1),col=far?'#39424f':'#4d5a6e';
const wa=Math.atan2(ty-el[1],tx-el[0]),spread=.5+fan*1.2,L=10+fan*12,pts=[];
for(let i=0;i<4;i++){const a=wa-spread/2+spread*i/3;pts.push([tx+Math.cos(a)*L,ty+Math.sin(a)*L]);}
g.lineCap='round';
for(const pass of [[INK,12,8.5],[col,8,5]]){
g.strokeStyle=pass[0];
g.lineWidth=pass[1];g.beginPath();g.moveTo(sx,sy);g.lineTo(el[0],el[1]);g.lineTo(tx,ty);g.stroke();
g.lineWidth=pass[2];g.beginPath();
for(const p of pts){g.moveTo(tx,ty);g.lineTo(p[0],p[1]);}
g.stroke();
}
}
function drawTail(g,P){
const a0=Math.PI+.3,s=.3+P.tail,L=18+P.tail*10;
for(const pass of [[INK,9.5],['#39424f',6]]){
g.strokeStyle=pass[0];g.lineWidth=pass[1];g.lineCap='round';g.beginPath();
for(let i=0;i<4;i++){
const a=a0-s/2+s*i/3;
g.moveTo(-24,-12);g.lineTo(-24+Math.cos(a)*L,-12+Math.sin(a)*L);
}
g.stroke();
}
}
function drawBody(g){
g.beginPath();
g.moveTo(-25,-16);
g.bezierCurveTo(-16,-36,8,-40,20,-28);
g.bezierCurveTo(30,-20,32,-6,24,4);
g.bezierCurveTo(16,14,-6,16,-18,8);
g.bezierCurveTo(-26,2,-28,-8,-25,-16);
g.closePath();
g.fillStyle='#4d5a6e';g.fill();g.strokeStyle=INK;g.lineWidth=3.5;g.lineJoin='round';g.stroke();
g.beginPath();
g.moveTo(21,-3);g.bezierCurveTo(22,6,12,13,2,12);g.bezierCurveTo(-4,6,4,-4,21,-3);
g.closePath();g.fillStyle='#f2e3c2';g.fill();g.strokeStyle=INK;g.lineWidth=2;g.stroke();
g.beginPath();g.ellipse(15,-16,7,9,.5,0,7);
g.fillStyle='#2fae9b';g.fill();g.strokeStyle=INK;g.lineWidth=2;g.stroke();
}
function drawNeckHead(g,P,nb){
const hx=P.hdx,hy=P.hdy,cx2=(nb[0]+hx)/2+P.bow,cy2=(nb[1]+hy)/2;
g.lineCap='round';
for(const pass of [[INK,13],['#4d5a6e',9]]){
g.strokeStyle=pass[0];g.lineWidth=pass[1];
g.beginPath();g.moveTo(nb[0],nb[1]);g.quadraticCurveTo(cx2,cy2,hx,hy);g.stroke();
}
g.beginPath();g.arc(hx,hy,12.5,0,7);
g.fillStyle='#4d5a6e';g.fill();g.strokeStyle=INK;g.lineWidth=3;g.stroke();
if(P.crest>.02){
for(const pass of [[INK,6],['#4d5a6e',3]]){
g.strokeStyle=pass[0];g.lineWidth=pass[1];g.beginPath();
for(let i=0;i<3;i++){
const a=-2.55+i*.33,l=4+P.crest*8;
g.moveTo(hx+Math.cos(a)*10,hy+Math.sin(a)*10);
g.lineTo(hx+Math.cos(a)*(10+l),hy+Math.sin(a)*(10+l));
}
g.stroke();
}
}
g.beginPath();g.arc(hx+5,hy-3.5,4.3,0,7);
g.fillStyle='#fff';g.fill();g.strokeStyle=INK;g.lineWidth=2;g.stroke();
g.beginPath();g.arc(hx+6.2,hy-3.2,2.1,0,7);g.fillStyle=INK;g.fill();
g.beginPath();g.arc(hx+6.9,hy-4,0.7,0,7);g.fillStyle='#fff';g.fill();
if(hype>.55){g.strokeStyle=INK;g.lineWidth=2.5;g.beginPath();g.moveTo(hx+1,hy-9.5);g.lineTo(hx+9.5,hy-6.5);g.stroke();}
g.save();g.translate(hx,hy);g.rotate(P.ba);
g.beginPath();g.moveTo(8,-5);g.lineTo(22,-1.5);g.lineTo(8,.5);g.closePath();
g.fillStyle='#ff9b2f';g.fill();g.strokeStyle=INK;g.lineWidth=2.5;g.stroke();
g.save();g.rotate(P.beak*.5);
g.beginPath();g.moveTo(8,.5);g.lineTo(19,3);g.lineTo(8,5);g.closePath();
g.fillStyle='#e07f1f';g.fill();g.stroke();
g.restore();g.restore();
}
function drawBird(g,P){
g.save();g.translate(P.x,-P.h);
const cr=Math.cos(P.rot),sr=Math.sin(P.rot),
rp=(x,y)=>[x*cr-y*sr,x*sr+y*cr],
hipF=rp(-6,5),hipN=rp(4,7),shF=rp(-3,-20),shN=rp(7,-18);
drawLeg(g,hipF[0],hipF[1],P.f1x,P.f1y,true);
drawWing(g,shF[0],shF[1],P.w1x,P.w1y,P.fl1,true);
g.save();g.rotate(P.rot);
g.translate(0,-10);g.scale(1+P.sq*.12,1-P.sq*.16);g.translate(0,10);
drawTail(g,P);drawBody(g);
g.restore();
drawNeckHead(g,P,rp(20,-26));
drawLeg(g,hipN[0],hipN[1],P.f0x,P.f0y,false);
drawWing(g,shN[0],shN[1],P.w0x,P.w0y,P.fl0,false);
g.restore();
}
function rr(g,x,y,w,h,r){
g.beginPath();g.moveTo(x+r,y);g.arcTo(x+w,y,x+w,y+h,r);g.arcTo(x+w,y+h,x,y+h,r);
g.arcTo(x,y+h,x,y,r);g.arcTo(x,y,x+w,y,r);g.closePath();
}
function drawBoombox(g,now,pulse){
g.save();g.translate(-310,-2);g.scale(.92,.92);
g.beginPath();g.ellipse(0,2,100,12,0,0,7);g.fillStyle='rgba(0,0,0,.35)';g.fill();
g.lineCap='round';
g.beginPath();g.moveTo(-38,-78);g.quadraticCurveTo(0,-106,38,-78);
g.strokeStyle=INK;g.lineWidth=10;g.stroke();
g.strokeStyle='#ff8b2f';g.lineWidth=6;g.stroke();
rr(g,-95,-78,190,78,8);g.fillStyle='#262b38';g.fill();g.strokeStyle=INK;g.lineWidth=4;g.stroke();
for(const sx of [-50,50]){
g.beginPath();g.arc(sx,-39,27,0,7);g.fillStyle='#171a24';g.fill();
g.strokeStyle='#ffe9b8';g.lineWidth=3;g.stroke();
g.beginPath();g.arc(sx,-39,17*(1+pulse*.13),0,7);g.fillStyle='#3a4152';g.fill();
g.strokeStyle=INK;g.lineWidth=3;g.stroke();
g.beginPath();g.arc(sx,-39,6,0,7);g.fillStyle='#ff8b2f';g.fill();g.strokeStyle=INK;g.lineWidth=2;g.stroke();
}
rr(g,-22,-66,44,20,3);g.fillStyle='#171a24';g.fill();g.strokeStyle='#ffe9b8';g.lineWidth=2;g.stroke();
for(const rx of [-10,10]){
g.save();g.translate(rx,-56);g.rotate(songBeat*3);
g.strokeStyle='#ffe9b8';g.lineWidth=2;
g.beginPath();g.arc(0,0,5,0,7);g.moveTo(-5,0);g.lineTo(5,0);g.moveTo(0,-5);g.lineTo(0,5);g.stroke();
g.restore();
}
for(let i=0;i<5;i++){
const hh=(6+18*Math.abs(Math.sin(songBeat*2.2+i*1.3)))*(audioOn?1:.25);
g.fillStyle=i%2?'#f5b73c':'#ff8b2f';
g.fillRect(-18+i*8,-38-hh,5,hh);
}
g.restore();
}
function drawCardboard(g){
g.beginPath();g.moveTo(-185,-8);g.lineTo(150,-26);g.lineTo(215,18);g.lineTo(-128,46);g.closePath();
g.fillStyle='#c98d4b';g.fill();g.strokeStyle=INK;g.lineWidth=4;g.lineJoin='round';g.stroke();
g.beginPath();g.moveTo(-128,46);g.lineTo(215,18);g.lineTo(215,28);g.lineTo(-128,56);g.closePath();
g.fillStyle='#9a6836';g.fill();g.strokeStyle=INK;g.lineWidth=3;g.stroke();
g.strokeStyle='rgba(23,19,14,.25)';g.lineWidth=3;
g.beginPath();g.moveTo(-90,8);g.lineTo(-30,2);g.moveTo(40,26);g.lineTo(110,18);g.moveTo(-60,30);g.lineTo(-10,26);g.stroke();
for(const [tx,ty,ro] of [[-160,-2,-.5],[185,10,.9]]){
g.save();g.translate(tx,ty);g.rotate(ro);
g.fillStyle='rgba(239,224,184,.85)';g.fillRect(-26,-8,52,16);
g.strokeStyle=INK;g.lineWidth=2;g.strokeRect(-26,-8,52,16);g.restore();
}
g.save();g.translate(40,4);g.rotate(-.06);g.scale(1,.5);
g.font='900 30px Impact,"Arial Black",sans-serif';g.textAlign='center';
g.fillStyle='rgba(53,88,110,.55)';g.fillText('VINNY ★',0,0);g.restore();
}
const crowdBirds=[{x:300,s:.62,ph:0},{x:345,s:.5,ph:1.3},{x:385,s:.56,ph:2.1},{x:262,s:.45,ph:.7}];
function drawCrowd(g){
for(const c of crowdBirds){
const bob=-Math.abs(Math.sin(songBeat*Math.PI+c.ph))*(2.5+excite*9)-excite*10;
g.save();g.translate(c.x,8+bob);g.scale(-c.s,c.s);
g.fillStyle='#151020';
g.beginPath();
g.ellipse(0,-16,20,15,0,0,7);
g.arc(16,-32,9,0,7);
g.moveTo(24,-34);g.lineTo(32,-31);g.lineTo(24,-28);
g.moveTo(-16,-22);g.lineTo(-33,-30);g.lineTo(-14,-10);
g.fill();
if(excite>.25){
g.beginPath();g.moveTo(-2,-26);g.lineTo(-14,-46-excite*8);g.lineTo(7,-30);g.closePath();g.fill();
}
g.restore();
}
}
/* ================= main loop ================= */
const meter=document.getElementById('meter');
for(let i=0;i<12;i++)meter.appendChild(document.createElement('i'));
const meterCells=[...meter.children],dot=document.getElementById('dot');
const moveBtns=[...document.querySelectorAll('[data-move]')];
let lastLit=-1,lastBtnKey='';
function update(dt,now){
songBeat+=dt*bpm/60;
updateChoreo(now);
pumpAudio();
hype=Math.max(0,hype-dt*.03);
excite*=Math.exp(-dt*1.8);
shake*=Math.exp(-dt*8);
if(!introDone&&songBeat>1.6){introDone=true;addWord(["LET'S GO!"],birdSX(),birdSY()-130*S,30);}
if(hype>.9&&!pending&&now-lastAuto>18){lastAuto=now;callMove('windmill',false);addWord(["CAN'T STOP!"],birdSX(),birdSY()-140*S,28);}
if(cur.id==='windmill'){
const lb=songBeat-cur.startBeat;
if(lb>1&&lb<5&&Math.sin((lb-1)*Math.PI)>.96&&Math.random()<dt*8)spawnDust(currentPose.x+rand(-20,20),1);
}
for(let i=dust.length-1;i>=0;i--){const p=dust[i];p.t+=dt;if(p.t>p.life){dust.splice(i,1);continue;}p.x+=p.vx*dt;p.y+=p.vy*dt;p.vy+=190*dt;}
for(let i=feathers.length-1;i>=0;i--){const p=feathers[i];p.t+=dt;if(p.t>p.life){feathers.splice(i,1);continue;}p.x+=p.vx*dt+Math.sin(p.t*4)*12*dt;p.y+=p.vy*dt;p.rot+=p.vr*dt;}
for(let i=words.length-1;i>=0;i--){words[i].t+=dt;if(words[i].t>words[i].life)words.splice(i,1);}
if(Math.random()<dt*.12&&flyers.length<4)flyers.push({x:-40,y:rand(40,HZ-70),v:rand(40,85),ph:rand(0,6)});
for(let i=flyers.length-1;i>=0;i--){flyers[i].x+=flyers[i].v*dt;if(flyers[i].x>W+40)flyers.splice(i,1);}
while(visKicks.length&&visKicks[0].t<now-.6)visKicks.shift();
/* ui */
const lit=Math.round(hype*12);
if(lit!==lastLit){lastLit=lit;meterCells.forEach((c,i)=>c.classList.toggle('lit',i<lit));}
let pulse=Math.exp(-(songBeat%1)*5);
for(let i=visKicks.length-1;i>=0;i--)if(visKicks[i].t<=now){pulse=Math.max(pulse,Math.exp(-(now-visKicks[i].t)*9));break;}
dot.style.transform='scale('+(1+pulse*1.1)+')';
const grp=id=>id.startsWith('toprock')?'toprock':(id==='freezeA'||id==='freezeB')?'freeze':id,
key=grp(cur.id)+'|'+(pending?grp(pending):'');
if(key!==lastBtnKey){
lastBtnKey=key;
moveBtns.forEach(b=>{
b.classList.toggle('on',grp(cur.id)===b.dataset.move);
b.classList.toggle('queued',!!pending&&grp(pending)===b.dataset.move);
});
}
return pulse;
}
function render(now,pulse){
ctx.save();
if(shake>.2&&!RM)ctx.translate(rand(-shake,shake),rand(-shake,shake));
/* sky */
const bands=[[0,.45,'#0b3e49'],[.45,.68,'#155e66'],[.68,.86,'#b9642c'],[.86,1.01,'#e89a33']];
for(const b of bands){ctx.fillStyle=b[2];ctx.fillRect(0,HZ*b[0],W,HZ*(b[1]-b[0])+1);}
ctx.beginPath();ctx.arc(W*.62,HZ-30*S,88*S,0,7);ctx.fillStyle='#ffe9b8';ctx.fill();
ctx.beginPath();ctx.arc(W*.62,HZ-30*S,98*S+pulse*5,0,7);
ctx.strokeStyle='rgba(255,233,184,.45)';ctx.lineWidth=3;ctx.stroke();
for(const b of buildings){
ctx.fillStyle='#191521';ctx.fillRect(b.x,HZ-b.ht,b.wd,b.ht);
ctx.fillStyle='rgba(255,233,184,.75)';
for(const wn of b.win)ctx.fillRect(wn[0],wn[1],4,6);
if(b.ant){
ctx.strokeStyle='#191521';ctx.lineWidth=3;
ctx.beginPath();ctx.moveTo(b.x+b.wd/2,HZ-b.ht);ctx.lineTo(b.x+b.wd/2,HZ-b.ht-26*S);ctx.stroke();
if(Math.sin(now*4)>0){ctx.fillStyle='#e5482e';ctx.beginPath();ctx.arc(b.x+b.wd/2,HZ-b.ht-26*S,3,0,7);ctx.fill();}
}
}
ctx.strokeStyle='#191521';ctx.lineWidth=2.5;ctx.lineCap='round';
for(const f of flyers){
const fl=Math.sin(now*10+f.ph)*3;
ctx.beginPath();ctx.moveTo(f.x-7,f.y);ctx.quadraticCurveTo(f.x-2,f.y-5-fl,f.x,f.y);
ctx.quadraticCurveTo(f.x+2,f.y-5-fl,f.x+7,f.y);ctx.stroke();
}
/* ground */
ctx.fillStyle='#2a2130';ctx.fillRect(0,HZ,W,H-HZ);
ctx.fillStyle='#191521';ctx.fillRect(0,HZ,W,7);
ctx.strokeStyle='rgba(23,19,14,.35)';ctx.lineWidth=2;
ctx.beginPath();
ctx.moveTo(W*.18,HZ);ctx.lineTo(W*.02,H);
ctx.moveTo(W*.5,HZ);ctx.lineTo(W*.5,H);
ctx.moveTo(W*.82,HZ);ctx.lineTo(W*.98,H);ctx.stroke();
/* world */
ctx.save();ctx.translate(CX,GY);ctx.scale(S,S);
ctx.setLineDash([14,10]);
ctx.strokeStyle='rgba(255,233,184,'+(.22+.12*pulse)+')';ctx.lineWidth=3;
ctx.beginPath();ctx.ellipse(10,12,255,54,0,0,7);ctx.stroke();
ctx.setLineDash([]);
ctx.save();ctx.translate(-262,66);ctx.scale(1,.4);ctx.rotate(-.08);
ctx.font='900 30px Impact,"Arial Black",sans-serif';ctx.textAlign='center';
ctx.fillStyle='rgba(255,233,184,.2)';ctx.fillText('BREAK ZONE \u2192',0,0);ctx.restore();
drawCardboard(ctx);
drawBoombox(ctx,now,pulse);
{ /* shadow */
const sh=clamp(1-(currentPose.h-46)/50,.4,1);
ctx.beginPath();ctx.ellipse(currentPose.x,3,38*sh,8*sh,0,0,7);
ctx.fillStyle='rgba(23,19,14,'+(.38*sh)+')';ctx.fill();
}
if(cur.id==='windmill'){ /* motion trails */
const lb=songBeat-cur.startBeat;
if(lb>1&&lb<5){
const th=(lb-1)*Math.PI;
ctx.save();ctx.translate(currentPose.x,-currentPose.h-12);
for(let k=0;k<3;k++){
ctx.beginPath();ctx.arc(0,0,25,th-.5-k*.55,th-.12-k*.55);
ctx.strokeStyle=k?'rgba(255,139,47,'+(.5-k*.14)+')':'rgba(255,233,184,.6)';
ctx.lineWidth=4+k;ctx.lineCap='round';ctx.stroke();
}
ctx.restore();
}
}
drawBird(ctx,currentPose);
for(const p of dust){
ctx.beginPath();ctx.arc(p.x,p.y,p.r,0,7);
ctx.fillStyle='rgba(255,233,184,'+((1-p.t/p.life)*.7)+')';ctx.fill();
}
for(const p of feathers){
ctx.save();ctx.translate(p.x,p.y);ctx.rotate(p.rot);ctx.globalAlpha=1-p.t/p.life;
ctx.beginPath();ctx.ellipse(0,0,7,2.6,0,0,7);ctx.fillStyle='#4d5a6e';ctx.fill();
ctx.strokeStyle=INK;ctx.lineWidth=1;ctx.stroke();
ctx.beginPath();ctx.moveTo(-7,0);ctx.lineTo(7,0);ctx.stroke();
ctx.restore();ctx.globalAlpha=1;
}
drawCrowd(ctx);
ctx.restore();
/* burst words */
for(const wd of words){
const age=wd.t/wd.life,sc=E.back(Math.min(1,age*3)),al=age>.7?1-(age-.7)/.3:1;
ctx.save();ctx.translate(wd.x,wd.y-age*wd.vy);ctx.rotate(wd.rot);ctx.scale(sc,sc);
ctx.globalAlpha=al;ctx.font='900 '+wd.size+'px Impact,"Arial Black",sans-serif';ctx.textAlign='center';
ctx.lineWidth=wd.size/7;ctx.strokeStyle=INK;ctx.strokeText(wd.txt,0,0);
ctx.fillStyle=wd.col;ctx.fillText(wd.txt,0,0);
ctx.restore();
}
ctx.globalAlpha=1;
ctx.restore(); /* shake */
ctx.save();ctx.globalAlpha=.07;
if(!RM)ctx.translate((Math.random()*180)|0,(Math.random()*180)|0);
ctx.fillStyle=grainPat;ctx.fillRect(-180,-180,W+360,H+360);
ctx.restore();
ctx.fillStyle=vign;ctx.fillRect(0,0,W,H);
}
function frame(){
const now=performance.now()/1000,dt=Math.min(.05,now-lastT);lastT=now;
const pulse=update(dt,now);
render(now,pulse);
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
/* ================= input ================= */
beatBtn.addEventListener('click',toggleBeat);
document.getElementById('hypeBtn').addEventListener('click',()=>hypePump(1));
moveBtns.forEach(b=>b.addEventListener('click',()=>callMove(b.dataset.move,true)));
const bpmRange=document.getElementById('bpmRange'),bpmVal=document.getElementById('bpmVal');
function setBpm(v){bpm=clamp(Math.round(v),88,132);bpmRange.value=bpm;bpmVal.textContent=bpm;}
bpmRange.addEventListener('input',()=>setBpm(+bpmRange.value));
cv.addEventListener('pointerdown',e=>{
const d=Math.hypot(e.clientX-birdSX(),e.clientY-birdSY());
hypePump(d<130*S?1:.4);
});
addEventListener('keydown',e=>{
if(e.repeat)return;
const k=e.key;
if(k==='1')callMove('toprock',true);
else if(k==='2')callMove('sixstep',true);
else if(k==='3')callMove('windmill',true);
else if(k==='4')callMove('freeze',true);
else if(k==='b'||k==='B')toggleBeat();
else if(k===' '){hypePump(1);e.preventDefault();}
else if(k==='ArrowUp'){setBpm(bpm+3);e.preventDefault();}
else if(k==='ArrowDown'){setBpm(bpm-3);e.preventDefault();}
});
</script>
</body>
</html>