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.
The bird is a plump European starling in an oversized mustard tracksuit, coral sneakers and a headband, drawn front-on so his steps, plants and freezes read clearly in silhouette. The look is a risograph linocut on cream paper — ink-black outlines, halftone dots, a wobbly print filter and a speckled chest under an iridescent head.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Starling Breaks</title>
<style>
*{box-sizing:border-box}
body{margin:0;background:#f2ead8;color:#111;font-family:"Courier New",Courier,monospace;background-image:radial-gradient(rgba(0,0,0,.08) 1px,transparent 1.2px);background-size:18px 18px}
header{display:flex;justify-content:space-between;align-items:flex-end;padding:18px 22px 12px;border-bottom:3px solid #111;flex-wrap:wrap;gap:10px}
h1{font-size:clamp(30px,5vw,56px);line-height:.9;margin:0;letter-spacing:-1px;text-transform:uppercase;font-weight:900}
h1 span{color:#17705f}
.tag{font-size:11px;text-transform:uppercase;letter-spacing:2px}
main{max-width:980px;margin:0 auto;padding:18px}
.stage-wrap{position:relative;border:3px solid #111;background:#f7f0dc;box-shadow:8px 8px 0 #111}
svg{display:block;width:100%;height:auto}
.beat-dots{position:absolute;top:12px;right:12px;display:flex;gap:6px}
.beat-dots i{width:12px;height:12px;border:2px solid #111;border-radius:50%;background:transparent;display:block}
.beat-dots i.on{background:#ff5a3c}
.move-label{position:absolute;top:10px;left:12px;background:#111;color:#f2ead8;padding:6px 10px;font-size:12px;letter-spacing:2px;text-transform:uppercase}
.deck{margin-top:18px;border:3px solid #111;background:#fff8e6;box-shadow:8px 8px 0 #111;padding:14px;display:grid;grid-template-columns:repeat(auto-fit,minmax(210px,1fr));gap:16px}
.deck h3{margin:0 0 8px;font-size:11px;letter-spacing:2px;text-transform:uppercase}
button{font-family:inherit;font-weight:700;text-transform:uppercase;letter-spacing:1px;border:2px solid #111;background:#f2ead8;padding:8px 10px;cursor:pointer;box-shadow:3px 3px 0 #111;font-size:12px}
button:active,button.active{transform:translate(2px,2px);box-shadow:1px 1px 0 #111;background:#e8a317}
button.queued{background:#ff5a3c;color:#fff}
.moves{display:flex;flex-wrap:wrap;gap:8px}
.swatches{display:flex;gap:8px}
.swatch{width:44px;height:38px;padding:0;display:flex;overflow:hidden}
.swatch i{flex:1;display:block}
input[type=range]{width:100%;accent-color:#ff5a3c}
.toggles{display:flex;flex-wrap:wrap;gap:8px}
.tempo-val{font-size:20px;font-weight:900}
footer{text-align:center;font-size:11px;letter-spacing:2px;text-transform:uppercase;padding:14px;opacity:.7}
</style>
</head>
<body>
<header>
<div><h1>Starling <span>Breaks</span></h1><div class="tag">A small bird, serious weight</div></div>
<div class="tag">Live on cardboard · Stage 01</div>
</header>
<main>
<div class="stage-wrap">
<div class="move-label" id="moveLabel">TOPROCK</div>
<div class="beat-dots"><i></i><i></i><i></i><i></i></div>
<svg id="stage" viewBox="0 0 900 620" role="img" aria-label="A starling breakdancing">
<defs>
<pattern id="dots" width="14" height="14" patternUnits="userSpaceOnUse"><circle cx="3" cy="3" r="2" fill="#111" opacity="0.18"/></pattern>
<filter id="rough" x="-20%" y="-20%" width="140%" height="140%"><feTurbulence type="fractalNoise" baseFrequency="0.015" numOctaves="2" seed="7" result="n"/><feDisplacementMap in="SourceGraphic" in2="n" scale="4"/></filter>
<linearGradient id="spot" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#fff3b0" stop-opacity="0.9"/><stop offset="1" stop-color="#fff3b0" stop-opacity="0"/></linearGradient>
</defs>
<rect width="900" height="620" fill="#f7f0dc"/>
<rect width="900" height="620" fill="url(#dots)" opacity="0.5"/>
<polygon points="340,0 560,0 720,540 180,540" fill="url(#spot)"/>
<text x="450" y="150" text-anchor="middle" font-family="Courier New" font-weight="900" font-size="92" fill="none" stroke="#111" stroke-width="2" opacity="0.12" letter-spacing="6">BREAKS</text>
<ellipse cx="450" cy="525" rx="260" ry="52" fill="#e8a317" opacity="0.18"/>
<g fill="none" stroke="#111" stroke-width="2" opacity="0.35">
<ellipse cx="450" cy="525" rx="260" ry="52"/>
<ellipse cx="450" cy="525" rx="190" ry="38" stroke-dasharray="8 8"/>
<ellipse cx="450" cy="525" rx="110" ry="22" stroke-dasharray="3 7"/>
</g>
<g id="crowd" fill="#111">
<g class="head" transform="translate(60 600)"><circle r="20"/><rect x="-24" y="8" width="48" height="28" rx="9"/></g>
<g class="head" transform="translate(170 605)"><circle r="24"/><rect x="-28" y="10" width="56" height="30" rx="10"/></g>
<g class="head" transform="translate(290 600)"><circle r="19"/><rect x="-23" y="8" width="46" height="26" rx="9"/></g>
<g class="head" transform="translate(620 600)"><circle r="21"/><rect x="-25" y="9" width="50" height="28" rx="9"/></g>
<g class="head" transform="translate(740 605)"><circle r="25"/><rect x="-29" y="10" width="58" height="30" rx="10"/></g>
<g class="head" transform="translate(850 600)"><circle r="20"/><rect x="-24" y="8" width="48" height="28" rx="9"/></g>
</g>
<ellipse id="shadow" cx="450" cy="535" rx="90" ry="16" fill="#111" opacity="0.25"/>
<g id="dust"></g>
<g id="bird" filter="url(#rough)">
<g id="tailG"><path d="M-20 -4 L20 -4 L30 30 L0 42 L-30 30 Z" fill="#14342b" stroke="#111" stroke-width="3" stroke-linejoin="round"/><path d="M0 0 L0 36 M-12 2 L-18 30 M12 2 L18 30" stroke="#f2ead8" stroke-width="2" opacity="0.6"/></g>
<g id="legL" transform="translate(-16 0)">
<path class="pants" d="M-11 0 H11 L9 42 H-9 Z" fill="#17705f" stroke="#111" stroke-width="3" stroke-linejoin="round"/>
<g id="shinL" transform="translate(0 40)"><path class="pants" d="M-9 0 H9 L7 38 H-7 Z" fill="#17705f" stroke="#111" stroke-width="3" stroke-linejoin="round"/>
<g id="footL" transform="translate(0 36)"><g transform="scale(-1 1)"><path class="sneaker" d="M-6 -6 H8 C17 -6 25 -1 25 7 C25 13 19 16 10 16 H-11 C-17 16 -19 11 -17 5 C-15 -1 -12 -6 -6 -6Z" fill="#ff5a3c" stroke="#111" stroke-width="3" stroke-linejoin="round"/><path d="M-17 11 H24" stroke="#fff" stroke-width="4"/><path d="M2 -6 L2 16 M10 -5 L10 16" stroke="#111" stroke-width="2" opacity="0.5"/></g></g></g>
</g>
<g id="legR" transform="translate(16 0)">
<path class="pants" d="M-11 0 H11 L9 42 H-9 Z" fill="#17705f" stroke="#111" stroke-width="3" stroke-linejoin="round"/>
<g id="shinR" transform="translate(0 40)"><path class="pants" d="M-9 0 H9 L7 38 H-7 Z" fill="#17705f" stroke="#111" stroke-width="3" stroke-linejoin="round"/>
<g id="footR" transform="translate(0 36)"><path class="sneaker" d="M-6 -6 H8 C17 -6 25 -1 25 7 C25 13 19 16 10 16 H-11 C-17 16 -19 11 -17 5 C-15 -1 -12 -6 -6 -6Z" fill="#ff5a3c" stroke="#111" stroke-width="3" stroke-linejoin="round"/><path d="M-17 11 H24" stroke="#fff" stroke-width="4"/><path d="M2 -6 L2 16 M10 -5 L10 16" stroke="#111" stroke-width="2" opacity="0.5"/></g></g>
</g>
<g id="torsoG">
<g id="wingBack" transform="translate(-30 -58)"><path class="jacket" d="M-9 0 H9 L6 36 H-6 Z" fill="#e8a317" stroke="#111" stroke-width="3" stroke-linejoin="round"/><g id="foreBack" transform="translate(0 34)"><path class="jacket" d="M-8 0 H8 L5 32 H-5 Z" fill="#e8a317" stroke="#111" stroke-width="3"/><g transform="translate(0 30)"><path d="M-8 0 L-14 14 L-4 10 L0 18 L4 10 L14 14 L8 0 Z" fill="#14342b" stroke="#111" stroke-width="2.5" stroke-linejoin="round"/></g></g></g>
<path class="jacket" d="M-38 2 C-50 -28 -44 -68 -20 -88 C-6 -100 16 -100 30 -86 C50 -66 52 -28 38 2 C20 12 -20 12 -38 2Z" fill="#e8a317" stroke="#111" stroke-width="3.5" stroke-linejoin="round"/>
<path d="M-26 -18 C-30 -42 -18 -62 0 -66 C18 -62 30 -42 26 -18 C18 -6 -18 -6 -26 -18Z" fill="#f7f0dc" opacity="0.85"/>
<g fill="#111" opacity="0.7"><circle cx="-10" cy="-38" r="2.2"/><circle cx="6" cy="-30" r="2"/><circle cx="14" cy="-44" r="2.2"/><circle cx="-4" cy="-50" r="1.8"/><circle cx="18" cy="-22" r="1.8"/><circle cx="-16" cy="-24" r="1.8"/></g>
<path d="M0 -84 L0 -8" stroke="#111" stroke-width="2.5" stroke-dasharray="5 4"/><circle cx="0" cy="-84" r="4" fill="#ff5a3c" stroke="#111" stroke-width="2"/>
<path d="M-18 -86 Q0 -98 18 -86 L14 -76 Q0 -86 -14 -76Z" fill="#111"/>
<g id="wingFront" transform="translate(30 -58)"><path class="jacket" d="M-9 0 H9 L6 36 H-6 Z" fill="#e8a317" stroke="#111" stroke-width="3" stroke-linejoin="round"/><g id="foreFront" transform="translate(0 34)"><path class="jacket" d="M-8 0 H8 L5 32 H-5 Z" fill="#e8a317" stroke="#111" stroke-width="3"/><g transform="translate(0 30)"><path d="M-8 0 L-14 14 L-4 10 L0 18 L4 10 L14 14 L8 0 Z" fill="#14342b" stroke="#111" stroke-width="2.5" stroke-linejoin="round"/></g></g></g>
<g id="headG" transform="translate(0 -88)">
<path d="M-14 -38 L-22 -54 L-8 -46 L-6 -60 L4 -46 L14 -56 L16 -40Z" fill="#14342b" stroke="#111" stroke-width="2.5" stroke-linejoin="round"/>
<circle cx="4" cy="-24" r="30" fill="#14342b" stroke="#111" stroke-width="3.5"/>
<g fill="#f2ead8" opacity="0.8"><circle cx="-12" cy="-30" r="2"/><circle cx="0" cy="-42" r="1.8"/><circle cx="18" cy="-36" r="1.6"/></g>
<path class="headband" d="M-24 -34 Q4 -50 30 -32" fill="none" stroke="#ff5a3c" stroke-width="8" stroke-linecap="round"/>
<path class="headband" d="M-24 -34 L-34 -18 M-20 -36 L-26 -14" stroke="#ff5a3c" stroke-width="5" stroke-linecap="round"/>
<circle cx="14" cy="-28" r="8" fill="#fff" stroke="#111" stroke-width="2.5"/><circle cx="16" cy="-27" r="3.8" fill="#111"/><circle cx="17.5" cy="-29" r="1.4" fill="#fff"/>
<circle cx="-8" cy="-28" r="5" fill="#fff" stroke="#111" stroke-width="2"/><circle cx="-7" cy="-27" r="2.4" fill="#111"/>
<path d="M22 -20 L48 -12 L24 -4 C20 -8 20 -16 22 -20Z" fill="#f2c230" stroke="#111" stroke-width="2.5" stroke-linejoin="round"/>
<circle cx="6" cy="-12" r="5" fill="#ff5a3c" opacity="0.35"/>
</g>
</g>
</g>
</svg>
</div>
<section class="deck">
<div><h3>Tempo</h3><div class="tempo-val"><span id="bpmVal">108</span> BPM</div><input id="bpm" type="range" min="84" max="132" value="108"><p style="font-size:11px;margin:6px 0 0">Faster = lighter, slower = heavier. Try 84 for the freeze.</p></div>
<div><h3>Call a move</h3><div class="moves"><button data-move="toprock">Toprock</button><button data-move="downrock">Downrock</button><button data-move="freeze">Freeze</button><button data-move="spin">Spin</button><button data-move="flare">Flare</button></div><p style="font-size:11px;margin:8px 0 0">Queued moves land after a recovery beat.</p></div>
<div><h3>Outfit</h3><div class="swatches"><button class="swatch active" data-outfit="0" aria-label="Mustard teal coral"><i style="background:#e8a317"></i><i style="background:#17705f"></i><i style="background:#ff5a3c"></i></button><button class="swatch" data-outfit="1" aria-label="Coral navy lime"><i style="background:#ff6b4a"></i><i style="background:#1e2a5a"></i><i style="background:#b8e62e"></i></button><button class="swatch" data-outfit="2" aria-label="Lime purple white"><i style="background:#7ac74f"></i><i style="background:#5b2a86"></i><i style="background:#f2f2f2"></i></button><button class="swatch" data-outfit="3" aria-label="Ink mustard teal"><i style="background:#222222"></i><i style="background:#e8a317"></i><i style="background:#17705f"></i></button></div></div>
<div><h3>Deck</h3><div class="toggles"><button id="soundBtn">Sound: Off</button><button id="slowBtn">Slow-mo</button><button id="autoBtn" class="active">Auto DJ</button></div><p style="font-size:11px;margin:8px 0 0">Sound is synthesized drums. Slow-mo is 0.35x.</p></div>
</section>
</main>
<footer>One file · No libraries · Weight over wiggle</footer>
<script>
const $=id=>document.getElementById(id);
const bird=$('bird'),torsoG=$('torsoG'),headG=$('headG'),wingBack=$('wingBack'),foreBack=$('foreBack'),wingFront=$('wingFront'),foreFront=$('foreFront'),legL=$('legL'),shinL=$('shinL'),footL=$('footL'),legR=$('legR'),shinR=$('shinR'),footR=$('footR'),tailG=$('tailG'),shadow=$('shadow'),dustG=$('dust'),moveLabel=$('moveLabel');
const base={rx:450,ry:418,rrot:0,trot:0,hrot:0,shL:28,elL:-18,shR:-28,elR:18,hipL:14,kneeL:-8,ankL:-6,hipR:-14,kneeR:8,ankR:6};
function P(o){return Object.assign({},base,o);}
const EASE={io:t=>t<.5?4*t*t*t:1-Math.pow(-2*t+2,3)/2,out:t=>1-Math.pow(1-t,3),in:t=>t*t*t,lin:t=>t,back:t=>{const c1=1.70158,c3=c1+1;return 1+c3*Math.pow(t-1,3)+c1*Math.pow(t-1,2);},bounce:t=>{const n1=7.5625,d1=2.75;if(t<1/d1)return n1*t*t;if(t<2/d1)return n1*(t-=1.5/d1)*t+.75;if(t<2.5/d1)return n1*(t-=2.25/d1)*t+.9375;return n1*(t-=2.625/d1)*t+.984375;}};
function ease(n,t){return (EASE[n]||EASE.io)(Math.max(0,Math.min(1,t)));}
const MOVES={
toprock:{beats:4,hits:[0.25,0.5,0.75],keys:[[0,P({ry:430,trot:6,shL:40,elL:-25,shR:-40,elR:25,hipL:22,kneeL:-20,ankL:-2,hipR:-22,kneeR:20,ankR:2}),'io'],[0.25,P({rx:425,ry:442,rrot:-5,trot:-7,hrot:5,shL:55,elL:-15,shR:-18,elR:30,hipL:38,kneeL:-18,ankL:-20,hipR:-8,kneeR:12,ankR:4}),'io'],[0.5,P({rx:450,ry:448,trot:10,hrot:-4,shL:35,elL:-30,shR:-35,elR:30,hipL:18,kneeL:-28,ankL:10,hipR:-18,kneeR:28,ankR:-10}),'io'],[0.75,P({rx:475,ry:442,rrot:5,trot:-7,hrot:5,shL:18,elL:-30,shR:-55,elR:-15,hipL:8,kneeL:-12,ankL:-4,hipR:38,kneeR:18,ankR:20}),'io'],[1,P({rx:450,ry:430,trot:6,shL:40,elL:-25,shR:-40,elR:25,hipL:22,kneeL:-20,ankL:-2,hipR:-22,kneeR:20,ankR:2}),'io']]},
downrock:{beats:4,hits:[0.38,0.62],keys:[[0,P({ry:445,trot:12,shL:50,elL:-30,shR:-50,elR:30,hipL:35,kneeL:-40,ankL:5,hipR:-35,kneeR:40,ankR:-5}),'in'],[0.18,P({ry:465,trot:22,hrot:8,shL:70,elL:-20,shR:-70,elR:20,hipL:50,kneeL:-55,ankL:5,hipR:-50,kneeR:55,ankR:-5}),'out'],[0.38,P({ry:485,rrot:14,trot:38,hrot:12,shL:95,elL:-8,shR:-55,elR:45,hipL:60,kneeL:-65,ankL:5,hipR:-15,kneeR:25,ankR:-10}),'io'],[0.62,P({ry:482,rrot:18,trot:42,hrot:-6,shL:90,elL:-12,shR:-30,elR:50,hipL:45,kneeL:-50,ankL:5,hipR:-78,kneeR:6,ankR:72}),'out'],[0.82,P({ry:460,rrot:6,trot:18,shL:60,elL:-25,shR:-60,elR:35,hipL:40,kneeL:-45,ankL:5,hipR:-40,kneeR:45,ankR:-5}),'io'],[1,P({ry:445,trot:12,shL:50,elL:-30,shR:-50,elR:30,hipL:35,kneeL:-40,ankL:5,hipR:-35,kneeR:40,ankR:-5}),'io']]},
freeze:{beats:4,hits:[0.38,1],keys:[[0,P({ry:452,trot:14,shL:55,elL:-35,shR:-55,elR:35,hipL:40,kneeL:-48,ankL:8,hipR:-40,kneeR:48,ankR:-8}),'out'],[0.22,P({ry:360,rrot:95,hrot:-10,shL:150,elL:-10,shR:-140,elR:10,hipL:20,kneeL:-10,ankL:-10,hipR:-20,kneeR:10,ankR:10}),'back'],[0.38,P({ry:315,rrot:168,trot:5,hrot:18,shL:172,elL:-6,shR:-168,elR:6,hipL:42,kneeL:-6,ankL:-36,hipR:-58,kneeR:-4,ankR:62}),'lin'],[0.68,P({ry:318,rrot:165,trot:3,hrot:22,shL:170,elL:-8,shR:-170,elR:8,hipL:38,kneeL:-8,ankL:-30,hipR:-62,kneeR:-6,ankR:56}),'in'],[0.85,P({ry:430,rrot:40,trot:10,shL:80,elL:-20,shR:-80,elR:20,hipL:30,kneeL:-30,ankL:0,hipR:-30,kneeR:30,ankR:0}),'bounce'],[1,P({ry:458,trot:12,hrot:-5,shL:50,elL:-30,shR:-50,elR:30,hipL:38,kneeL:-45,ankL:7,hipR:-38,kneeR:45,ankR:-7}),'io']]},
spin:{beats:4,hits:[0.2,0.7],keys:[[0,P({ry:470,rrot:0,trot:25,hrot:10,shL:85,elL:-15,shR:-85,elR:15,hipL:55,kneeL:-60,ankL:5,hipR:-55,kneeR:60,ankR:-5}),'in'],[0.2,P({ry:478,rrot:90,trot:30,shL:90,elL:-10,shR:-90,elR:10,hipL:70,kneeL:-20,ankL:-50,hipR:-70,kneeR:-20,ankR:50}),'lin'],[0.45,P({ry:475,rrot:270,trot:28,shL:88,elL:-12,shR:-88,elR:12,hipL:20,kneeL:-10,ankL:-10,hipR:-20,kneeR:-10,ankR:10}),'lin'],[0.7,P({ry:478,rrot:450,trot:30,shL:90,elL:-10,shR:-90,elR:10,hipL:70,kneeL:-20,ankL:-50,hipR:-70,kneeR:-20,ankR:50}),'lin'],[0.9,P({ry:470,rrot:630,trot:20,shL:80,elL:-18,shR:-80,elR:18,hipL:40,kneeL:-40,ankL:0,hipR:-40,kneeR:40,ankR:0}),'out'],[1,P({ry:455,rrot:720,trot:12,shL:50,elL:-30,shR:-50,elR:30,hipL:38,kneeL:-45,ankL:7,hipR:-38,kneeR:45,ankR:-7}),'io']]},
flare:{beats:4,hits:[0.38,1],keys:[[0,P({ry:450,trot:12,shL:50,elL:-30,shR:-50,elR:30,hipL:38,kneeL:-45,ankL:7,hipR:-38,kneeR:45,ankR:-7}),'in'],[0.2,P({ry:468,trot:20,shL:65,elL:-20,shR:-65,elR:20,hipL:55,kneeL:-60,ankL:5,hipR:-55,kneeR:60,ankR:-5}),'out'],[0.38,P({ry:335,rrot:55,trot:-5,hrot:-8,shL:110,elL:-40,shR:-110,elR:40,hipL:75,kneeL:-15,ankL:-60,hipR:-25,kneeR:-10,ankR:15}),'lin'],[0.62,P({ry:325,rrot:200,shL:130,elL:-30,shR:-130,elR:30,hipL:-20,kneeL:-10,ankL:30,hipR:-80,kneeR:-15,ankR:65}),'in'],[0.82,P({ry:380,rrot:320,trot:8,shL:70,elL:-25,shR:-70,elR:25,hipL:30,kneeL:-30,ankL:0,hipR:-30,kneeR:30,ankR:0}),'bounce'],[1,P({ry:460,rrot:360,trot:14,shL:52,elL:-32,shR:-52,elR:32,hipL:40,kneeL:-48,ankL:8,hipR:-40,kneeR:48,ankR:-8}),'io']]},
recovery:{beats:2,hits:[],keys:[[0,P({ry:455,trot:10,shL:45,elL:-28,shR:-45,elR:28,hipL:35,kneeL:-42,ankL:6,hipR:-35,kneeR:42,ankR:-6}),'out'],[0.4,P({ry:425,trot:-2,hrot:6,shL:30,elL:-20,shR:-30,elR:20,hipL:16,kneeL:-10,ankL:-6,hipR:-16,kneeR:10,ankR:6}),'io'],[0.7,P({ry:428,trot:2,hrot:14,shL:38,elL:-22,shR:-38,elR:22,hipL:18,kneeL:-12,ankL:-6,hipR:-18,kneeR:12,ankR:6}),'io'],[1,P({ry:438,trot:6,shL:42,elL:-26,shR:-42,elR:26,hipL:24,kneeL:-22,ankL:-2,hipR:-24,kneeR:22,ankR:2}),'io']]}
};
let bpm=108,slowmo=false,auto=true,soundOn=false,beatClock=0,lastTime=performance.now(),currentName='toprock',currentMove=MOVES.toprock,moveStartBeat=0,moveHitsFired=new Set(),queue=[],particles=[],squashY=1,prevRy=418,velSmooth=0,headLag=0,lastBeatInt=-1,audioCtx=null,noiseBuf=null;
function startMove(name){currentName=name;currentMove=MOVES[name];moveStartBeat=beatClock;moveHitsFired=new Set();moveLabel.textContent=name.toUpperCase();document.querySelectorAll('[data-move]').forEach(b=>{b.classList.toggle('active',b.dataset.move===name);b.classList.toggle('queued',queue.includes(b.dataset.move));});}
function pickAuto(){const names=['toprock','downrock','freeze','spin','flare','toprock','freeze'];let n=names[Math.floor(Math.random()*names.length)];if(n===currentName)n='toprock';return n;}
function sample(move,prog){const keys=move.keys;if(prog<=0)return keys[0][1];if(prog>=1)return keys[keys.length-1][1];for(let i=0;i<keys.length-1;i++){const a=keys[i],b=keys[i+1];if(prog>=a[0]&&prog<=b[0]){const local=(prog-a[0])/((b[0]-a[0])||1);const e=ease(a[2]||'io',local);const out={};for(const k in a[1])out[k]=a[1][k]+(b[1][k]-a[1][k])*e;return out;}}return keys[keys.length-1][1];}
function spawnDust(x,y,n){for(let i=0;i<n;i++)particles.push({x:x+(Math.random()-0.5)*60,y:y-Math.random()*6,vx:(Math.random()-0.5)*90,vy:-Math.random()*50-10,life:1,r:3+Math.random()*6});}
function updateDust(dt){dustG.innerHTML='';particles=particles.filter(p=>p.life>0);for(const p of particles){p.life-=dt*1.4;p.x+=p.vx*dt;p.y+=p.vy*dt;p.vy+=60*dt;p.vx*=0.96;const c=document.createElementNS('http://www.w3.org/2000/svg','circle');c.setAttribute('cx',p.x);c.setAttribute('cy',p.y);c.setAttribute('r',p.r*(0.5+p.life*0.5));c.setAttribute('fill','#d9c7a5');c.setAttribute('stroke','#111');c.setAttribute('stroke-width','1.5');c.setAttribute('opacity',Math.max(0,p.life));dustG.appendChild(c);}}
function ensureAudio(){if(audioCtx)return;audioCtx=new (window.AudioContext||window.webkitAudioContext)();const len=audioCtx.sampleRate*0.5;noiseBuf=audioCtx.createBuffer(1,len,audioCtx.sampleRate);const d=noiseBuf.getChannelData(0);for(let i=0;i<len;i++)d[i]=Math.random()*2-1;}
function kick(){if(!audioCtx)return;const o=audioCtx.createOscillator(),g=audioCtx.createGain();o.type='sine';o.frequency.setValueAtTime(150,audioCtx.currentTime);o.frequency.exponentialRampToValueAtTime(40,audioCtx.currentTime+0.12);g.gain.setValueAtTime(0.5,audioCtx.currentTime);g.gain.exponentialRampToValueAtTime(0.001,audioCtx.currentTime+0.18);o.connect(g).connect(audioCtx.destination);o.start();o.stop(audioCtx.currentTime+0.2);}
function snare(){if(!audioCtx)return;const s=audioCtx.createBufferSource();s.buffer=noiseBuf;const f=audioCtx.createBiquadFilter();f.type='highpass';f.frequency.value=1200;const g=audioCtx.createGain();g.gain.setValueAtTime(0.35,audioCtx.currentTime);g.gain.exponentialRampToValueAtTime(0.001,audioCtx.currentTime+0.12);s.connect(f).connect(g).connect(audioCtx.destination);s.start();s.stop(audioCtx.currentTime+0.15);}
function hat(){if(!audioCtx)return;const s=audioCtx.createBufferSource();s.buffer=noiseBuf;const f=audioCtx.createBiquadFilter();f.type='highpass';f.frequency.value=6000;const g=audioCtx.createGain();g.gain.setValueAtTime(0.12,audioCtx.currentTime);g.gain.exponentialRampToValueAtTime(0.001,audioCtx.currentTime+0.05);s.connect(f).connect(g).connect(audioCtx.destination);s.start();s.stop(audioCtx.currentTime+0.06);}
const dots=document.querySelectorAll('.beat-dots i');
const crowdHeads=[...document.querySelectorAll('#crowd .head')];
const crowdPos=crowdHeads.map(h=>{const m=h.getAttribute('transform').match(/-?\d+/g);return{x:+m[0],y:+m[1]};});
function frame(now){const dt=Math.min(0.05,(now-lastTime)/1000);lastTime=now;const timeScale=slowmo?0.35:1;beatClock+=dt*(bpm/60)*timeScale;const prog=(beatClock-moveStartBeat)/currentMove.beats;
if(currentMove.hits)for(const h of currentMove.hits)if(prog>=h&&!moveHitsFired.has(h)){moveHitsFired.add(h);spawnDust(450,530,7);}
const beatInt=Math.floor(beatClock);
if(beatInt!==lastBeatInt){lastBeatInt=beatInt;dots.forEach((d,i)=>d.classList.toggle('on',i===beatInt%4));if(soundOn&&audioCtx){const b=beatInt%4;if(b===0||b===2)kick();else snare();hat();}}
const bp=beatClock%1;
crowdHeads.forEach((h,i)=>{const y=-Math.abs(Math.sin(bp*Math.PI+i*0.7))*7;h.setAttribute('transform',`translate(${crowdPos[i].x} ${crowdPos[i].y+y})`);});
let pose=sample(currentMove,Math.min(1,Math.max(0,prog)));
if(currentName==='freeze'&&prog>0.38&&prog<0.7){pose.rrot+=(Math.random()-0.5)*2.5;pose.ry+=(Math.random()-0.5)*3;pose.trot+=(Math.random()-0.5)*2;}
const vel=(pose.ry-prevRy)/Math.max(dt,0.001);prevRy=pose.ry;velSmooth=velSmooth*0.8+vel*0.2;
const targetY=velSmooth>120?0.86:velSmooth<-120?1.08:1;squashY+=(targetY-squashY)*0.18;const squashX=1+(1-squashY)*0.85;
headLag=Math.sin(beatClock*Math.PI*2)*3+velSmooth*0.02;
bird.setAttribute('transform',`translate(${pose.rx} ${pose.ry}) rotate(${pose.rrot})`);
torsoG.setAttribute('transform',`rotate(${pose.trot}) scale(${squashX} ${squashY})`);
headG.setAttribute('transform',`translate(0 -88) rotate(${pose.hrot+headLag})`);
wingBack.setAttribute('transform',`translate(-30 -58) rotate(${pose.shL})`);foreBack.setAttribute('transform',`translate(0 34) rotate(${pose.elL})`);
wingFront.setAttribute('transform',`translate(30 -58) rotate(${pose.shR})`);foreFront.setAttribute('transform',`translate(0 34) rotate(${pose.elR})`);
legL.setAttribute('transform',`translate(-16 0) rotate(${pose.hipL})`);shinL.setAttribute('transform',`translate(0 40) rotate(${pose.kneeL})`);footL.setAttribute('transform',`translate(0 36) rotate(${pose.ankL})`);
legR.setAttribute('transform',`translate(16 0) rotate(${pose.hipR})`);shinR.setAttribute('transform',`translate(0 40) rotate(${pose.kneeR})`);footR.setAttribute('transform',`translate(0 36) rotate(${pose.ankR})`);
tailG.setAttribute('transform',`rotate(${Math.sin(beatClock*6)*8+pose.trot*0.5})`);
const hgt=Math.max(0,470-pose.ry);const sc=Math.max(0.45,1-hgt/260);shadow.setAttribute('cx',pose.rx);shadow.setAttribute('rx',90*sc);shadow.setAttribute('opacity',0.28*sc+0.05);
updateDust(dt);
if(prog>=1){if(queue.length){if(currentName!=='recovery')startMove('recovery');else startMove(queue.shift());}else if(currentName==='recovery'){if(auto)startMove(pickAuto());else startMove('recovery');}else startMove('recovery');}
requestAnimationFrame(frame);}
$('bpm').addEventListener('input',e=>{bpm=+e.target.value;$('bpmVal').textContent=bpm;});
function progNearEnd(){return (beatClock-moveStartBeat)/currentMove.beats>0.85;}
document.querySelectorAll('[data-move]').forEach(b=>{b.addEventListener('click',()=>{const m=b.dataset.move;if(currentName==='recovery'||progNearEnd()){queue=[];startMove(m);}else{if(!queue.includes(m))queue.push(m);b.classList.add('queued');}});});
const outfits=[{jacket:'#e8a317',pants:'#17705f',sneaker:'#ff5a3c',band:'#ff5a3c'},{jacket:'#ff6b4a',pants:'#1e2a5a',sneaker:'#b8e62e',band:'#1e2a5a'},{jacket:'#7ac74f',pants:'#5b2a86',sneaker:'#f2f2f2',band:'#5b2a86'},{jacket:'#222222',pants:'#e8a317',sneaker:'#17705f',band:'#e8a317'}];
document.querySelectorAll('[data-outfit]').forEach(b=>{b.addEventListener('click',()=>{document.querySelectorAll('[data-outfit]').forEach(x=>x.classList.remove('active'));b.classList.add('active');const o=outfits[+b.dataset.outfit];document.querySelectorAll('.jacket').forEach(el=>el.setAttribute('fill',o.jacket));document.querySelectorAll('.pants').forEach(el=>el.setAttribute('fill',o.pants));document.querySelectorAll('.sneaker').forEach(el=>el.setAttribute('fill',o.sneaker));document.querySelectorAll('.headband').forEach(el=>el.setAttribute('stroke',o.band));});});
$('soundBtn').addEventListener('click',()=>{soundOn=!soundOn;if(soundOn){ensureAudio();audioCtx.resume();}$('soundBtn').textContent='Sound: '+(soundOn?'On':'Off');$('soundBtn').classList.toggle('active',soundOn);});
$('slowBtn').addEventListener('click',()=>{slowmo=!slowmo;$('slowBtn').classList.toggle('active',slowmo);});
$('autoBtn').addEventListener('click',()=>{auto=!auto;$('autoBtn').classList.toggle('active',auto);$('autoBtn').textContent=auto?'Auto DJ':'Auto DJ: Off';});
startMove('toprock');requestAnimationFrame(frame);
</script>
</body>
</html>