27:18Gemini 4 Hydrogen: Cheaper Tokens, More Expensive Answers?
Nov 13, 2026 · YouTube
18:40ERNIE 5.5 Flash Preview vs GLM 5.3 Flash: Baidu's new model is very impressive!
Nov 11, 2026 · YouTube
36:22Fable 5.2 robbing me blind, plus: Nex AGI's N2.6 vs GLM 5.3 Flash
Nov 11, 2026 · YouTube
14:03Gemini 4 Carbon: Can it handle my challenges? (First look with one-shot coding tests)
Nov 10, 2026 · YouTube
47:51Ranking major U.S. Open-Weights Models. (Where does Ripple 65B fit in?)
Nov 6, 2026 · YouTube
10:57"Crouton Alpha": the latest super-efficient coding model by Meta?
Nov 3, 2026 · YouTube
38:58Testing Particle-1: a post-trained DeepSeek V4.1 Flash specializing in coding and efficiency
Oct 28, 2026 · YouTube
22:14GPT-6.1 Terra Pro vs Terra: How Much Better Is Pro?
Oct 27, 2026 · YouTube
29:55Kimi K3.1 Pro Looked Incredible. Then I Kept Testing.
Oct 26, 2026 · YouTube
31:28Is this company reselling GPT Sol 6.1 at a loss (for non-agentic/text-only)?🤔 Is this sustainable?
Oct 20, 2026 · YouTube
17:58Nemotron 3.5 Super VL vs Opus 5.5: Testing NVIDIA’s new coding juggernaut
Oct 15, 2026 · YouTube
16:04Testing a *massive* new open-weights model 🤯: Muse Spark Open (vs. Nemotron, Opus 5.5, and more)
Oct 15, 2026 · YouTube
16:43Gemini 4 Argon: Great 🐈 Animation, but a broken escalator. (Coding challenge vs Opus 5.5, Sol 6.1)
Oct 13, 2026 · YouTube
16:37Meta's new "Muse Wonder" AI is impressive... but can it compete with Anthropic's models?
Oct 12, 2026 · YouTube
11:12WOW... Haiku 5.5 is very impressive. RIP OpenAI?
Oct 7, 2026 · YouTube
22:47Mistral Large 4 Is Cheap. But Is It Good Enough to Compete?
Oct 6, 2026 · YouTube
22:02Is Pareto 26.10 Preview Just Routing to GPT Sol? I Tested It Against Sol 6.1 Pro
Oct 1, 2026 · YouTube
11:22I Tested Sonnet 5.5 Against Opus 5.5. Is It Actually Cheaper?
Sep 28, 2026 · YouTube
30:00Did Elon Musk's team "train the gay away"... and make Grok 4.7 worse?
Sep 26, 2026 · YouTube
19:21I Tested the Stealth "Space Bunny Alpha" Model Before It Went Offline
Sep 23, 2026 · YouTube
10:58So... how much better is Claude Opus 5.5 vs. GPT-6 Sol, really?
Sep 23, 2026 · YouTube
9:38Claude Opus 5.5... the BEST new model for coding!! 🤩
Sep 22, 2026 · YouTube
29:00Grok 4.7: improved, but GLM 5.3 FlashX beat it anyway (benchmark against Gemini, Sol, etc.)
Sep 22, 2026 · YouTube
You make short animated pieces for the web.
Build me a single HTML page: a bird, breakdancing.
data: URIs are fine.localStorage, sessionStorage, cookies or indexedDB — sandboxed frame, they throw.html block.Two sentences: what the bird is, and the look you went for.
The single fenced html block.
Three bullets.
He is a gangly, top-heavy street pigeon wearing a red bucket hat, gold chain, and oversized yellow kicks, engineered with a rigid-body-style procedural skeleton and inverse kinematics. The aesthetic draws from late-90s underground vector flash animation and cardboard street-art cutouts, framed against a vibrant subway-tile stage with dynamic floor shadows and beat-reactive boombox speakers.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>B-BOY COO // PIGEON ROCK</title>
<style>
:root {
--bg: #141218;
--wall: #23202a;
--floor: #1a1820;
--accent: #ff3355;
--gold: #ffcc22;
--cyan: #00e5ff;
--cardboard: #d2b48c;
--ink: #0f0e12;
}
* { box-sizing: border-box; user-select: none; -webkit-user-select: none; }
body, html {
margin: 0; padding: 0; width: 100%; height: 100%;
background: var(--bg); color: #fff;
font-family: 'Courier New', Courier, monospace;
overflow: hidden; display: flex; flex-direction: column;
align-items: center; justify-content: center;
}
#stage-wrap {
position: relative; width: 100vw; height: 100vh;
display: flex; align-items: center; justify-content: center;
}
canvas {
width: 100%; height: 100%; object-fit: contain;
max-width: 1280px; max-height: 820px;
}
#hud {
position: absolute; bottom: 20px; left: 50%; transform: translateX(-50%);
background: rgba(15, 14, 18, 0.92);
border: 3px solid var(--ink);
box-shadow: 0 8px 0 var(--ink), 0 0 0 2px var(--gold);
padding: 12px 18px; border-radius: 6px;
display: flex; gap: 16px; align-items: center; flex-wrap: wrap;
justify-content: center; width: max-content; max-width: 94vw;
}
.ctrl-group {
display: flex; flex-direction: column; gap: 6px; align-items: center;
}
.ctrl-label {
font-size: 11px; font-weight: 900; letter-spacing: 1.5px;
color: var(--gold); text-transform: uppercase;
}
.btn-row { display: flex; gap: 6px; }
button {
background: #2b2735; color: #fff;
border: 2px solid var(--ink);
box-shadow: 0 4px 0 var(--ink);
padding: 7px 11px; font-family: inherit;
font-weight: 800; font-size: 12px; cursor: pointer;
border-radius: 4px; transition: transform 0.05s, box-shadow 0.05s, background 0.1s;
text-transform: uppercase; position: relative;
}
button:hover { background: #3a3447; }
button:active, button.active {
transform: translateY(3px);
box-shadow: 0 1px 0 var(--ink);
background: var(--accent); color: var(--ink);
}
button.freeze-btn { border-color: var(--cyan); color: var(--cyan); }
button.freeze-btn.active { background: var(--cyan); color: var(--ink); }
.key-hint {
font-size: 9px; opacity: 0.65; display: block; margin-top: 2px;
}
input[type="range"] {
-webkit-appearance: none; appearance: none;
width: 110px; height: 8px; background: #2b2735;
border: 2px solid var(--ink); border-radius: 4px; outline: none;
}
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none; appearance: none;
width: 18px; height: 18px; background: var(--gold);
border: 2px solid var(--ink); border-radius: 2px; cursor: pointer;
}
.divider { width: 2px; height: 42px; background: #332e3d; }
#banner {
position: absolute; top: 18px; left: 24px;
pointer-events: none;
}
#banner h1 {
margin: 0; font-size: 26px; font-weight: 900; letter-spacing: -1px;
color: var(--gold); text-shadow: 3px 3px 0 var(--ink), -1px -1px 0 var(--ink);
}
#banner .sub {
font-size: 12px; font-weight: 700; color: var(--cyan);
background: var(--ink); display: inline-block; padding: 2px 6px; margin-top: 3px;
}
#move-callout {
position: absolute; top: 18px; right: 24px;
font-size: 28px; font-weight: 900; color: #fff;
text-shadow: 4px 4px 0 var(--accent), 6px 6px 0 var(--ink);
font-style: italic; pointer-events: none;
transition: transform 0.1s;
}
@media (max-width: 720px) {
#hud { padding: 8px 10px; gap: 8px; bottom: 8px; }
button { padding: 5px 7px; font-size: 10px; }
.divider { display: none; }
#banner h1 { font-size: 18px; }
#move-callout { font-size: 20px; }
}
</style>
</head>
<body>
<div id="stage-wrap">
<canvas id="stage" width="1200" height="750"></canvas>
<div id="banner">
<h1>B-BOY PIGEON // CYPHER 84</h1>
<div class="sub">CLICK STAGE OR USE DECK TO ROCK</div>
</div>
<div id="move-callout">TOPROCK</div>
<div id="hud">
<div class="ctrl-group">
<div class="ctrl-label">Routine Flow</div>
<div class="btn-row">
<button id="btn-auto" class="active" title="Auto Cypher">CYPHER<span class="key-hint">[A]</span></button>
<button data-move="toprock">TOPROCK<span class="key-hint">[1]</span></button>
<button data-move="sixstep">6-STEP<span class="key-hint">[2]</span></button>
<button data-move="windmill">WINDMILL<span class="key-hint">[3]</span></button>
<button data-move="headspin">HEADSPIN<span class="key-hint">[4]</span></button>
</div>
</div>
<div class="divider"></div>
<div class="ctrl-group">
<div class="ctrl-label">Hold Freeze (Hold Key)</div>
<div class="btn-row">
<button class="freeze-btn" data-freeze="baby">BABY<span class="key-hint">[Q]</span></button>
<button class="freeze-btn" data-freeze="chair">AIR CHAIR<span class="key-hint">[W]</span></button>
<button class="freeze-btn" data-freeze="hollow">BEAK STAND<span class="key-hint">[E]</span></button>
</div>
</div>
<div class="divider"></div>
<div class="ctrl-group">
<div class="ctrl-label">BPM: <span id="bpm-val">116</span></div>
<input type="range" id="bpm-slider" min="84" max="148" value="116">
</div>
<div class="ctrl-group">
<div class="ctrl-label">Boombox</div>
<div class="btn-row">
<button id="btn-audio">SOUND: OFF<span class="key-hint">[M]</span></button>
<button id="btn-scratch">SCRATCH<span class="key-hint">[SPACE]</span></button>
</div>
</div>
</div>
</div>
<script>
(() => {
const canvas = document.getElementById('stage');
const ctx = canvas.getContext('2d');
const calloutEl = document.getElementById('move-callout');
// --- AUDIO SYNTHESIS (808 Boombox) ---
let audioCtx = null;
let soundEnabled = false;
let noiseBuffer = null;
function initAudio() {
if (!audioCtx) {
const AC = window.AudioContext || window.webkitAudioContext;
audioCtx = new AC();
const bufferSize = audioCtx.sampleRate * 0.5;
noiseBuffer = audioCtx.createBuffer(1, bufferSize, audioCtx.sampleRate);
const data = noiseBuffer.getChannelData(0);
for (let i = 0; i < bufferSize; i++) data[i] = Math.random() * 2 - 1;
}
if (audioCtx.state === 'suspended') audioCtx.resume();
}
function playKick(time = 0) {
if (!soundEnabled || !audioCtx) return;
const t = audioCtx.currentTime + time;
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.frequency.setValueAtTime(135, t);
osc.frequency.exponentialRampToValueAtTime(34, t + 0.14);
gain.gain.setValueAtTime(0.85, t);
gain.gain.exponentialRampToValueAtTime(0.001, t + 0.28);
osc.connect(gain); gain.connect(audioCtx.destination);
osc.start(t); osc.stop(t + 0.29);
}
function playSnare(time = 0) {
if (!soundEnabled || !audioCtx) return;
const t = audioCtx.currentTime + time;
// Noise snap
const noise = audioCtx.createBufferSource();
noise.buffer = noiseBuffer;
const filter = audioCtx.createBiquadFilter();
filter.type = 'highpass'; filter.frequency.setValueAtTime(600, t);
const nGain = audioCtx.createGain();
nGain.gain.setValueAtTime(0.55, t);
nGain.gain.exponentialRampToValueAtTime(0.01, t + 0.16);
noise.connect(filter); filter.connect(nGain); nGain.connect(audioCtx.destination);
noise.start(t); noise.stop(t + 0.17);
// Body
const osc = audioCtx.createOscillator();
const oGain = audioCtx.createGain();
osc.type = 'triangle';
osc.frequency.setValueAtTime(210, t);
osc.frequency.exponentialRampToValueAtTime(90, t + 0.09);
oGain.gain.setValueAtTime(0.45, t);
oGain.gain.exponentialRampToValueAtTime(0.01, t + 0.1);
osc.connect(oGain); oGain.connect(audioCtx.destination);
osc.start(t); osc.stop(t + 0.11);
}
function playHat(open = false, time = 0) {
if (!soundEnabled || !audioCtx) return;
const t = audioCtx.currentTime + time;
const noise = audioCtx.createBufferSource();
noise.buffer = noiseBuffer;
const filter = audioCtx.createBiquadFilter();
filter.type = 'bandpass'; filter.frequency.setValueAtTime(8500, t); filter.Q.value = 1.5;
const gain = audioCtx.createGain();
const dur = open ? 0.18 : 0.045;
gain.gain.setValueAtTime(open ? 0.25 : 0.18, t);
gain.gain.exponentialRampToValueAtTime(0.005, t + dur);
noise.connect(filter); filter.connect(gain); gain.connect(audioCtx.destination);
noise.start(t); noise.stop(t + dur + 0.01);
}
function playScratch() {
if (!soundEnabled || !audioCtx) return;
const t = audioCtx.currentTime;
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.type = 'sawtooth';
osc.frequency.setValueAtTime(180, t);
osc.frequency.linearRampToValueAtTime(720, t + 0.07);
osc.frequency.linearRampToValueAtTime(140, t + 0.16);
osc.frequency.linearRampToValueAtTime(520, t + 0.23);
gain.gain.setValueAtTime(0.32, t);
gain.gain.exponentialRampToValueAtTime(0.01, t + 0.25);
osc.connect(gain); gain.connect(audioCtx.destination);
osc.start(t); osc.stop(t + 0.26);
}
// --- TIMING & STATE ---
let bpm = 116;
let beatPos = 0; // Continuous beat counter
let lastStep = -1;
let autoCypher = true;
let currentMove = 'toprock';
let activeFreeze = null;
let moveStartBeat = 0;
let scratchTimer = 0;
let speakerPulse = 0;
const FLOOR_Y = 590;
const STAGE_X = 600;
// Particles (dust, sweat, sparks)
const particles = [];
function spawnParticles(x, y, type, count = 6) {
for (let i = 0; i < count; i++) {
const ang = -Math.PI * 0.1 - Math.random() * Math.PI * 0.8;
const spd = 2 + Math.random() * 7;
particles.push({
x, y,
vx: Math.cos(ang) * spd + (Math.random() - 0.5) * 3,
vy: Math.sin(ang) * spd - Math.random() * 2,
size: type === 'dust' ? 8 + Math.random() * 10 : 4 + Math.random() * 5,
life: 1,
decay: type === 'dust' ? 0.025 + Math.random() * 0.02 : 0.04 + Math.random() * 0.03,
type
});
}
}
// --- EASING & MATH HELPERS ---
const clamp = (v, a, b) => Math.max(a, Math.min(b, v));
const lerp = (a, b, t) => a + (b - a) * t;
// Snap curve: holds pose, then whips quickly to next pose (weight/breaking feel)
function snapEase(t, power = 3.5) {
t = clamp(t, 0, 1);
return t < 0.5 ? 0.5 * Math.pow(2 * t, power) : 1 - 0.5 * Math.pow(2 * (1 - t), power);
}
// Overshoot bounce for heavy landings
function impactBounce(t) {
const c1 = 1.70158;
const c3 = c1 + 1;
return 1 + c3 * Math.pow(t - 1, 3) + c1 * Math.pow(t - 1, 2);
}
// 2-Bone Inverse Kinematics Solver
function solveIK(rootX, rootY, targetX, targetY, L1, L2, flipBend = false) {
let dx = targetX - rootX;
let dy = targetY - rootY;
let dist = Math.hypot(dx, dy);
const maxReach = (L1 + L2) * 0.995;
if (dist > maxReach) {
dx = (dx / dist) * maxReach;
dy = (dy / dist) * maxReach;
dist = maxReach;
}
const minReach = Math.abs(L1 - L2) * 1.05;
if (dist < minReach) dist = minReach;
const baseAngle = Math.atan2(dy, dx);
const cosAngle1 = clamp((dist * dist + L1 * L1 - L2 * L2) / (2 * dist * L1), -1, 1);
const angle1 = baseAngle + (flipBend ? -1 : 1) * Math.acos(cosAngle1);
const jointX = rootX + Math.cos(angle1) * L1;
const jointY = rootY + Math.sin(angle1) * L1;
const endX = rootX + dx;
const endY = rootY + dy;
return { jx: jointX, jy: jointY, ex: endX, ey: endY };
}
// --- RIG & PHYSICS STATE ---
// Current smoothed rig state (allows whip-fast transitions + springy recovery)
const rig = {
hipX: STAGE_X, hipY: FLOOR_Y - 125,
torsoAngle: 0, // 0 = upright, PI = upside down
chestSquash: 1,
spinPhase: 0, // 3D azimuth rotation
headBob: 0,
headAngle: 0,
footLX: STAGE_X - 40, footLY: FLOOR_Y, footLAng: 0,
footRX: STAGE_X + 40, footRY: FLOOR_Y, footRAng: 0,
wingLX: STAGE_X - 65, wingLY: FLOOR_Y - 135,
wingRX: STAGE_X + 65, wingRY: FLOOR_Y - 135,
bendL: false, bendR: true,
wingBendL: true, wingBendR: false
};
// Spring velocities for organic overshoot on freezes/drops
const vel = {};
for (const k in rig) if (typeof rig[k] === 'number') vel[k] = 0;
let prevFootLY = FLOOR_Y, prevFootRY = FLOOR_Y, prevHipY = FLOOR_Y - 125;
function setCallout(text) {
if (calloutEl.textContent !== text) {
calloutEl.textContent = text;
calloutEl.style.transform = 'scale(1.25) rotate(-3deg)';
setTimeout(() => { calloutEl.style.transform = 'scale(1) rotate(0deg)'; }, 120);
}
}
// --- CHOREOGRAPHY ENGINE ---
function computeTargetPose(beat) {
const b = beat;
const barBeat = b % 4;
const subBeat = b % 1;
const snapSub = snapEase(subBeat, 4.0);
const target = {
hipX: STAGE_X, hipY: FLOOR_Y - 125,
torsoAngle: 0,
chestSquash: 1,
spinPhase: 0,
headBob: 0,
headAngle: 0,
footLX: STAGE_X - 45, footLY: FLOOR_Y, footLAng: 0,
footRX: STAGE_X + 45, footRY: FLOOR_Y, footRAng: 0,
wingLX: STAGE_X - 75, wingLY: FLOOR_Y - 140,
wingRX: STAGE_X + 75, wingRY: FLOOR_Y - 140,
bendL: true, bendR: false,
wingBendL: true, wingBendR: false
};
// Check active freeze override
const freezeMode = activeFreeze || (autoCypher && (b % 16 >= 14.0) ? (Math.floor(b / 16) % 2 === 0 ? 'baby' : 'chair') : null);
if (freezeMode) {
if (freezeMode === 'baby') {
setCallout('BABY FREEZE!');
target.hipX = STAGE_X - 15;
target.hipY = FLOOR_Y - 68;
target.torsoAngle = 1.95;
target.spinPhase = 0.4;
target.headAngle = -0.6;
target.headBob = 8;
// Both wings planted hard on cardboard
target.wingLX = STAGE_X - 35; target.wingLY = FLOOR_Y;
target.wingRX = STAGE_X + 25; target.wingRY = FLOOR_Y;
// Legs stacked and locked on elbow
target.footLX = STAGE_X - 95; target.footLY = FLOOR_Y - 115; target.footLAng = -0.8;
target.footRX = STAGE_X - 115; target.footRY = FLOOR_Y - 145; target.footRAng = -1.1;
target.bendL = true; target.bendR = true;
} else if (freezeMode === 'chair') {
setCallout('AIR CHAIR!');
target.hipX = STAGE_X + 10;
target.hipY = FLOOR_Y - 78;
target.torsoAngle = 2.45;
target.spinPhase = -0.5;
target.headAngle = 0.4;
// Single wing stab
target.wingRX = STAGE_X + 5; target.wingRY = FLOOR_Y;
target.wingLX = STAGE_X - 70; target.wingLY = FLOOR_Y - 165;
// Flare kicks into the air
target.footLX = STAGE_X - 55; target.footLY = FLOOR_Y - 210; target.footLAng = -0.5;
target.footRX = STAGE_X + 75; target.footRY = FLOOR_Y - 180; target.footRAng = 0.6;
} else if (freezeMode === 'hollow') {
setCallout('BEAK HOLLOWBACK!');
target.hipX = STAGE_X;
target.hipY = FLOOR_Y - 165;
target.torsoAngle = Math.PI + 0.18;
target.spinPhase = 0.15;
target.headAngle = -0.35;
target.wingLX = STAGE_X - 48; target.wingLY = FLOOR_Y;
target.wingRX = STAGE_X + 48; target.wingRY = FLOOR_Y;
target.footLX = STAGE_X + 45; target.footLY = FLOOR_Y - 275; target.footLAng = 0.4;
target.footRX = STAGE_X + 80; target.footRY = FLOOR_Y - 250; target.footRAng = 0.6;
}
return target;
}
// Auto-cypher switches moves every 16 beats (4 bars)
let move = currentMove;
if (autoCypher) {
const cycle = Math.floor((b % 64) / 16);
const moves = ['toprock', 'sixstep', 'windmill', 'headspin'];
move = moves[cycle];
currentMove = move;
updateMoveButtons();
}
if (move === 'toprock') {
setCallout('INDIAN STEP // TOPROCK');
// 2-beat alternating cross-step with heavy bounce
const phase2 = b % 2;
const side = phase2 < 1 ? 1 : -1;
const stepT = phase2 % 1;
const whip = snapEase(stepT, 3.8);
// Bounce drop on the beat, pop up on the "&"
const bounce = Math.sin(subBeat * Math.PI);
target.hipY = FLOOR_Y - 122 + (1 - bounce) * 18;
target.chestSquash = 1 - (1 - bounce) * 0.12;
// Cross step X translation
const crossActive = stepT < 0.55 ? snapEase(stepT / 0.55, 3) : 1 - snapEase((stepT - 0.55) / 0.45, 3);
target.hipX = STAGE_X + side * crossActive * 34;
target.torsoAngle = side * crossActive * 0.18;
target.spinPhase = -side * crossActive * 0.45;
// Signature Pigeon Neck Jerk on the beat!
const neckPop = Math.pow(Math.sin(subBeat * Math.PI), 4);
target.headBob = neckPop * 18;
target.headAngle = -side * crossActive * 0.25;
if (side > 0) {
// Right foot kicks across left
target.footLX = STAGE_X - 35; target.footLY = FLOOR_Y;
target.footRX = lerp(STAGE_X + 50, STAGE_X - 85, crossActive);
target.footRY = FLOOR_Y - Math.sin(crossActive * Math.PI) * 48;
target.footRAng = -crossActive * 0.55;
} else {
// Left foot kicks across right
target.footRX = STAGE_X + 35; target.footRY = FLOOR_Y;
target.footLX = lerp(STAGE_X - 50, STAGE_X + 85, crossActive);
target.footLY = FLOOR_Y - Math.sin(crossActive * Math.PI) * 48;
target.footLAng = crossActive * 0.55;
}
// Arms flare out on cross, cross chest on return
const armSpread = crossActive;
target.wingLX = target.hipX - 45 - armSpread * 65;
target.wingLY = target.hipY - 20 - armSpread * 35 + (1 - bounce) * 15;
target.wingRX = target.hipX + 45 + armSpread * 65;
target.wingRY = target.hipY - 20 - armSpread * 35 + (1 - bounce) * 15;
target.bendL = true; target.bendR = false;
} else if (move === 'sixstep') {
setCallout('6-STEP FOOTWORK');
// 6 distinct rhythmic floor positions per 3 beats (2 steps per beat)
const stepFloat = (b * 2) % 6;
const stepIdx = Math.floor(stepFloat);
const stepFrac = snapEase(stepFloat % 1, 4.2);
// Keyframes for 6-step circle: [hipXOff, hipYOff, torsoAng, spin, fLX, fLY, fRX, fRY, wLX, wLY, wRX, wRY]
const frames = [
// 1: Hook right leg in front
[ 10, -52, 0.35, 0.3, -55, 0, 25, -12, -55, 0, 65, -45],
// 2: Left leg steps back
[ 22, -62, 0.55, 0.8, -65, -25, 35, 0, -45, 0, 40, 0],
// 3: Open push-up / crab position
[ 0, -58, 0.15, 1.2, -70, 0, 70, 0, -38, 0, 38, 0],
// 4: Left leg hooks across front
[-15, -50, -0.35, -0.4, -20, -15, 55, 0, -65, -45, 55, 0],
// 5: Right leg steps around
[-22, -55, -0.45, -0.8, -45, 0, 65, -25, -35, -60, 45, 0],
// 6: Squat recovery sweep
[ 0, -48, 0.05, 0.0, -55, 0, 55, 0, -75, -55, 75, -55]
];
const cur = frames[stepIdx];
const nxt = frames[(stepIdx + 1) % 6];
const k = (i) => lerp(cur[i], nxt[i], stepFrac);
target.hipX = STAGE_X + k(0);
target.hipY = FLOOR_Y + k(1);
target.torsoAngle = k(2);
target.spinPhase = k(3);
target.headBob = Math.sin(stepFloat * Math.PI) * 10;
const liftL = cur[5] !== nxt[5] ? Math.sin(stepFrac * Math.PI) * 28 : 0;
const liftR = cur[7] !== nxt[7] ? Math.sin(stepFrac * Math.PI) * 28 : 0;
target.footLX = STAGE_X + k(4); target.footLY = FLOOR_Y + k(5) - liftL;
target.footRX = STAGE_X + k(6); target.footRY = FLOOR_Y + k(7) - liftR;
target.wingLX = STAGE_X + k(8); target.wingLY = FLOOR_Y + k(9);
target.wingRX = STAGE_X + k(10); target.wingRY = FLOOR_Y + k(11);
target.bendL = true; target.bendR = false;
} else if (move === 'windmill') {
setCallout('BELLY WINDMILLS');
// Continuous high-torque rotation on upper back/wings with wide V-legs
const rev = b * 0.85 * Math.PI * 2;
const cosR = Math.cos(rev);
const sinR = Math.sin(rev);
// Collapse into coin-drop then whip around
target.hipX = STAGE_X + cosR * 28;
target.hipY = FLOOR_Y - 58 + sinR * 18;
target.torsoAngle = 2.15 + sinR * 0.45;
target.spinPhase = rev;
target.headAngle = cosR * 0.4;
// Wide V-split legs whipping through the air
const legRadius = 135;
const tilt = 0.55;
target.footLX = target.hipX + Math.cos(rev + 0.9) * legRadius;
target.footLY = target.hipY - 55 + Math.sin(rev + 0.9) * (legRadius * tilt);
target.footLAng = Math.cos(rev) * 0.8;
target.footRX = target.hipX + Math.cos(rev - 0.9) * legRadius;
target.footRY = target.hipY - 55 + Math.sin(rev - 0.9) * (legRadius * tilt);
target.footRAng = Math.sin(rev) * 0.8;
// Wings roll across the cardboard
target.wingLX = target.hipX + Math.cos(rev + 2.2) * 55;
target.wingLY = Math.min(FLOOR_Y, target.hipY + 25 + Math.sin(rev + 2.2) * 35);
target.wingRX = target.hipX + Math.cos(rev - 2.2) * 55;
target.wingRY = Math.min(FLOOR_Y, target.hipY + 25 + Math.sin(rev - 2.2) * 35);
} else if (move === 'headspin') {
setCallout('BUCKET-HAT HEADSPIN');
// First 2 beats: pump & tap legs; Next 2 beats: tucked fast drill spin
const phrase = b % 8;
const isDrill = phrase >= 4;
const speed = isDrill ? 2.4 : 1.1;
const rev = b * speed * Math.PI * 2;
target.hipX = STAGE_X + Math.sin(b * Math.PI) * 6;
target.hipY = FLOOR_Y - (isDrill ? 148 : 138);
target.torsoAngle = Math.PI + Math.sin(rev) * (isDrill ? 0.04 : 0.12);
target.spinPhase = rev;
target.headBob = -6;
if (!isDrill) {
// Wide bicycle/split balance with wing taps on floor
const splitW = 105;
target.footLX = target.hipX + Math.cos(rev) * splitW;
target.footLY = target.hipY - 85 + Math.sin(rev * 2) * 20;
target.footRX = target.hipX - Math.cos(rev) * splitW;
target.footRY = target.hipY - 85 - Math.sin(rev * 2) * 20;
// Wings tap floor to push rotation
const tapL = Math.max(0, Math.sin(rev));
const tapR = Math.max(0, -Math.sin(rev));
target.wingLX = target.hipX - 65;
target.wingLY = lerp(FLOOR_Y - 50, FLOOR_Y, tapL);
target.wingRX = target.hipX + 65;
target.wingRY = lerp(FLOOR_Y - 50, FLOOR_Y, tapR);
} else {
// Tight tucked drill spin!
const tuckW = 35;
target.footLX = target.hipX + Math.cos(rev) * tuckW;
target.footLY = target.hipY - 45;
target.footRX = target.hipX - Math.cos(rev) * tuckW;
target.footRY = target.hipY - 55;
target.wingLX = target.hipX + Math.cos(rev + 1.5) * 30;
target.wingLY = target.hipY + 30;
target.wingRX = target.hipX - Math.cos(rev + 1.5) * 30;
target.wingRY = target.hipY + 30;
}
}
return target;
}
// --- DRAWING FUNCTIONS ---
function drawStage(beat) {
// Subway brick wall background
ctx.fillStyle = '#1b1822';
ctx.fillRect(0, 0, canvas.width, FLOOR_Y - 45);
// Subtle brick tiles
ctx.strokeStyle = '#24202e';
ctx.lineWidth = 2;
const brickW = 80, brickH = 36;
for (let y = 60; y < FLOOR_Y - 45; y += brickH) {
const offset = (Math.floor(y / brickH) % 2) * (brickW / 2);
for (let x = -brickW; x < canvas.width + brickW; x += brickW) {
ctx.strokeRect(x + offset, y, brickW, brickH);
}
}
// Spotlight cone
const spotGrad = ctx.createRadialGradient(STAGE_X, 40, 20, STAGE_X, FLOOR_Y - 60, 520);
spotGrad.addColorStop(0, 'rgba(255, 220, 120, 0.16)');
spotGrad.addColorStop(0.6, 'rgba(0, 229, 255, 0.05)');
spotGrad.addColorStop(1, 'rgba(20, 18, 24, 0)');
ctx.fillStyle = spotGrad;
ctx.beginPath();
ctx.moveTo(STAGE_X - 140, 0);
ctx.lineTo(STAGE_X + 140, 0);
ctx.lineTo(STAGE_X + 520, FLOOR_Y + 80);
ctx.lineTo(STAGE_X - 520, FLOOR_Y + 80);
ctx.closePath();
ctx.fill();
// Concrete floor
ctx.fillStyle = '#131117';
ctx.fillRect(0, FLOOR_Y - 45, canvas.width, canvas.height - (FLOOR_Y - 45));
ctx.strokeStyle = '#09080c';
ctx.lineWidth = 5;
ctx.beginPath();
ctx.moveTo(0, FLOOR_Y - 45);
ctx.lineTo(canvas.width, FLOOR_Y - 45);
ctx.stroke();
// Boombox in background left
drawBoombox(210, FLOOR_Y - 30);
// Flattened Cardboard Cypher Mat
ctx.save();
ctx.translate(STAGE_X, FLOOR_Y + 4);
ctx.fillStyle = '#0b0a0e';
ctx.beginPath();
ctx.moveTo(-295, -34); ctx.lineTo(285, -38);
ctx.lineTo(335, 36); ctx.lineTo(-325, 32);
ctx.closePath();
ctx.fill();
ctx.fillStyle = '#c59b6d';
ctx.strokeStyle = '#0f0e12';
ctx.lineWidth = 5;
ctx.beginPath();
ctx.moveTo(-290, -38); ctx.lineTo(280, -42);
ctx.lineTo(330, 30); ctx.lineTo(-320, 26);
ctx.closePath();
ctx.fill();
ctx.stroke();
// Cardboard fold creases & tape
ctx.strokeStyle = '#9c764c';
ctx.lineWidth = 3;
ctx.beginPath();
ctx.moveTo(-10, -40); ctx.lineTo(-5, 28);
ctx.moveTo(-298, -8); ctx.lineTo(302, -10);
ctx.stroke();
// Spray-painted stencil on cardboard
ctx.fillStyle = 'rgba(255, 51, 85, 0.28)';
ctx.font = '900 32px Impact, sans-serif';
ctx.fillText('CYPHER KING', -240, 18);
ctx.restore();
}
function drawBoombox(x, y) {
const pulse = 1 + speakerPulse * 0.08;
ctx.save();
ctx.translate(x, y);
ctx.scale(pulse, 2 - pulse);
// Handle
ctx.strokeStyle = '#0f0e12'; ctx.lineWidth = 8;
ctx.fillStyle = '#555064';
ctx.beginPath();
ctx.rect(-75, -138, 150, 22);
ctx.stroke();
ctx.fillStyle = '#1b1822';
ctx.fillRect(-55, -130, 110, 16);
// Main Chassis
ctx.fillStyle = '#393447';
ctx.strokeStyle = '#0f0e12';
ctx.lineWidth = 5;
ctx.fillRect(-115, -118, 230, 118);
ctx.strokeRect(-115, -118, 230, 118);
// Tape deck center
ctx.fillStyle = '#231f2d';
ctx.fillRect(-35, -80, 70, 48);
ctx.strokeRect(-35, -80, 70, 48);
// EQ LEDs
const eqColors = ['#00e5ff', '#22ff88', '#ffcc22', '#ff3355'];
for (let i = 0; i < 4; i++) {
const h = (speakerPulse * 24 + ((i * 7) % 12)) * (soundEnabled ? 1 : 0.35);
ctx.fillStyle = eqColors[i];
ctx.fillRect(-26 + i * 14, -90 - h, 10, h);
}
// Dual Woofers
[-72, 72].forEach(wx => {
const r = 36 + speakerPulse * 6;
ctx.fillStyle = '#141218';
ctx.beginPath(); ctx.arc(wx, -55, r, 0, Math.PI * 2); ctx.fill(); ctx.stroke();
ctx.strokeStyle = '#554e68'; ctx.lineWidth = 2;
ctx.beginPath(); ctx.arc(wx, -55, r * 0.65, 0, Math.PI * 2); ctx.stroke();
ctx.fillStyle = '#ffcc22';
ctx.beginPath(); ctx.arc(wx, -55, 9 + speakerPulse * 3, 0, Math.PI * 2); ctx.fill();
ctx.strokeStyle = '#0f0e12'; ctx.lineWidth = 5;
});
ctx.restore();
}
function drawLimb(x1, y1, x2, y2, w1, w2, color) {
const ang = Math.atan2(y2 - y1, x2 - x1);
const nx = -Math.sin(ang), ny = Math.cos(ang);
ctx.fillStyle = color;
ctx.strokeStyle = '#0f0e12';
ctx.lineWidth = 5;
ctx.lineJoin = 'round';
ctx.beginPath();
ctx.moveTo(x1 + nx * w1, y1 + ny * w1);
ctx.lineTo(x2 + nx * w2, y2 + ny * w2);
ctx.lineTo(x2 - nx * w2, y2 - ny * w2);
ctx.lineTo(x1 - nx * w1, y1 - ny * w1);
ctx.closePath();
ctx.fill();
ctx.stroke();
}
function drawSneaker(x, y, angle, side) {
ctx.save();
ctx.translate(x, y);
ctx.rotate(angle);
// Flip direction based on side/spin
const dir = side >= 0 ? 1 : -1;
ctx.scale(dir, 1);
// Chunky B-boy shell-toe sneaker
ctx.fillStyle = '#ffcc22';
ctx.strokeStyle = '#0f0e12';
ctx.lineWidth = 5;
ctx.lineJoin = 'round';
ctx.beginPath();
ctx.moveTo(-18, -22);
ctx.lineTo(10, -20);
ctx.quadraticCurveTo(38, -14, 42, 0);
ctx.lineTo(-22, 0);
ctx.closePath();
ctx.fill();
ctx.stroke();
// White rubber sole & shell toe
ctx.fillStyle = '#ffffff';
ctx.fillRect(-23, -7, 65, 8);
ctx.strokeRect(-23, -7, 65, 8);
// Classic 2 stripes
ctx.fillStyle = '#ff3355';
ctx.beginPath();
ctx.moveTo(-4, -19); ctx.lineTo(4, -7); ctx.lineTo(11, -7); ctx.lineTo(3, -19);
ctx.fill();
ctx.restore();
}
function drawWing(shoulderX, shoulderY, handX, handY, bendDir) {
const ik = solveIK(shoulderX, shoulderY, handX, handY, 48, 48, bendDir);
drawLimb(shoulderX, shoulderY, ik.jx, ik.jy, 15, 12, '#6d7588');
drawLimb(ik.jx, ik.jy, ik.ex, ik.ey, 13, 10, '#858fa4');
// Feathered wing-hand tips
const ang = Math.atan2(ik.ey - ik.jy, ik.ex - ik.jx);
ctx.save();
ctx.translate(ik.ex, ik.ey);
ctx.rotate(ang);
ctx.fillStyle = '#474d5c';
ctx.strokeStyle = '#0f0e12';
ctx.lineWidth = 4.5;
for (let i = -1; i <= 1; i++) {
ctx.beginPath();
ctx.ellipse(8, i * 6, 14, 5, i * 0.25, 0, Math.PI * 2);
ctx.fill();
ctx.stroke();
}
ctx.restore();
}
function drawLeg(hipX, hipY, footX, footY, footAng, bendDir, side) {
const ik = solveIK(hipX, hipY, footX, footY - 12, 52, 52, bendDir);
// Baggy track pants thigh + scaled bird shin
drawLimb(hipX, hipY, ik.jx, ik.jy, 16, 13, '#ff3355');
drawLimb(ik.jx, ik.jy, ik.ex, ik.ey, 7, 5, '#ff9922');
// Knee cap accent
ctx.fillStyle = '#ffffff';
ctx.beginPath(); ctx.arc(ik.jx, ik.jy, 5, 0, Math.PI * 2); ctx.fill();
drawSneaker(footX, footY, footAng, side);
}
function drawPigeon() {
const {
hipX, hipY, torsoAngle, chestSquash, spinPhase,
headBob, headAngle,
footLX, footLY, footLAng, footRX, footRY, footRAng,
wingLX, wingLY, wingRX, wingRY
} = rig;
const azX = Math.sin(spinPhase);
const azZ = Math.cos(spinPhase);
// Torso vector
const torsoLen = 78 * chestSquash;
const upX = Math.sin(torsoAngle);
const upY = -Math.cos(torsoAngle);
const rightX = -upY;
const rightY = upX;
const chestX = hipX + upX * torsoLen;
const chestY = hipY + upY * torsoLen;
const neckX = chestX + upX * (26 + headBob * 0.4) + rightX * (headBob * 0.7 * (azX >= 0 ? 1 : -1));
const neckY = chestY + upY * (26 + headBob * 0.4) + rightY * (headBob * 0.7);
// Dynamic floor shadow
const lowestY = Math.max(footLY, footRY, wingLY, wingRY, chestY);
const shadowSpread = clamp(1.2 - (FLOOR_Y - hipY) / 260, 0.45, 1.35);
ctx.fillStyle = 'rgba(10, 9, 14, 0.55)';
ctx.beginPath();
ctx.ellipse(hipX, FLOOR_Y + 2, 95 * shadowSpread, 18 * shadowSpread, 0, 0, Math.PI * 2);
ctx.fill();
// Shoulder & Hip sockets with 3D azimuth offset
const hipSpread = 22 * azZ;
const shSpread = 34 * azZ;
const hLX = hipX - rightX * hipSpread, hLY = hipY - rightY * hipSpread;
const hRX = hipX + rightX * hipSpread, hRY = hipY + rightY * hipSpread;
const sLX = chestX - rightX * shSpread, sLY = chestY - rightY * shSpread;
const sRX = chestX + rightX * shSpread, sRY = chestY + rightY * shSpread;
// Depth sort back limbs vs front limbs
const leftInFront = azX >= 0;
// 1. Back Limbs
if (leftInFront) {
drawLeg(hRX, hRY, footRX, footRY, footRAng, rig.bendR, 1);
drawWing(sRX, sRY, wingRX, wingRY, rig.wingBendR);
} else {
drawLeg(hLX, hLY, footLX, footLY, footLAng, rig.bendL, -1);
drawWing(sLX, sLY, wingLX, wingLY, rig.wingBendL);
}
// 2. Tail Feathers
ctx.save();
ctx.translate(hipX - rightX * azX * 14, hipY - upY * 10);
ctx.rotate(torsoAngle + Math.PI - azX * 0.4);
ctx.fillStyle = '#474d5c';
ctx.strokeStyle = '#0f0e12';
ctx.lineWidth = 5;
for (let i = -1; i <= 1; i++) {
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.lineTo(i * 16 - 10, 44);
ctx.lineTo(i * 16 + 10, 44);
ctx.closePath();
ctx.fill();
ctx.stroke();
}
ctx.restore();
// 3. Plump Pigeon Torso + Iridescent Crop
ctx.save();
const midX = (hipX + chestX) * 0.5;
const midY = (hipY + chestY) * 0.5;
ctx.translate(midX, midY);
ctx.rotate(torsoAngle);
// Body ellipse (puffed chest)
ctx.fillStyle = '#7c8599';
ctx.strokeStyle = '#0f0e12';
ctx.lineWidth = 5.5;
ctx.beginPath();
ctx.ellipse(azX * 8, 0, 42 / chestSquash, 48 * chestSquash, 0, 0, Math.PI * 2);
ctx.fill();
ctx.stroke();
// Iridescent green/magenta pigeon neck band
ctx.fillStyle = '#18d89b';
ctx.beginPath();
ctx.ellipse(azX * 10, -24 * chestSquash, 32, 16, 0, 0, Math.PI * 2);
ctx.fill();
ctx.stroke();
ctx.fillStyle = '#d438a8';
ctx.beginPath();
ctx.ellipse(azX * 12, -16 * chestSquash, 26, 9, 0, 0, Math.PI);
ctx.fill();
// Heavy Gold Rope Chain (swings with gravity/spin)
ctx.strokeStyle = '#0f0e12';
ctx.lineWidth = 9;
ctx.beginPath();
ctx.arc(azX * 12, -18 * chestSquash, 24, 0.15, Math.PI - 0.15);
ctx.stroke();
ctx.strokeStyle = '#ffcc22';
ctx.lineWidth = 5;
ctx.stroke();
ctx.restore();
// 4. Head, Beak, Shades & Bucket Hat
ctx.save();
ctx.translate(neckX, neckY);
ctx.rotate(torsoAngle + headAngle);
const faceDir = azX >= -0.15 ? 1 : -1;
ctx.scale(faceDir, 1);
// Head sphere
ctx.fillStyle = '#7c8599';
ctx.strokeStyle = '#0f0e12';
ctx.lineWidth = 5;
ctx.beginPath();
ctx.arc(0, -14, 24, 0, Math.PI * 2);
ctx.fill();
ctx.stroke();
// Bulging orange pigeon eye
ctx.fillStyle = '#ff6600';
ctx.beginPath(); ctx.arc(10, -16, 8.5, 0, Math.PI * 2); ctx.fill(); ctx.stroke();
ctx.fillStyle = '#0f0e12';
ctx.beginPath(); ctx.arc(12, -16, 3.5, 0, Math.PI * 2); ctx.fill();
ctx.fillStyle = '#ffffff';
ctx.beginPath(); ctx.arc(9, -19, 2.5, 0, Math.PI * 2); ctx.fill();
// Sharp Beak + white cere
ctx.fillStyle = '#ffcc22';
ctx.beginPath();
ctx.moveTo(20, -20);
ctx.lineTo(42, -12);
ctx.lineTo(19, -5);
ctx.closePath();
ctx.fill();
ctx.stroke();
ctx.fillStyle = '#ffffff';
ctx.beginPath();
ctx.ellipse(21, -18, 5, 4, 0.3, 0, Math.PI * 2);
ctx.fill();
ctx.stroke();
// Old-school Red Kangol-style Bucket Hat
ctx.fillStyle = '#ff3355';
ctx.beginPath();
// Brim
ctx.moveTo(-34, -22);
ctx.quadraticCurveTo(0, -30, 34, -20);
ctx.lineTo(26, -32);
ctx.lineTo(-26, -32);
ctx.closePath();
ctx.fill();
ctx.stroke();
// Crown
ctx.beginPath();
ctx.moveTo(-24, -30);
ctx.lineTo(-19, -49);
ctx.lineTo(20, -49);
ctx.lineTo(24, -30);
ctx.closePath();
ctx.fill();
ctx.stroke();
// White stripe on hat
ctx.fillStyle = '#ffffff';
ctx.fillRect(-22, -37, 44, 5);
ctx.restore();
// 5. Front Limbs
if (leftInFront) {
drawLeg(hLX, hLY, footLX, footLY, footLAng, rig.bendL, -1);
drawWing(sLX, sLY, wingLX, wingLY, rig.wingBendL);
} else {
drawLeg(hRX, hRY, footRX, footRY, footRAng, rig.bendR, 1);
drawWing(sRX, sRY, wingRX, wingRY, rig.wingBendR);
}
}
function updateAndDrawParticles() {
for (let i = particles.length - 1; i >= 0; i--) {
const p = particles[i];
p.x += p.vx;
p.y += p.vy;
p.vy += 0.25; // gravity
p.life -= p.decay;
if (p.life <= 0) {
particles.splice(i, 1);
continue;
}
ctx.save();
ctx.globalAlpha = clamp(p.life, 0, 1);
if (p.type === 'dust') {
ctx.fillStyle = '#e5dac3';
ctx.strokeStyle = '#0f0e12';
ctx.lineWidth = 3;
ctx.beginPath();
ctx.arc(p.x, p.y, p.size * p.life, 0, Math.PI * 2);
ctx.fill();
ctx.stroke();
} else {
ctx.fillStyle = '#00e5ff';
ctx.beginPath();
ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2);
ctx.fill();
}
ctx.restore();
}
}
// --- MAIN ANIMATION LOOP ---
let lastTime = performance.now();
function tick(now) {
const dt = Math.min((now - lastTime) / 1000, 0.05);
lastTime = now;
// Advance beat clock (unless vinyl scratch is active)
if (scratchTimer > 0) {
scratchTimer -= dt;
beatPos -= dt * 3.5; // Rewind spin effect!
} else {
beatPos += dt * (bpm / 60);
}
// Sequencer for 16th-note Breakbeat
const step16 = Math.floor(((beatPos % 4) + 4) % 4 * 4);
if (step16 !== lastStep && scratchTimer <= 0) {
lastStep = step16;
// Classic electro breakbeat pattern
const kicks = [1,0,0,0, 0,0,1,0, 0,0,1,0, 0,0,0,0];
const snares = [0,0,0,0, 1,0,0,0, 0,0,0,0, 1,0,0,1];
const hats = [1,0,1,1, 1,0,1,0, 1,0,1,1, 1,0,2,0];
if (kicks[step16]) { playKick(); speakerPulse = 1.0; }
if (snares[step16]) { playSnare(); speakerPulse = 0.75; }
if (hats[step16]) { playHat(hats[step16] === 2); }
}
speakerPulse = Math.max(0, speakerPulse - dt * 6);
// Compute target choreography pose
const target = computeTargetPose(beatPos);
// Spring-damper physics integration for snappy weight & overshoot
const stiffness = activeFreeze ? 290 : 220;
const damping = activeFreeze ? 18 : 21;
for (const k in target) {
if (typeof target[k] === 'number') {
const diff = target[k] - rig[k];
vel[k] += diff * stiffness * dt;
vel[k] *= Math.exp(-damping * dt);
rig[k] += vel[k] * dt;
} else {
rig[k] = target[k];
}
}
// Detect heavy floor impacts to spawn dust clouds
if (prevFootLY < FLOOR_Y - 12 && rig.footLY >= FLOOR_Y - 4) {
spawnParticles(rig.footLX, FLOOR_Y, 'dust', 5);
}
if (prevFootRY < FLOOR_Y - 12 && rig.footRY >= FLOOR_Y - 4) {
spawnParticles(rig.footRX, FLOOR_Y, 'dust', 5);
}
if (prevHipY < FLOOR_Y - 85 && rig.hipY >= FLOOR_Y - 75) {
spawnParticles(rig.hipX, FLOOR_Y - 10, 'dust', 9);
}
if (currentMove === 'headspin' && Math.random() < 0.35) {
spawnParticles(rig.hipX, FLOOR_Y - 18, 'sweat', 1);
}
prevFootLY = rig.footLY;
prevFootRY = rig.footRY;
prevHipY = rig.hipY;
// Render Scene
ctx.clearRect(0, 0, canvas.width, canvas.height);
drawStage(beatPos);
drawPigeon();
updateAndDrawParticles();
requestAnimationFrame(tick);
}
// --- UI & CONTROLS ---
const moveBtns = document.querySelectorAll('button[data-move]');
const btnAuto = document.getElementById('btn-auto');
const freezeBtns = document.querySelectorAll('button[data-freeze]');
const bpmSlider = document.getElementById('bpm-slider');
const bpmVal = document.getElementById('bpm-val');
const btnAudio = document.getElementById('btn-audio');
const btnScratch = document.getElementById('btn-scratch');
function updateMoveButtons() {
btnAuto.classList.toggle('active', autoCypher);
moveBtns.forEach(b => {
b.classList.toggle('active', !autoCypher && b.dataset.move === currentMove);
});
}
btnAuto.addEventListener('click', () => {
autoCypher = true;
updateMoveButtons();
});
moveBtns.forEach(btn => {
btn.addEventListener('click', () => {
autoCypher = false;
currentMove = btn.dataset.move;
updateMoveButtons();
spawnParticles(rig.hipX, FLOOR_Y - 20, 'dust', 7);
});
});
function triggerFreeze(type) {
activeFreeze = type;
freezeBtns.forEach(b => b.classList.toggle('active', b.dataset.freeze === type));
if (type) {
spawnParticles(rig.hipX, FLOOR_Y - 5, 'dust', 10);
playSnare();
}
}
freezeBtns.forEach(btn => {
const f = btn.dataset.freeze;
btn.addEventListener('pointerdown', () => triggerFreeze(f));
btn.addEventListener('pointerup', () => triggerFreeze(null));
btn.addEventListener('pointerleave', () => { if (activeFreeze === f) triggerFreeze(null); });
});
bpmSlider.addEventListener('input', (e) => {
bpm = parseInt(e.target.value, 10);
bpmVal.textContent = bpm;
});
function toggleAudio() {
initAudio();
soundEnabled = !soundEnabled;
btnAudio.innerHTML = `SOUND: ${soundEnabled ? 'ON' : 'OFF'}<span class="key-hint">[M]</span>`;
btnAudio.classList.toggle('active', soundEnabled);
}
btnAudio.addEventListener('click', toggleAudio);
function doScratch() {
scratchTimer = 0.26;
playScratch();
spawnParticles(rig.hipX, rig.hipY, 'sweat', 8);
setCallout('WIKI-WIKI SCRATCH!');
}
btnScratch.addEventListener('click', doScratch);
// Clicking stage directly cycles freezes or pops a scratch
canvas.addEventListener('pointerdown', (e) => {
if (!soundEnabled && !audioCtx) toggleAudio();
const rect = canvas.getBoundingClientRect();
const clickX = (e.clientX - rect.left) * (canvas.width / rect.width);
if (clickX < 340) {
doScratch();
} else {
const freezes = ['baby', 'chair', 'hollow'];
triggerFreeze(freezes[Math.floor(Math.random() * freezes.length)]);
}
});
window.addEventListener('pointerup', () => {
if (activeFreeze) triggerFreeze(null);
});
// Keyboard shortcuts
window.addEventListener('keydown', (e) => {
if (e.repeat) return;
const k = e.key.toLowerCase();
if (k === 'a') { autoCypher = true; updateMoveButtons(); }
if (k === '1') { autoCypher = false; currentMove = 'toprock'; updateMoveButtons(); }
if (k === '2') { autoCypher = false; currentMove = 'sixstep'; updateMoveButtons(); }
if (k === '3') { autoCypher = false; currentMove = 'windmill'; updateMoveButtons(); }
if (k === '4') { autoCypher = false; currentMove = 'headspin'; updateMoveButtons(); }
if (k === 'q') triggerFreeze('baby');
if (k === 'w') triggerFreeze('chair');
if (k === 'e') triggerFreeze('hollow');
if (k === 'm') toggleAudio();
if (k === ' ') { e.preventDefault(); doScratch(); }
});
window.addEventListener('keyup', (e) => {
const k = e.key.toLowerCase();
if (['q', 'w', 'e'].includes(k)) triggerFreeze(null);
});
requestAnimationFrame(tick);
})();
</script>
</body>
</html>
snapEase) fed into an underdamped spring-damper physics solver and a 2-bone Inverse Kinematics rig, so feet and wings plant firmly onto the cardboard while the torso whips, drops, and recoils with momentum.1–4), hold Q/W/E (or click the stage) to slam into an instant physics-overshot freeze on the cardboard, drag the BPM slider (84–148) to scale both the dance velocity and the synthesized Web Audio 808 breakbeat, or hit SPACE (or click the boombox) to trigger a vinyl record scratch that physically rewinds the beat clock and the bird's rotation.0 radians; with more time I would add an intermediate coin-drop transition animation and true 3D polygon sorting for the tail and wing feathers.