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.
It is a Secretary Bird (Sagittarius serpentarius) — long black-feathered thighs, yellow stilt shins, vermilion bare face mask, hooked beak, and seven whip-like crest quills built for stomping, wide V-splits, and headspins. The look is a tactile Risograph zine print on warm newsprint and a taped corrugated-cardboard mat, using bold charcoal ink, slate plumage, vermilion accents, and off-register cyan/magenta motion smears.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SAGITTARIUS // CYPHER NO. 01</title>
<style>
:root {
--paper: #f2eadc;
--paper-dark: #e5dac6;
--ink: #1d1c21;
--vermilion: #e8482c;
--ochre: #e09f2d;
--slate: #7e8f9c;
--cyan: #1d9ab8;
--magenta: #d6336c;
--cardboard: #cfa976;
}
* {
box-sizing: border-box;
user-select: none;
-webkit-user-select: none;
}
html, body {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
overflow: hidden;
background: var(--paper);
color: var(--ink);
font-family: ui-monospace, "Cascadia Mono", "SFMono-Regular", Menlo, Consolas, monospace;
}
.app {
position: relative;
width: 100vw;
height: 100vh;
display: grid;
grid-template-rows: auto 1fr auto;
overflow: hidden;
}
canvas#stage {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
display: block;
cursor: crosshair;
z-index: 1;
}
.hud-top, .hud-bottom {
position: relative;
z-index: 2;
pointer-events: none;
padding: 14px 18px;
}
.hud-top {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 12px;
flex-wrap: wrap;
}
.stamp-box {
pointer-events: auto;
background: var(--paper);
border: 2px solid var(--ink);
box-shadow: 4px 4px 0 var(--ink);
padding: 8px 12px;
}
.title-line {
font-weight: 900;
font-size: 13px;
letter-spacing: 0.08em;
text-transform: uppercase;
display: flex;
align-items: center;
gap: 8px;
}
.sub-line {
font-size: 11px;
opacity: 0.78;
margin-top: 3px;
letter-spacing: 0.04em;
}
.status-cluster {
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
}
.beat-strip {
display: flex;
align-items: center;
gap: 10px;
}
.leds {
display: flex;
gap: 5px;
align-items: center;
}
.led {
width: 10px;
height: 10px;
border: 2px solid var(--ink);
border-radius: 50%;
background: transparent;
transition: transform 0.06s ease-out, background 0.06s ease-out;
}
.led.on {
background: var(--vermilion);
transform: scale(1.22);
}
.led.accent.on {
background: var(--ochre);
transform: scale(1.32);
}
.move-readout {
font-weight: 900;
font-size: 13px;
letter-spacing: 0.08em;
color: var(--vermilion);
min-width: 96px;
text-align: right;
}
.hud-bottom {
display: flex;
justify-content: space-between;
align-items: flex-end;
gap: 12px;
flex-wrap: wrap;
}
.deck {
pointer-events: auto;
background: rgba(242, 234, 220, 0.94);
border: 2px solid var(--ink);
box-shadow: 4px 4px 0 var(--ink);
padding: 8px 10px;
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px;
}
.btn-group {
display: flex;
flex-wrap: wrap;
gap: 5px;
}
button {
appearance: none;
background: var(--paper);
color: var(--ink);
border: 2px solid var(--ink);
padding: 5px 9px;
font-family: inherit;
font-size: 11px;
font-weight: 800;
letter-spacing: 0.06em;
text-transform: uppercase;
cursor: pointer;
box-shadow: 2px 2px 0 var(--ink);
transition: transform 0.06s ease, background 0.1s ease, color 0.1s ease;
}
button:hover {
background: var(--paper-dark);
}
button:active {
transform: translate(2px, 2px);
box-shadow: 0 0 0 var(--ink);
}
button.active {
background: var(--ink);
color: var(--paper);
}
button.hot.active {
background: var(--vermilion);
color: var(--paper);
}
button.sound-on {
background: var(--ochre);
color: var(--ink);
}
.slider-wrap {
display: flex;
align-items: center;
gap: 7px;
padding: 0 4px;
font-size: 11px;
font-weight: 800;
letter-spacing: 0.05em;
}
input[type="range"] {
-webkit-appearance: none;
appearance: none;
width: 82px;
height: 6px;
background: var(--ink);
outline: none;
cursor: pointer;
}
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 14px;
height: 14px;
background: var(--vermilion);
border: 2px solid var(--ink);
cursor: pointer;
}
input[type="range"]::-moz-range-thumb {
width: 14px;
height: 14px;
background: var(--vermilion);
border: 2px solid var(--ink);
border-radius: 0;
cursor: pointer;
}
.val-tag {
min-width: 44px;
text-align: right;
font-variant-numeric: tabular-nums;
}
.hint-pill {
pointer-events: none;
font-size: 10px;
letter-spacing: 0.05em;
opacity: 0.72;
padding: 4px 8px;
background: rgba(242, 234, 220, 0.8);
border: 1.5px solid var(--ink);
}
@media (max-width: 760px) {
.hud-top, .hud-bottom { padding: 8px 10px; }
.sub-line { display: none; }
.hint-pill { display: none; }
input[type="range"] { width: 64px; }
}
</style>
</head>
<body>
<div class="app">
<canvas id="stage"></canvas>
<header class="hud-top">
<div class="stamp-box">
<div class="title-line">
<span>⊕ SAGITTARIUS // CYPHER NO. 01</span>
</div>
<div class="sub-line">SECRETARY BIRD · CARDBOARD EDITION · RISO 4-INK</div>
</div>
<div class="stamp-box status-cluster">
<div class="beat-strip">
<div class="leds" id="leds">
<div class="led accent"></div>
<div class="led"></div>
<div class="led"></div>
<div class="led"></div>
</div>
<div class="move-readout" id="moveReadout">TOPROCK</div>
</div>
<button id="btnAudio" type="button">BEAT: OFF</button>
</div>
</header>
<div></div>
<footer class="hud-bottom">
<div class="deck">
<div class="btn-group" id="moveButtons">
<button data-mode="CYPHER" class="active" type="button">CYPHER</button>
<button data-mode="TOPROCK" type="button">TOPROCK</button>
<button data-mode="SIXSTEP" type="button">6-STEP</button>
<button data-mode="WINDMILL" type="button">WINDMILL</button>
<button data-mode="FLARE" type="button">FLARE</button>
<button data-mode="HEADSPIN" type="button">HEADSPIN</button>
<button data-mode="FREEZE" class="hot" type="button">FREEZE!</button>
</div>
</div>
<div class="deck">
<div class="slider-wrap">
<label for="bpmSlider">BPM</label>
<input id="bpmSlider" type="range" min="86" max="126" value="104" step="1">
<span class="val-tag" id="bpmVal">104</span>
</div>
<div class="slider-wrap">
<label for="snapSlider">SNAP</label>
<input id="snapSlider" type="range" min="0" max="100" value="72" step="1">
<span class="val-tag" id="snapVal">WHIP</span>
</div>
<button id="btnScuffClear" type="button" title="Wipe cardboard chalk marks">WIPE MAT</button>
</div>
<div class="hint-pill">CLICK MAT: STOMP HYPE · SPACE: DROP FREEZE</div>
</footer>
</div>
<script>
(() => {
'use strict';
// --- Vector math helpers ---
const v3 = (x = 0, y = 0, z = 0) => [x, y, z];
const add = (a, b) => [a[0] + b[0], a[1] + b[1], a[2] + b[2]];
const sub = (a, b) => [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
const mul = (a, s) => [a[0] * s, a[1] * s, a[2] * s];
const dot = (a, b) => a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
const cross = (a, b) => [
a[1] * b[2] - a[2] * b[1],
a[2] * b[0] - a[0] * b[2],
a[0] * b[1] - a[1] * b[0]
];
const len = (a) => Math.hypot(a[0], a[1], a[2]);
const norm = (a) => {
const l = len(a);
return l > 1e-6 ? [a[0] / l, a[1] / l, a[2] / l] : [0, 1, 0];
};
const lerp = (a, b, t) => a + (b - a) * t;
const lerpV = (a, b, t) => [
a[0] + (b[0] - a[0]) * t,
a[1] + (b[1] - a[1]) * t,
a[2] + (b[2] - a[2]) * t
];
const clamp = (x, lo, hi) => Math.max(lo, Math.min(hi, x));
const rotY = (p, a) => {
const c = Math.cos(a), s = Math.sin(a);
return [p[0] * c + p[2] * s, p[1], -p[0] * s + p[2] * c];
};
// Weighted breaking ease: anticipation dip -> explosive whip -> slight overshoot -> pocket settle
function snapEase(t, snapAmt) {
t = clamp(t, 0, 1);
if (snapAmt < 0.08) {
// Smooth flow
return t * t * (3 - 2 * t);
}
const power = 1.6 + snapAmt * 3.4;
// Slight anticipation in first 12%
const anti = snapAmt * 0.075;
if (t < 0.14) {
const u = t / 0.14;
return -anti * Math.sin(u * Math.PI);
}
// Main whip from 0.14 to 0.72, overshoot + settle 0.72 to 1.0
const u = (t - 0.14) / 0.86;
const base = 1 - Math.pow(1 - u, power);
const over = snapAmt * 0.09 * Math.sin(u * Math.PI * 1.45) * Math.pow(1 - u, 1.3);
return clamp(base + over, -0.1, 1.12);
}
// Analytical 3D 2-bone IK
function solveIK(root, target, l1, l2, bendHint) {
const diff = sub(target, root);
let d = len(diff);
const minD = Math.abs(l1 - l2) + 2;
const maxD = l1 + l2 - 0.5;
d = clamp(d, minD, maxD);
const dir = norm(diff);
const clampedTarget = add(root, mul(dir, d));
let perp = sub(bendHint, mul(dir, dot(bendHint, dir)));
if (len(perp) < 1e-4) {
const fallback = Math.abs(dir[1]) < 0.9 ? [0, 1, 0] : [0, 0, 1];
perp = sub(fallback, mul(dir, dot(fallback, dir)));
}
perp = norm(perp);
const a = (l1 * l1 - l2 * l2 + d * d) / (2 * d);
const h = Math.sqrt(Math.max(0, l1 * l1 - a * a));
const mid = add(add(root, mul(dir, a)), mul(perp, h));
return { mid, end: clampedTarget };
}
// --- Pose Interpolation & Blending ---
function clonePose(p) {
return {
pelvis: [...p.pelvis],
chest: [...p.chest],
head: [...p.head],
headLook: [...p.headLook],
headUp: [...p.headUp],
chestFwd: [...p.chestFwd],
handL: [...p.handL],
handR: [...p.handR],
footL: [...p.footL],
footR: [...p.footR],
kneeHintL: [...p.kneeHintL],
kneeHintR: [...p.kneeHintR],
elbowHintL: [...p.elbowHintL],
elbowHintR: [...p.elbowHintR],
wingSpreadL: p.wingSpreadL,
wingSpreadR: p.wingSpreadR,
spinYaw: p.spinYaw || 0
};
}
function blendPose(a, b, t) {
return {
pelvis: lerpV(a.pelvis, b.pelvis, t),
chest: lerpV(a.chest, b.chest, t),
head: lerpV(a.head, b.head, t),
headLook: norm(lerpV(a.headLook, b.headLook, t)),
headUp: norm(lerpV(a.headUp, b.headUp, t)),
chestFwd: norm(lerpV(a.chestFwd, b.chestFwd, t)),
handL: lerpV(a.handL, b.handL, t),
handR: lerpV(a.handR, b.handR, t),
footL: lerpV(a.footL, b.footL, t),
footR: lerpV(a.footR, b.footR, t),
kneeHintL: norm(lerpV(a.kneeHintL, b.kneeHintL, t)),
kneeHintR: norm(lerpV(a.kneeHintR, b.kneeHintR, t)),
elbowHintL: norm(lerpV(a.elbowHintL, b.elbowHintL, t)),
elbowHintR: norm(lerpV(a.elbowHintR, b.elbowHintR, t)),
wingSpreadL: lerp(a.wingSpreadL, b.wingSpreadL, t),
wingSpreadR: lerp(a.wingSpreadR, b.wingSpreadR, t),
spinYaw: lerp(a.spinYaw || 0, b.spinYaw || 0, t)
};
}
function evalKeyframeTrack(keys, phase, snap) {
// keys: [{t, pose}] with wrapping at loopLen
let idx = 0;
for (let i = 0; i < keys.length - 1; i++) {
if (phase >= keys[i].t && phase <= keys[i + 1].t) {
idx = i;
break;
}
}
const k0 = keys[idx];
const k1 = keys[idx + 1];
const span = Math.max(0.001, k1.t - k0.t);
const localT = clamp((phase - k0.t) / span, 0, 1);
return blendPose(k0.pose, k1.pose, snapEase(localT, snap));
}
// --- Move Choreographies ---
// 1. TOPROCK: Indian step / cross-step + burns + chest bounce
const toprockKeys = [
{
t: 0.0, // Beat 1: R leg crosses front-left, burns open!
pose: {
pelvis: [-8, 82, 4], chest: [-4, 120, -2], head: [-2, 144, 2],
headLook: [0.42, -0.05, 0.9], headUp: [-0.08, 1, 0.05], chestFwd: [0.22, 0.08, 0.97],
handL: [-54, 122, 10], handR: [48, 128, 8],
footL: [-14, 0, -6], footR: [-26, 0, 26],
kneeHintL: [-0.2, 0.2, 1], kneeHintR: [0.5, 0.2, 0.85],
elbowHintL: [-0.4, -0.7, -0.5], elbowHintR: [0.4, -0.7, -0.5],
wingSpreadL: 0.9, wingSpreadR: 0.9
}
},
{
t: 0.48, // "And": Upbeat rebound + arms pull in
pose: {
pelvis: [0, 94, 0], chest: [0, 132, 4], head: [0, 156, 8],
headLook: [0.05, -0.1, 0.99], headUp: [0, 1, 0.1], chestFwd: [0, 0, 1],
handL: [12, 114, 22], handR: [-12, 118, 25],
footL: [-15, 0, 0], footR: [12, 14, 10],
kneeHintL: [-0.2, 0.1, 1], kneeHintR: [0.2, 0.1, 1],
elbowHintL: [-1, -0.2, 0], elbowHintR: [1, -0.2, 0],
wingSpreadL: 0.2, wingSpreadR: 0.2
}
},
{
t: 1.0, // Beat 2: Drop into center stance, tight cross arms
pose: {
pelvis: [0, 80, 0], chest: [0, 118, 6], head: [0, 141, 12],
headLook: [0, -0.18, 0.98], headUp: [0, 0.98, 0.2], chestFwd: [0, -0.08, 0.99],
handL: [16, 104, 24], handR: [-16, 108, 28],
footL: [-17, 0, 0], footR: [17, 0, 2],
kneeHintL: [-0.35, 0.1, 0.9], kneeHintR: [0.35, 0.1, 0.9],
elbowHintL: [-1, -0.3, -0.2], elbowHintR: [1, -0.3, -0.2],
wingSpreadL: 0.18, wingSpreadR: 0.18
}
},
{
t: 1.5, // "And": Hop prep
pose: {
pelvis: [2, 93, 0], chest: [2, 131, 2], head: [2, 155, 6],
headLook: [-0.15, 0.04, 0.98], headUp: [0, 1, 0.05], chestFwd: [-0.1, 0.05, 0.99],
handL: [-28, 122, 14], handR: [28, 122, 14],
footL: [-10, 15, 12], footR: [15, 0, -2],
kneeHintL: [-0.2, 0.2, 1], kneeHintR: [0.2, 0.2, 1],
elbowHintL: [-0.6, -0.7, -0.3], elbowHintR: [0.6, -0.7, -0.3],
wingSpreadL: 0.48, wingSpreadR: 0.48
}
},
{
t: 2.0, // Beat 3: L leg crosses front-right, asymmetric burn!
pose: {
pelvis: [8, 81, 4], chest: [4, 119, -3], head: [2, 143, 2],
headLook: [-0.48, 0.02, 0.88], headUp: [0.1, 1, 0.05], chestFwd: [-0.24, 0.08, 0.97],
handL: [-48, 130, 8], handR: [56, 118, 10],
footL: [26, 0, 26], footR: [14, 0, -6],
kneeHintL: [-0.45, 0.2, 0.88], kneeHintR: [0.2, 0.2, 1],
elbowHintL: [-0.4, -0.7, -0.5], elbowHintR: [0.4, -0.7, -0.5],
wingSpreadL: 0.95, wingSpreadR: 0.88
}
},
{
t: 2.5, // "And": Upbeat bounce
pose: {
pelvis: [0, 94, 0], chest: [0, 132, 2], head: [0, 156, 6],
headLook: [0, 0.05, 1], headUp: [0, 1, 0], chestFwd: [0, 0.04, 1],
handL: [-26, 118, 16], handR: [26, 118, 16],
footL: [-18, 10, 8], footR: [18, 0, 0],
kneeHintL: [-0.25, 0.1, 0.95], kneeHintR: [0.25, 0.1, 0.95],
elbowHintL: [-0.7, -0.5, -0.3], elbowHintR: [0.7, -0.5, -0.3],
wingSpreadL: 0.4, wingSpreadR: 0.4
}
},
{
t: 3.0, // Beat 4: Deep squat bounce + shoulder hit
pose: {
pelvis: [-2, 74, 2], chest: [-4, 112, 8], head: [-4, 136, 14],
headLook: [0.22, -0.1, 0.97], headUp: [-0.08, 1, 0.1], chestFwd: [0.15, -0.05, 0.98],
handL: [-42, 132, 6], handR: [24, 96, 20],
footL: [-20, 0, 4], footR: [20, 0, -4],
kneeHintL: [-0.45, 0.1, 0.9], kneeHintR: [0.45, 0.1, 0.9],
elbowHintL: [-0.5, 0.8, -0.3], elbowHintR: [0.7, -0.5, -0.4],
wingSpreadL: 0.72, wingSpreadR: 0.3
}
},
{
t: 3.55, // "And": Rebound up ready for beat 1
pose: {
pelvis: [-4, 92, 2], chest: [-2, 130, 2], head: [-2, 154, 4],
headLook: [0.18, 0.02, 0.98], headUp: [-0.04, 1, 0], chestFwd: [0.1, 0.04, 0.99],
handL: [-22, 118, 16], handR: [22, 118, 16],
footL: [-15, 0, -4], footR: [12, 16, 12],
kneeHintL: [-0.2, 0.1, 1], kneeHintR: [0.3, 0.2, 0.9],
elbowHintL: [-0.7, -0.6, -0.3], elbowHintR: [0.7, -0.6, -0.3],
wingSpreadL: 0.45, wingSpreadR: 0.45
}
}
];
toprockKeys.push({ t: 4.0, pose: toprockKeys[0].pose });
// 2. SIX-STEP (1, &, 2, 3, &, 4)
const sixStepKeys = [
{
t: 0.0, // Step 1: R hand down, R leg hooked across front
pose: {
pelvis: [6, 24, -2], chest: [12, 46, -10], head: [14, 68, -4],
headLook: [0.35, 0.15, 0.92], headUp: [0.1, 0.98, -0.15], chestFwd: [0.25, 0.2, 0.94],
handR: [24, 0, -18], handL: [-28, 34, 16],
footL: [-24, 0, -14], footR: [-10, 0, 30],
kneeHintL: [-0.4, 0.8, 0.2], kneeHintR: [0.4, 0.9, 0.3],
elbowHintL: [-0.6, 0.2, -0.6], elbowHintR: [0.5, 0.2, -0.8],
wingSpreadL: 0.72, wingSpreadR: 0.25
}
},
{
t: 0.5, // Step 2: L swings out/forward, L hand plants
pose: {
pelvis: [0, 27, 2], chest: [0, 48, -6], head: [0, 70, 2],
headLook: [0, 0.08, 0.99], headUp: [0, 0.99, -0.1], chestFwd: [0, 0.2, 0.98],
handR: [26, 0, -10], handL: [-24, 0, -12],
footR: [22, 0, 28], footL: [-26, 8, 22],
kneeHintL: [-0.5, 0.85, 0.2], kneeHintR: [0.5, 0.85, 0.2],
elbowHintL: [-0.5, 0.3, -0.7], elbowHintR: [0.5, 0.3, -0.7],
wingSpreadL: 0.25, wingSpreadR: 0.25
}
},
{
t: 1.0, // Step 3: Plank / Push-up hit on beat 2
pose: {
pelvis: [0, 32, -16], chest: [0, 40, 14], head: [0, 56, 26],
headLook: [0, 0.1, 0.99], headUp: [0, 0.96, 0.26], chestFwd: [0, -0.35, 0.94],
handL: [-22, 0, 18], handR: [22, 0, 18],
footL: [-16, 0, -48], footR: [16, 0, -48],
kneeHintL: [-0.2, 0.95, 0], kneeHintR: [0.2, 0.95, 0],
elbowHintL: [-0.6, 0.5, -0.6], elbowHintR: [0.6, 0.5, -0.6],
wingSpreadL: 0.3, wingSpreadR: 0.3
}
},
{
t: 1.72, // Hold plank pocket slightly before step 4
pose: {
pelvis: [-2, 30, -14], chest: [-2, 42, 12], head: [-2, 58, 24],
headLook: [-0.15, 0.12, 0.98], headUp: [0, 0.96, 0.24], chestFwd: [-0.1, -0.25, 0.96],
handL: [-22, 0, 14], handR: [22, 0, 14],
footL: [-16, 0, -44], footR: [16, 0, -46],
kneeHintL: [-0.2, 0.95, 0], kneeHintR: [0.2, 0.95, 0],
elbowHintL: [-0.6, 0.5, -0.6], elbowHintR: [0.6, 0.5, -0.6],
wingSpreadL: 0.3, wingSpreadR: 0.3
}
},
{
t: 2.1, // Step 4: L hooks forward under, R hand lifts
pose: {
pelvis: [-6, 24, -2], chest: [-12, 46, -10], head: [-14, 68, -4],
headLook: [-0.35, 0.15, 0.92], headUp: [-0.1, 0.98, -0.15], chestFwd: [-0.25, 0.2, 0.94],
handL: [-24, 0, -18], handR: [28, 34, 16],
footR: [24, 0, -14], footL: [10, 0, 30],
kneeHintL: [-0.4, 0.9, 0.3], kneeHintR: [0.4, 0.8, 0.2],
elbowHintL: [-0.5, 0.2, -0.8], elbowHintR: [0.6, 0.2, -0.6],
wingSpreadL: 0.25, wingSpreadR: 0.72
}
},
{
t: 2.55, // Step 5: R sweeps wide, both hands plant briefly
pose: {
pelvis: [0, 27, 2], chest: [0, 48, -6], head: [0, 70, 2],
headLook: [0, 0.08, 0.99], headUp: [0, 0.99, -0.1], chestFwd: [0, 0.2, 0.98],
handL: [-26, 0, -10], handR: [24, 0, -12],
footL: [-22, 0, 28], footR: [28, 10, 18],
kneeHintL: [-0.5, 0.85, 0.2], kneeHintR: [0.5, 0.85, 0.2],
elbowHintL: [-0.5, 0.3, -0.7], elbowHintR: [0.5, 0.3, -0.7],
wingSpreadL: 0.25, wingSpreadR: 0.25
}
},
{
t: 3.1, // Step 6: Pop chest up with open front squat + left wing flare!
pose: {
pelvis: [-2, 26, 4], chest: [-4, 54, 6], head: [-4, 76, 10],
headLook: [-0.12, 0.1, 0.98], headUp: [-0.08, 0.99, 0.1], chestFwd: [0, 0.18, 0.98],
handR: [26, 0, -16], handL: [-40, 56, 22],
footL: [-24, 0, 22], footR: [20, 0, 20],
kneeHintL: [-0.6, 0.75, 0.3], kneeHintR: [0.6, 0.75, 0.3],
elbowHintL: [-0.5, 0.7, -0.4], elbowHintR: [0.6, 0.2, -0.7],
wingSpreadL: 0.9, wingSpreadR: 0.25
}
},
{
t: 3.65,
pose: {
pelvis: [0, 24, 2], chest: [2, 50, 0], head: [2, 72, 4],
headLook: [0.1, 0.1, 0.99], headUp: [0, 0.99, 0], chestFwd: [0.1, 0.2, 0.97],
handR: [25, 0, -17], handL: [-32, 42, 18],
footL: [-23, 0, 6], footR: [12, 12, 25],
kneeHintL: [-0.5, 0.8, 0.2], kneeHintR: [0.5, 0.8, 0.3],
elbowHintL: [-0.5, 0.5, -0.5], elbowHintR: [0.5, 0.2, -0.8],
wingSpreadL: 0.75, wingSpreadR: 0.25
}
}
];
sixStepKeys.push({ t: 4.0, pose: sixStepKeys[0].pose });
// 3. WINDMILL (2 beats per revolution)
function poseWindmill(beatPhase, snap) {
// 2 revolutions per 4-beat bar
const revPhase = (beatPhase % 2) / 2; // 0..1
const surge = snap * 0.22 * Math.sin(revPhase * Math.PI * 2);
const theta = (revPhase * Math.PI * 2) + surge; // 0: stab, PI: back
const c = Math.cos(theta), s = Math.sin(theta);
// Body spins around Y while rolling over chest/back
const yaw = -theta;
const chestY = 20 - 6 * c; // lower on back (theta=PI -> c=-1? wait: at stab chest=22, back=14)
const pelvY = 28 - 4 * c;
const localPelvis = [0, pelvY, -18];
const localChest = [0, 18 + 5 * c, 14];
const localHead = [0, 26 + 8 * c, 28];
// Wide V-split legs sweeping tilted plane
const legTilt = 26 * c;
const localFootL = [-46, 46 - legTilt * 0.5, -52];
const localFootR = [46, 46 + legTilt * 0.5, -52];
// Arms: stab vs open whip
const stab = Math.max(0, c);
const localHandR = [lerp(42, 10, stab), lerp(16, 4, stab), lerp(16, 8, stab)];
const localHandL = [lerp(-42, -24, stab), lerp(20, 0, stab), lerp(14, 18, stab)];
const headUp = rotY([s * 0.85, c * 0.85, 0.35], yaw);
const headLook = rotY([0, 0.25, 0.96], yaw);
const chestFwd = rotY([s, c, 0.2], yaw);
return {
pelvis: rotY(localPelvis, yaw),
chest: rotY(localChest, yaw),
head: rotY(localHead, yaw),
headLook: norm(headLook),
headUp: norm(headUp),
chestFwd: norm(chestFwd),
handL: rotY(localHandL, yaw),
handR: rotY(localHandR, yaw),
footL: rotY(localFootL, yaw),
footR: rotY(localFootR, yaw),
kneeHintL: rotY([-0.5, 0.8, 0.2], yaw),
kneeHintR: rotY([0.5, 0.8, 0.2], yaw),
elbowHintL: rotY([-0.7, 0.5, -0.3], yaw),
elbowHintR: rotY([0.7, 0.5, -0.3], yaw),
wingSpreadL: 0.35 + 0.5 * (1 - stab),
wingSpreadR: 0.3 + 0.5 * (1 - stab)
};
}
// 4. FLARE (Alternating weight over hands, legs sweeping high)
function poseFlare(beatPhase, snap) {
const revPhase = (beatPhase % 2) / 2;
const theta = revPhase * Math.PI * 2 + snap * 0.16 * Math.sin(revPhase * Math.PI * 2);
const c = Math.cos(theta), s = Math.sin(theta);
// Center of mass shifts over planted hand
const comX = 16 * c;
const comY = 56 + 10 * Math.abs(s);
const yaw = -theta;
const pelvis = [comX - 10 * s, comY, -8 * c];
const chest = [comX * 0.7, comY - 12, 10 * c];
const head = [comX * 0.6, comY - 2, 24 * c];
// Alternating hand plant on mat
const rPlant = c > -0.25 ? clamp((c + 0.25) / 0.35, 0, 1) : 0;
const lPlant = c < 0.25 ? clamp((0.25 - c) / 0.35, 0, 1) : 0;
const handR = [22, lerp(30, 0, rPlant), lerp(-10, 4, rPlant)];
const handL = [-22, lerp(30, 0, lPlant), lerp(-10, 4, lPlant)];
// Straight high V-legs rotating around torso
const legYaw = yaw;
const footL = add(pelvis, rotY([-54, 18 + 18 * s, -48], legYaw));
const footR = add(pelvis, rotY([54, 18 - 18 * s, -48], legYaw));
footL[1] = Math.max(12, footL[1]);
footR[1] = Math.max(12, footR[1]);
return {
pelvis,
chest,
head,
headLook: norm([0.15 * s, -0.1, 0.98]),
headUp: norm([0.2 * c, 0.95, 0.2]),
chestFwd: norm([s * 0.5, -0.2, c * 0.85]),
handL,
handR,
footL,
footR,
kneeHintL: norm([-c, 0.9, -s]),
kneeHintR: norm([c, 0.9, s]),
elbowHintL: [-0.6, 0.4, -0.6],
elbowHintR: [0.6, 0.4, -0.6],
wingSpreadL: 0.3 + 0.45 * (1 - lPlant),
wingSpreadR: 0.3 + 0.45 * (1 - rPlant)
};
}
// 5. HEADSPIN (2 beats wide V-spin -> 2 beats fast tight drill)
function poseHeadspin(beatPhase, snap) {
const drillT = clamp((beatPhase - 1.85) / 0.35, 0, 1);
const drill = snapEase(drillT, snap);
// Angular speed doubles in drill!
const baseAngle = beatPhase <= 2.0
? beatPhase * Math.PI * 3.5
: (2.0 * Math.PI * 3.5) + (beatPhase - 2.0) * Math.PI * 7.5;
const wobble = Math.sin(beatPhase * 11) * (1 - drill * 0.5) * 3.5;
const head = [wobble * 0.4, 14, 0];
const chest = [wobble * 0.8, 45, wobble * 0.3];
const pelvis = [wobble * 1.1, 80, -wobble * 0.3];
const spread = lerp(52, 8, drill);
const legH = lerp(118, 154, drill);
const localFootL = [-spread, legH, lerp(8, -5, drill)];
const localFootR = [spread, legH, lerp(-8, 5, drill)];
// Wings pump out in wide phase, tuck tight in drill
const wingOut = lerp(38, 16, drill);
const wingY = lerp(30, 56, drill);
const localHandL = [-wingOut, wingY, lerp(14, 16, drill)];
const localHandR = [wingOut, wingY, lerp(-14, 16, drill)];
return {
pelvis,
chest,
head,
headLook: norm(rotY([0, -0.15, 1], baseAngle)),
headUp: norm([wobble * 0.02, -1, 0]), // Crown planted!
chestFwd: norm(rotY([0, 0, 1], baseAngle)),
handL: add([0, 0, 0], rotY(localHandL, baseAngle)),
handR: add([0, 0, 0], rotY(localHandR, baseAngle)),
footL: rotY(localFootL, baseAngle),
footR: rotY(localFootR, baseAngle),
kneeHintL: rotY([-0.5, 0, 0.8], baseAngle),
kneeHintR: rotY([0.5, 0, -0.8], baseAngle),
elbowHintL: rotY([-0.8, -0.4, 0], baseAngle),
elbowHintR: rotY([0.8, -0.4, 0], baseAngle),
wingSpreadL: lerp(0.75, 0.15, drill),
wingSpreadR: lerp(0.75, 0.15, drill),
spinYaw: baseAngle
};
}
// 6. FREEZE (Two locked poses with live isometric tremor & chest breath)
function poseFreeze(variant, timeSec) {
const breath = Math.sin(timeSec * 3.8) * 1.1;
const quiver = Math.sin(timeSec * 31) * 0.45;
if (variant % 2 === 0) {
// Baby Freeze: R hand planted, head cocked watching camera, legs hooked & arrowed
return {
pelvis: [-12 + quiver, 52 + breath * 0.6, -4],
chest: [8 + quiver * 0.5, 28 + breath, 4],
head: [10, 17, 20],
headLook: norm([0.12, 0.08, 0.99]),
headUp: norm([0.92, 0.35, -0.15]),
chestFwd: norm([0.35, -0.35, 0.87]),
handR: [18, 0, 4],
handL: [-32, 34 + breath, 28],
footR: [-36, 76 + breath, 18],
footL: [-48, 114 + breath * 1.3, -12],
kneeHintR: [-0.3, 0.9, 0.4],
kneeHintL: [-0.5, 0.5, 0.7],
elbowHintR: [0.6, 0.4, -0.7],
elbowHintL: [-0.5, 0.6, -0.5],
wingSpreadL: 0.92,
wingSpreadR: 0.22
};
}
// Variant 1: Hollowback / Scissor Pike Freeze
return {
pelvis: [quiver, 90 + breath * 0.7, -8],
chest: [quiver * 0.5, 48 + breath, 4],
head: [0, 26, 18],
headLook: norm([0, -0.08, 0.99]),
headUp: norm([0, -0.88, 0.46]),
chestFwd: norm([0, -0.5, 0.86]),
handL: [-18, 0, 6],
handR: [20, 0, -4],
footL: [-38, 118 + breath, 24],
footR: [30, 136 + breath * 1.2, -26],
kneeHintL: [-0.5, 0.3, 0.8],
kneeHintR: [0.5, 0.3, -0.8],
elbowHintL: [-0.7, 0.3, -0.6],
elbowHintR: [0.7, 0.3, -0.6],
wingSpreadL: 0.25,
wingSpreadR: 0.25
};
}
// 7. GODOWN (Standing -> Floor in 2 beats)
function poseGoDown(tNorm, snap) {
const p = tNorm * 2.0; // 0..2
const keys = [
{ t: 0.0, pose: toprockKeys[0].pose },
{
t: 0.38, // Slight spiral hop
pose: {
pelvis: [4, 98, 0], chest: [4, 134, 4], head: [4, 156, 8],
headLook: [0.25, -0.1, 0.96], headUp: [0, 1, 0], chestFwd: [0.2, 0, 0.98],
handL: [-34, 128, 12], handR: [34, 118, 12],
footL: [-14, 12, 6], footR: [16, 18, -6],
kneeHintL: [-0.3, 0.2, 0.9], kneeHintR: [0.3, 0.2, 0.9],
elbowHintL: [-0.6, -0.5, -0.4], elbowHintR: [0.6, -0.5, -0.4],
wingSpreadL: 0.65, wingSpreadR: 0.65
}
},
{
t: 1.25, // Drop impact onto R hand & L foot
pose: {
pelvis: [8, 22, -2], chest: [12, 42, -8], head: [14, 62, -2],
headLook: [0.3, 0.12, 0.94], headUp: [0.1, 0.98, -0.1], chestFwd: [0.25, 0.15, 0.95],
handR: [24, 0, -16], handL: [-30, 38, 18],
footL: [-28, 0, 14], footR: [8, 10, 26],
kneeHintL: [-0.5, 0.85, 0.2], kneeHintR: [0.5, 0.85, 0.2],
elbowHintL: [-0.6, 0.3, -0.5], elbowHintR: [0.5, 0.2, -0.8],
wingSpreadL: 0.78, wingSpreadR: 0.25
}
},
{ t: 2.0, pose: sixStepKeys[0].pose }
];
return evalKeyframeTrack(keys, p, snap);
}
// 8. KIPUP (Floor -> Standing in 2 beats)
function poseKipUp(tNorm, snap) {
const p = tNorm * 2.0;
const keys = [
{ t: 0.0, pose: sixStepKeys[6].pose },
{
t: 0.55, // Coiled on upper back ready to kick
pose: {
pelvis: [0, 26, -10], chest: [0, 18, 8], head: [0, 24, 24],
headLook: [0, 0.6, 0.8], headUp: [0, 0.3, -0.95], chestFwd: [0, 0.85, 0.5],
handL: [-24, 0, 14], handR: [24, 0, 14],
footL: [-14, 52, 10], footR: [14, 52, 10],
kneeHintL: [-0.3, 0.4, 0.9], kneeHintR: [0.3, 0.4, 0.9],
elbowHintL: [-0.6, 0.8, 0], elbowHintR: [0.6, 0.8, 0],
wingSpreadL: 0.25, wingSpreadR: 0.25
}
},
{
t: 1.05, // Explosive airborne snap!
pose: {
pelvis: [0, 92, 0], chest: [0, 128, 4], head: [0, 150, 8],
headLook: [0, 0.05, 0.99], headUp: [0, 0.99, 0.1], chestFwd: [0, 0.1, 0.99],
handL: [-42, 134, 8], handR: [42, 134, 8],
footL: [-16, 26, 8], footR: [16, 26, 8],
kneeHintL: [-0.3, 0.2, 0.95], kneeHintR: [0.3, 0.2, 0.95],
elbowHintL: [-0.5, -0.6, -0.4], elbowHintR: [0.5, -0.6, -0.4],
wingSpreadL: 0.88, wingSpreadR: 0.88
}
},
{
t: 1.5, // Deep landing compression
pose: {
pelvis: [0, 68, 2], chest: [0, 106, 8], head: [0, 130, 14],
headLook: [0, -0.1, 0.99], headUp: [0, 0.98, 0.15], chestFwd: [0, -0.05, 0.99],
handL: [-32, 98, 16], handR: [32, 98, 16],
footL: [-18, 0, 4], footR: [18, 0, 4],
kneeHintL: [-0.45, 0.15, 0.9], kneeHintR: [0.45, 0.15, 0.9],
elbowHintL: [-0.7, -0.5, -0.3], elbowHintR: [0.7, -0.5, -0.3],
wingSpreadL: 0.6, wingSpreadR: 0.6
}
},
{ t: 2.0, pose: toprockKeys[0].pose }
];
return evalKeyframeTrack(keys, p, snap);
}
// --- Choreography & State Machine ---
// Round sequence in beats
const cypherRound = [
{ move: 'TOPROCK', beats: 8 },
{ move: 'GODOWN', beats: 2 },
{ move: 'SIXSTEP', beats: 8 },
{ move: 'WINDMILL', beats: 8 },
{ move: 'FLARE', beats: 4 },
{ move: 'HEADSPIN', beats: 8 },
{ move: 'FREEZE', beats: 4 },
{ move: 'KIPUP', beats: 2 }
];
const totalCypherBeats = cypherRound.reduce((s, r) => s + r.beats, 0);
const state = {
mode: 'CYPHER', // CYPHER | TOPROCK | SIXSTEP | WINDMILL | FLARE | HEADSPIN | FREEZE
bpm: 104,
snap: 0.72,
freezeVariant: 0,
audioOn: false,
// Master beat accumulator
beatFloat: 0,
lastBeatIdx: -1,
lastStep16: -1,
timeSec: 0,
// Active move tracking for smooth transitions
currentMove: 'TOPROCK',
prevPose: null,
transFromPose: null,
transTimer: 1,
transDur: 0.35,
// Visual juice
hypeShake: 0,
beatPulse: 0,
snarePulse: 0,
poseHistory: [],
scuffs: [],
dust: [],
prevFootLOnFloor: true,
prevFootROnFloor: true,
prevHandLOnFloor: false,
prevHandROnFloor: false
};
// --- Spring-Damped Crest & Tail Physics ---
const CREST_COUNT = 7;
const crestTips = Array.from({ length: CREST_COUNT }, () => ({ pos: [0, 150, 0], vel: [0, 0, 0] }));
const tailTips = [
{ pos: [0, 70, -50], vel: [0, 0, 0] },
{ pos: [0, 70, -50], vel: [0, 0, 0] }
];
function stepFeatherPhysics(dt, headPos, headLook, headUp, pelvisPos, chestPos) {
const right = norm(cross(headUp, headLook));
const back = mul(headLook, -1);
for (let i = 0; i < CREST_COUNT; i++) {
const fan = (i / (CREST_COUNT - 1)) - 0.5; // -0.5..0.5
const quillLen = 28 + (1 - Math.abs(fan)) * 10;
const anchor = add(headPos, add(mul(headUp, 7), mul(back, 5)));
const restDir = norm(add(
add(mul(back, 0.92), mul(headUp, 0.48 - Math.abs(fan) * 0.25)),
mul(right, fan * 0.75)
));
const target = add(anchor, mul(restDir, quillLen));
const f = crestTips[i];
const spring = sub(target, f.pos);
const k = 180;
const damp = 14;
f.vel[0] += (spring[0] * k - f.vel[0] * damp) * dt;
f.vel[1] += (spring[1] * k - f.vel[1] * damp - 45) * dt;
f.vel[2] += (spring[2] * k - f.vel[2] * damp) * dt;
f.pos[0] += f.vel[0] * dt;
f.pos[1] += f.vel[1] * dt;
f.pos[2] += f.vel[2] * dt;
// Length constraint + floor brush
const dVec = sub(f.pos, anchor);
const dLen = len(dVec);
if (dLen > 1e-4) {
const clamped = add(anchor, mul(dVec, quillLen / dLen));
f.pos[0] = lerp(f.pos[0], clamped[0], 0.65);
f.pos[1] = lerp(f.pos[1], clamped[1], 0.65);
f.pos[2] = lerp(f.pos[2], clamped[2], 0.65);
}
if (f.pos[1] < 2) {
f.pos[1] = 2;
f.vel[1] = Math.max(0, f.vel[1]);
}
}
// Two long central tail plumes
const spineDir = norm(sub(pelvisPos, chestPos));
const spineRight = norm(cross(spineDir, [0, 1, 0]));
for (let i = 0; i < 2; i++) {
const side = i === 0 ? -1 : 1;
const anchor = add(pelvisPos, mul(spineDir, 6));
const target = add(anchor, add(mul(spineDir, 58), mul(spineRight, side * 7)));
const t = tailTips[i];
const spring = sub(target, t.pos);
t.vel[0] += (spring[0] * 120 - t.vel[0] * 11) * dt;
t.vel[1] += (spring[1] * 120 - t.vel[1] * 11 - 40) * dt;
t.vel[2] += (spring[2] * 120 - t.vel[2] * 11) * dt;
t.pos[0] += t.vel[0] * dt;
t.pos[1] += t.vel[1] * dt;
t.pos[2] += t.vel[2] * dt;
if (t.pos[1] < 2) t.pos[1] = 2;
}
}
// --- Web Audio Procedural Breakbeat Synthesizer ---
let audioCtx = null;
let noiseBuf = null;
let masterGain = null;
let crackleNode = null;
function initAudio() {
if (audioCtx) return;
const AC = window.AudioContext || window.webkitAudioContext;
if (!AC) return;
audioCtx = new AC();
masterGain = audioCtx.createGain();
masterGain.gain.value = 0.65;
masterGain.connect(audioCtx.destination);
// Noise buffer for snare, hats, vinyl crackle
const lenSamples = audioCtx.sampleRate * 2;
noiseBuf = audioCtx.createBuffer(1, lenSamples, audioCtx.sampleRate);
const data = noiseBuf.getChannelData(0);
for (let i = 0; i < lenSamples; i++) {
data[i] = Math.random() * 2 - 1;
}
}
function startCrackle() {
if (!audioCtx || crackleNode) return;
const buf = audioCtx.createBuffer(1, audioCtx.sampleRate * 3, audioCtx.sampleRate);
const d = buf.getChannelData(0);
for (let i = 0; i < d.length; i++) {
d[i] = Math.random() < 0.0022 ? (Math.random() * 2 - 1) * 0.55 : (Math.random() * 2 - 1) * 0.015;
}
const src = audioCtx.createBufferSource();
src.buffer = buf;
src.loop = true;
const hp = audioCtx.createBiquadFilter();
hp.type = 'bandpass';
hp.frequency.value = 2600;
const g = audioCtx.createGain();
g.gain.value = 0.14;
src.connect(hp);
hp.connect(g);
g.connect(masterGain);
src.start();
crackleNode = src;
}
function playKick(accent = 1.0) {
if (!audioCtx || !state.audioOn) return;
const t = audioCtx.currentTime;
const osc = audioCtx.createOscillator();
const g = audioCtx.createGain();
osc.type = 'sine';
osc.frequency.setValueAtTime(145, t);
osc.frequency.exponentialRampToValueAtTime(46, t + 0.09);
g.gain.setValueAtTime(0.95 * accent, t);
g.gain.exponentialRampToValueAtTime(0.001, t + 0.26);
osc.connect(g);
g.connect(masterGain);
osc.start(t);
osc.stop(t + 0.27);
}
function playSnare(vel = 1.0) {
if (!audioCtx || !state.audioOn) return;
const t = audioCtx.currentTime;
// Noise crack
const n = audioCtx.createBufferSource();
n.buffer = noiseBuf;
const bp = audioCtx.createBiquadFilter();
bp.type = 'bandpass';
bp.frequency.setValueAtTime(1950, t);
bp.Q.value = 0.8;
const ng = audioCtx.createGain();
ng.gain.setValueAtTime(0.55 * vel, t);
ng.gain.exponentialRampToValueAtTime(0.001, t + 0.16);
n.connect(bp);
bp.connect(ng);
ng.connect(masterGain);
n.start(t);
n.stop(t + 0.17);
// Body thump
const osc = audioCtx.createOscillator();
osc.type = 'triangle';
osc.frequency.setValueAtTime(195, t);
osc.frequency.exponentialRampToValueAtTime(108, t + 0.08);
const og = audioCtx.createGain();
og.gain.setValueAtTime(0.45 * vel, t);
og.gain.exponentialRampToValueAtTime(0.001, t + 0.11);
osc.connect(og);
og.connect(masterGain);
osc.start(t);
osc.stop(t + 0.12);
}
function playHat(vel = 0.4, open = false) {
if (!audioCtx || !state.audioOn) return;
const t = audioCtx.currentTime;
const n = audioCtx.createBufferSource();
n.buffer = noiseBuf;
const hp = audioCtx.createBiquadFilter();
hp.type = 'highpass';
hp.frequency.setValueAtTime(7200, t);
const g = audioCtx.createGain();
const dur = open ? 0.16 : 0.045;
g.gain.setValueAtTime(0.22 * vel, t);
g.gain.exponentialRampToValueAtTime(0.001, t + dur);
n.connect(hp);
hp.connect(g);
g.connect(masterGain);
n.start(t);
n.stop(t + dur + 0.01);
}
function playBass(freq, dur = 0.18) {
if (!audioCtx || !state.audioOn) return;
const t = audioCtx.currentTime;
const osc = audioCtx.createOscillator();
osc.type = 'sawtooth';
osc.frequency.setValueAtTime(freq, t);
const lp = audioCtx.createBiquadFilter();
lp.type = 'lowpass';
lp.frequency.setValueAtTime(480, t);
lp.frequency.exponentialRampToValueAtTime(140, t + dur);
const g = audioCtx.createGain();
g.gain.setValueAtTime(0.28, t);
g.gain.exponentialRampToValueAtTime(0.001, t + dur);
osc.connect(lp);
lp.connect(g);
g.connect(masterGain);
osc.start(t);
osc.stop(t + dur + 0.02);
}
// Classic 16-step boom-bap breakbeat pattern
function triggerBreakStep(step16) {
const kickSteps = [1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0];
const snareSteps = [0, 0, 0, 0, 1, 0, 0, 0.28, 0, 0.24, 0, 0, 1, 0, 0, 0.3];
const hatSteps = [0.8, 0.25, 0.6, 0.2, 0.9, 0.25, 0.7, 0.3, 0.8, 0.25, 0.65, 0.25, 0.9, 0.3, 0.75, 0.4];
const bassNotes = { 0: 55, 7: 55, 10: 65.41, 14: 73.42 }; // A1, C2, D2
if (kickSteps[step16]) {
playKick(kickSteps[step16]);
state.beatPulse = Math.max(state.beatPulse, 0.95);
}
if (snareSteps[step16]) {
playSnare(snareSteps[step16]);
if (snareSteps[step16] > 0.5) state.snarePulse = 1.0;
}
if (hatSteps[step16]) {
playHat(hatSteps[step16], step16 === 14);
}
if (bassNotes[step16]) {
playBass(bassNotes[step16], step16 === 14 ? 0.12 : 0.22);
}
}
// --- Canvas & Risograph Grain Setup ---
const canvas = document.getElementById('stage');
const ctx = canvas.getContext('2d');
let W = 0, H = 0, DPR = 1;
// Offscreen paper grain tile
const grainTile = document.createElement('canvas');
grainTile.width = 160;
grainTile.height = 160;
{
const gctx = grainTile.getContext('2d');
const img = gctx.createImageData(160, 160);
for (let i = 0; i < img.data.length; i += 4) {
const n = (Math.random() - 0.5) * 28;
img.data[i] = 242 + n;
img.data[i + 1] = 234 + n;
img.data[i + 2] = 220 + n;
img.data[i + 3] = 255;
}
gctx.putImageData(img, 0, 0);
// Tiny paper fibers
gctx.strokeStyle = 'rgba(29,28,33,0.06)';
gctx.lineWidth = 1;
for (let i = 0; i < 24; i++) {
const x = Math.random() * 160, y = Math.random() * 160;
gctx.beginPath();
gctx.moveTo(x, y);
gctx.lineTo(x + (Math.random() - 0.5) * 14, y + (Math.random() - 0.5) * 14);
gctx.stroke();
}
}
let grainPattern = null;
function resize() {
DPR = Math.min(window.devicePixelRatio || 1, 2);
W = window.innerWidth;
H = window.innerHeight;
canvas.width = Math.floor(W * DPR);
canvas.height = Math.floor(H * DPR);
grainPattern = ctx.createPattern(grainTile, 'repeat');
}
window.addEventListener('resize', resize);
resize();
// --- 3D Camera Projection ---
function getCamera() {
const minDim = Math.min(W, H);
return {
cx: W * 0.5 + (Math.random() - 0.5) * state.hypeShake * 7,
cy: H * 0.53 + (Math.random() - 0.5) * state.hypeShake * 7,
camY: 132,
camZ: 455,
pitch: 0.24,
focal: minDim * 0.92
};
}
function project(p, cam) {
const cosT = Math.cos(cam.pitch);
const sinT = Math.sin(cam.pitch);
const dy = p[1] - cam.camY;
const dz = cam.camZ - p[2];
const camDepth = Math.max(40, dz * cosT - dy * sinT);
const camUp = dy * cosT + dz * sinT;
const scale = cam.focal / camDepth;
return {
x: cam.cx + p[0] * scale,
y: cam.cy - camUp * scale,
depth: camDepth,
scale
};
}
// --- Cardboard Floor Mat & Chalk Scuffs ---
function emitDust(worldPos, count = 6, speed = 45) {
for (let i = 0; i < count; i++) {
const a = Math.random() * Math.PI * 2;
const spd = (0.3 + Math.random() * 0.7) * speed;
state.dust.push({
x: worldPos[0] + (Math.random() - 0.5) * 10,
y: Math.max(1, worldPos[1] + 2),
z: worldPos[2] + (Math.random() - 0.5) * 10,
vx: Math.cos(a) * spd,
vy: 15 + Math.random() * 35,
vz: Math.sin(a) * spd,
life: 1.0,
size: 2.2 + Math.random() * 3.5,
col: Math.random() < 0.2 ? '#e8482c' : '#1d1c21'
});
}
}
function addScuff(x, z, dx, dz) {
if (Math.abs(x) > 165 || Math.abs(z) > 108) return;
state.scuffs.push({ x, z, dx, dz, alpha: 0.45 });
if (state.scuffs.length > 90) state.scuffs.shift();
}
function drawStageEnvironment(cam, pelvisPos) {
// 1. Paper grain background
ctx.fillStyle = grainPattern || '#f2eadc';
ctx.fillRect(0, 0, W, H);
// 2. Risograph Halftone Sunburst / Beat Aura behind dancer
const centerP = project([0, 72, -30], cam);
const ringRadius = Math.min(W, H) * (0.24 + state.beatPulse * 0.045);
ctx.save();
ctx.fillStyle = state.snarePulse > 0.35 ? 'rgba(232, 72, 44, 0.16)' : 'rgba(224, 159, 45, 0.15)';
ctx.beginPath();
ctx.arc(centerP.x, centerP.y, ringRadius, 0, Math.PI * 2);
ctx.fill();
// Halftone dots grid inside the circle
ctx.clip();
ctx.fillStyle = 'rgba(29, 28, 33, 0.10)';
const step = 14;
const x0 = Math.floor((centerP.x - ringRadius) / step) * step;
const y0 = Math.floor((centerP.y - ringRadius) / step) * step;
for (let y = y0; y < centerP.y + ringRadius; y += step) {
for (let x = x0; x < centerP.x + ringRadius; x += step) {
const dist = Math.hypot(x - centerP.x, y - centerP.y);
const r = Math.max(0.6, (1 - dist / ringRadius) * (2.6 + state.beatPulse * 1.4));
ctx.beginPath();
ctx.arc(x, y, r, 0, Math.PI * 2);
ctx.fill();
}
}
ctx.restore();
// 3. Corrugated Cardboard Breaker Mat (slightly skewed in 3D space)
const matPts = [
[-178, 0, -116],
[ 178, 0, -116],
[ 184, 0, 118],
[-172, 0, 118]
].map(p => project(p, cam));
// Mat drop ink shadow
ctx.save();
ctx.fillStyle = '#1d1c21';
ctx.beginPath();
ctx.moveTo(matPts[0].x + 6, matPts[0].y + 8);
ctx.lineTo(matPts[1].x + 6, matPts[1].y + 8);
ctx.lineTo(matPts[2].x + 6, matPts[2].y + 8);
ctx.lineTo(matPts[3].x + 6, matPts[3].y + 8);
ctx.closePath();
ctx.fill();
// Corrugated front thickness edge
ctx.fillStyle = '#9e7a4c';
ctx.strokeStyle = '#1d1c21';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(matPts[3].x, matPts[3].y);
ctx.lineTo(matPts[2].x, matPts[2].y);
ctx.lineTo(matPts[2].x, matPts[2].y + 6);
ctx.lineTo(matPts[3].x, matPts[3].y + 6);
ctx.closePath();
ctx.fill();
ctx.stroke();
// Cardboard top surface
ctx.fillStyle = '#d3b07c';
ctx.beginPath();
ctx.moveTo(matPts[0].x, matPts[0].y);
ctx.lineTo(matPts[1].x, matPts[1].y);
ctx.lineTo(matPts[2].x, matPts[2].y);
ctx.lineTo(matPts[3].x, matPts[3].y);
ctx.closePath();
ctx.fill();
ctx.stroke();
// Corrugation ribs & fold crease across the cardboard
ctx.save();
ctx.clip();
ctx.strokeStyle = 'rgba(130, 92, 46, 0.16)';
ctx.lineWidth = 1.2;
for (let z = -110; z <= 110; z += 11) {
const a = project([-185, 0, z], cam);
const b = project([185, 0, z], cam);
ctx.beginPath();
ctx.moveTo(a.x, a.y);
ctx.lineTo(b.x, b.y);
ctx.stroke();
}
// Fold crease down middle
const f0 = project([-6, 0, -118], cam);
const f1 = project([4, 0, 118], cam);
ctx.strokeStyle = 'rgba(29, 28, 33, 0.25)';
ctx.lineWidth = 2;
ctx.setLineDash([8, 6]);
ctx.beginPath();
ctx.moveTo(f0.x, f0.y);
ctx.lineTo(f1.x, f1.y);
ctx.stroke();
ctx.setLineDash([]);
// Graffiti arrow chalk ring
ctx.strokeStyle = 'rgba(232, 72, 44, 0.28)';
ctx.lineWidth = 2.5;
ctx.beginPath();
for (let a = 0; a <= Math.PI * 1.65; a += 0.2) {
const rp = project([Math.cos(a) * 84, 0, Math.sin(a) * 56], cam);
if (a === 0) ctx.moveTo(rp.x, rp.y);
else ctx.lineTo(rp.x, rp.y);
}
ctx.stroke();
// Accumulated talon & spin scuffs
for (const sc of state.scuffs) {
const p1 = project([sc.x, 0, sc.z], cam);
const p2 = project([sc.x + sc.dx, 0, sc.z + sc.dz], cam);
ctx.strokeStyle = `rgba(29, 28, 33, ${sc.alpha})`;
ctx.lineWidth = 2.2;
ctx.beginPath();
ctx.moveTo(p1.x, p1.y);
ctx.lineTo(p2.x, p2.y);
ctx.stroke();
}
ctx.restore();
// Silver gaffer tape corners
const tapeCorners = [
[[-188, 0, -104], [-164, 0, -124], [-156, 0, -116], [-180, 0, -96]],
[[ 164, 0, -124], [ 188, 0, -104], [ 180, 0, -96], [ 156, 0, -116]],
[[-184, 0, 104], [-156, 0, 126], [-148, 0, 118], [-176, 0, 96]],
[[ 156, 0, 126], [ 192, 0, 104], [ 184, 0, 96], [ 148, 0, 118]]
];
ctx.fillStyle = '#b4b9bc';
ctx.strokeStyle = '#1d1c21';
ctx.lineWidth = 1.5;
for (const quad of tapeCorners) {
const pts = quad.map(q => project(q, cam));
ctx.beginPath();
ctx.moveTo(pts[0].x, pts[0].y);
for (let i = 1; i < 4; i++) ctx.lineTo(pts[i].x, pts[i].y);
ctx.closePath();
ctx.fill();
ctx.stroke();
}
ctx.restore();
// 4. Center-of-Gravity Floor Shadow under Pelvis
const shadowScale = clamp(1.1 - pelvisPos[1] / 210, 0.45, 1.15);
const shC = project([pelvisPos[0], 0, pelvisPos[2]], cam);
const shR = project([pelvisPos[0] + 38 * shadowScale, 0, pelvisPos[2]], cam);
const shF = project([pelvisPos[0], 0, pelvisPos[2] + 24 * shadowScale], cam);
const rx = Math.max(10, Math.abs(shR.x - shC.x));
const ry = Math.max(6, Math.abs(shF.y - shC.y));
ctx.save();
ctx.fillStyle = `rgba(29, 28, 33, ${clamp(0.42 - pelvisPos[1] / 300, 0.14, 0.42)})`;
ctx.beginPath();
ctx.ellipse(shC.x, shC.y, rx, ry, 0, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
// --- Bird Renderer (Z-sorted + Risograph Smear Pass) ---
function drawTaperedLimb(p1, p2, w1, w2, fill, stroke, outline = true) {
const dx = p2.x - p1.x, dy = p2.y - p1.y;
const L = Math.hypot(dx, dy) || 1;
const nx = -dy / L, ny = dx / L;
ctx.beginPath();
ctx.moveTo(p1.x + nx * w1, p1.y + ny * w1);
ctx.lineTo(p2.x + nx * w2, p2.y + ny * w2);
ctx.lineTo(p2.x - nx * w2, p2.y - ny * w2);
ctx.lineTo(p1.x - nx * w1, p1.y - ny * w1);
ctx.closePath();
ctx.fillStyle = fill;
ctx.fill();
if (outline) {
ctx.strokeStyle = stroke;
ctx.lineWidth = 2.2;
ctx.lineJoin = 'round';
ctx.stroke();
}
}
function renderBirdPose(pose, cam, ghostMode = null) {
// ghostMode: null | { color, offsetX, offsetY, alpha }
const isGhost = Boolean(ghostMode);
const ox = isGhost ? ghostMode.offsetX : 0;
const oy = isGhost ? ghostMode.offsetY : 0;
const ink = isGhost ? ghostMode.color : '#1d1c21';
const whitePlume = isGhost ? ghostMode.color : '#f7f2e8';
const slate = isGhost ? ghostMode.color : '#7e8f9c';
const ochre = isGhost ? ghostMode.color : '#e09f2d';
const vermilion = isGhost ? ghostMode.color : '#e8482c';
// Compute shoulders & hips from torso frame
const spine = norm(sub(pose.chest, pose.pelvis));
const rightDir = norm(cross(pose.chestFwd, spine));
const hipL = add(pose.pelvis, mul(rightDir, -10));
const hipR = add(pose.pelvis, mul(rightDir, 10));
const shL = add(pose.chest, mul(rightDir, -14));
const shR = add(pose.chest, mul(rightDir, 14));
// Solve 3D IK for legs (long secretary bird legs: 46 + 44) & wings (34 + 34)
const legL = solveIK(hipL, pose.footL, 46, 44, pose.kneeHintL);
const legR = solveIK(hipR, pose.footR, 46, 44, pose.kneeHintR);
const armL = solveIK(shL, pose.handL, 34, 34, pose.elbowHintL);
const armR = solveIK(shR, pose.handR, 34, 34, pose.elbowHintR);
const neckBase = add(pose.chest, mul(spine, 6));
const neckMid = add(lerpV(neckBase, pose.head, 0.5), mul(pose.chestFwd, 5));
const P = (pt) => {
const pr = project(pt, cam);
pr.x += ox;
pr.y += oy;
return pr;
};
const drawables = [];
// Helper for talon foot
const pushLeg = (hip, ik, foot, side) => {
const pHip = P(hip), pKnee = P(ik.mid), pFoot = P(ik.end);
// Black feathered "shorts" thigh
drawables.push({
depth: (pHip.depth + pKnee.depth) * 0.5,
draw: () => {
drawTaperedLimb(pHip, pKnee, 8.5 * pHip.scale, 6.2 * pKnee.scale, ink, ink, !isGhost);
// Knee joint cap
ctx.beginPath();
ctx.arc(pKnee.x, pKnee.y, 4.5 * pKnee.scale, 0, Math.PI * 2);
ctx.fillStyle = ochre;
ctx.fill();
if (!isGhost) { ctx.strokeStyle = ink; ctx.lineWidth = 1.8; ctx.stroke(); }
}
});
// Slender yellow shin + 3 talons
drawables.push({
depth: (pKnee.depth + pFoot.depth) * 0.5,
draw: () => {
drawTaperedLimb(pKnee, pFoot, 3.6 * pKnee.scale, 2.4 * pFoot.scale, ochre, ink, !isGhost);
// 3 splayed toes
const toeDir = norm(add(pose.chestFwd, [side * 0.25, 0, 0.4]));
const toeR = norm(cross(toeDir, [0, 1, 0]));
for (let t = -1; t <= 1; t++) {
const toeEnd = P(add(ik.end, add(mul(toeDir, 10), mul(toeR, t * 5))));
ctx.beginPath();
ctx.moveTo(pFoot.x, pFoot.y);
ctx.lineTo(toeEnd.x, toeEnd.y);
ctx.strokeStyle = ink;
ctx.lineWidth = 2.6 * pFoot.scale;
ctx.lineCap = 'round';
ctx.stroke();
if (!isGhost) {
ctx.strokeStyle = ochre;
ctx.lineWidth = 1.4 * pFoot.scale;
ctx.stroke();
}
}
}
});
};
pushLeg(hipL, legL, pose.footL, -1);
pushLeg(hipR, legR, pose.footR, 1);
// Wings (arms + layered flight feathers)
const pushWing = (sh, ik, spread) => {
const pSh = P(sh), pEl = P(ik.mid), pWr = P(ik.end);
drawables.push({
depth: (pSh.depth + pEl.depth) * 0.5,
draw: () => {
drawTaperedLimb(pSh, pEl, 7 * pSh.scale, 5.5 * pEl.scale, slate, ink, !isGhost);
}
});
drawables.push({
depth: (pEl.depth + pWr.depth) * 0.5,
draw: () => {
// Wing primary flight feathers fan!
const wingAxis = sub(ik.end, ik.mid);
const fanBack = norm(sub(pose.pelvis, pose.chest));
const featherCount = 5;
for (let f = 0; f < featherCount; f++) {
const frac = (f + 1) / featherCount;
const root3 = lerpV(ik.mid, ik.end, frac * 0.85);
const tip3 = add(root3, add(mul(fanBack, (10 + spread * 24) * (1 - frac * 0.35)), mul(norm(wingAxis), 8 * spread)));
const pA = P(root3), pB = P(tip3);
drawTaperedLimb(pA, pB, 3.5 * pA.scale, 1.4 * pB.scale, f % 2 === 0 ? ink : slate, ink, !isGhost);
}
drawTaperedLimb(pEl, pWr, 5.5 * pEl.scale, 3.2 * pWr.scale, whitePlume, ink, !isGhost);
}
});
};
pushWing(shL, armL, pose.wingSpreadL);
pushWing(shR, armR, pose.wingSpreadR);
// Tail Plumes (spring-driven)
const pPelvis = P(pose.pelvis);
for (let i = 0; i < 2; i++) {
const tWorld = tailTips[i].pos;
const pTail = P(tWorld);
drawables.push({
depth: (pPelvis.depth + pTail.depth) * 0.5,
draw: () => {
drawTaperedLimb(pPelvis, pTail, 4.2 * pPelvis.scale, 1.8 * pTail.scale, slate, ink, !isGhost);
// Black tip on long central tail feather
const tipBase = P(lerpV(pose.pelvis, tWorld, 0.72));
drawTaperedLimb(tipBase, pTail, 2.5 * pTail.scale, 1.6 * pTail.scale, ink, ink, false);
}
});
}
// Torso (Plump avian chest + slate cape)
const pChest = P(pose.chest);
drawables.push({
depth: (pPelvis.depth + pChest.depth) * 0.5,
draw: () => {
drawTaperedLimb(pPelvis, pChest, 11.5 * pPelvis.scale, 15.5 * pChest.scale, whitePlume, ink, !isGhost);
// Slate back/chest shading accent
const midTorso = { x: (pPelvis.x + pChest.x) * 0.5, y: (pPelvis.y + pChest.y) * 0.5, scale: (pPelvis.scale + pChest.scale) * 0.5 };
ctx.beginPath();
ctx.ellipse(pChest.x, pChest.y, 14 * pChest.scale, 13 * pChest.scale, 0, 0, Math.PI * 2);
ctx.fillStyle = whitePlume;
ctx.fill();
if (!isGhost) {
ctx.strokeStyle = ink;
ctx.lineWidth = 2.3;
ctx.stroke();
// Subtle plumage V-mark on chest
ctx.beginPath();
ctx.moveTo(midTorso.x - 5 * midTorso.scale, midTorso.y - 3 * midTorso.scale);
ctx.lineTo(midTorso.x, midTorso.y + 4 * midTorso.scale);
ctx.lineTo(midTorso.x + 5 * midTorso.scale, midTorso.y - 3 * midTorso.scale);
ctx.strokeStyle = slate;
ctx.lineWidth = 2;
ctx.stroke();
}
}
});
// Neck
const pNeckB = P(neckBase), pNeckM = P(neckMid), pHead = P(pose.head);
drawables.push({
depth: (pNeckB.depth + pHead.depth) * 0.5,
draw: () => {
drawTaperedLimb(pNeckB, pNeckM, 7.5 * pNeckB.scale, 6.2 * pNeckM.scale, whitePlume, ink, !isGhost);
drawTaperedLimb(pNeckM, pHead, 6.2 * pNeckM.scale, 5.5 * pHead.scale, whitePlume, ink, !isGhost);
}
});
// Head + Vermilion Mask + Hooked Beak + 7 Crest Quills
drawables.push({
depth: pHead.depth,
draw: () => {
// Crest quills first (behind skull if facing camera)
const crown3 = add(pose.head, add(mul(pose.headUp, 7), mul(pose.headLook, -5)));
const pCrown = P(crown3);
for (let i = 0; i < CREST_COUNT; i++) {
const tip3 = crestTips[i].pos;
const pTip = P(tip3);
const pMid = P(lerpV(crown3, tip3, 0.58));
// Grey shaft
ctx.beginPath();
ctx.moveTo(pCrown.x, pCrown.y);
ctx.lineTo(pMid.x, pMid.y);
ctx.strokeStyle = slate;
ctx.lineWidth = 2.2 * pHead.scale;
ctx.stroke();
// Bold black spatula quill tip!
drawTaperedLimb(pMid, pTip, 2.2 * pHead.scale, 3.6 * pTip.scale, ink, ink, false);
}
// Skull
const headR = 10.5 * pHead.scale;
ctx.beginPath();
ctx.arc(pHead.x, pHead.y, headR, 0, Math.PI * 2);
ctx.fillStyle = whitePlume;
ctx.fill();
if (!isGhost) {
ctx.strokeStyle = ink;
ctx.lineWidth = 2.3;
ctx.stroke();
}
// Beak tip in 3D so orientation reads cleanly in spins & freezes!
const beakBase3 = add(pose.head, mul(pose.headLook, 8));
const beakTip3 = add(pose.head, add(mul(pose.headLook, 19), mul(pose.headUp, -3.5)));
const pBeakB = P(beakBase3);
const pBeakT = P(beakTip3);
// Vermilion bare facial skin around eye/beak
const mask3 = add(pose.head, add(mul(pose.headLook, 4.5), mul(pose.headUp, 1)));
const pMask = P(mask3);
ctx.beginPath();
ctx.ellipse(pMask.x, pMask.y, 6.4 * pHead.scale, 4.8 * pHead.scale, 0, 0, Math.PI * 2);
ctx.fillStyle = vermilion;
ctx.fill();
// Hooked Raptor Beak
drawTaperedLimb(pBeakB, pBeakT, 4.4 * pHead.scale, 1.0 * pBeakT.scale, ochre, ink, !isGhost);
if (!isGhost) {
// Sharp eye + fierce brow line
const eye3 = add(pose.head, add(mul(pose.headLook, 5.2), mul(pose.headUp, 2.4)));
const pEye = P(eye3);
ctx.beginPath();
ctx.arc(pEye.x, pEye.y, 2.6 * pHead.scale, 0, Math.PI * 2);
ctx.fillStyle = '#1d1c21';
ctx.fill();
ctx.beginPath();
ctx.arc(pEye.x - 0.6 * pHead.scale, pEye.y - 0.6 * pHead.scale, 0.9 * pHead.scale, 0, Math.PI * 2);
ctx.fillStyle = '#f7f2e8';
ctx.fill();
}
}
});
// Painter's sort back-to-front
drawables.sort((a, b) => b.depth - a.depth);
ctx.save();
if (isGhost) ctx.globalAlpha = ghostMode.alpha;
for (const d of drawables) d.draw();
ctx.restore();
}
// --- UI & Controls Wiring ---
const moveButtons = document.querySelectorAll('#moveButtons button');
const moveReadout = document.getElementById('moveReadout');
const leds = document.querySelectorAll('#leds .led');
const bpmSlider = document.getElementById('bpmSlider');
const bpmVal = document.getElementById('bpmVal');
const snapSlider = document.getElementById('snapSlider');
const snapVal = document.getElementById('snapVal');
const btnAudio = document.getElementById('btnAudio');
const btnScuffClear = document.getElementById('btnScuffClear');
function setMode(mode) {
if (mode === 'FREEZE' && state.mode === 'FREEZE') {
// Cycle freeze variant on repeat click!
state.freezeVariant = (state.freezeVariant + 1) % 2;
state.hypeShake = 0.65;
}
state.mode = mode;
for (const b of moveButtons) {
b.classList.toggle('active', b.dataset.mode === mode);
}
triggerTransition(mode === 'FREEZE' ? 0.22 : 0.38);
}
for (const b of moveButtons) {
b.addEventListener('click', () => setMode(b.dataset.mode));
}
bpmSlider.addEventListener('input', () => {
state.bpm = Number(bpmSlider.value);
bpmVal.textContent = state.bpm;
});
snapSlider.addEventListener('input', () => {
const v = Number(snapSlider.value);
state.snap = v / 100;
snapVal.textContent = v < 25 ? 'FLOW' : v < 65 ? 'GROOVE' : 'WHIP';
});
btnAudio.addEventListener('click', () => {
initAudio();
if (audioCtx && audioCtx.state === 'suspended') {
audioCtx.resume();
}
state.audioOn = !state.audioOn;
if (state.audioOn) startCrackle();
btnAudio.textContent = state.audioOn ? 'BEAT: ON' : 'BEAT: OFF';
btnAudio.classList.toggle('sound-on', state.audioOn);
});
btnScuffClear.addEventListener('click', () => {
state.scuffs.length = 0;
});
canvas.addEventListener('pointerdown', () => {
// Hype stomp + micro camera punch + dust burst
state.hypeShake = 0.9;
state.beatPulse = 1.0;
if (state.prevPose) {
emitDust(state.prevPose.footL, 8, 70);
emitDust(state.prevPose.footR, 8, 70);
if (state.currentMove === 'FREEZE') {
state.freezeVariant = (state.freezeVariant + 1) % 2;
triggerTransition(0.2);
}
}
});
window.addEventListener('keydown', (e) => {
if (e.code === 'Space') {
e.preventDefault();
if (state.mode === 'FREEZE') {
setMode('CYPHER');
} else {
setMode('FREEZE');
state.hypeShake = 0.85;
}
}
});
function triggerTransition(durBeats = 0.35) {
if (state.prevPose) {
state.transFromPose = clonePose(state.prevPose);
state.transTimer = 0;
state.transDur = durBeats;
}
}
// --- Main Animation Loop ---
function evaluateActiveMove(beatFloat) {
let moveName = state.mode;
let localBeat = beatFloat % 4;
let moveProgress = 0;
if (state.mode === 'CYPHER') {
let rem = beatFloat % totalCypherBeats;
for (const seg of cypherRound) {
if (rem < seg.beats) {
moveName = seg.move;
localBeat = rem % 4;
moveProgress = rem / seg.beats;
break;
}
rem -= seg.beats;
}
} else {
localBeat = beatFloat % 4;
moveProgress = localBeat / 4;
}
if (moveName !== state.currentMove) {
if (moveName === 'FREEZE') {
state.freezeVariant = (state.freezeVariant + 1) % 2;
}
state.currentMove = moveName;
triggerTransition(moveName === 'FREEZE' ? 0.22 : 0.36);
}
moveReadout.textContent = moveName === 'SIXSTEP' ? '6-STEP' : moveName === 'GODOWN' ? 'GO-DOWN' : moveName === 'KIPUP' ? 'KIP-UP' : moveName;
switch (moveName) {
case 'TOPROCK': return evalKeyframeTrack(toprockKeys, localBeat, state.snap);
case 'SIXSTEP': return evalKeyframeTrack(sixStepKeys, localBeat, state.snap);
case 'WINDMILL': return poseWindmill(localBeat, state.snap);
case 'FLARE': return poseFlare(localBeat, state.snap);
case 'HEADSPIN': return poseHeadspin(beatFloat % 4, state.snap);
case 'FREEZE': return poseFreeze(state.freezeVariant, state.timeSec);
case 'GODOWN': return poseGoDown(moveProgress, state.snap);
case 'KIPUP': return poseKipUp(moveProgress, state.snap);
default: return evalKeyframeTrack(toprockKeys, localBeat, state.snap);
}
}
let lastTime = performance.now();
function frame(now) {
const dt = Math.min(0.05, (now - lastTime) / 1000);
lastTime = now;
state.timeSec += dt;
const beatsPerSec = state.bpm / 60;
const dBeat = dt * beatsPerSec;
state.beatFloat += dBeat;
// Beat & 16th-note triggers
const beatIdx = Math.floor(state.beatFloat);
const step16 = Math.floor(state.beatFloat * 4) % 16;
if (step16 !== state.lastStep16) {
state.lastStep16 = step16;
triggerBreakStep(step16);
}
if (beatIdx !== state.lastBeatIdx) {
state.lastBeatIdx = beatIdx;
const bInBar = beatIdx % 4;
leds.forEach((el, i) => el.classList.toggle('on', i === bInBar));
if (!state.audioOn) state.beatPulse = 0.85;
}
// Decay juices
state.beatPulse = Math.max(0, state.beatPulse - dt * 4.5);
state.snarePulse = Math.max(0, state.snarePulse - dt * 5.5);
state.hypeShake = Math.max(0, state.hypeShake - dt * 4.2);
// Evaluate dancer pose + smooth transitions
let pose = evaluateActiveMove(state.beatFloat);
if (state.transFromPose && state.transTimer < 1) {
state.transTimer = Math.min(1, state.transTimer + dBeat / state.transDur);
const blendT = snapEase(state.transTimer, state.snap * 0.7);
pose = blendPose(state.transFromPose, pose, blendT);
if (state.transTimer >= 1) state.transFromPose = null;
}
// Ground contact detection -> chalk scuffs & dust!
const fLFloor = pose.footL[1] < 3.5;
const fRFloor = pose.footR[1] < 3.5;
const hLFloor = pose.handL[1] < 3.5;
const hRFloor = pose.handR[1] < 3.5;
if (fLFloor && !state.prevFootLOnFloor) {
emitDust(pose.footL, 5, 42);
addScuff(pose.footL[0], pose.footL[2], (Math.random() - 0.5) * 14, (Math.random() - 0.5) * 10);
}
if (fRFloor && !state.prevFootROnFloor) {
emitDust(pose.footR, 5, 42);
addScuff(pose.footR[0], pose.footR[2], (Math.random() - 0.5) * 14, (Math.random() - 0.5) * 10);
}
if (hLFloor && !state.prevHandLOnFloor) emitDust(pose.handL, 4, 32);
if (hRFloor && !state.prevHandROnFloor) emitDust(pose.handR, 4, 32);
// Continuous spin scuffs during headspin / windmill
if (state.currentMove === 'HEADSPIN' && Math.random() < 0.4) {
addScuff(pose.head[0] + (Math.random() - 0.5) * 12, pose.head[2] + (Math.random() - 0.5) * 12, (Math.random() - 0.5) * 10, (Math.random() - 0.5) * 10);
} else if (state.currentMove === 'WINDMILL' && Math.random() < 0.35) {
addScuff(pose.chest[0], pose.chest[2], (Math.random() - 0.5) * 18, (Math.random() - 0.5) * 18);
}
state.prevFootLOnFloor = fLFloor;
state.prevFootROnFloor = fRFloor;
state.prevHandLOnFloor = hLFloor;
state.prevHandROnFloor = hRFloor;
// Step feather & dust physics
stepFeatherPhysics(dt, pose.head, pose.headLook, pose.headUp, pose.pelvis, pose.chest);
for (let i = state.dust.length - 1; i >= 0; i--) {
const p = state.dust[i];
p.x += p.vx * dt;
p.y += p.vy * dt;
p.z += p.vz * dt;
p.vy -= 95 * dt;
if (p.y < 0) { p.y = 0; p.vy *= -0.25; }
p.life -= dt * 2.1;
if (p.life <= 0) state.dust.splice(i, 1);
}
// Measure pose velocity for riso smear ghosts
let speed = 0;
if (state.prevPose) {
speed = (
len(sub(pose.footL, state.prevPose.footL)) +
len(sub(pose.footR, state.prevPose.footR)) +
len(sub(pose.handL, state.prevPose.handL)) +
len(sub(pose.handR, state.prevPose.handR))
) / Math.max(0.008, dt);
}
state.poseHistory.push(clonePose(pose));
if (state.poseHistory.length > 4) state.poseHistory.shift();
state.prevPose = clonePose(pose);
// --- Render ---
ctx.save();
ctx.scale(DPR, DPR);
const cam = getCamera();
drawStageEnvironment(cam, pose.pelvis);
// Off-register cyan & magenta riso ghosts on fast whips/spins
if (speed > 280 && state.poseHistory.length >= 3) {
const ghostA = state.poseHistory[0];
const ghostB = state.poseHistory[1];
renderBirdPose(ghostA, cam, { color: '#1d9ab8', offsetX: -5, offsetY: 2, alpha: 0.24 });
renderBirdPose(ghostB, cam, { color: '#d6336c', offsetX: 5, offsetY: -2, alpha: 0.22 });
}
// Main bird render
renderBirdPose(pose, cam, null);
// Chalk dust particles
for (const d of state.dust) {
const dp = project([d.x, d.y, d.z], cam);
ctx.save();
ctx.globalAlpha = clamp(d.life, 0, 1) * 0.75;
ctx.fillStyle = d.col;
ctx.beginPath();
ctx.arc(dp.x, dp.y, d.size * dp.scale, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
// Corner printmaker registration crosshairs
ctx.strokeStyle = 'rgba(29, 28, 33, 0.35)';
ctx.lineWidth = 1.2;
const regMarks = [[22, H - 64], [W - 22, H - 64]];
for (const [rx, ry] of regMarks) {
ctx.beginPath();
ctx.arc(rx, ry, 6, 0, Math.PI * 2);
ctx.moveTo(rx - 10, ry); ctx.lineTo(rx + 10, ry);
ctx.moveTo(rx, ry - 10); ctx.lineTo(rx, ry + 10);
ctx.stroke();
}
ctx.restore();
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
})();
</script>
</body>
</html>
TOPROCK (Indian cross-steps with wide wing burns and chest rebounds), a GO-DOWN spiral drop, a syncopated 6-STEP (1-&-2-3-&-4 with alternating planted wings), WINDMILL barrel rolls, FLARE weight-shifts, a two-speed HEADSPIN (wide straddle accelerating into a tight drill), asymmetric FREEZE holds with isometric breathing/quiver, and a KIP-UP recovery. Instead of sine interpolation, keyframes pass through snapEase(), which dips slightly in anticipation, whips fast through the transition, overshoots, and settles into the beat pocket while spring-damped crest quills and chalk scuffs react to ground strikes.CYPHER round and isolating any individual move (repeat-clicking FREEZE! or hitting Space alternates between the Baby Freeze and Hollowback Pike Freeze). BPM drives both the choreography clock and a procedural Web Audio boom-bap breakbeat (BEAT: ON/OFF), SNAP morphs the easing curve from FLOW to razor WHIP and adjusts rotational surge, clicking the cardboard mat triggers a stomp/hype impact with dust and camera punch, and WIPE MAT clears accumulated talon scuffs.TOPROCK straight from HEADSPIN), the bird relies on a short weighted blend rather than inserting a custom intermediate kip-up or cartwheel transition for every manual pair; building a full transition graph and adding subtle wing-feather wind bending during flares would make manual move-calling even cleaner.