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 feral city pigeon in a backwards red cap, gold chain and tiny white sneakers — the undisputed king of the alley cardboard. I went for risograph-poster meets Saturday-morning cartoon: thick ink outlines, dusty halftone grain, iridescent neck sheen and a boombox that actually bumps.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>PIGEON BREAKS — Street Bird Battle</title>
<style>
*{box-sizing:border-box;margin:0}
html,body{height:100%;background:#0e0e0e;color:#f2e8cf;font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;overflow:hidden}
#wrap{position:relative;width:100vw;height:100vh;display:flex;flex-direction:column;background:#f2e8cf}
header{
display:flex;align-items:center;justify-content:space-between;
background:#111;border-bottom:4px solid #111;padding:10px 14px;gap:12px;
position:relative;z-index:3;
}
header h1{font-size:clamp(13px,2.2vw,20px);letter-spacing:.14em;font-weight:900;line-height:1}
header h1 span{color:#e9c46a;background:#111;padding:2px 6px;border:2px solid #f2e8cf;transform:rotate(-1deg);display:inline-block}
header h1 i{font-style:normal;color:#2a9d8f}
.sub{font-size:10px;letter-spacing:.18em;opacity:.9;text-align:right;line-height:1.2}
.sub b{color:#e9c46a}
#c{flex:1;display:block;width:100%;height:100%;background:#f2e8cf;cursor:grab}
#c:active{cursor:grabbing}
#ui{
position:absolute;left:10px;right:10px;bottom:10px;
display:flex;flex-wrap:wrap;gap:8px;z-index:5;pointer-events:none;
}
.panel{
pointer-events:auto;
background:#111;border:2px solid #f2e8cf;box-shadow:6px 6px 0 #000;
padding:8px 10px;display:flex;align-items:center;gap:8px;flex-wrap:wrap;
}
.panel label{font-size:11px;letter-spacing:.12em;font-weight:800;display:flex;align-items:center;gap:8px}
.panel input[type=range]{width:110px;accent-color:#e9c46a}
.read{font-size:11px;background:#f2e8cf;color:#111;padding:3px 6px;border:2px solid #000;font-weight:900;min-width:74px;text-align:center}
button{
appearance:none;border:2px solid #000;background:#e9c46a;color:#111;
font:900 11px/1 ui-monospace,monospace;letter-spacing:.08em;padding:7px 10px;cursor:pointer;
box-shadow:3px 3px 0 #000;transition:transform .06s;
}
button:hover{transform:translate(-1px,-1px);box-shadow:4px 4px 0 #000}
button:active{transform:translate(2px,2px);box-shadow:1px 1px 0 #000}
button.alt{background:#2a9d8f;color:#fff}
button.hot{background:#c44536;color:#fff}
button.ghost{background:#f2e8cf}
.hint{font-size:10px;opacity:.8;letter-spacing:.08em}
@media(max-width:700px){
#ui{bottom:6px;left:6px;right:6px}
.panel{padding:6px 8px}
header{padding:8px 10px}
}
</style>
</head>
<body>
<div id="wrap">
<header>
<h1><span>PIGEON BREAKS</span> <i>— CARD-BOARD BATTLE 98</i></h1>
<div class="sub">BREAKDANCE OR BUST<br><b>TOPROCK → FOOTWORK → POWER → FREEZE</b></div>
</header>
<canvas id="c" width="1200" height="700" aria-label="Breakdancing pigeon stage"></canvas>
<div id="ui">
<div class="panel">
<label>TEMPO <input id="tempo" type="range" min="72" max="158" value="98" step="1"></label>
<div class="read" id="bpmRead">98 BPM</div>
<span class="hint">drag needle →</span>
</div>
<div class="panel">
<button data-move="toprock">TOP ROCK</button>
<button data-move="sixstep">6-STEP</button>
<button data-move="freeze">FREEZE</button>
<button data-move="windmill">WINDMILL</button>
<button data-move="headspin">HEADSPIN</button>
</div>
<div class="panel">
<button id="hypeBtn" class="hot">🔥 HYPE + CROWD</button>
<button id="seedBtn" class="ghost">🍞 TOSS SEEDS</button>
<label>FLOOR <input id="floor" type="range" min="0" max="100" value="35"></label>
</div>
<div class="panel hint">CLICK CARDBOARD FOR DUST • HOLD & DRAG BOOMBOX NEEDLE TO SCRATCH • SPACE = HYPE • 1-5 = MOVES</div>
</div>
</div>
<script>
(() => {
const canvas = document.getElementById('c');
const ctx = canvas.getContext('2d');
const tempoEl = document.getElementById('tempo');
const floorEl = document.getElementById('floor');
const bpmRead = document.getElementById('bpmRead');
const hypeBtn = document.getElementById('hypeBtn');
const seedBtn = document.getElementById('seedBtn');
let W=1200,H=700, DPR=Math.min(2, window.devicePixelRatio||1);
let groundY = 520;
let bpm = 98;
let floorVal = 35;
let hype = 0;
let hypeTime = 0;
let shake = 0;
let scratch = 0;
let isDragging = false;
function resize(){
const rect = canvas.getBoundingClientRect();
W = rect.width; H = rect.height;
canvas.width = Math.round(W*DPR);
canvas.height = Math.round(H*DPR);
ctx.setTransform(DPR,0,0,DPR,0,0);
groundY = H*0.78;
}
window.addEventListener('resize', resize);
resize();
// particles
const dust=[];
const seeds=[];
const feathers=[];
let birdX = 0, birdY=0;
// moves
const moves = [
{name:'toprock', dur:2100},
{name:'drop', dur:520},
{name:'sixstep', dur:3000},
{name:'freeze', dur:1500},
{name:'windmill', dur:2400},
{name:'headspin', dur:2600},
{name:'flare', dur:1900},
{name:'pose', dur:1300},
];
let moveIndex=0;
let moveT=0;
let moveDur=moves[0].dur;
let queuedMove=null;
function queueMove(name){
const idx = moves.findIndex(m=>m.name===name);
if(idx>=0){ queuedMove = idx; // jump soon
// if currently in toprock/pose, cut short
if(['toprock','pose'].includes(moves[moveIndex].name)) moveT = moveDur*0.85;
}
}
document.querySelectorAll('[data-move]').forEach(b=>{
b.addEventListener('click',()=>queueMove(b.dataset.move));
});
tempoEl.addEventListener('input', e=>{ bpm=+e.target.value; bpmRead.textContent=bpm+' BPM'; });
floorEl.addEventListener('input', e=>{ floorVal=+e.target.value; });
hypeBtn.addEventListener('click', triggerHype);
seedBtn.addEventListener('click', tossSeeds);
function triggerHype(){
hype=1; hypeTime=0; shake=14;
for(let i=0;i<18;i++) feathers.push({x: W*0.5 + (Math.random()-0.5)*120, y: groundY-80, vx:(Math.random()-0.5)*10, vy:-Math.random()*9-2, r:Math.random()*Math.PI, vr:(Math.random()-0.5)*0.5, life:1});
for(let i=0;i<10;i++) dust.push({x:W*0.5+(Math.random()-0.5)*180, y:groundY-2, vx:(Math.random()-0.5)*6, vy:-Math.random()*4, life:1, r: 4+Math.random()*8});
}
function tossSeeds(){
for(let i=0;i<7;i++){
seeds.push({x: W*0.5 + (Math.random()-0.5)*260, y: 60+Math.random()*40, vx:(Math.random()-0.5)*2, vy:0, rot:Math.random()*Math.PI, vr:(Math.random()-0.5)*0.2, bounce:0});
}
shake=6;
}
// mouse interaction
let mouse={x:0,y:0,down:false};
canvas.addEventListener('pointerdown', e=>{
const r=canvas.getBoundingClientRect();
mouse.x=e.clientX-r.left; mouse.y=e.clientY-r.top; mouse.down=true;
// check boombox area
const bx=W*0.78, by=groundY-92;
if(mouse.x>bx-90 && mouse.x<bx+90 && mouse.y>by-46 && mouse.y<by+46){
isDragging=true;
} else {
// dust puff
dust.push({x:mouse.x, y:groundY-2, vx:(Math.random()-0.5)*5, vy:-Math.random()*5-1, life:1, r:6+Math.random()*10});
if(Math.random()<0.3) triggerHype();
}
});
canvas.addEventListener('pointermove', e=>{
const r=canvas.getBoundingClientRect();
mouse.x=e.clientX-r.left; mouse.y=e.clientY-r.top;
if(isDragging){
// scratch: map x to tempo offset
const bx=W*0.78;
let off = (mouse.x - bx)/70; // -1.3 to 1.3
scratch = off*0.9;
bpmRead.textContent = (bpm + Math.round(off*18)) + ' BPM *SCRATCH*';
}
});
window.addEventListener('pointerup', ()=>{
isDragging=false; scratch=0; bpmRead.textContent=bpm+' BPM';
});
canvas.addEventListener('click', e=>{
// also seed chance
if(Math.random()<0.15) tossSeeds();
});
window.addEventListener('keydown', e=>{
if(e.code==='Space'){ e.preventDefault(); triggerHype(); }
if(e.key==='1') queueMove('toprock');
if(e.key==='2') queueMove('sixstep');
if(e.key==='3') queueMove('freeze');
if(e.key==='4') queueMove('windmill');
if(e.key==='5') queueMove('headspin');
});
// helpers
const lerp=(a,b,t)=>a+(b-a)*t;
const clamp=(v,a,b)=>Math.max(a,Math.min(b,v));
function easeInOutCubic(t){return t<0.5?4*t*t*t:1-Math.pow(-2*t+2,3)/2}
function easeOutBack(t){const c1=1.70158,c3=c1+1;return 1+c3*Math.pow(t-1,3)+c1*Math.pow(t-1,2)}
function easeOutBounce(t){
const n1=7.5625,d1=2.75;
if(t<1/d1) return n1*t*t;
else if(t<2/d1) return n1*(t-=1.5/d1)*t+0.75;
else if(t<2.5/d1) return n1*(t-=2.25/d1)*t+0.9375;
else return n1*(t-=2.625/d1)*t+0.984375;
}
// drawing helpers
function drawCardboard(){
// base
ctx.save();
// cardboard color with tape
const cardY = groundY;
ctx.fillStyle='#d8c3a0';
ctx.fillRect(0, cardY, W, H-cardY);
// cardboard corrugation lines
ctx.strokeStyle='rgba(0,0,0,0.07)';
ctx.lineWidth=1;
for(let y=cardY; y<H; y+=6){
ctx.beginPath(); ctx.moveTo(0,y+ (y%12===0?0:1)); ctx.lineTo(W,y); ctx.stroke();
}
// tape
ctx.fillStyle='#c9b08a';
ctx.fillRect(W*0.18, cardY, 44, H-cardY);
ctx.fillRect(W*0.62, cardY, 36, H-cardY);
ctx.fillStyle='rgba(0,0,0,0.08)';
ctx.fillRect(W*0.18, cardY, 44, 8);
ctx.fillRect(W*0.62, cardY, 36, 8);
// scuff marks
ctx.strokeStyle='rgba(0,0,0,0.09)';
ctx.lineWidth=2;
for(let i=0;i<6;i++){
const x = (W*0.15 + i*W*0.14) + Math.sin(i)*20;
ctx.beginPath();
ctx.moveTo(x, cardY+12);
ctx.lineTo(x+ (i%2? 40:-30), cardY+28);
ctx.stroke();
}
// edge shadow
ctx.fillStyle='rgba(0,0,0,0.12)';
ctx.fillRect(0, cardY, W, 10);
ctx.restore();
}
function drawWall(){
// brick wall
ctx.save();
ctx.fillStyle='#e9c46a';
ctx.fillRect(0,0,W, groundY);
// vignette
const g = ctx.createLinearGradient(0,0,0,groundY);
g.addColorStop(0,'rgba(0,0,0,0.18)');
g.addColorStop(0.5,'rgba(0,0,0,0)');
g.addColorStop(1,'rgba(0,0,0,0.22)');
ctx.fillStyle=g; ctx.fillRect(0,0,W,groundY);
// bricks
ctx.strokeStyle='rgba(17,17,17,0.18)';
ctx.lineWidth=2;
const bh=28, bw=86;
for(let y=10; y<groundY-10; y+=bh){
const offset = (Math.floor(y/bh)%2)? bw/2 : 0;
for(let x=-bw; x<W+bw; x+=bw){
ctx.strokeRect(x+offset, y, bw-2, bh-2);
// brick shading
ctx.fillStyle='rgba(196,69,54,0.08)';
if((x+y)%3===0) ctx.fillRect(x+offset+2,y+2,bw-6,bh-6);
}
}
// graffiti tag
ctx.save();
ctx.translate(W*0.14, 78);
ctx.rotate(-0.06);
ctx.fillStyle='#111';
ctx.font='900 42px ui-monospace, monospace';
ctx.fillText('PIGEON',0,0);
ctx.fillStyle='#c44536';
ctx.fillText('BREAKS',0,34);
ctx.fillStyle='#2a9d8f';
ctx.font='800 14px ui-monospace, monospace';
ctx.fillText('★ EST. ALLEYWAY ★ DO NOT FEED AFTER MIDNIGHT',0,56);
// crown
ctx.fillStyle='#e9c46a';
ctx.beginPath(); ctx.moveTo(210,-38); ctx.lineTo(225,-58); ctx.lineTo(235,-40); ctx.lineTo(245,-60); ctx.lineTo(255,-38); ctx.closePath(); ctx.fill();
ctx.strokeStyle='#111'; ctx.lineWidth=2; ctx.stroke();
ctx.restore();
// posters
ctx.fillStyle='#f2e8cf'; ctx.strokeStyle='#111'; ctx.lineWidth=3;
ctx.fillRect(W*0.68, 22, 140, 92); ctx.strokeRect(W*0.68,22,140,92);
ctx.fillStyle='#111'; ctx.font='900 11px ui-monospace, monospace';
ctx.fillText('TONIGHT!', W*0.68+10, 42);
ctx.fillText('BATTLE ROYALE', W*0.68+10, 58);
ctx.fillStyle='#c44536'; ctx.fillRect(W*0.68+10,66,120,10);
ctx.fillStyle='#fff'; ctx.font='800 9px ui-monospace, monospace';
ctx.fillText('WINNER GETS BAGEL', W*0.68+14, 74);
ctx.restore();
}
function drawBoombox(beatPhase){
const bx=W*0.78, by=groundY-92;
ctx.save();
ctx.translate(bx,by);
// shadow
ctx.fillStyle='rgba(0,0,0,0.18)';
ctx.beginPath(); ctx.ellipse(0,46,92,14,0,0,Math.PI*2); ctx.fill();
// body
ctx.fillStyle='#111';
ctx.strokeStyle='#000'; ctx.lineWidth=3;
// main box with slight perspective
ctx.beginPath();
ctx.roundRect(-88,-42,176,84,10);
ctx.fill(); ctx.stroke();
// speakers
const pulse = 1 + Math.sin(beatPhase*Math.PI*2)*0.06 + hype*0.04;
ctx.fillStyle='#2b2b2b';
ctx.beginPath(); ctx.arc(-48,0,30,0,Math.PI*2); ctx.fill();
ctx.beginPath(); ctx.arc(48,0,30,0,Math.PI*2); ctx.fill();
ctx.strokeStyle='#f2e8cf'; ctx.lineWidth=2;
ctx.stroke();
// speaker cones pulsing
ctx.fillStyle='#444';
ctx.beginPath(); ctx.arc(-48,0,18*pulse,0,Math.PI*2); ctx.fill();
ctx.beginPath(); ctx.arc(48,0,18*pulse,0,Math.PI*2); ctx.fill();
ctx.fillStyle='#e9c46a';
ctx.beginPath(); ctx.arc(-48,0,6,0,Math.PI*2); ctx.fill();
ctx.beginPath(); ctx.arc(48,0,6,0,Math.PI*2); ctx.fill();
// cassette
ctx.fillStyle='#f2e8cf';
ctx.fillRect(-22,-10,44,20);
ctx.strokeRect(-22,-10,44,20);
ctx.fillStyle='#111';
ctx.beginPath(); ctx.arc(-10,0,7,0,Math.PI*2); ctx.fill();
ctx.beginPath(); ctx.arc(10,0,7,0,Math.PI*2); ctx.fill();
// spinning reels
const spin = Date.now()*0.008*(1+scratch*0.6);
ctx.fillStyle='#2a9d8f';
for(let k of [-10,10]){
ctx.save(); ctx.translate(k,0); ctx.rotate(spin);
ctx.fillRect(-6,-2,12,4); ctx.fillRect(-2,-6,4,12);
ctx.restore();
}
// VU meter
ctx.fillStyle='#000'; ctx.fillRect(-68,28,136,10);
const vu = 0.3 + 0.7*Math.abs(Math.sin(beatPhase*Math.PI*2 + Math.sin(Date.now()*0.002)*0.5));
ctx.fillStyle= vu>0.8 ? '#c44536' : '#2a9d8f';
ctx.fillRect(-66,30, 132*vu, 6);
// needle
ctx.strokeStyle='#e9c46a'; ctx.lineWidth=2;
ctx.beginPath(); ctx.moveTo(0,-42); ctx.lineTo(scratch*18, -52); ctx.stroke();
ctx.fillStyle='#e9c46a'; ctx.beginPath(); ctx.arc(scratch*18,-52,4,0,Math.PI*2); ctx.fill();
// text
ctx.fillStyle='#f2e8cf'; ctx.font='800 8px ui-monospace, monospace';
ctx.fillText('JVC • BASS BOOST', -52, -28);
ctx.restore();
}
// bird drawing
function drawBird(state){
const {bx,by,brot,sx,sy, headRot, beakOpen, wingL, wingR, legL, legR, tailRot, eyeBlink, capTilt} = state;
ctx.save();
ctx.translate(bx,by);
ctx.rotate(brot);
ctx.scale(sx,sy);
// shadow on ground (inverse transform for shadow)
ctx.save();
ctx.rotate(-brot);
const shScale = clamp(1 - (groundY - by)/220, 0.3, 1);
ctx.globalAlpha=0.18*shScale;
ctx.fillStyle='#000';
ctx.beginPath(); ctx.ellipse(0, groundY-by, 70*shScale, 14*shScale,0,0,Math.PI*2); ctx.fill();
ctx.restore();
// tail
ctx.save();
ctx.translate(-46, 6);
ctx.rotate(tailRot);
ctx.fillStyle='#5a5d63';
ctx.strokeStyle='#111'; ctx.lineWidth=3;
for(let i=-1;i<=1;i++){
ctx.save(); ctx.rotate(i*0.35);
ctx.beginPath(); ctx.ellipse(-10,0,18,7,0,0,Math.PI*2); ctx.fill(); ctx.stroke();
ctx.restore();
}
ctx.restore();
// legs (behind body)
function drawLeg(side, ang){
const hipX = side*14, hipY=18;
const thighLen=26, shinLen=28, footLen=26;
const hipA = ang.hip, kneeA=ang.knee, ankleA=ang.ankle;
ctx.save();
ctx.translate(hipX, hipY);
ctx.rotate(hipA);
// thigh (baggy leg warmer)
ctx.strokeStyle='#111'; ctx.lineWidth=7; ctx.lineCap='round';
ctx.beginPath(); ctx.moveTo(0,0); ctx.lineTo(0,thighLen); ctx.stroke();
ctx.strokeStyle='#2a9d8f'; ctx.lineWidth=6;
ctx.beginPath(); ctx.moveTo(0,0); ctx.lineTo(0,thighLen); ctx.stroke();
// stripes
ctx.strokeStyle='#e9c46a'; ctx.lineWidth=2;
ctx.beginPath(); ctx.moveTo(-4,10); ctx.lineTo(4,10); ctx.stroke();
ctx.beginPath(); ctx.moveTo(-4,16); ctx.lineTo(4,16); ctx.stroke();
ctx.translate(0,thighLen);
ctx.rotate(kneeA);
// shin
ctx.strokeStyle='#111'; ctx.lineWidth=6; ctx.beginPath(); ctx.moveTo(0,0); ctx.lineTo(0,shinLen); ctx.stroke();
ctx.strokeStyle='#f2e8cf'; ctx.lineWidth=5; ctx.beginPath(); ctx.moveTo(0,0); ctx.lineTo(0,shinLen); ctx.stroke();
// leg band
ctx.fillStyle='#c44536'; ctx.fillRect(-5, shinLen-8,10,6);
ctx.translate(0,shinLen);
ctx.rotate(ankleA);
// sneaker
ctx.fillStyle='#fff'; ctx.strokeStyle='#111'; ctx.lineWidth=2.5;
ctx.beginPath();
ctx.roundRect(-9,-4,22,12,4);
ctx.fill(); ctx.stroke();
ctx.fillStyle='#e9c46a';
ctx.beginPath(); ctx.ellipse(4,6,10,3,0,0,Math.PI*2); ctx.fill();
ctx.fillStyle='#111';
ctx.beginPath(); ctx.arc(8,-1,1.2,0,Math.PI*2); ctx.fill();
ctx.beginPath(); ctx.arc(4,-1,1.2,0,Math.PI*2); ctx.fill();
// swoosh
ctx.strokeStyle='#c44536'; ctx.lineWidth=1.5;
ctx.beginPath(); ctx.moveTo(-4,2); ctx.quadraticCurveTo(2,4,10,1); ctx.stroke();
ctx.restore();
}
drawLeg(-1, legL);
drawLeg(1, legR);
// body
ctx.save();
// body base
ctx.fillStyle='#8a8d93';
ctx.strokeStyle='#111'; ctx.lineWidth=4;
ctx.beginPath();
ctx.ellipse(0,0,52,38,0,0,Math.PI*2);
ctx.fill(); ctx.stroke();
// belly lighter
ctx.fillStyle='#d9dadb';
ctx.beginPath(); ctx.ellipse(2,10,34,22,0,0,Math.PI*2); ctx.fill();
// chest iridescent
const grad = ctx.createLinearGradient(-20,-30,20,10);
grad.addColorStop(0,'#2a9d8f'); grad.addColorStop(0.5,'#6a4c93'); grad.addColorStop(1,'#2a9d8f');
ctx.fillStyle=grad;
ctx.beginPath(); ctx.ellipse(-8,-12,22,18, -0.3,0,Math.PI*2); ctx.fill();
// highlight dots
ctx.fillStyle='rgba(255,255,255,0.35)';
for(let i=0;i<3;i++){
ctx.beginPath(); ctx.arc(-14+i*6, -16+i*2, 1.5,0,Math.PI*2); ctx.fill();
}
// wing attachment shadow
ctx.fillStyle='rgba(0,0,0,0.12)';
ctx.beginPath(); ctx.ellipse(-18, -4, 14,10,0,0,Math.PI*2); ctx.fill();
ctx.beginPath(); ctx.ellipse(18, -4, 14,10,0,0,Math.PI*2); ctx.fill();
ctx.restore();
// wings as arms
function drawWing(side, ang, isSupport){
const sx = side*18, sy=-6;
const l1=34, l2=30;
ctx.save();
ctx.translate(sx,sy);
ctx.rotate(ang.a1);
// upper wing (arm)
ctx.strokeStyle='#111'; ctx.lineWidth=8; ctx.lineCap='round';
ctx.beginPath(); ctx.moveTo(0,0); ctx.lineTo(0,l1); ctx.stroke();
ctx.strokeStyle= side<0 ? '#6b6e73' : '#7a7d83';
ctx.lineWidth=6.5; ctx.beginPath(); ctx.moveTo(0,0); ctx.lineTo(0,l1); ctx.stroke();
// elbow
ctx.fillStyle='#111'; ctx.beginPath(); ctx.arc(0,l1,5,0,Math.PI*2); ctx.fill();
ctx.translate(0,l1);
ctx.rotate(ang.a2);
// forearm
ctx.strokeStyle='#111'; ctx.lineWidth=7; ctx.beginPath(); ctx.moveTo(0,0); ctx.lineTo(0,l2); ctx.stroke();
ctx.strokeStyle='#8a8d93'; ctx.lineWidth=5.5; ctx.beginPath(); ctx.moveTo(0,0); ctx.lineTo(0,l2); ctx.stroke();
// hand / wing tip (mitten with feathers)
ctx.translate(0,l2);
// if supporting, draw splayed hand on ground
if(isSupport){
ctx.fillStyle='#f2e8cf'; ctx.strokeStyle='#111'; ctx.lineWidth=2;
ctx.beginPath(); ctx.ellipse(0,4,14,7,0,0,Math.PI*2); ctx.fill(); ctx.stroke();
// fingers
ctx.fillStyle='#111';
for(let i=-1;i<=1;i++){ ctx.beginPath(); ctx.ellipse(i*6,6,3,2,0,0,Math.PI*2); ctx.fill(); }
} else {
// feathered hand
ctx.rotate(ang.hand||0);
ctx.fillStyle='#111';
ctx.beginPath(); ctx.ellipse(0,6,10,8,0,0,Math.PI*2); ctx.fill();
ctx.fillStyle='#f2e8cf'; ctx.beginPath(); ctx.ellipse(0,6,7,5,0,0,Math.PI*2); ctx.fill();
// feathers
ctx.fillStyle='#8a8d93'; ctx.strokeStyle='#111'; ctx.lineWidth=1.5;
for(let i=-1;i<=1;i++){
ctx.save(); ctx.rotate(i*0.5);
ctx.beginPath(); ctx.ellipse(0,12,4,7,0,0,Math.PI*2); ctx.fill(); ctx.stroke();
ctx.restore();
}
// thumb
ctx.fillStyle='#f2e8cf'; ctx.beginPath(); ctx.ellipse(-6,2,4,3, -0.6,0,Math.PI*2); ctx.fill(); ctx.stroke();
}
ctx.restore();
}
// determine support: if body low and move is floor
const isFloor = ['sixstep','freeze','windmill','flare','headspin'].includes(moves[moveIndex].name);
drawWing(-1, wingL, isFloor && wingL.support);
drawWing(1, wingR, isFloor && wingR.support);
// head
ctx.save();
ctx.translate(28,-22);
ctx.rotate(headRot);
// neck
ctx.fillStyle='#8a8d93'; ctx.beginPath(); ctx.ellipse(-6,8,12,10,0,0,Math.PI*2); ctx.fill();
// head base
ctx.fillStyle='#d9dadb'; ctx.strokeStyle='#111'; ctx.lineWidth=3.5;
ctx.beginPath(); ctx.arc(0,0,24,0,Math.PI*2); ctx.fill(); ctx.stroke();
// cap
ctx.save(); ctx.rotate(capTilt);
ctx.fillStyle='#c44536'; ctx.strokeStyle='#111'; ctx.lineWidth=2.5;
ctx.beginPath(); ctx.ellipse(2,-10,20,14,0,0,Math.PI*2); ctx.fill(); ctx.stroke();
ctx.fillStyle='#111'; ctx.beginPath(); ctx.ellipse(2,-10,20,4,0,0,Math.PI*2); ctx.fill();
// brim backwards
ctx.fillStyle='#c44536'; ctx.beginPath(); ctx.ellipse(18,-6,14,6,0.2,0,Math.PI*2); ctx.fill(); ctx.stroke();
ctx.fillStyle='#e9c46a'; ctx.font='900 7px ui-monospace, monospace'; ctx.fillText('P', -4,-6);
ctx.restore();
// beak
ctx.fillStyle='#e9c46a'; ctx.strokeStyle='#111'; ctx.lineWidth=2.5;
ctx.beginPath();
ctx.moveTo(18,-4); ctx.lineTo(32,0); ctx.lineTo(18,6); ctx.closePath(); ctx.fill(); ctx.stroke();
// nostril
ctx.fillStyle='#111'; ctx.beginPath(); ctx.arc(20,-1,1.2,0,Math.PI*2); ctx.fill();
// beak open
if(beakOpen>0.1){
ctx.fillStyle='#c44536';
ctx.beginPath(); ctx.moveTo(22,0); ctx.lineTo(28, beakOpen*6); ctx.lineTo(22, beakOpen*4); ctx.closePath(); ctx.fill();
}
// eye
const eyeX=6, eyeY=-6;
ctx.fillStyle='#fff'; ctx.beginPath(); ctx.arc(eyeX,eyeY,7,0,Math.PI*2); ctx.fill(); ctx.stroke();
if(eyeBlink<0.9){
ctx.fillStyle='#111'; ctx.beginPath(); ctx.arc(eyeX+1,eyeY,4.5,0,Math.PI*2); ctx.fill();
ctx.fillStyle='#fff'; ctx.beginPath(); ctx.arc(eyeX+2.5,eyeY-1.5,1.6,0,Math.PI*2); ctx.fill();
// angry brow
ctx.strokeStyle='#111'; ctx.lineWidth=2; ctx.beginPath(); ctx.moveTo(0,-12); ctx.lineTo(12,-10); ctx.stroke();
} else {
// blink line
ctx.strokeStyle='#111'; ctx.lineWidth=2; ctx.beginPath(); ctx.moveTo(eyeX-6,eyeY); ctx.lineTo(eyeX+6,eyeY); ctx.stroke();
}
// cheek blush when hype
if(hype>0.2){
ctx.fillStyle='rgba(196,69,54,0.35)'; ctx.beginPath(); ctx.ellipse(-2,6,6,3,0,0,Math.PI*2); ctx.fill();
}
// gold chain
ctx.strokeStyle='#e9c46a'; ctx.lineWidth=2.5; ctx.beginPath();
ctx.arc(-4,14,10,0.2,Math.PI-0.2); ctx.stroke();
ctx.fillStyle='#e9c46a'; ctx.strokeStyle='#111'; ctx.lineWidth=1.5;
ctx.beginPath(); ctx.roundRect(-8,20,8,8,2); ctx.fill(); ctx.stroke();
ctx.fillStyle='#111'; ctx.font='800 6px ui-monospace, monospace'; ctx.fillText('P',-6,26);
ctx.restore();
ctx.restore();
}
// main loop
let last=performance.now();
let beatPhase=0;
function frame(now){
requestAnimationFrame(frame);
const dt = Math.min(40, now-last); last=now;
const tempoScale = (bpm/98) * (1 + scratch*0.4);
moveT += dt * tempoScale;
if(moveT > moveDur){
moveT=0;
if(queuedMove!==null){ moveIndex=queuedMove; queuedMove=null; }
else { moveIndex = (moveIndex+1)%moves.length; }
moveDur = moves[moveIndex].dur * (0.95 + Math.random()*0.1);
// add dust on transition
if(['drop','windmill','headspin'].includes(moves[moveIndex].name)){
for(let i=0;i<6;i++) dust.push({x:W*0.5+(Math.random()-0.5)*80, y:groundY-2, vx:(Math.random()-0.5)*4, vy:-Math.random()*3, life:1, r:5+Math.random()*6});
shake=8;
}
}
const t = moveT/moveDur;
beatPhase = (now * (bpm/60000) ) % 1;
if(hype>0){ hypeTime+=dt; hype -= dt*0.0006; if(hypeTime>3000) hype= Math.max(0,hype -0.02); if(hype<0.01) hype=0; shake*=0.96; }
else shake*=0.92;
if(shake<0.1) shake=0;
// update seeds physics
for(let s of seeds){
s.vy += 0.45; s.x+=s.vx; s.y+=s.vy; s.rot+=s.vr;
if(s.y>groundY-4){ s.y=groundY-4; s.vy*=-0.35; s.vx*=0.85; s.bounce++; if(Math.abs(s.vy)<0.6) s.vy=0; }
}
// remove old seeds
for(let i=seeds.length-1;i>=0;i--) if(seeds[i].bounce>4) seeds.splice(i,1);
// dust
for(let d of dust){ d.x+=d.vx; d.y+=d.vy; d.vy+=0.12; d.life-=0.015; d.vx*=0.99; }
for(let i=dust.length-1;i>=0;i--) if(dust[i].life<=0) dust.splice(i,1);
for(let f of feathers){ f.x+=f.vx; f.y+=f.vy; f.vy+=0.18; f.vx*=0.99; f.r+=f.vr; f.life-=0.008; }
for(let i=feathers.length-1;i>=0;i--) if(feathers[i].life<=0) feathers.splice(i,1);
// compute bird state per move
let state = {
bx: W*0.5, by: groundY-62, brot:0, sx:1, sy:1,
headRot:0, beakOpen:0, tailRot:0, eyeBlink:0, capTilt:0.12,
wingL:{a1:-0.6,a2:0.9,hand:0, support:false},
wingR:{a1:0.6,a2:0.9,hand:0, support:false},
legL:{hip:0.2,knee:0.6,ankle:0},
legR:{hip:-0.2,knee:0.6,ankle:0},
};
const m = moves[moveIndex].name;
const globalBeat = beatPhase;
const bSin = Math.sin(globalBeat*Math.PI*2);
const bCos = Math.cos(globalBeat*Math.PI*2);
const hit = Math.pow(Math.max(0, Math.sin(globalBeat*Math.PI*2 + Math.PI*0.5)), 8); // sharp hit on downbeat
if(m==='toprock'){
// weight: bounce with hang time, squash on landing
const bounce = Math.abs(Math.sin(globalBeat*Math.PI*2));
const hang = Math.pow(bounce, 0.6);
state.by = groundY-62 - hang*16 - Math.sin(globalBeat*Math.PI*4)*2;
// squash on impact (when feet hit)
const impact = Math.max(0, Math.sin(globalBeat*Math.PI*2 -0.2));
const squash = Math.pow(impact,3)*0.22;
state.sy = 1 - squash*0.7;
state.sx = 1 + squash*0.5;
state.brot = bSin*0.06 + Math.sin(t*Math.PI*2)*0.04;
state.headRot = bSin*0.18;
state.tailRot = bSin*0.2;
state.beakOpen = hit*0.6;
// cross-step footwork: 2 beats per cycle
const step = (globalBeat*2)%1;
// legs cross
state.legL.hip = Math.sin(step*Math.PI*2)*0.45 -0.15;
state.legR.hip = Math.sin(step*Math.PI*2 + Math.PI)*0.45 +0.15;
state.legL.knee = 0.5 + Math.abs(Math.sin(step*Math.PI*2))*0.7;
state.legR.knee = 0.5 + Math.abs(Math.sin(step*Math.PI*2+Math.PI))*0.7;
state.legL.ankle = -state.legL.knee*0.3;
state.legR.ankle = -state.legR.knee*0.3;
// arms: toprock cross
state.wingL.a1 = -0.9 + bSin*0.7;
state.wingL.a2 = 1.0 + bCos*0.5;
state.wingR.a1 = 0.9 - bSin*0.7;
state.wingR.a2 = 1.0 - bCos*0.5;
state.wingL.hand = bSin*0.3;
state.wingR.hand = -bSin*0.3;
// occasional look
state.capTilt = 0.12 + bSin*0.05;
// eye blink
state.eyeBlink = (Math.random()<0.008?1:0);
// add subtle camera sway
state.bx += Math.sin(globalBeat*Math.PI*2)*6;
}
else if(m==='drop'){
// anticipation crouch then drop
const p = easeInOutCubic(t);
state.by = lerp(groundY-62, groundY-28, p);
state.brot = lerp(0, 0.35, p);
state.sy = 1 - Math.sin(p*Math.PI)*0.18;
state.sx = 1 + Math.sin(p*Math.PI)*0.15;
state.legL.hip = lerp(0.2, 0.9, p);
state.legR.hip = lerp(-0.2, 0.9, p);
state.legL.knee = lerp(0.6, 1.3, p);
state.legR.knee = lerp(0.6, 1.3, p);
state.wingL.a1 = lerp(-0.6, -1.2, p); state.wingL.a2=lerp(0.9,1.6,p);
state.wingR.a1 = lerp(0.6, 1.2, p); state.wingR.a2=lerp(0.9,1.6,p);
state.headRot = lerp(0, -0.4, p);
state.tailRot = -0.3;
if(t>0.7) shake=4;
}
else if(m==='sixstep'){
// low circular footwork - weight shifts
state.by = groundY-24 - Math.abs(Math.sin(t*Math.PI*6))*4; // bob with steps
state.bx = W*0.5 + Math.cos(t*Math.PI*2)*18;
state.brot = Math.sin(t*Math.PI*2)*0.18 -0.08;
// squash with each step
const stepPhase = (t*6)%1;
const stepHit = Math.pow(Math.max(0, Math.sin(stepPhase*Math.PI)),2);
state.sy = 1 - stepHit*0.12;
state.sx = 1 + stepHit*0.08;
// one wing supports, other gestures
const supportSide = Math.floor(t*6)%2===0;
state.wingL.support = supportSide;
state.wingR.support = !supportSide;
// support wing planted
if(supportSide){
state.wingL.a1 = -0.2; state.wingL.a2=0.3;
state.wingR.a1 = 0.8 + Math.sin(t*Math.PI*6)*0.4; state.wingR.a2=1.1;
} else {
state.wingR.a1 = 0.2; state.wingR.a2=0.3;
state.wingL.a1 = -0.8 - Math.sin(t*Math.PI*6)*0.4; state.wingL.a2=1.1;
}
// legs cycle around body
const legCycle = t*Math.PI*2*3; // 3 circles
state.legL.hip = Math.sin(legCycle)*0.9 +0.2;
state.legL.knee = 0.9 + Math.cos(legCycle)*0.5;
state.legL.ankle = -0.4;
state.legR.hip = Math.sin(legCycle+Math.PI)*0.9 +0.2;
state.legR.knee = 0.9 + Math.cos(legCycle+Math.PI)*0.5;
state.legR.ankle = -0.4;
state.headRot = Math.sin(t*Math.PI*4)*0.5;
state.tailRot = Math.sin(t*Math.PI*6)*0.4;
state.beakOpen = stepHit*0.4;
// dust puffs on steps
if(stepPhase<0.08 && Math.random()<0.6){
dust.push({x:state.bx+(Math.random()-0.5)*30, y:groundY-2, vx:(Math.random()-0.5)*2, vy:-Math.random()*2, life:1, r:4+Math.random()*5});
}
}
else if(m==='freeze'){
// baby freeze - held balance with micro wobble and breathing
const hold = 1 - Math.pow(1-t,2);
state.by = groundY-20 - Math.sin(hold*Math.PI)*2;
state.bx = W*0.5 + Math.sin(now*0.0015)*4;
state.brot = -0.75 + Math.sin(now*0.003)*0.04 + Math.sin(now*0.007)*0.02; // wobble
state.sy = 1 + Math.sin(now*0.004)*0.02; // breathing
state.sx = 1 - Math.sin(now*0.004)*0.015;
// support on head + one wing
state.wingL.support=true; state.wingL.a1=-0.1; state.wingL.a2=0.2;
state.wingR.a1= -0.6; state.wingR.a2=0.8; state.wingR.hand=0.4;
state.legL.hip = -0.6 + Math.sin(now*0.002)*0.05; state.legL.knee=0.3; state.legL.ankle=0.5;
state.legR.hip = 0.9; state.legR.knee=1.1; state.legR.ankle=-0.6;
state.headRot = 0.6; // head on ground
state.tailRot = 0.2;
state.beakOpen = 0.2 + Math.sin(now*0.005)*0.1;
state.eyeBlink = Math.sin(now*0.001)>0.98?1:0;
// recovery tremor
if(t>0.85){ state.brot += (t-0.85)*2; state.by += (t-0.85)*10; }
}
else if(m==='windmill'){
// power: rolling on back, legs scissoring
const friction = 1 - floorVal/100*0.45; // slick = faster
const spins = 2.2 * friction;
const rot = t* Math.PI*2*spins;
state.by = groundY-16 - Math.abs(Math.sin(rot))*6;
state.bx = W*0.5 + Math.sin(rot)*12;
state.brot = rot + Math.sin(rot*2)*0.15;
// squash when back hits
const backHit = Math.pow(Math.max(0, Math.sin(rot)),3);
state.sy = 1 - backHit*0.18;
state.sx = 1 + backHit*0.12;
// legs wide scissor
const scissor = Math.sin(rot*2)*1.1;
state.legL.hip = 0.6 + scissor; state.legL.knee=0.2; state.legL.ankle=0.1;
state.legR.hip = 0.6 - scissor; state.legR.knee=0.2; state.legR.ankle=0.1;
// wings tucked then flare for momentum
state.wingL.a1 = -0.4 + Math.sin(rot)*0.3; state.wingL.a2=0.6;
state.wingR.a1 = 0.4 - Math.sin(rot)*0.3; state.wingR.a2=0.6;
state.headRot = Math.sin(rot)*0.3;
state.tailRot = Math.sin(rot*2)*0.5;
// motion trail hint via shake
shake = 3 + backHit*6;
if(backHit>0.5) dust.push({x:state.bx, y:groundY-2, vx:(Math.random()-0.5)*3, vy:-1, life:0.9, r:6});
}
else if(m==='headspin'){
const friction = 1 - floorVal/100*0.5;
const spins = 3.5 * friction * (0.7 + t*0.6); // accelerates
const rot = t* Math.PI*2*spins;
state.by = groundY-34;
state.bx = W*0.5 + Math.cos(rot*0.5)*6; // slight drift
state.brot = Math.PI + rot; // inverted + spin
// legs: start tucked, extend
const extend = easeOutBack(clamp(t*1.4,0,1));
const legSpread = lerp(0.3, 1.2, extend);
const legSpin = rot*0.9;
state.legL.hip = Math.sin(legSpin)*legSpread;
state.legL.knee = 0.4 + (1-extend)*0.6;
state.legR.hip = Math.sin(legSpin+Math.PI)*legSpread;
state.legR.knee = 0.4 + (1-extend)*0.6;
state.legL.ankle=0; state.legR.ankle=0;
// wings tucked tight
state.wingL.a1=-0.2; state.wingL.a2=0.1;
state.wingR.a1=0.2; state.wingR.a2=0.1;
state.headRot = 0; // head planted
state.tailRot = Math.sin(rot)*0.2;
// cap flies slightly?
state.capTilt = Math.sin(rot)*0.15 +0.12;
shake = 5 + t*4;
// speed lines
if(t>0.2 && Math.random()<0.4){
dust.push({x:state.bx + Math.cos(rot)*40, y:state.by + Math.sin(rot)*10, vx:Math.cos(rot+Math.PI/2)*4, vy:Math.sin(rot+Math.PI/2)*2, life:0.7, r:3});
}
}
else if(m==='flare'){
// flare-like leg sweeps, body low
state.by = groundY-22;
state.bx = W*0.5 + Math.sin(t*Math.PI*4)*14;
state.brot = Math.sin(t*Math.PI*4)*0.22;
const sweep = t*Math.PI*2*2;
state.legL.hip = Math.sin(sweep)*1.0 +0.3; state.legL.knee=0.5; state.legL.ankle=-0.2;
state.legR.hip = Math.sin(sweep+Math.PI)*1.0 +0.3; state.legR.knee=0.5; state.legR.ankle=-0.2;
state.wingL.support = Math.sin(sweep)>0;
state.wingR.support = !state.wingL.support;
state.wingL.a1 = state.wingL.support ? -0.15 : -0.9; state.wingL.a2 = state.wingL.support?0.25:1.2;
state.wingR.a1 = state.wingR.support ? 0.15 : 0.9; state.wingR.a2 = state.wingR.support?0.25:1.2;
state.headRot = Math.sin(sweep)*0.4;
state.tailRot = Math.sin(sweep*2)*0.3;
// impact squash
const hit2 = Math.pow(Math.max(0, Math.sin(sweep)),2);
state.sy = 1 - hit2*0.14; state.sx=1+hit2*0.1;
}
else if(m==='pose'){
// jump and freeze finale
const jump = Math.sin(t*Math.PI); // 0->1->0
const up = easeOutBounce(t<0.5? t*2 : 1) * (t<0.5?1:1);
if(t<0.45){
state.by = groundY-62 - jump*90;
state.brot = -0.2 + jump*0.4;
state.sy = 1 - jump*0.12; state.sx=1+jump*0.08;
state.legL.hip=-0.4; state.legL.knee=1.2; state.legR.hip=0.4; state.legR.knee=1.2;
state.wingL.a1=-1.1; state.wingL.a2=0.7; state.wingR.a1=1.1; state.wingR.a2=0.7;
state.headRot=-0.3; state.beakOpen=0.8;
} else {
// landing squash then heroic pose
const landT = (t-0.45)/0.55;
const landSquash = Math.exp(-landT*8)*0.3*Math.sin(landT*18);
state.by = groundY-62 + landSquash*20;
state.sy = 1 - landSquash*0.5; state.sx=1+landSquash*0.4;
state.brot = lerp(0.2, -0.15, easeOutBack(landT));
// heroic: one wing up, one leg up
state.wingL.a1 = lerp(-1.1, -2.0, easeOutBack(landT)); state.wingL.a2=lerp(0.7,0.4,landT);
state.wingR.a1 = lerp(1.1, 0.3, easeOutBack(landT)); state.wingR.a2=lerp(0.7,1.0,landT);
state.legL.hip = lerp(-0.4, -0.9, easeOutBack(landT)); state.legL.knee=lerp(1.2,0.2,landT);
state.legR.hip = lerp(0.4, 0.6, easeOutBack(landT)); state.legR.knee=lerp(1.2,0.9,landT);
state.headRot = lerp(-0.3, 0.35, easeOutBack(landT));
state.beakOpen = lerp(0.8, 0.1, landT);
state.tailRot = lerp(0, 0.4, landT);
if(landT<0.1 && t>0.5) { shake=10; for(let i=0;i<8;i++) dust.push({x:W*0.5, y:groundY-2, vx:(Math.random()-0.5)*7, vy:-Math.random()*4-1, life:1, r:5+Math.random()*7}); }
}
state.eyeBlink = t>0.7 && Math.random()<0.02?1:0;
}
// global hype influence
if(hype>0){
state.bx += (Math.random()-0.5)*hype*6;
state.by += (Math.random()-0.5)*hype*4;
state.beakOpen = Math.max(state.beakOpen, hype*0.7);
}
// store for particles
birdX=state.bx; birdY=state.by;
// ---- RENDER ----
ctx.save();
if(shake) ctx.translate((Math.random()-0.5)*shake, (Math.random()-0.5)*shake);
// clear
ctx.fillStyle='#f2e8cf';
ctx.fillRect(0,0,W,H);
drawWall();
drawCardboard();
// crowd silhouettes (simple)
ctx.fillStyle='rgba(17,17,17,0.92)';
ctx.beginPath();
for(let i=0;i<W;i+=44){
const h = 18 + Math.sin(i*0.05 + now*0.001)*4 + (hype? Math.random()*6:0);
ctx.moveTo(i, groundY);
ctx.lineTo(i+10, groundY-h-10);
ctx.lineTo(i+22, groundY-h);
ctx.lineTo(i+34, groundY-h-8);
ctx.lineTo(i+44, groundY);
}
ctx.fill();
// hands up when hype
if(hype>0.3){
ctx.fillStyle='#111';
for(let i=0;i<5;i++){
const x = W*0.2 + i*W*0.15 + Math.sin(now*0.005+i)*6;
ctx.fillRect(x, groundY-28, 4, 18);
ctx.beginPath(); ctx.arc(x+2, groundY-30, 6,0,Math.PI*2); ctx.fill();
}
}
drawBoombox(beatPhase);
// seeds
for(let s of seeds){
ctx.save(); ctx.translate(s.x,s.y); ctx.rotate(s.rot);
ctx.fillStyle='#e9c46a'; ctx.strokeStyle='#111'; ctx.lineWidth=1.5;
ctx.beginPath(); ctx.ellipse(0,0,6,4,0,0,Math.PI*2); ctx.fill(); ctx.stroke();
ctx.fillStyle='#c44536'; ctx.beginPath(); ctx.arc(2,-1,1,0,Math.PI*2); ctx.fill();
ctx.restore();
}
// bird
drawBird(state);
// dust
for(let d of dust){
ctx.globalAlpha = clamp(d.life,0,1)*0.45;
ctx.fillStyle='#a89a7d';
ctx.beginPath(); ctx.arc(d.x,d.y,d.r*d.life,0,Math.PI*2); ctx.fill();
}
ctx.globalAlpha=1;
// feathers
for(let f of feathers){
ctx.save(); ctx.translate(f.x,f.y); ctx.rotate(f.r);
ctx.globalAlpha=f.life*0.9;
ctx.fillStyle='#8a8d93'; ctx.strokeStyle='#111'; ctx.lineWidth=1.2;
ctx.beginPath(); ctx.ellipse(0,0,8,3,0,0,Math.PI*2); ctx.fill(); ctx.stroke();
ctx.globalAlpha=1; ctx.restore();
}
// spotlight
ctx.save();
ctx.globalAlpha=0.08;
ctx.fillStyle='#e9c46a';
ctx.beginPath();
ctx.moveTo(W*0.5-120,0); ctx.lineTo(W*0.5+120,0); ctx.lineTo(birdX+40, groundY); ctx.lineTo(birdX-40, groundY); ctx.closePath(); ctx.fill();
ctx.restore();
// halftone grain overlay
ctx.save();
ctx.globalAlpha=0.07;
ctx.fillStyle='#111';
for(let y=0;y<H;y+=8){
for(let x=(y%16===0?0:4); x<W; x+=8){
if(Math.random()<0.5) ctx.fillRect(x,y,1,1);
}
}
ctx.restore();
// move label
ctx.save();
ctx.fillStyle='#111'; ctx.strokeStyle='#f2e8cf'; ctx.lineWidth=2;
const label = moves[moveIndex].name.toUpperCase();
ctx.font='900 13px ui-monospace, monospace';
const tw = ctx.measureText(label).width;
const lx = W*0.5 - tw/2 -12, ly= 112;
ctx.fillRect(lx, ly-18, tw+24, 22);
ctx.strokeRect(lx, ly-18, tw+24, 22);
ctx.fillStyle= hype>0.4 ? '#e9c46a' : '#f2e8cf';
ctx.fillText(label, lx+12, ly-2);
// progress bar
ctx.fillStyle='#2a9d8f'; ctx.fillRect(lx, ly+4, (tw+24)*t, 4);
ctx.strokeStyle='#111'; ctx.lineWidth=1.5; ctx.strokeRect(lx, ly+4, tw+24,4);
ctx.restore();
// beat indicator
ctx.save();
ctx.translate(18, 132);
ctx.fillStyle='#111'; ctx.fillRect(0,0, 64,18);
ctx.fillStyle='#f2e8cf'; ctx.font='800 10px ui-monospace, monospace'; ctx.fillText('BEAT',6,12);
ctx.fillStyle= hit>0.3 ? '#c44536' : '#2a9d8f';
ctx.beginPath(); ctx.arc(52,9,6,0,Math.PI*2); ctx.fill();
if(hit>0.3){ ctx.fillStyle='rgba(196,69,54,0.25)'; ctx.beginPath(); ctx.arc(52,9,12,0,Math.PI*2); ctx.fill(); }
ctx.restore();
ctx.restore();
}
requestAnimationFrame(frame);
// initial dust
for(let i=0;i<4;i++) dust.push({x:W*0.5+(Math.random()-0.5)*200, y:groundY-2, vx:0, vy:0, life:0.6, r:8});
})();
</script>
</body>
</html>
The move set, and how you got the timing to feel like weight rather than interpolation: Toprock → drop → 6-step → baby-freeze → windmill → headspin → flare → jump-pose. Weight comes from beat-synced hang-time vs. impact: pow(sin(beat),8) creates sharp hits, squash/stretch on landing (sy=1-hit*0.22), parabolic jump arcs, hold-frames on freezes with breathing wobble, and dust/shake only on downbeats — not linear lerps.
What your controls change: Tempo (72–158 BPM) scales all durations and beat phase; Floor (slick cardboard → sticky concrete) reduces spin velocity for windmills/headspins; Hype fires feathers/crowd/shake and adds random impulse; Seed Toss spawns physics crumbs the bird tracks; 1-5 / buttons queue the next break move; dragging the boombox needle scratches tempo live.
The part you would fix with more time: True two-bone IK with planted contacts so hands/feet stick to the cardboard during footwork instead of procedural angles, plus a proper audio engine (WebAudio synth drums) synced to the visual beat for call-and-response scratching.