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 lanky nocturnal heron in a bucket hat and high-top sneakers, using its oversized wings as planted arms to whip its spindly legs overhead. The look is gritty late-80s risograph street-poster ink: stark bone-white paper grain, heavy dark navy linework, offset electric-cyan and hot-coral spot fills, and scuffed cardboard underfoot.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>B-BOY HERON // CARDBOARD SESSION</title>
<style>
:root {
--paper: #f2ece1;
--ink: #121624;
--coral: #ff473a;
--cyan: #00b8d4;
--ochre: #e5b869;
--ochre-dark: #c99746;
}
* { box-sizing: border-box; user-select: none; -webkit-user-select: none; }
body, html {
margin: 0; padding: 0; width: 100%; height: 100%;
background-color: var(--paper);
color: var(--ink);
font-family: ui-monospace, 'Cascadia Code', 'Source Code Pro', Menlo, Consolas, monospace;
overflow: hidden;
display: flex;
flex-direction: column;
}
#stage-wrap {
position: relative;
flex: 1;
width: 100%;
overflow: hidden;
}
canvas {
display: block;
width: 100%;
height: 100%;
}
/* Risograph paper noise overlay */
.grain {
pointer-events: none;
position: fixed;
inset: 0;
opacity: 0.22;
mix-blend-mode: multiply;
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
}
header {
position: absolute;
top: 18px; left: 22px; right: 22px;
display: flex;
justify-content: space-between;
align-items: flex-start;
pointer-events: none;
z-index: 5;
}
.title-block h1 {
margin: 0;
font-size: 18px;
letter-spacing: 0.08em;
text-transform: uppercase;
background: var(--ink);
color: var(--paper);
padding: 4px 10px;
display: inline-block;
box-shadow: 4px 4px 0 var(--coral);
}
.title-block .sub {
margin-top: 8px;
font-size: 11px;
font-weight: 700;
letter-spacing: 0.05em;
}
.move-readout {
text-align: right;
}
#move-name {
font-size: 22px;
font-weight: 900;
letter-spacing: 0.04em;
color: var(--ink);
text-shadow: 2px 2px 0 var(--cyan);
}
#beat-dots {
margin-top: 4px;
display: flex;
gap: 6px;
justify-content: flex-end;
}
.dot {
width: 10px; height: 10px;
border: 2px solid var(--ink);
background: transparent;
}
.dot.active { background: var(--coral); }
.dot.downbeat.active { background: var(--cyan); transform: scale(1.25); }
/* Controls Deck */
#deck {
background: var(--ink);
color: var(--paper);
padding: 12px 20px;
display: flex;
flex-wrap: wrap;
gap: 16px 24px;
align-items: center;
justify-content: space-between;
border-top: 4px solid var(--coral);
z-index: 10;
}
.ctrl-group {
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
}
.label {
font-size: 10px;
text-transform: uppercase;
letter-spacing: 0.1em;
color: var(--ochre);
margin-right: 2px;
}
button {
font-family: inherit;
font-size: 12px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.05em;
background: var(--paper);
color: var(--ink);
border: 2px solid var(--paper);
padding: 6px 11px;
cursor: pointer;
box-shadow: 3px 3px 0 var(--cyan);
transition: transform 0.06s, box-shadow 0.06s;
}
button:hover {
background: #fff;
}
button:active, button.held {
transform: translate(2px, 2px);
box-shadow: 1px 1px 0 var(--coral);
background: var(--coral);
color: var(--paper);
border-color: var(--coral);
}
button.Queued {
border-color: var(--cyan);
background: var(--cyan);
color: var(--ink);
}
button.audio-on {
background: var(--ochre);
border-color: var(--ochre);
color: var(--ink);
}
input[type="range"] {
-webkit-appearance: none;
appearance: none;
width: 96px;
height: 6px;
background: var(--paper);
outline: none;
cursor: pointer;
}
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
width: 14px; height: 18px;
background: var(--coral);
border: 2px solid var(--paper);
cursor: pointer;
}
.hint {
font-size: 10px;
opacity: 0.75;
letter-spacing: 0.04em;
}
@media (max-width: 760px) {
#deck { padding: 10px 12px; gap: 10px; }
button { padding: 5px 8px; font-size: 11px; }
.hint { display: none; }
}
</style>
</head>
<body>
<div id="stage-wrap">
<header>
<div class="title-block">
<h1>NIGHT HERON // CYPHER 88</h1>
<div class="sub">INK & CORRUGATED BOARD</div>
</div>
<div class="move-readout">
<div id="move-name">INDIAN STEP</div>
<div id="beat-dots">
<div class="dot downbeat active"></div>
<div class="dot"></div>
<div class="dot"></div>
<div class="dot"></div>
</div>
</div>
</header>
<canvas id="c"></canvas>
</div>
<div id="deck">
<div class="ctrl-group">
<span class="label">CALL SET</span>
<button data-set="TOPROCK" title="Key: 1">1. Toprock</button>
<button data-set="FOOTWORK" title="Key: 2">2. 6-Step</button>
<button data-set="POWER" title="Key: 3">3. Windmill</button>
<button data-set="FLARE" title="Key: 4">4. Airchair Freeze</button>
</div>
<div class="ctrl-group">
<button id="btn-freeze" title="Hold SPACEBAR to stall & scratch">HOLD FREEZE [SPACE]</button>
<button id="btn-audio" title="Toggle WebAudio Boombox">BOOMBOX: OFF</button>
</div>
<div class="ctrl-group">
<span class="label">BPM <span id="bpm-val">112</span></span>
<input type="range" id="bpm" min="84" max="142" value="112">
<span class="label">FLAVOR</span>
<input type="range" id="grit" min="0" max="100" value="45" title="Pose exaggeration & ink misregistration">
<span class="hint">[CLICK FLOOR TO TOSS HYPE]</span>
</div>
</div>
<div class="grain"></div>
<script>
(() => {
const canvas = document.getElementById('c');
const ctx = canvas.getContext('2d');
const moveNameEl = document.getElementById('move-name');
const dots = Array.from(document.querySelectorAll('.dot'));
const bpmSlider = document.getElementById('bpm');
const bpmValEl = document.getElementById('bpm-val');
const gritSlider = document.getElementById('grit');
const btnFreeze = document.getElementById('btn-freeze');
const btnAudio = document.getElementById('btn-audio');
const setButtons = Array.from(document.querySelectorAll('button[data-set]'));
let W = 800, H = 600, DPR = 1;
function resize() {
DPR = Math.min(window.devicePixelRatio || 1, 2);
const rect = canvas.parentElement.getBoundingClientRect();
W = rect.width;
H = rect.height;
canvas.width = W * DPR;
canvas.height = H * DPR;
ctx.setTransform(DPR, 0, 0, DPR, 0, 0);
}
window.addEventListener('resize', resize);
resize();
// --- AUDIO: Minimal WebAudio 808 Breakbeat + Vinyl Scratch ---
let audioCtx = null;
let audioOn = false;
let lastStepPlayed = -1;
function initAudio() {
if (!audioCtx) {
const AC = window.AudioContext || window.webkitAudioContext;
audioCtx = new AC();
}
if (audioCtx.state === 'suspended') audioCtx.resume();
}
function playKick(time, punch = 1) {
if (!audioCtx || !audioOn) return;
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.type = 'sine';
osc.frequency.setValueAtTime(145 * punch, time);
osc.frequency.exponentialRampToValueAtTime(38, time + 0.14);
gain.gain.setValueAtTime(0.65, time);
gain.gain.exponentialRampToValueAtTime(0.001, time + 0.28);
osc.connect(gain);
gain.connect(audioCtx.destination);
osc.start(time);
osc.stop(time + 0.29);
}
function playSnare(time, rim = false) {
if (!audioCtx || !audioOn) return;
const bufSize = audioCtx.sampleRate * (rim ? 0.05 : 0.16);
const buf = audioCtx.createBuffer(1, bufSize, audioCtx.sampleRate);
const data = buf.getChannelData(0);
for (let i = 0; i < bufSize; i++) data[i] = (Math.random() * 2 - 1) * Math.exp(-i / (bufSize * 0.35));
const noise = audioCtx.createBufferSource();
noise.buffer = buf;
const filter = audioCtx.createBiquadFilter();
filter.type = rim ? 'bandpass' : 'highpass';
filter.frequency.value = rim ? 1800 : 900;
const gain = audioCtx.createGain();
gain.gain.setValueAtTime(rim ? 0.25 : 0.42, time);
gain.gain.exponentialRampToValueAtTime(0.01, time + (rim ? 0.05 : 0.16));
noise.connect(filter);
filter.connect(gain);
gain.connect(audioCtx.destination);
noise.start(time);
if (!rim) {
const body = audioCtx.createOscillator();
const bGain = audioCtx.createGain();
body.type = 'triangle';
body.frequency.setValueAtTime(190, time);
body.frequency.exponentialRampToValueAtTime(95, time + 0.08);
bGain.gain.setValueAtTime(0.28, time);
bGain.gain.exponentialRampToValueAtTime(0.001, time + 0.09);
body.connect(bGain);
bGain.connect(audioCtx.destination);
body.start(time);
body.stop(time + 0.1);
}
}
function playHat(time, open = false) {
if (!audioCtx || !audioOn) return;
const dur = open ? 0.18 : 0.04;
const bufSize = Math.floor(audioCtx.sampleRate * dur);
const buf = audioCtx.createBuffer(1, bufSize, audioCtx.sampleRate);
const data = buf.getChannelData(0);
for (let i = 0; i < bufSize; i++) data[i] = (Math.random() * 2 - 1) * Math.exp(-i / (bufSize * 0.4));
const src = audioCtx.createBufferSource();
src.buffer = buf;
const hp = audioCtx.createBiquadFilter();
hp.type = 'highpass';
hp.frequency.value = 6500;
const g = audioCtx.createGain();
g.gain.value = open ? 0.16 : 0.11;
src.connect(hp); hp.connect(g); g.connect(audioCtx.destination);
src.start(time);
}
function playScratch() {
if (!audioCtx || !audioOn) return;
const now = audioCtx.currentTime;
const osc = audioCtx.createOscillator();
const g = audioCtx.createGain();
osc.type = 'sawtooth';
osc.frequency.setValueAtTime(220, now);
osc.frequency.exponentialRampToValueAtTime(780, now + 0.06);
osc.frequency.exponentialRampToValueAtTime(150, now + 0.13);
const bp = audioCtx.createBiquadFilter();
bp.type = 'bandpass';
bp.frequency.value = 1200;
g.gain.setValueAtTime(0.18, now);
g.gain.exponentialRampToValueAtTime(0.001, now + 0.14);
osc.connect(bp); bp.connect(g); g.connect(audioCtx.destination);
osc.start(now);
osc.stop(now + 0.15);
}
// Classic syncopated 16-step b-boy pattern
const KICK_PAT = [1,0,0,0, 0,0,1,0, 0,0,1,0, 0,0,0,0];
const SNARE_PAT = [0,0,0,0, 1,0,0,1, 0,0,0,0, 1,0,0,0];
const HAT_PAT = [1,0,1,0, 1,0,1,1, 1,0,1,0, 1,0,2,0]; // 2 = open
// --- EASING & WEIGHT CURVES ---
// Snappy throw + heavy settle / hold
function snapHold(t) {
if (t < 0.38) {
const u = t / 0.38;
// Fast whip with tiny overshoot
return 1 + 0.12 * Math.sin(u * Math.PI) * (1 - u) - Math.pow(1 - u, 3);
}
return 1;
}
function heavyDrop(t) {
if (t < 0.45) {
const u = t / 0.45;
return u * u * (3 - 2 * u);
}
// Impact compression rebound
const u = (t - 0.45) / 0.55;
return 1 - Math.sin(u * Math.PI * 2) * 0.06 * (1 - u);
}
function lerp(a, b, t) { return a + (b - a) * t; }
function lerpPose(A, B, t) {
const out = {};
for (const k in A) {
out[k] = lerp(A[k], B[k], t);
}
return out;
}
// --- KEYFRAME POSES ---
// Coordinates are relative to ground center (0, 0), Y up is negative!
// Floor is at y = 0.
const POSES = {
// Standing ready bounce
READY: {
hipX: 0, hipY: -102,
torsoAng: -0.06,
neckBend: 0.25, headAng: 0.05,
footLX: -28, footLY: 0, kneeLDir: -1,
footRX: 28, footRY: 0, kneeRDir: 1,
handLX: -46, handLY: -95, elbLDir: -1,
handRX: 44, handRY: -102, elbRDir: 1,
spin: 0, hatTilt: 0
},
// Toprock cross-step left
TOP_CROSS_L: {
hipX: 16, hipY: -94,
torsoAng: 0.22,
neckBend: -0.15, headAng: -0.12,
footLX: 38, footLY: -4, kneeLDir: 1,
footRX: -18, footRY: 0, kneeRDir: -1,
handLX: -68, handLY: -135, elbLDir: -1,
handRX: 66, handRY: -118, elbRDir: 1,
spin: -0.4, hatTilt: -0.08
},
// Toprock open hopkick
TOP_OPEN: {
hipX: -6, hipY: -112,
torsoAng: -0.12,
neckBend: 0.35, headAng: 0.1,
footLX: -44, footLY: 0, kneeLDir: -1,
footRX: 52, footRY: -18, kneeRDir: 1,
handLX: 20, handLY: -112, elbLDir: -1,
handRX: -18, handRY: -108, elbRDir: 1,
spin: 0.2, hatTilt: 0.05
},
// Toprock cross-step right
TOP_CROSS_R: {
hipX: -18, hipY: -92,
torsoAng: -0.24,
neckBend: 0.4, headAng: 0.15,
footLX: 16, footLY: 0, kneeLDir: 1,
footRX: -42, footRY: -4, kneeRDir: -1,
handLX: -64, handLY: -125, elbLDir: -1,
handRX: 72, handRY: -140, elbRDir: 1,
spin: 0.45, hatTilt: 0.08
},
// Drop transition (Godown / Knee drop)
GODOWN: {
hipX: -8, hipY: -46,
torsoAng: 0.45,
neckBend: -0.2, headAng: -0.1,
footLX: -34, footLY: 0, kneeLDir: -1,
footRX: 22, footRY: 0, kneeRDir: 1,
handLX: -42, handLY: -2, elbLDir: -1,
handRX: 48, handRY: -58, elbRDir: 1,
spin: -0.3, hatTilt: -0.05
},
// 6-Step Footwork poses (wings planted on cardboard!)
FW_1_HOOK: {
hipX: -10, hipY: -38,
torsoAng: 0.65,
neckBend: -0.35, headAng: -0.15,
footLX: 36, footLY: -4, kneeLDir: 1,
footRX: -12, footRY: 0, kneeRDir: -1,
handLX: -42, handLY: 0, elbLDir: -1,
handRX: 26, handRY: -52, elbRDir: 1,
spin: -0.6, hatTilt: -0.1
},
FW_2_BACK: {
hipX: 0, hipY: -44,
torsoAng: 0.2,
neckBend: 0.1, headAng: 0.0,
footLX: -48, footLY: 0, kneeLDir: -1,
footRX: 42, footRY: 0, kneeRDir: 1,
handLX: -28, handLY: 0, elbLDir: -1,
handRX: 28, handRY: 0, elbRDir: 1,
spin: 0.0, hatTilt: 0
},
FW_3_SWEEP: {
hipX: 16, hipY: -36,
torsoAng: -0.55,
neckBend: 0.4, headAng: 0.2,
footLX: -66, footLY: -12, kneeLDir: -1, // sweeping kick!
footRX: 18, footRY: 0, kneeRDir: 1,
handLX: -38, handLY: -54, elbLDir: -1,
handRX: 42, handRY: 0, elbRDir: 1,
spin: 0.7, hatTilt: 0.1
},
FW_4_FRONT: {
hipX: 4, hipY: -34,
torsoAng: -0.15,
neckBend: 0.25, headAng: 0.08,
footLX: -22, footLY: 0, kneeLDir: -1,
footRX: -46, footRY: -6, kneeRDir: -1,
handLX: -18, handLY: 0, elbLDir: -1,
handRX: 38, handRY: 0, elbRDir: 1,
spin: 0.3, hatTilt: 0.04
},
// Power: Windmill / Continuous Flare
MILL_ENTRY: {
hipX: -14, hipY: -52,
torsoAng: 1.35,
neckBend: -0.4, headAng: -0.3,
footLX: -78, footLY: -76, kneeLDir: -1,
footRX: 64, footRY: -28, kneeRDir: 1,
handLX: -28, handLY: 0, elbLDir: -1,
handRX: 22, handRY: -6, elbRDir: 1,
spin: -0.8, hatTilt: -0.15
},
MILL_BACK: {
hipX: 0, hipY: -26, // Upper back / shoulders on cardboard!
torsoAng: 2.85,
neckBend: 0.15, headAng: 0.0,
footLX: -72, footLY: -108, kneeLDir: -1, // wide V-legs in the air
footRX: 74, footRY: -104, kneeRDir: 1,
handLX: -48, handLY: -14, elbLDir: -1,
handRX: 48, handRY: -14, elbRDir: 1,
spin: 0.0, hatTilt: 0.18
},
MILL_WHIP: {
hipX: 16, hipY: -54,
torsoAng: -1.45,
neckBend: 0.45, headAng: 0.25,
footLX: -58, footLY: -34, kneeLDir: -1,
footRX: 82, footRY: -88, kneeRDir: 1,
handLX: -22, handLY: -8, elbLDir: -1,
handRX: 26, handRY: 0, elbRDir: 1,
spin: 0.85, hatTilt: 0.12
},
// Signature Freezes (Hard dead-stops with recoil)
BABY_FREEZE: {
hipX: 18, hipY: -62,
torsoAng: 1.85,
neckBend: -0.55, headAng: -0.45,
footLX: 68, footLY: -92, kneeLDir: 1,
footRX: 42, footRY: -122, kneeRDir: -1,
handLX: -26, handLY: 0, elbLDir: -1,
handRX: 18, handRY: 0, elbRDir: 1,
spin: -0.4, hatTilt: -0.2
},
HOLLOWBACK: {
hipX: -6, hipY: -124,
torsoAng: 3.02, // Inverted!
neckBend: -0.65, headAng: -0.7,
footLX: -48, footLY: -162, kneeLDir: 1,
footRX: -16, footRY: -182, kneeRDir: 1,
handLX: -26, handLY: 0, elbLDir: -1,
handRX: 26, handRY: 0, elbRDir: 1,
spin: 0.15, hatTilt: -0.25
},
AIRCHAIR: {
hipX: 12, hipY: -74,
torsoAng: -2.15,
neckBend: 0.55, headAng: 0.4,
footLX: -66, footLY: -138, kneeLDir: -1, // high bicycles
footRX: 42, footRY: -155, kneeRDir: 1,
handLX: -62, handLY: -118, elbLDir: -1, // one wing pointing to sky!
handRX: 6, handRY: 0, elbRDir: 1, // entire bird balanced on right wing!
spin: 0.5, hatTilt: 0.22
},
// Recovery breath / dust-off
RECOVER: {
hipX: 4, hipY: -96,
torsoAng: 0.12,
neckBend: 0.42, headAng: 0.22,
footLX: -24, footLY: 0, kneeLDir: -1,
footRX: 32, footRY: 0, kneeRDir: 1,
handLX: -34, handLY: -76, elbLDir: -1, // brushing off knee/hat
handRX: 22, handRY: -138, elbRDir: 1, // adjusting bucket hat!
spin: 0.15, hatTilt: -0.12
}
};
// --- CHOREOGRAPHY SETS ---
// Each step has: pose, beats duration, ease type, move label, impact dust trigger
const SETS = {
TOPROCK: [
{ pose: 'READY', beats: 1, ease: 'drop', label: 'UPROCK BOUNCE', dust: 0 },
{ pose: 'TOP_CROSS_L', beats: 1, ease: 'snap', label: 'INDIAN STEP', dust: 1 },
{ pose: 'TOP_OPEN', beats: 1, ease: 'drop', label: 'INDIAN STEP', dust: 0 },
{ pose: 'TOP_CROSS_R', beats: 1, ease: 'snap', label: 'CROSS STEP', dust: 1 },
{ pose: 'TOP_OPEN', beats: 0.5, ease: 'snap', label: 'SALSA ROCK', dust: 0 },
{ pose: 'TOP_CROSS_L', beats: 1.5, ease: 'snap', label: 'SALSA ROCK', dust: 1 },
{ pose: 'RECOVER', beats: 2, ease: 'drop', label: 'HAT CHECK', dust: 0 }
],
FOOTWORK: [
{ pose: 'GODOWN', beats: 1, ease: 'drop', label: 'KNEE DROP', dust: 2 },
{ pose: 'FW_1_HOOK', beats: 0.75, ease: 'snap', label: '6-STEP // HOOK', dust: 1 },
{ pose: 'FW_2_BACK', beats: 0.75, ease: 'snap', label: '6-STEP // BACK', dust: 0 },
{ pose: 'FW_3_SWEEP', beats: 0.75, ease: 'snap', label: 'CC SWEEP', dust: 2 },
{ pose: 'FW_4_FRONT', beats: 0.75, ease: 'snap', label: '6-STEP // THRU', dust: 1 },
{ pose: 'FW_1_HOOK', beats: 0.75, ease: 'snap', label: 'ZULU SPIN', dust: 1 },
{ pose: 'FW_3_SWEEP', beats: 0.75, ease: 'snap', label: 'LEG THREAD', dust: 1 },
{ pose: 'BABY_FREEZE',beats: 2.5, ease: 'snap', label: 'BABY FREEZE!', dust: 3, freezeHit: true }
],
POWER: [
{ pose: 'GODOWN', beats: 1, ease: 'drop', label: 'COIN DROP', dust: 1 },
{ pose: 'MILL_ENTRY', beats: 0.6, ease: 'snap', label: 'WINDMILL ENTRY', dust: 2 },
{ pose: 'MILL_BACK', beats: 0.6, ease: 'snap', label: 'BACKSPIN MILL', dust: 3 },
{ pose: 'MILL_WHIP', beats: 0.6, ease: 'snap', label: 'BARREL MILL', dust: 1 },
{ pose: 'MILL_ENTRY', beats: 0.55,ease: 'snap', label: 'BELLY MILL', dust: 2 },
{ pose: 'MILL_BACK', beats: 0.55,ease: 'snap', label: 'POWER WHIP', dust: 3 },
{ pose: 'MILL_WHIP', beats: 0.6, ease: 'snap', label: 'FLARE EXIT', dust: 2 },
{ pose: 'HOLLOWBACK', beats: 2.5, ease: 'snap', label: 'HOLLOWBACK!', dust: 3, freezeHit: true },
{ pose: 'RECOVER', beats: 1, ease: 'drop', label: 'RESET BREATH', dust: 1 }
],
FLARE: [
{ pose: 'TOP_OPEN', beats: 1, ease: 'snap', label: 'WIND UP', dust: 0 },
{ pose: 'MILL_WHIP', beats: 0.75, ease: 'snap', label: 'VIRGIN FLARE', dust: 2 },
{ pose: 'MILL_ENTRY', beats: 0.75, ease: 'snap', label: 'AIR FLARE WHIP', dust: 2 },
{ pose: 'AIRCHAIR', beats: 3.5, ease: 'snap', label: 'ONE-WING AIRCHAIR!', dust: 3, freezeHit: true },
{ pose: 'GODOWN', beats: 1, ease: 'drop', label: 'TOUCHDOWN', dust: 2 },
{ pose: 'RECOVER', beats: 1, ease: 'drop', label: 'BRUSH SHOULDERS',dust: 0 }
]
};
const SET_ORDER = ['TOPROCK', 'FOOTWORK', 'POWER', 'TOPROCK', 'FLARE'];
let routineIdx = 0;
let currentSetKey = 'TOPROCK';
let queuedSetKey = null;
let stepIdx = 0;
let stepBeatProgress = 0;
let prevPose = { ...POSES.READY };
let freezeHeld = false;
// Spring physics for secondary motion (crest feathers, bucket hat, impact recoil)
let crestAngle = 0, crestVel = 0;
let camShake = 0;
let freezeFlash = 0;
let totalBeats = 0;
// Particles: scuffs, sweat/ink droplets, hype shouts
const particles = [];
const cardboardScuffs = [];
for (let i = 0; i < 18; i++) {
cardboardScuffs.push({
x: (Math.random() - 0.5) * 240,
y: (Math.random() - 0.5) * 28,
len: 12 + Math.random() * 34,
ang: (Math.random() - 0.5) * 0.35
});
}
function spawnDust(x, y, count, burst = false) {
for (let i = 0; i < count; i++) {
const a = -Math.PI * Math.random();
const sp = (burst ? 140 : 75) * (0.4 + Math.random());
particles.push({
type: burst && i % 3 === 0 ? 'star' : 'ink',
x, y,
vx: Math.cos(a) * sp + (Math.random() - 0.5) * 60,
vy: Math.sin(a) * sp * 0.65 - 20,
r: burst ? 3 + Math.random() * 4 : 2 + Math.random() * 3,
life: 1,
decay: 1.4 + Math.random() * 1.2,
color: i % 2 === 0 ? '#00b8d4' : '#ff473a'
});
}
}
const HYPE_WORDS = ['FRESH!', 'YO!', 'HOLD IT!', 'B-BIRD!', 'OFF THE HEAD!', 'ILL!', 'BURN!'];
function spawnHype(x, y, customWord) {
const word = customWord || HYPE_WORDS[Math.floor(Math.random() * HYPE_WORDS.length)];
particles.push({
type: 'word',
word,
x, y,
vx: (Math.random() - 0.5) * 40,
vy: -65 - Math.random() * 35,
rot: (Math.random() - 0.5) * 0.35,
life: 1,
decay: 0.95
});
spawnDust(x, y, 8, true);
}
// --- 2-BONE INVERSE KINEMATICS ---
function solveIK(x1, y1, x2, y2, L1, L2, bendDir) {
let dx = x2 - x1;
let dy = y2 - y1;
let d = Math.hypot(dx, dy);
const maxReach = (L1 + L2) * 0.995;
if (d > maxReach) {
dx = (dx / d) * maxReach;
dy = (dy / d) * maxReach;
d = maxReach;
}
if (d < Math.abs(L1 - L2) + 0.01) d = Math.abs(L1 - L2) + 0.01;
const cosA = (L1 * L1 + d * d - L2 * L2) / (2 * L1 * d);
const clamped = Math.max(-1, Math.min(1, cosA));
const a = Math.acos(clamped);
const base = Math.atan2(dy, dx);
const jointAngle = base + bendDir * a;
return {
jx: x1 + Math.cos(jointAngle) * L1,
jy: y1 + Math.sin(jointAngle) * L1,
ex: x1 + dx,
ey: y1 + dy
};
}
// --- CONTROLS & INTERACTION ---
function queueSet(key, immediate = false) {
setButtons.forEach(b => b.classList.toggle('Queued', b.dataset.set === key));
if (immediate) {
prevPose = { ...currentPoseSnapshot };
currentSetKey = key;
stepIdx = 0;
stepBeatProgress = 0;
setButtons.forEach(b => b.classList.remove('Queued'));
triggerStepFX(SETS[currentSetKey][0]);
} else {
queuedSetKey = key;
}
}
setButtons.forEach(btn => {
btn.addEventListener('click', () => {
initAudio();
queueSet(btn.dataset.set, true);
});
});
function setFreeze(state) {
if (freezeHeld === state) return;
freezeHeld = state;
btnFreeze.classList.toggle('held', freezeHeld);
if (freezeHeld) {
initAudio();
playScratch();
camShake = 7;
freezeFlash = 1;
spawnHype(W * 0.5 + (Math.random() - 0.5) * 140, H * 0.42, 'FREEZE!!');
}
}
btnFreeze.addEventListener('pointerdown', (e) => { e.preventDefault(); setFreeze(true); });
window.addEventListener('pointerup', () => setFreeze(false));
window.addEventListener('keydown', (e) => {
if (e.code === 'Space') { e.preventDefault(); setFreeze(true); }
if (e.key === '1') queueSet('TOPROCK', true);
if (e.key === '2') queueSet('FOOTWORK', true);
if (e.key === '3') queueSet('POWER', true);
if (e.key === '4') queueSet('FLARE', true);
});
window.addEventListener('keyup', (e) => {
if (e.code === 'Space') setFreeze(false);
});
btnAudio.addEventListener('click', () => {
initAudio();
audioOn = !audioOn;
btnAudio.textContent = audioOn ? 'BOOMBOX: ON' : 'BOOMBOX: OFF';
btnAudio.classList.toggle('audio-on', audioOn);
});
bpmSlider.addEventListener('input', () => {
bpmValEl.textContent = bpmSlider.value;
});
canvas.addEventListener('pointerdown', (e) => {
initAudio();
const rect = canvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
spawnHype(x, y);
if (audioOn) playSnare(audioCtx.currentTime, true);
});
function triggerStepFX(step) {
moveNameEl.textContent = step.label;
const floorY = H * 0.77;
const cx = W * 0.5;
if (step.dust > 0) {
spawnDust(cx + (Math.random() - 0.5) * 70, floorY - 4, step.dust * 5, !!step.freezeHit);
}
if (step.freezeHit) {
camShake = 9;
freezeFlash = 0.85;
spawnHype(cx + 95, floorY - 150, 'FREEZE!');
if (audioOn && audioCtx) playKick(audioCtx.currentTime, 1.35);
}
}
// --- DRAWING HELPERS (Riso Offset Ink Aesthetic) ---
let currentPoseSnapshot = { ...POSES.READY };
function drawPolygon(pts, fill, stroke, lineW = 3.2, offsetX = 0, offsetY = 0) {
if (fill) {
ctx.fillStyle = fill;
ctx.beginPath();
ctx.moveTo(pts[0][0] + offsetX, pts[0][1] + offsetY);
for (let i = 1; i < pts.length; i++) ctx.lineTo(pts[i][0] + offsetX, pts[i][1] + offsetY);
ctx.closePath();
ctx.fill();
}
if (stroke) {
ctx.strokeStyle = stroke;
ctx.lineWidth = lineW;
ctx.lineJoin = 'round';
ctx.lineCap = 'round';
ctx.beginPath();
ctx.moveTo(pts[0][0], pts[0][1]);
for (let i = 1; i < pts.length; i++) ctx.lineTo(pts[i][0], pts[i][1]);
ctx.closePath();
ctx.stroke();
}
}
function drawCardboard(cx, fy, gritAmt, beatPulse) {
ctx.save();
ctx.translate(cx, fy);
const matW = Math.min(W * 0.76, 480);
const matH = 54;
// Misregistered cyan print shadow under mat
const off = 3 + gritAmt * 4;
ctx.fillStyle = 'rgba(0, 184, 212, 0.28)';
ctx.beginPath();
ctx.moveTo(-matW * 0.44 + off, -matH * 0.55 + off);
ctx.lineTo(matW * 0.46 + off, -matH * 0.5 + off);
ctx.lineTo(matW * 0.54 + off, matH * 0.55 + off);
ctx.lineTo(-matW * 0.52 + off, matH * 0.48 + off);
ctx.closePath();
ctx.fill();
// Corrugated cardboard body
const pts = [
[-matW * 0.44, -matH * 0.55],
[matW * 0.46, -matH * 0.50],
[matW * 0.54, matH * 0.55],
[-matW * 0.52, matH * 0.48]
];
drawPolygon(pts, '#e5b869', '#121624', 3.2);
// Corrugated edge Ridges
ctx.fillStyle = '#c99746';
ctx.strokeStyle = '#121624';
ctx.lineWidth = 2.2;
ctx.beginPath();
ctx.moveTo(pts[3][0], pts[3][1]);
ctx.lineTo(pts[2][0], pts[2][1]);
ctx.lineTo(pts[2][0], pts[2][1] + 8);
ctx.lineTo(pts[3][0], pts[3][1] + 8);
ctx.closePath();
ctx.fill();
ctx.stroke();
// Duct tape strips on corners & center fold
ctx.strokeStyle = '#121624';
ctx.lineWidth = 1.8;
// Center fold seam
ctx.beginPath();
ctx.moveTo(6, -matH * 0.52);
ctx.lineTo(-10, matH * 0.51);
ctx.stroke();
// Tape patches
const tapes = [
[-matW * 0.43, -matH * 0.25, 36, 12, -0.25],
[matW * 0.36, matH * 0.18, 42, 12, 0.2]
];
for (const [tx, ty, tw, th, tang] of tapes) {
ctx.save();
ctx.translate(tx, ty);
ctx.rotate(tang);
ctx.fillStyle = '#d8d5cc';
ctx.fillRect(-tw/2, -th/2, tw, th);
ctx.strokeRect(-tw/2, -th/2, tw, th);
ctx.restore();
}
// Scuff marks
ctx.strokeStyle = 'rgba(18, 22, 36, 0.28)';
ctx.lineWidth = 1.6;
for (const s of cardboardScuffs) {
if (Math.abs(s.x) > matW * 0.4) continue;
ctx.beginPath();
ctx.moveTo(s.x, s.y);
ctx.lineTo(s.x + Math.cos(s.ang) * s.len, s.y + Math.sin(s.ang) * s.len * 0.3);
ctx.stroke();
}
// Boombox sketched in background corner
const bx = -matW * 0.36;
const by = -matH * 0.55 - 48;
const thump = audioOn ? beatPulse * 3.5 : beatPulse * 1.2;
ctx.save();
ctx.translate(bx, by - thump * 0.5);
ctx.scale(1 + thump * 0.015, 1 - thump * 0.01);
// Handle
ctx.strokeStyle = '#121624';
ctx.lineWidth = 3;
ctx.strokeRect(-30, -14, 60, 14);
// Box
ctx.fillStyle = '#121624';
ctx.fillRect(-46, -4, 92, 48);
ctx.fillStyle = '#00b8d4';
ctx.fillRect(-46 + off*0.6, -4 + off*0.6, 92, 10);
ctx.strokeStyle = '#121624';
ctx.strokeRect(-46, -4, 92, 48);
// Speakers
for (const sx of [-25, 25]) {
ctx.fillStyle = '#f2ece1';
ctx.beginPath();
ctx.arc(sx, 22, 14 + thump * 0.5, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#ff473a';
ctx.beginPath();
ctx.arc(sx, 22, 5, 0, Math.PI * 2);
ctx.fill();
}
// Cassette deck
ctx.fillStyle = '#e5b869';
ctx.fillRect(-9, 14, 18, 13);
ctx.restore();
ctx.restore();
}
function drawSneaker(x, y, footAngle, isLeft, risoOff) {
ctx.save();
ctx.translate(x, y);
ctx.rotate(footAngle);
const dir = isLeft ? -1 : 1;
ctx.scale(dir, 1);
// High-top sneaker silhouette (pointing right in local coords)
// Misregistered coral fill
ctx.fillStyle = '#ff473a';
ctx.beginPath();
ctx.moveTo(-9 + risoOff, -18 + risoOff * 0.5);
ctx.lineTo(7 + risoOff, -18 + risoOff * 0.5);
ctx.lineTo(13 + risoOff, -7 + risoOff * 0.5);
ctx.lineTo(26 + risoOff, -5 + risoOff * 0.5);
ctx.lineTo(27 + risoOff, 2 + risoOff * 0.5);
ctx.lineTo(-11 + risoOff, 2 + risoOff * 0.5);
ctx.closePath();
ctx.fill();
// Crisp white rubber sole + toe cap
ctx.fillStyle = '#f2ece1';
ctx.fillRect(-11, -3, 38, 5.5);
ctx.beginPath();
ctx.arc(21, -3, 6, Math.PI, 0);
ctx.fill();
// Ink outline
ctx.strokeStyle = '#121624';
ctx.lineWidth = 2.8;
ctx.lineJoin = 'round';
ctx.beginPath();
ctx.moveTo(-9, -18);
ctx.lineTo(7, -18);
ctx.lineTo(12, -8);
ctx.lineTo(25, -5);
ctx.lineTo(27, 2.5);
ctx.lineTo(-11, 2.5);
ctx.closePath();
ctx.stroke();
// Sole line & laces
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(-10, -2.5);
ctx.lineTo(26, -2.5);
// Stripes
ctx.moveTo(-2, -13); ctx.lineTo(5, -4);
ctx.moveTo(3, -13); ctx.lineTo(10, -4);
ctx.stroke();
ctx.restore();
}
function drawWingArm(shX, shY, elbX, elbY, handX, handY, risoOff, isPlanted) {
// Upper wing + feathered forearm fan
const ang1 = Math.atan2(elbY - shY, elbX - shX);
const ang2 = Math.atan2(handY - elbY, handX - elbX);
const n1x = -Math.sin(ang1), n1y = Math.cos(ang1);
const n2x = -Math.sin(ang2), n2y = Math.cos(ang2);
// Feathered fan along forearm
const featherBow = isPlanted ? 14 : 24;
const midX = (elbX + handX) * 0.5 + n2x * featherBow;
const midY = (elbY + handY) * 0.5 + n2y * featherBow;
// Riso cyan spot fill on wing
ctx.fillStyle = '#00b8d4';
ctx.beginPath();
ctx.moveTo(shX + n1x * 9 + risoOff, shY + n1y * 9 + risoOff);
ctx.lineTo(elbX + n1x * 11 + risoOff, elbY + n1y * 11 + risoOff);
ctx.quadraticCurveTo(midX + risoOff, midY + risoOff, handX + risoOff, handY + risoOff);
ctx.lineTo(elbX - n1x * 6 + risoOff, elbY - n1y * 6 + risoOff);
ctx.lineTo(shX - n1x * 7 + risoOff, shY - n1y * 7 + risoOff);
ctx.closePath();
ctx.fill();
// White primary tips
ctx.fillStyle = '#f2ece1';
ctx.beginPath();
ctx.moveTo(shX + n1x * 8, shY + n1y * 8);
ctx.lineTo(elbX + n1x * 9, elbY + n1y * 9);
ctx.lineTo(handX, handY);
ctx.lineTo(elbX - n1x * 5, elbY - n1y * 5);
ctx.closePath();
ctx.fill();
// Ink feathers & outline
ctx.strokeStyle = '#121624';
ctx.lineWidth = 3.2;
ctx.lineJoin = 'round';
ctx.lineCap = 'round';
ctx.beginPath();
ctx.moveTo(shX + n1x * 8, shY + n1y * 8);
ctx.lineTo(elbX + n1x * 10, elbY + n1y * 10);
ctx.quadraticCurveTo(midX, midY, handX, handY);
ctx.lineTo(elbX - n1x * 6, elbY - n1y * 6);
ctx.lineTo(shX - n1x * 7, shY - n1y * 7);
ctx.stroke();
// Primary feather notches
ctx.lineWidth = 2.2;
for (let t = 0.35; t <= 0.8; t += 0.22) {
const fx = lerp(elbX, handX, t);
const fy = lerp(elbY, handY, t);
ctx.beginPath();
ctx.moveTo(fx, fy);
ctx.lineTo(fx + n2x * (featherBow * 0.65), fy + n2y * (featherBow * 0.65));
ctx.stroke();
}
// Tracksuit wristband at "wrist"
ctx.save();
ctx.translate(lerp(elbX, handX, 0.78), lerp(elbY, handY, 0.78));
ctx.rotate(ang2);
ctx.fillStyle = '#ff473a';
ctx.strokeStyle = '#121624';
ctx.lineWidth = 2.4;
ctx.fillRect(-5, -7, 10, 14);
ctx.strokeRect(-5, -7, 10, 14);
ctx.restore();
}
function drawHeron(cx, fy, pose, gritAmt, beatPulse) {
const risoOff = 1.8 + gritAmt * 4.2;
// Exaggerate pose based on Flavor slider
const exag = 0.82 + gritAmt * 0.45;
const hipX = cx + pose.hipX * exag;
const hipY = fy + pose.hipY;
const torsoLen = 52;
// Torso angle: 0 is straight up (-Y)
const tAng = pose.torsoAng * (0.9 + gritAmt * 0.22);
const chestX = hipX + Math.sin(tAng) * torsoLen;
const chestY = hipY - Math.cos(tAng) * torsoLen;
// Foreshortened shoulder/hip width via spin
const spinCos = Math.cos(pose.spin * Math.PI * 0.5);
const shHalf = 19 * spinCos;
const hipHalf = 13 * spinCos;
const perpX = Math.cos(tAng);
const perpY = Math.sin(tAng);
const shLX = chestX - perpX * shHalf;
const shLY = chestY - perpY * shHalf;
const shRX = chestX + perpX * shHalf;
const shRY = chestY + perpY * shHalf;
const hpLX = hipX - perpX * hipHalf;
const hpLY = hipY - perpY * hipHalf;
const hpRX = hipX + perpX * hipHalf;
const hpRY = hipY + perpY * hipHalf;
// Solve Bird Legs (spindly tarsus! Note: bird ankle bends "backward" visually, we use kneeDir)
const fLX = cx + pose.footLX * exag;
const fLY = fy + Math.min(0, pose.footLY);
const fRX = cx + pose.footRX * exag;
const fRY = fy + Math.min(0, pose.footRY);
const legL = solveIK(hpLX, hpLY, fLX, fLY - 10, 52, 54, pose.kneeLDir >= 0 ? 1 : -1);
const legR = solveIK(hpRX, hpRY, fRX, fRY - 10, 52, 54, pose.kneeRDir >= 0 ? 1 : -1);
// Solve Wing Arms
const hLX = cx + pose.handLX * exag;
const hLY = fy + Math.min(0, pose.handLY);
const hRX = cx + pose.handRX * exag;
const hRY = fy + Math.min(0, pose.handRY);
const armL = solveIK(shLX, shLY, hLX, hLY, 44, 48, pose.elbLDir >= 0 ? 1 : -1);
const armR = solveIK(shRX, shRY, hRX, hRY, 44, 48, pose.elbRDir >= 0 ? 1 : -1);
// Dynamic shadow on cardboard directly under center of mass
const comX = (hipX + chestX) * 0.5;
const heightFactor = Math.max(0.25, 1 - Math.abs(pose.hipY) / 190);
ctx.fillStyle = 'rgba(18, 22, 36, 0.22)';
ctx.beginPath();
ctx.ellipse(comX, fy + 3, 62 * heightFactor, 11 * heightFactor, 0, 0, Math.PI * 2);
ctx.fill();
// Helper to draw a spindly scaled heron leg + baggy b-boy shorts cuff
function drawLeg(hx, hy, leg, footX, footY, isLeft) {
// Baggy track shorts upper thigh
const thAng = Math.atan2(leg.jy - hy, leg.jx - hx);
const nx = -Math.sin(thAng), ny = Math.cos(thAng);
const cuffX = lerp(hx, leg.jx, 0.62);
const cuffY = lerp(hy, leg.jy, 0.62);
const shortPts = [
[hx - nx * 11, hy - ny * 11],
[hx + nx * 11, hy + ny * 11],
[cuffX + nx * 13, cuffY + ny * 13],
[cuffX - nx * 13, cuffY - ny * 13]
];
drawPolygon(shortPts, '#121624', '#121624', 2.5);
// Racing stripe on shorts
ctx.strokeStyle = '#00b8d4';
ctx.lineWidth = 2.5;
ctx.beginPath();
ctx.moveTo(hx + nx * 9, hy + ny * 9);
ctx.lineTo(cuffX + nx * 10, cuffY + ny * 10);
ctx.stroke();
// Spindly ochre heron leg
ctx.strokeStyle = '#e5b869';
ctx.lineWidth = 6.5;
ctx.lineCap = 'round';
ctx.beginPath();
ctx.moveTo(cuffX, cuffY);
ctx.lineTo(leg.jx, leg.jy);
ctx.lineTo(leg.ex, leg.ey);
ctx.stroke();
ctx.strokeStyle = '#121624';
ctx.lineWidth = 3;
ctx.beginPath();
ctx.moveTo(cuffX, cuffY);
ctx.lineTo(leg.jx, leg.jy);
ctx.lineTo(leg.ex, leg.ey);
ctx.stroke();
// Knobby bird ankle joint
ctx.fillStyle = '#e5b869';
ctx.beginPath();
ctx.arc(leg.jx, leg.jy, 4.5, 0, Math.PI * 2);
ctx.fill();
ctx.stroke();
// Foot angle: flat when planted near floor, dynamic when airborne
const airborne = Math.min(1, Math.abs(footY - fy) / 28);
const shinAng = Math.atan2(leg.ey - leg.jy, leg.ex - leg.jx) - Math.PI * 0.5;
const footAng = shinAng * airborne * 0.65;
drawSneaker(footX, footY, footAng, isLeft, risoOff);
}
// Back limb ordering based on spin
const leftInFront = pose.spin >= 0;
if (leftInFront) {
drawWingArm(shRX, shRY, armR.jx, armR.jy, hRX, hRY, risoOff, hRY >= fy - 6);
drawLeg(hpRX, hpRY, legR, fRX, fRY, false);
} else {
drawWingArm(shLX, shLY, armL.jx, armL.jy, hLX, hLY, risoOff, hLY >= fy - 6);
drawLeg(hpLX, hpLY, legL, fLX, fLY, true);
}
// Tail feathers jutting from hip
const tailDirX = -Math.sin(tAng) * 26 - perpX * spinCos * 10;
const tailDirY = Math.cos(tAng) * 26;
drawPolygon([
[hipX - perpX * 8, hipY - perpY * 8],
[hipX + tailDirX - 6, hipY + tailDirY],
[hipX + tailDirX + 8, hipY + tailDirY - 4],
[hipX + perpX * 8, hipY + perpY * 8]
], '#00b8d4', '#121624', 2.8, risoOff, risoOff * 0.5);
// --- TORSO: Retro Windbreaker Vest over Heron Plumage ---
const torsoPts = [
[shLX - perpX * 4, shLY - perpY * 4],
[shRX + perpX * 4, shRY + perpY * 4],
[hpRX + perpX * 3, hpRY + perpY * 3],
[hpLX - perpX * 3, hpLY - perpY * 3]
];
// Riso coral jacket fill
drawPolygon(torsoPts, '#ff473a', '#121624', 3.4, risoOff * 0.8, risoOff * 0.6);
// Bold geometric 80s chevron across chest
const midTorsoX = (chestX + hipX) * 0.5;
const midTorsoY = (chestY + hipY) * 0.5;
drawPolygon([
[shLX, shLY],
[shRX, shRY],
[midTorsoX + perpX * 14, midTorsoY + perpY * 14],
[midTorsoX, midTorsoY + 8],
[midTorsoX - perpX * 14, midTorsoY - perpY * 14]
], '#121624', null);
// Gold B-Boy chain swinging from neck!
ctx.strokeStyle = '#e5b869';
ctx.lineWidth = 2.8;
ctx.beginPath();
ctx.moveTo(chestX - perpX * 9, chestY - perpY * 9);
ctx.quadraticCurveTo(
chestX + Math.sin(tAng + crestAngle * 0.5) * 22,
chestY + 20,
chestX + perpX * 9, chestY + perpY * 9
);
ctx.stroke();
// --- S-CURVED HERON NECK & HEAD ---
const neckLen = 40;
const nAng = tAng + pose.neckBend * 0.9;
const headX = chestX + Math.sin(nAng) * neckLen;
// Prevent head from ever clipping below cardboard!
const rawHeadY = chestY - Math.cos(nAng) * neckLen;
const headY = Math.min(fy - 18, rawHeadY);
const ctrl1X = chestX + Math.sin(tAng - 0.55) * 24;
const ctrl1Y = chestY - Math.cos(tAng - 0.55) * 24;
const ctrl2X = headX - Math.sin(nAng + 0.55) * 18;
const ctrl2Y = headY + Math.cos(nAng + 0.55) * 18;
// Neck cyan offset shadow
ctx.strokeStyle = '#00b8d4';
ctx.lineWidth = 14;
ctx.lineCap = 'round';
ctx.beginPath();
ctx.moveTo(chestX + risoOff, chestY + risoOff * 0.5);
ctx.bezierCurveTo(ctrl1X + risoOff, ctrl1Y, ctrl2X + risoOff, ctrl2Y, headX + risoOff, headY);
ctx.stroke();
// Neck ink outline + bone white fill
ctx.strokeStyle = '#121624';
ctx.lineWidth = 16;
ctx.beginPath();
ctx.moveTo(chestX, chestY);
ctx.bezierCurveTo(ctrl1X, ctrl1Y, ctrl2X, ctrl2Y, headX, headY);
ctx.stroke();
ctx.strokeStyle = '#f2ece1';
ctx.lineWidth = 9.5;
ctx.beginPath();
ctx.moveTo(chestX, chestY);
ctx.bezierCurveTo(ctrl1X, ctrl1Y, ctrl2X, ctrl2Y, headX, headY);
ctx.stroke();
// Head, Dagger Beak, Crest Plumes & Bucket Hat
ctx.save();
ctx.translate(headX, headY);
const faceDir = pose.spin >= -0.15 ? 1 : -1;
ctx.rotate(tAng * 0.55 + pose.headAng);
ctx.scale(faceDir, 1);
// Two long night-heron white crest plumes whipping with spring physics
for (let p = 0; p < 2; p++) {
const pLag = crestAngle * (1.2 + p * 0.45);
ctx.strokeStyle = '#121624';
ctx.lineWidth = 5.5 - p * 1.2;
ctx.beginPath();
ctx.moveTo(-8, -8);
ctx.quadraticCurveTo(-28, -18 + pLag * 24, -48 + p * 8, -4 + pLag * 38);
ctx.stroke();
ctx.strokeStyle = p === 0 ? '#f2ece1' : '#00b8d4';
ctx.lineWidth = 2.5;
ctx.stroke();
}
// Sleek navy cap of head
ctx.fillStyle = '#121624';
ctx.beginPath();
ctx.ellipse(0, 0, 15, 11, 0, 0, Math.PI * 2);
ctx.fill();
// White cheek patch
ctx.fillStyle = '#f2ece1';
ctx.beginPath();
ctx.ellipse(3, 3, 10, 6.5, 0, 0, Math.PI * 2);
ctx.fill();
// Sharp long heron dagger beak
drawPolygon([
[9, -5],
[48, 1.5],
[9, 6.5]
], '#e5b869', '#121624', 2.8, risoOff * 0.5, risoOff * 0.4);
// Beak center ridge
ctx.beginPath();
ctx.moveTo(10, 0.5);
ctx.lineTo(42, 1.5);
ctx.strokeStyle = '#121624';
ctx.lineWidth = 1.6;
ctx.stroke();
// Fierce ruby night-heron eye with b-boy brow
ctx.fillStyle = '#ff473a';
ctx.beginPath();
ctx.arc(6, -2, 3.8, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#121624';
ctx.beginPath();
ctx.arc(7, -2, 1.6, 0, Math.PI * 2);
ctx.fill();
// Determined brow slash
ctx.strokeStyle = '#121624';
ctx.lineWidth = 2.6;
ctx.beginPath();
ctx.moveTo(1, -6.5);
ctx.lineTo(12, -3.5);
ctx.stroke();
// Kangol-style B-Boy Bucket Hat (with padded headspin crown!)
ctx.save();
ctx.translate(-2, -9);
ctx.rotate(-0.14 + pose.hatTilt + crestAngle * 0.18);
// Hat brim & crown
drawPolygon([
[-21, 4],
[22, 5],
[17, -1],
[13, -13],
[-13, -13],
[-17, -1]
], '#00b8d4', '#121624', 2.8, risoOff * 0.5, risoOff * 0.4);
// Coral sweatband stripe
ctx.fillStyle = '#ff473a';
ctx.fillRect(-14, -5, 28, 4);
ctx.strokeRect(-14, -5, 28, 4);
ctx.restore();
ctx.restore(); // end head
// Foreground limb
if (leftInFront) {
drawLeg(hpLX, hpLY, legL, fLX, fLY, true);
drawWingArm(shLX, shLY, armL.jx, armL.jy, hLX, hLY, risoOff, hLY >= fy - 6);
} else {
drawLeg(hpRX, hpRY, legR, fRX, fRY, false);
drawWingArm(shRX, shRY, armR.jx, armR.jy, hRX, hRY, risoOff, hRY >= fy - 6);
}
}
function drawParticles(dt) {
for (let i = particles.length - 1; i >= 0; i--) {
const p = particles[i];
p.life -= p.decay * dt;
if (p.life <= 0) {
particles.splice(i, 1);
continue;
}
p.x += p.vx * dt;
p.y += p.vy * dt;
if (p.type !== 'word') p.vy += 260 * dt; // gravity on dust/ink
ctx.save();
ctx.globalAlpha = Math.min(1, p.life * 1.4);
if (p.type === 'ink') {
ctx.fillStyle = p.color;
ctx.strokeStyle = '#121624';
ctx.lineWidth = 1.5;
ctx.beginPath();
ctx.arc(p.x, p.y, p.r * p.life, 0, Math.PI * 2);
ctx.fill();
ctx.stroke();
} else if (p.type === 'star') {
ctx.translate(p.x, p.y);
ctx.rotate(p.life * 6);
ctx.strokeStyle = '#121624';
ctx.lineWidth = 2.4;
const s = p.r * 1.8;
ctx.beginPath();
ctx.moveTo(-s, 0); ctx.lineTo(s, 0);
ctx.moveTo(0, -s); ctx.lineTo(0, s);
ctx.stroke();
} else if (p.type === 'word') {
ctx.translate(p.x, p.y);
ctx.rotate(p.rot);
ctx.font = '900 17px ui-monospace, Menlo, Consolas, monospace';
ctx.fillStyle = '#121624';
ctx.fillText(p.word, 2, 2);
ctx.fillStyle = '#ff473a';
ctx.fillText(p.word, 0, 0);
}
ctx.restore();
}
}
// --- MAIN LOOP ---
let lastTime = performance.now();
function frame(now) {
const dt = Math.min(0.05, (now - lastTime) / 1000);
lastTime = now;
const bpm = parseFloat(bpmSlider.value);
const gritAmt = parseFloat(gritSlider.value) / 100;
const beatsPerSec = bpm / 60;
if (!freezeHeld) {
const deltaBeats = dt * beatsPerSec;
totalBeats += deltaBeats;
stepBeatProgress += deltaBeats;
// 16th-note drum trigger
const sixteenth = Math.floor(totalBeats * 4) % 16;
if (sixteenth !== lastStepPlayed) {
lastStepPlayed = sixteenth;
if (audioOn && audioCtx) {
const t = audioCtx.currentTime + 0.01;
if (KICK_PAT[sixteenth]) playKick(t, sixteenth === 0 ? 1.15 : 1);
if (SNARE_PAT[sixteenth]) playSnare(t, sixteenth === 7);
if (HAT_PAT[sixteenth]) playHat(t, HAT_PAT[sixteenth] === 2);
}
// Update 4-beat HUD indicator
if (sixteenth % 4 === 0) {
const beatInBar = Math.floor(sixteenth / 4);
dots.forEach((d, idx) => d.classList.toggle('active', idx === beatInBar));
}
}
// Advance choreography step
const activeSet = SETS[currentSetKey];
let curStep = activeSet[stepIdx];
if (stepBeatProgress >= curStep.beats) {
stepBeatProgress -= curStep.beats;
prevPose = { ...POSES[curStep.pose] };
stepIdx++;
if (stepIdx >= activeSet.length) {
stepIdx = 0;
if (queuedSetKey) {
currentSetKey = queuedSetKey;
queuedSetKey = null;
setButtons.forEach(b => b.classList.remove('Queued'));
} else {
routineIdx = (routineIdx + 1) % SET_ORDER.length;
currentSetKey = SET_ORDER[routineIdx];
}
}
curStep = SETS[currentSetKey][stepIdx];
triggerStepFX(curStep);
}
}
// Compute weighted pose interpolation
const curStep = SETS[currentSetKey][stepIdx];
const targetPose = POSES[curStep.pose];
const rawT = Math.min(1, stepBeatProgress / curStep.beats);
const easedT = curStep.ease === 'snap' ? snapHold(rawT) : heavyDrop(rawT);
const interp = lerpPose(prevPose, targetPose, easedT);
// Subtle groove bounce on standing moves, tight isometric tremor on freezes
const beatFrac = totalBeats % 1;
const beatPulse = Math.pow(1 - beatFrac, 3);
if (!freezeHeld && !curStep.freezeHit && interp.hipY < -75) {
interp.hipY += beatPulse * 6.5;
interp.headAng += beatPulse * 0.08;
} else if (freezeHeld || (curStep.freezeHit && rawT > 0.38)) {
// Muscle tension micro-tremor during hard freeze holds!
interp.hipX += Math.sin(now * 0.055) * 1.1;
interp.hipY += Math.cos(now * 0.07) * 0.8;
}
// Secondary feather spring physics driven by hip + head velocity
const headMoveImpulse = (interp.hipX - currentPoseSnapshot.hipX) * 0.08
+ (interp.torsoAng - currentPoseSnapshot.torsoAng) * 0.9;
crestVel += (-crestAngle * 42 - crestVel * 7.5 - headMoveImpulse * 18) * dt;
crestAngle = Math.max(-1.2, Math.min(1.2, crestAngle + crestVel * dt));
currentPoseSnapshot = interp;
// Decay screen shake & freeze flash
camShake = Math.max(0, camShake - dt * 28);
freezeFlash = Math.max(0, freezeFlash - dt * 3.2);
// --- RENDER ---
ctx.clearRect(0, 0, W, H);
// Background graphic sun / riso spotlight disc
const cx = W * 0.5;
const fy = Math.min(H * 0.79, H - 52);
ctx.save();
if (camShake > 0) {
ctx.translate((Math.random() - 0.5) * camShake, (Math.random() - 0.5) * camShake);
}
// Graphic halftone-style riso circle behind dancer
const discR = Math.min(W, H) * 0.31;
ctx.fillStyle = freezeFlash > 0.1 ? 'rgba(255, 71, 58, 0.16)' : 'rgba(229, 184, 105, 0.24)';
ctx.beginPath();
ctx.arc(cx - 12, fy - discR * 0.68, discR + beatPulse * 5, 0, Math.PI * 2);
ctx.fill();
// Freeze action speed-lines when stalled
if (freezeHeld || freezeFlash > 0.2) {
ctx.strokeStyle = 'rgba(0, 184, 212, 0.45)';
ctx.lineWidth = 2;
for (let i = 0; i < 8; i++) {
const a = (i / 8) * Math.PI * 2 + 0.3;
ctx.beginPath();
ctx.moveTo(cx + Math.cos(a) * (discR * 0.78), (fy - 95) + Math.sin(a) * (discR * 0.78));
ctx.lineTo(cx + Math.cos(a) * (discR * 1.08), (fy - 95) + Math.sin(a) * (discR * 1.08));
ctx.stroke();
}
}
// Horizon ink rule
ctx.strokeStyle = 'rgba(18, 22, 36, 0.18)';
ctx.lineWidth = 1.5;
ctx.beginPath();
ctx.moveTo(0, fy - 18);
ctx.lineTo(W, fy - 18);
ctx.stroke();
drawCardboard(cx, fy, gritAmt, beatPulse);
drawHeron(cx, fy, interp, gritAmt, beatPulse);
drawParticles(dt);
ctx.restore();
requestAnimationFrame(frame);
}
triggerStepFX(SETS[currentSetKey][0]);
requestAnimationFrame(frame);
})();
</script>
</body>
</html>
snapHold whips into position in the first 38% of the beat with slight overshoot and locks dead-still, while heavyDrop accelerates into the floor with impact compression, camera shake, and isometric muscle tremor on holds).1–4 or deck buttons), hold [SPACE] / HOLD FREEZE to scratch the record and stall the bird mid-air in an isometric freeze, toggle a synthesized WebAudio 808 breakbeat boombox synced to the BPM slider (which scales both beat and physics speed), crank FLAVOR to push both pose extension and risograph ink misregistration, or click anywhere on the cardboard to toss hype shouts and dust.