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 corvid — crow, raven, something that watches from power lines — built from sharp charcoal strokes and glitch artifacts. The look is raw ink on wet paper: high contrast, jagged edges, occasional digital decay, like a zine scanned wrong on purpose.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>BREAKBIRD</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
min-height: 100vh;
background: #0d0d0d;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-family: 'Courier New', Courier, monospace;
color: #e8e8e8;
overflow: hidden;
position: relative;
}
body::before {
content: '';
position: fixed;
inset: 0;
background-image:
radial-gradient(ellipse at 20% 30%, rgba(255,255,255,0.02) 0%, transparent 50%),
radial-gradient(ellipse at 80% 70%, rgba(255,100,50,0.015) 0%, transparent 40%);
pointer-events: none;
}
canvas { display: block; filter: contrast(1.15); }
.ui {
position: fixed;
bottom: 24px;
left: 50%;
transform: translateX(-50%);
display: flex;
gap: 16px;
flex-wrap: wrap;
justify-content: center;
z-index: 10;
pointer-events: auto;
}
.knob {
position: relative;
width: 120px;
height: 120px;
pointer-events: auto;
touch-action: none;
}
.knob svg { width: 100%; height: 100%; }
.knob-label {
position: absolute;
bottom: -22px;
left: 50%;
transform: translateX(-50%);
font-size: 10px;
letter-spacing: 0.15em;
text-transform: uppercase;
color: #666;
white-space: nowrap;
pointer-events: none;
}
.knob-value {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 14px;
font-weight: 700;
color: #e8e8e8;
pointer-events: none;
text-shadow: 0 0 8px rgba(255,100,50,0.6);
}
.readout {
position: fixed;
top: 20px;
left: 50%;
transform: translateX(-50%);
font-size: 11px;
letter-spacing: 0.2em;
color: #444;
text-transform: uppercase;
z-index: 10;
min-height: 16px;
}
.credits {
position: fixed;
bottom: 20px;
right: 20px;
font-size: 9px;
color: #222;
letter-spacing: 0.1em;
}
@media (max-width: 500px) {
.knob { width: 90px; height: 90px; }
.knob-value { font-size: 11px; }
}
@media (prefers-reduced-motion: reduce) {
canvas { animation: none !important; }
}
</style>
</head>
<body>
<canvas id="c"></canvas>
<div class="readout" id="readout">INITIALIZING...</div>
<div class="ui">
<div class="knob" id="knob-tempo" data-value="1.0">
<svg viewBox="0 0 120 120">
<circle cx="60" cy="60" r="52" fill="none" stroke="#1a1a1a" stroke-width="8"/>
<circle cx="60" cy="60" r="52" fill="none" stroke="#333" stroke-width="8" stroke-dasharray="326.7" stroke-dashoffset="326.7" id="tempo-track"/>
<circle cx="60" cy="60" r="52" fill="none" stroke="#ff6432" stroke-width="8" stroke-dasharray="326.7" stroke-dashoffset="326.7" stroke-linecap="round" id="tempo-fill"/>
<line x1="60" y1="8" x2="60" y2="20" stroke="#ff6432" stroke-width="3" stroke-linecap="round" id="tempo-mark"/>
</svg>
<div class="knob-label">tempo</div>
<div class="knob-value" id="val-tempo">1.00x</div>
</div>
<div class="knob" id="knob-weight" data-value="1.0">
<svg viewBox="0 0 120 120">
<circle cx="60" cy="60" r="52" fill="none" stroke="#1a1a1a" stroke-width="8"/>
<circle cx="60" cy="60" r="52" fill="none" stroke="#333" stroke-width="8" stroke-dasharray="326.7" stroke-dashoffset="326.7" id="weight-track"/>
<circle cx="60" cy="60" r="52" fill="none" stroke="#00d4aa" stroke-width="8" stroke-dasharray="326.7" stroke-dashoffset="326.7" stroke-linecap="round" id="weight-fill"/>
<line x1="60" y1="8" x2="60" y2="20" stroke="#00d4aa" stroke-width="3" stroke-linecap="round" id="weight-mark"/>
</svg>
<div class="knob-label">weight</div>
<div class="knob-value" id="val-weight">1.00x</div>
</div>
<div class="knob" id="knob-glitch" data-value="0.3">
<svg viewBox="0 0 120 120">
<circle cx="60" cy="60" r="52" fill="none" stroke="#1a1a1a" stroke-width="8"/>
<circle cx="60" cy="60" r="52" fill="none" stroke="#333" stroke-width="8" stroke-dasharray="326.7" stroke-dashoffset="326.7" id="glitch-track"/>
<circle cx="60" cy="60" r="52" fill="none" stroke="#ffcc00" stroke-width="8" stroke-dasharray="326.7" stroke-dashoffset="326.7" stroke-linecap="round" id="glitch-fill"/>
<line x1="60" y1="8" x2="60" y2="20" stroke="#ffcc00" stroke-width="3" stroke-linecap="round" id="glitch-mark"/>
</svg>
<div class="knob-label">glitch</div>
<div class="knob-value" id="val-glitch">0.30</div>
</div>
<div class="knob" id="knob-style" data-value="0.5">
<svg viewBox="0 0 120 120">
<circle cx="60" cy="60" r="52" fill="none" stroke="#1a1a1a" stroke-width="8"/>
<circle cx="60" cy="60" r="52" fill="none" stroke="#333" stroke-width="8" stroke-dasharray="326.7" stroke-dashoffset="326.7" id="style-track"/>
<circle cx="60" cy="60" r="52" fill="none" stroke="#aa66ff" stroke-width="8" stroke-dasharray="326.7" stroke-dashoffset="326.7" stroke-linecap="round" id="style-fill"/>
<line x1="60" y1="8" x2="60" y2="20" stroke="#aa66ff" stroke-width="3" stroke-linecap="round" id="style-mark"/>
</svg>
<div class="knob-label">flavor</div>
<div class="knob-value" id="val-style">FLOW</div>
</div>
</div>
<div class="credits">BREAKBIRD v1.0 — click/drag knobs</div>
<script>
(() => {
const canvas = document.getElementById('c');
const ctx = canvas.getContext('2d');
let W, H, DPR;
function resize() {
DPR = Math.min(window.devicePixelRatio || 1, 2);
W = canvas.width = Math.min(window.innerWidth, 800) * DPR;
H = canvas.height = Math.min(window.innerHeight * 0.85, 600) * DPR;
canvas.style.width = (W / DPR) + 'px';
canvas.style.height = (H / DPR) + 'px';
ctx.setTransform(DPR, 0, 0, DPR, 0, 0);
}
window.addEventListener('resize', resize);
resize();
// --- Controls ---
const params = {
tempo: 1.0,
weight: 1.0,
glitch: 0.3,
style: 0.5
};
const knobEls = {
tempo: document.getElementById('knob-tempo'),
weight: document.getElementById('knob-weight'),
glitch: document.getElementById('knob-glitch'),
style: document.getElementById('knob-style')
};
const fillEls = {
tempo: document.getElementById('tempo-fill'),
weight: document.getElementById('weight-fill'),
glitch: document.getElementById('glitch-fill'),
style: document.getElementById('style-fill')
};
const markEls = {
tempo: document.getElementById('tempo-mark'),
weight: document.getElementById('weight-mark'),
glitch: document.getElementById('glitch-mark'),
style: document.getElementById('style-mark')
};
const valEls = {
tempo: document.getElementById('val-tempo'),
weight: document.getElementById('val-weight'),
glitch: document.getElementById('val-glitch'),
style: document.getElementById('val-style')
};
const CIRC = 2 * Math.PI * 52;
function updateKnob(k, v) {
params[k] = v;
const offset = CIRC * (1 - v);
fillEls[k].style.strokeDashoffset = offset;
const deg = -90 + v * 360;
markEls[k].setAttribute('transform', `rotate(${deg} 60 60)`);
if (k === 'tempo') valEls[k].textContent = v.toFixed(2) + 'x';
else if (k === 'weight') valEls[k].textContent = v.toFixed(2) + 'x';
else if (k === 'glitch') valEls[k].textContent = v.toFixed(2);
else valEls[k].textContent = v < 0.33 ? 'POWER' : v < 0.66 ? 'FLOW' : 'TECH';
knobEls[k].dataset.value = v.toFixed(2);
}
function knobDrag(e, k) {
const rect = knobEls[k].getBoundingClientRect();
const cx = rect.left + rect.width / 2;
const cy = rect.top + rect.height / 2;
const clientX = e.touches ? e.touches[0].clientX : e.clientX;
const clientY = e.touches ? e.touches[0].clientY : e.clientY;
const angle = Math.atan2(clientY - cy, clientX - cx);
let v = (angle + Math.PI / 2) / (2 * Math.PI);
if (v < 0) v += 1;
v = Math.max(0, Math.min(1, v));
if (k === 'tempo') v = 0.3 + v * 1.7; // 0.3 - 2.0
else if (k === 'weight') v = 0.2 + v * 1.6; // 0.2 - 1.8
else if (k === 'glitch') v = v * 1.0; // 0 - 1
else v = v * 1.0; // 0 - 1
updateKnob(k, v);
}
Object.keys(knobEls).forEach(k => {
let active = false;
const down = (e) => { active = true; e.preventDefault(); knobDrag(e, k); };
const move = (e) => { if (active) knobDrag(e, k); };
const up = () => { active = false; };
knobEls[k].addEventListener('mousedown', down);
knobEls[k].addEventListener('touchstart', down, {passive:false});
window.addEventListener('mousemove', move);
window.addEventListener('touchmove', move, {passive:false});
window.addEventListener('mouseup', up);
window.addEventListener('touchend', up);
});
// Init knobs
updateKnob('tempo', 1.0);
updateKnob('weight', 1.0);
updateKnob('glitch', 0.3);
updateKnob('style', 0.5);
// --- Bird Geometry ---
// Procedural bird made of shapes: body, head, beak, wings, legs, tail
// Each part has local transform, animated by pose
const bird = {
// Base proportions (in bird-space units, ~100 = body height)
scale: 1,
// Pose state
pose: {
// Root
rootX: 0, rootY: 0, rootRot: 0,
// Spine segments (3)
spine: [0, 0, 0],
// Head
headRot: 0, headBob: 0,
// Beak
beakOpen: 0,
// Wings (each: shoulder, elbow, wrist)
wingL: [0, 0, 0],
wingR: [0, 0, 0],
// Legs (each: hip, knee, ankle)
legL: [0, 0, 0],
legR: [0, 0, 0],
// Tail
tailFan: 0, tailRot: 0,
// Feathers ruffle
ruffle: 0
},
// Target pose for interpolation
target: {},
// Current move
move: null,
moveProgress: 0,
moveDuration: 0,
moveStartTime: 0,
// Physics
velocity: { rootY: 0, rootRot: 0 },
grounded: true,
// Visual
glitchOffset: 0,
glitchTimer: 0,
featherJitter: [],
// Initialize feather jitter
init() {
for (let i = 0; i < 30; i++) {
this.featherJitter.push({ phase: Math.random() * Math.PI * 2, amp: 0.5 + Math.random() * 1.5, speed: 0.5 + Math.random() * 2 });
}
}
};
bird.init();
// --- Move Definitions ---
// Each move: name, duration (ms at tempo=1), phases with keyframes
// Phases: 0-1 progress, each keyframe defines target pose values
// We'll interpolate between keyframes with custom easing per parameter
const moves = {
// TOPROCK
'toprock-1': {
name: 'TOP ROCK',
duration: 1800,
phases: [
{ t: 0.00, pose: { rootY: 0, rootRot: 0, spine: [0,0,0], headRot: 0, headBob: 0, wingL: [-0.2,0.3,0], wingR: [0.2,-0.3,0], legL: [0.15,-0.4,0.2], legR: [-0.15,0.4,-0.2], tailFan: 0.2, tailRot: 0, beakOpen: 0, ruffle: 0.3 }},
{ t: 0.12, pose: { rootY: -8, rootRot: -0.08, spine: [0.1,0.05,0], headRot: 0.15, headBob: -3, wingL: [-0.4,0.6,0.2], wingR: [0.4,-0.6,-0.2], legL: [0.3,-0.6,0.4], legR: [-0.3,0.2,-0.1], tailFan: 0.4, tailRot: -0.1, beakOpen: 0.1, ruffle: 0.6 }},
{ t: 0.25, pose: { rootY: 4, rootRot: 0.1, spine: [-0.15,-0.1,0], headRot: -0.2, headBob: 5, wingL: [-0.1,0.2,-0.1], wingR: [0.1,-0.2,0.1], legL: [0,-0.2,0], legR: [0,0.5,-0.3], tailFan: 0.1, tailRot: 0.15, beakOpen: 0, ruffle: 0.4 }},
{ t: 0.37, pose: { rootY: -6, rootRot: -0.05, spine: [0.1,0,0.05], headRot: 0.1, headBob: -2, wingL: [-0.3,0.5,0.1], wingR: [0.3,-0.5,-0.1], legL: [0.2,-0.5,0.3], legR: [-0.2,0.3,-0.2], tailFan: 0.3, tailRot: -0.05, beakOpen: 0.15, ruffle: 0.7 }},
{ t: 0.50, pose: { rootY: 2, rootRot: 0.08, spine: [-0.1,0.05,-0.05], headRot: -0.15, headBob: 4, wingL: [0,0.1,0], wingR: [0,-0.1,0], legL: [-0.1,0.4,-0.2], legR: [0.1,-0.3,0.1], tailFan: 0, tailRot: 0.1, beakOpen: 0, ruffle: 0.3 }},
{ t: 0.62, pose: { rootY: -7, rootRot: -0.1, spine: [0.15,0.1,0], headRot: 0.2, headBob: -4, wingL: [-0.5,0.7,0.3], wingR: [0.5,-0.7,-0.3], legL: [0.4,-0.7,0.5], legR: [-0.4,0.1,-0.1], tailFan: 0.5, tailRot: -0.15, beakOpen: 0.2, ruffle: 0.8 }},
{ t: 0.75, pose: { rootY: 5, rootRot: 0.12, spine: [-0.2,-0.15,0], headRot: -0.25, headBob: 6, wingL: [0.1,0,-0.2], wingR: [-0.1,0,0.2], legL: [-0.2,0.3,-0.3], legR: [0.2,-0.5,0.2], tailFan: 0.1, tailRot: 0.2, beakOpen: 0, ruffle: 0.4 }},
{ t: 0.87, pose: { rootY: -4, rootRot: -0.03, spine: [0.05,0,0], headRot: 0.05, headBob: -1, wingL: [-0.2,0.4,0.1], wingR: [0.2,-0.4,-0.1], legL: [0.1,-0.3,0.1], legR: [-0.1,0.2,-0.1], tailFan: 0.2, tailRot: -0.03, beakOpen: 0.1, ruffle: 0.5 }},
{ t: 1.00, pose: { rootY: 0, rootRot: 0, spine: [0,0,0], headRot: 0, headBob: 0, wingL: [-0.2,0.3,0], wingR: [0.2,-0.3,0], legL: [0.15,-0.4,0.2], legR: [-0.15,0.4,-0.2], tailFan: 0.2, tailRot: 0, beakOpen: 0, ruffle: 0.3 }}
]
},
'toprock-2': {
name: 'SIDE STEP',
duration: 1600,
phases: [
{ t: 0.00, pose: { rootY: 0, rootRot: 0, rootX: 0, spine: [0,0,0], headRot: 0, headBob: 0, wingL: [-0.2,0.3,0], wingR: [0.2,-0.3,0], legL: [0.15,-0.4,0.2], legR: [-0.15,0.4,-0.2], tailFan: 0.2, tailRot: 0, beakOpen: 0, ruffle: 0.3 }},
{ t: 0.15, pose: { rootY: -5, rootRot: 0, rootX: -12, spine: [0.2,0.1,0], headRot: -0.3, headBob: -3, wingL: [-0.6,0.8,0.3], wingR: [0.2,-0.2,0], legL: [0.5,-0.8,0.6], legR: [-0.1,0.2,-0.1], tailFan: 0.5, tailRot: 0.2, beakOpen: 0.1, ruffle: 0.7 }},
{ t: 0.30, pose: { rootY: 6, rootRot: 0, rootX: -12, spine: [-0.2,-0.1,0], headRot: 0.2, headBob: 5, wingL: [-0.1,0.1,-0.1], wingR: [0.5,-0.7,-0.2], legL: [-0.2,0.3,-0.2], legR: [0.3,-0.6,0.4], tailFan: 0.1, tailRot: -0.1, beakOpen: 0, ruffle: 0.4 }},
{ t: 0.45, pose: { rootY: -4, rootRot: 0, rootX: 12, spine: [-0.1,0,0.1], headRot: 0.25, headBob: -2, wingL: [0.3,-0.5,-0.2], wingR: [-0.6,0.8,0.3], legL: [0.1,-0.2,0.1], legR: [0.5,-0.8,0.6], tailFan: 0.5, tailRot: -0.2, beakOpen: 0.15, ruffle: 0.7 }},
{ t: 0.60, pose: { rootY: 7, rootRot: 0, rootX: 12, spine: [0.15,0.1,0], headRot: -0.2, headBob: 6, wingL: [0.5,-0.7,-0.2], wingR: [-0.1,0.1,-0.1], legL: [0.3,-0.6,0.4], legR: [-0.2,0.3,-0.2], tailFan: 0.1, tailRot: 0.15, beakOpen: 0, ruffle: 0.4 }},
{ t: 0.75, pose: { rootY: -3, rootRot: 0, rootX: 0, spine: [0,0,0], headRot: 0, headBob: -1, wingL: [-0.2,0.4,0.1], wingR: [0.2,-0.4,-0.1], legL: [0.1,-0.3,0.1], legR: [-0.1,0.2,-0.1], tailFan: 0.3, tailRot: 0, beakOpen: 0.1, ruffle: 0.5 }},
{ t: 1.00, pose: { rootY: 0, rootRot: 0, rootX: 0, spine: [0,0,0], headRot: 0, headBob: 0, wingL: [-0.2,0.3,0], wingR: [0.2,-0.3,0], legL: [0.15,-0.4,0.2], legR: [-0.15,0.4,-0.2], tailFan: 0.2, tailRot: 0, beakOpen: 0, ruffle: 0.3 }}
]
},
// DOWNROCK / FOOTWORK
'sixstep': {
name: 'SIX STEP',
duration: 2200,
phases: [
{ t: 0.00, pose: { rootY: 25, rootRot: 0, rootX: 0, spine: [-0.3,-0.2,0], headRot: 0, headBob: 0, wingL: [0.8, -0.5, 0.3], wingR: [0.8, -0.5, 0.3], legL: [0.6, -1.2, 0.8], legR: [-0.6, 0.5, -0.3], tailFan: 0.3, tailRot: 0, beakOpen: 0, ruffle: 0.2 }},
{ t: 0.16, pose: { rootY: 22, rootRot: -0.2, rootX: -8, spine: [-0.2,-0.1,0.1], headRot: 0.2, headBob: -2, wingL: [1.0, -0.3, 0.5], wingR: [0.6, -0.7, 0.2], legL: [0.3, -0.8, 0.4], legR: [-0.8, 1.0, -0.5], tailFan: 0.5, tailRot: 0.15, beakOpen: 0.1, ruffle: 0.5 }},
{ t: 0.33, pose: { rootY: 28, rootRot: 0.1, rootX: -4, spine: [-0.4,-0.3,-0.1], headRot: -0.15, headBob: 4, wingL: [0.5, -0.8, 0.1], wingR: [1.0, -0.3, 0.5], legL: [-0.5, 0.8, -0.4], legR: [0.4, -1.0, 0.6], tailFan: 0.2, tailRot: -0.1, beakOpen: 0, ruffle: 0.3 }},
{ t: 0.50, pose: { rootY: 20, rootRot: 0.25, rootX: 8, spine: [-0.1,0,0.2], headRot: 0.3, headBob: -3, wingL: [0.7, -0.4, 0.4], wingR: [0.4, -0.9, 0.1], legL: [0.2, -0.6, 0.3], legR: [-0.9, 1.2, -0.6], tailFan: 0.6, tailRot: 0.2, beakOpen: 0.2, ruffle: 0.6 }},
{ t: 0.66, pose: { rootY: 30, rootRot: -0.1, rootX: 4, spine: [-0.5,-0.4,0], headRot: -0.2, headBob: 5, wingL: [0.4, -0.9, 0.1], wingR: [0.7, -0.4, 0.4], legL: [-0.7, 1.1, -0.5], legR: [0.3, -0.7, 0.4], tailFan: 0.1, tailRot: -0.15, beakOpen: 0, ruffle: 0.3 }},
{ t: 0.83, pose: { rootY: 18, rootRot: -0.2, rootX: -6, spine: [-0.2,0.1,-0.1], headRot: 0.25, headBob: -2, wingL: [1.0, -0.3, 0.5], wingR: [0.5, -0.8, 0.1], legL: [0.4, -0.9, 0.5], legR: [-0.6, 0.6, -0.3], tailFan: 0.5, tailRot: 0.1, beakOpen: 0.15, ruffle: 0.5 }},
{ t: 1.00, pose: { rootY: 25, rootRot: 0, rootX: 0, spine: [-0.3,-0.2,0], headRot: 0, headBob: 0, wingL: [0.8, -0.5, 0.3], wingR: [0.8, -0.5, 0.3], legL: [0.6, -1.2, 0.8], legR: [-0.6, 0.5, -0.3], tailFan: 0.3, tailRot: 0, beakOpen: 0, ruffle: 0.2 }}
]
},
'cc': {
name: 'CC\'S',
duration: 1800,
phases: [
{ t: 0.00, pose: { rootY: 25, rootRot: 0, rootX: 0, spine: [-0.3,-0.2,0], headRot: 0, headBob: 0, wingL: [0.8, -0.5, 0.3], wingR: [0.8, -0.5, 0.3], legL: [0.6, -1.2, 0.8], legR: [-0.6, 0.5, -0.3], tailFan: 0.3, tailRot: 0, beakOpen: 0, ruffle: 0.2 }},
{ t: 0.12, pose: { rootY: 15, rootRot: 0.4, rootX: 10, spine: [0.2,0.3,0.2], headRot: -0.4, headBob: -5, wingL: [1.2, 0.2, 0.8], wingR: [0.3, -1.0, 0], legL: [0.8, -0.5, 1.0], legR: [-1.0, 1.5, -0.8], tailFan: 0.7, tailRot: 0.3, beakOpen: 0.3, ruffle: 0.8 }},
{ t: 0.25, pose: { rootY: 35, rootRot: -0.3, rootX: -5, spine: [-0.5,-0.4,-0.2], headRot: 0.3, headBob: 8, wingL: [0.2, -1.1, -0.1], wingR: [1.2, 0.2, 0.8], legL: [-0.8, 1.3, -0.7], legR: [0.6, -0.8, 0.5], tailFan: 0.1, tailRot: -0.2, beakOpen: 0, ruffle: 0.3 }},
{ t: 0.37, pose: { rootY: 12, rootRot: -0.5, rootX: -12, spine: [0.3,0.4,0.3], headRot: 0.5, headBob: -6, wingL: [0.3, -1.0, 0], wingR: [1.3, 0.4, 1.0], legL: [-1.0, 1.5, -0.8], legR: [0.8, -0.5, 1.0], tailFan: 0.8, tailRot: -0.35, beakOpen: 0.4, ruffle: 0.9 }},
{ t: 0.50, pose: { rootY: 38, rootRot: 0.2, rootX: 0, spine: [-0.6,-0.5,0], headRot: -0.35, headBob: 10, wingL: [1.3, 0.4, 1.0], wingR: [0.2, -1.1, -0.1], legL: [0.7, -0.7, 0.6], legR: [-0.7, 1.2, -0.6], tailFan: 0.1, tailRot: 0.1, beakOpen: 0, ruffle: 0.3 }},
{ t: 0.62, pose: { rootY: 10, rootRot: 0.5, rootX: 12, spine: [-0.1,0.2,0.4], headRot: -0.5, headBob: -7, wingL: [1.4, 0.5, 1.1], wingR: [0.1, -1.2, -0.2], legL: [0.9, -0.4, 1.1], legR: [-1.1, 1.6, -0.9], tailFan: 0.9, tailRot: 0.4, beakOpen: 0.5, ruffle: 1.0 }},
{ t: 0.75, pose: { rootY: 40, rootRot: -0.2, rootX: 5, spine: [-0.4,-0.3,-0.1], headRot: 0.4, headBob: 8, wingL: [0.1, -1.2, -0.2], wingR: [1.4, 0.5, 1.1], legL: [-0.9, 1.4, -0.8], legR: [0.5, -0.6, 0.4], tailFan: 0.1, tailRot: -0.1, beakOpen: 0, ruffle: 0.3 }},
{ t: 0.87, pose: { rootY: 18, rootRot: 0, rootX: -2, spine: [-0.2,-0.1,0], headRot: 0.1, headBob: -2, wingL: [0.7, -0.6, 0.2], wingR: [0.7, -0.6, 0.2], legL: [0.4, -1.0, 0.6], legR: [-0.5, 0.7, -0.4], tailFan: 0.4, tailRot: 0.05, beakOpen: 0.1, ruffle: 0.4 }},
{ t: 1.00, pose: { rootY: 25, rootRot: 0, rootX: 0, spine: [-0.3,-0.2,0], headRot: 0, headBob: 0, wingL: [0.8, -0.5, 0.3], wingR: [0.8, -0.5, 0.3], legL: [0.6, -1.2, 0.8], legR: [-0.6, 0.5, -0.3], tailFan: 0.3, tailRot: 0, beakOpen: 0, ruffle: 0.2 }}
]
},
// POWER MOVES
'windmill': {
name: 'WINDMILL',
duration: 2800,
phases: [
{ t: 0.00, pose: { rootY: 25, rootRot: 0, rootX: 0, spine: [-0.3,-0.2,0], headRot: 0, headBob: 0, wingL: [0.8, -0.5, 0.3], wingR: [0.8, -0.5, 0.3], legL: [0.6, -1.2, 0.8], legR: [-0.6, 0.5, -0.3], tailFan: 0.3, tailRot: 0, beakOpen: 0, ruffle: 0.2 }},
{ t: 0.08, pose: { rootY: 10, rootRot: -0.3, rootX: -15, spine: [0.5,0.8,0.5], headRot: -0.6, headBob: -8, wingL: [1.5, 1.0, 1.2], wingR: [1.5, 1.0, 1.2], legL: [-1.2, 1.8, -1.0], legR: [-1.2, 1.8, -1.0], tailFan: 1.0, tailRot: 0.5, beakOpen: 0.4, ruffle: 1.0 }},
{ t: 0.18, pose: { rootY: 5, rootRot: -1.2, rootX: -10, spine: [1.0,1.2,0.8], headRot: -1.0, headBob: -12, wingL: [1.8, 1.5, 1.5], wingR: [1.8, 1.5, 1.5], legL: [-1.5, 2.0, -1.2], legR: [-1.5, 2.0, -1.2], tailFan: 1.0, tailRot: 0.8, beakOpen: 0.5, ruffle: 1.0 }},
{ t: 0.28, pose: { rootY: 15, rootRot: -2.2, rootX: 5, spine: [0.8,1.0,0.5], headRot: -0.8, headBob: -5, wingL: [1.6, 1.2, 1.3], wingR: [1.6, 1.2, 1.3], legL: [-1.3, 1.8, -1.0], legR: [-1.3, 1.8, -1.0], tailFan: 0.8, tailRot: 0.6, beakOpen: 0.3, ruffle: 0.8 }},
{ t: 0.38, pose: { rootY: 30, rootRot: -3.1, rootX: 15, spine: [-0.2,0.2,0.3], headRot: 0.2, headBob: 5, wingL: [1.2, 0.5, 1.0], wingR: [1.2, 0.5, 1.0], legL: [-1.0, 1.5, -0.8], legR: [-1.0, 1.5, -0.8], tailFan: 0.5, tailRot: 0.3, beakOpen: 0.1, ruffle: 0.5 }},
{ t: 0.48, pose: { rootY: 8, rootRot: -4.0, rootX: 10, spine: [0.6,0.9,0.6], headRot: -0.7, headBob: -10, wingL: [1.7, 1.4, 1.4], wingR: [1.7, 1.4, 1.4], legL: [-1.4, 1.9, -1.1], legR: [-1.4, 1.9, -1.1], tailFan: 1.0, tailRot: 0.9, beakOpen: 0.5, ruffle: 1.0 }},
{ t: 0.58, pose: { rootY: 20, rootRot: -5.0, rootX: -5, spine: [0.4,0.6,0.3], headRot: -0.4, headBob: -3, wingL: [1.4, 1.0, 1.2], wingR: [1.4, 1.0, 1.2], legL: [-1.2, 1.7, -0.9], legR: [-1.2, 1.7, -0.9], tailFan: 0.7, tailRot: 0.5, beakOpen: 0.2, ruffle: 0.7 }},
{ t: 0.68, pose: { rootY: 35, rootRot: -6.0, rootX: -12, spine: [-0.3,0.1,0.2], headRot: 0.3, headBob: 8, wingL: [1.0, 0.3, 0.8], wingR: [1.0, 0.3, 0.8], legL: [-0.8, 1.3, -0.6], legR: [-0.8, 1.3, -0.6], tailFan: 0.4, tailRot: 0.2, beakOpen: 0, ruffle: 0.4 }},
{ t: 0.78, pose: { rootY: 12, rootRot: -6.8, rootX: -8, spine: [0.7,1.1,0.7], headRot: -0.8, headBob: -11, wingL: [1.8, 1.6, 1.5], wingR: [1.8, 1.6, 1.5], legL: [-1.5, 2.0, -1.2], legR: [-1.5, 2.0, -1.2], tailFan: 1.0, tailRot: 1.0, beakOpen: 0.6, ruffle: 1.0 }},
{ t: 0.88, pose: { rootY: 28, rootRot: -7.5, rootX: 8, spine: [0.1,0.3,0.2], headRot: 0.1, headBob: 3, wingL: [1.2, 0.6, 1.0], wingR: [1.2, 0.6, 1.0], legL: [-1.0, 1.5, -0.8], legR: [-1.0, 1.5, -0.8], tailFan: 0.5, tailRot: 0.4, beakOpen: 0.1, ruffle: 0.5 }},
{ t: 1.00, pose: { rootY: 25, rootRot: 0, rootX: 0, spine: [-0.3,-0.2,0], headRot: 0, headBob: 0, wingL: [0.8, -0.5, 0.3], wingR: [0.8, -0.5, 0.3], legL: [0.6, -1.2, 0.8], legR: [-0.6, 0.5, -0.3], tailFan: 0.3, tailRot: 0, beakOpen: 0, ruffle: 0.2 }}
]
},
'flare': {
name: 'FLARE',
duration: 3000,
phases: [
{ t: 0.00, pose: { rootY: 25, rootRot: 0, rootX: 0, spine: [-0.3,-0.2,0], headRot: 0, headBob: 0, wingL: [0.8, -0.5, 0.3], wingR: [0.8, -0.5, 0.3], legL: [0.6, -1.2, 0.8], legR: [-0.6, 0.5, -0.3], tailFan: 0.3, tailRot: 0, beakOpen: 0, ruffle: 0.2 }},
{ t: 0.10, pose: { rootY: 5, rootRot: 0.2, rootX: -10, spine: [0.8,1.0,0.6], headRot: -0.5, headBob: -10, wingL: [1.6, 1.2, 1.4], wingR: [-0.5, -1.5, -0.5], legL: [-1.5, 2.0, -1.2], legR: [1.2, -1.5, 1.0], tailFan: 1.0, tailRot: 0.6, beakOpen: 0.5, ruffle: 1.0 }},
{ t: 0.20, pose: { rootY: -2, rootRot: 1.0, rootX: -5, spine: [1.2,1.5,1.0], headRot: -1.2, headBob: -15, wingL: [1.8, 1.8, 1.8], wingR: [0.2, -1.8, 0.2], legL: [-1.8, 2.2, -1.5], legR: [1.5, -1.8, 1.3], tailFan: 1.0, tailRot: 1.0, beakOpen: 0.6, ruffle: 1.0 }},
{ t: 0.30, pose: { rootY: 10, rootRot: 1.8, rootX: 8, spine: [1.0,1.2,0.8], headRot: -0.8, headBob: -8, wingL: [1.5, 1.4, 1.5], wingR: [1.0, -1.0, 0.8], legL: [-1.4, 1.8, -1.1], legR: [1.0, -1.2, 0.9], tailFan: 0.8, tailRot: 0.7, beakOpen: 0.3, ruffle: 0.8 }},
{ t: 0.40, pose: { rootY: 30, rootRot: 2.5, rootX: 15, spine: [0.3,0.5,0.3], headRot: 0.2, headBob: 5, wingL: [1.0, 0.5, 1.0], wingR: [1.5, 1.2, 1.3], legL: [-0.8, 1.3, -0.6], legR: [0.5, -0.8, 0.5], tailFan: 0.5, tailRot: 0.3, beakOpen: 0.1, ruffle: 0.5 }},
{ t: 0.50, pose: { rootY: 0, rootRot: 3.3, rootX: 10, spine: [1.1,1.4,0.9], headRot: -1.0, headBob: -12, wingL: [1.7, 1.6, 1.7], wingR: [-0.3, -1.6, -0.2], legL: [-1.6, 2.1, -1.3], legR: [1.4, -1.6, 1.2], tailFan: 1.0, tailRot: 1.1, beakOpen: 0.5, ruffle: 1.0 }},
{ t: 0.60, pose: { rootY: 18, rootRot: 4.1, rootX: -5, spine: [0.8,1.0,0.6], headRot: -0.6, headBob: -5, wingL: [1.4, 1.2, 1.3], wingR: [1.2, -0.8, 1.0], legL: [-1.2, 1.7, -0.9], legR: [0.8, -1.0, 0.7], tailFan: 0.7, tailRot: 0.6, beakOpen: 0.2, ruffle: 0.7 }},
{ t: 0.70, pose: { rootY: 35, rootRot: 4.8, rootX: -12, spine: [0.2,0.4,0.2], headRot: 0.3, headBob: 8, wingL: [1.0, 0.4, 0.9], wingR: [1.6, 1.3, 1.4], legL: [-0.7, 1.2, -0.5], legR: [0.4, -0.7, 0.4], tailFan: 0.4, tailRot: 0.2, beakOpen: 0, ruffle: 0.4 }},
{ t: 0.80, pose: { rootY: 8, rootRot: 5.5, rootX: -8, spine: [1.0,1.3,0.8], headRot: -0.9, headBob: -11, wingL: [1.8, 1.7, 1.6], wingR: [0.1, -1.7, 0.1], legL: [-1.7, 2.2, -1.4], legR: [1.6, -1.7, 1.4], tailFan: 1.0, tailRot: 1.2, beakOpen: 0.6, ruffle: 1.0 }},
{ t: 0.90, pose: { rootY: 22, rootRot: 6.1, rootX: 5, spine: [0.4,0.6,0.3], headRot: -0.3, headBob: 2, wingL: [1.3, 0.8, 1.1], wingR: [1.3, 0.8, 1.1], legL: [-1.0, 1.5, -0.8], legR: [0.6, -0.9, 0.5], tailFan: 0.6, tailRot: 0.5, beakOpen: 0.15, ruffle: 0.6 }},
{ t: 1.00, pose: { rootY: 25, rootRot: 0, rootX: 0, spine: [-0.3,-0.2,0], headRot: 0, headBob: 0, wingL: [0.8, -0.5, 0.3], wingR: [0.8, -0.5, 0.3], legL: [0.6, -1.2, 0.8], legR: [-0.6, 0.5, -0.3], tailFan: 0.3, tailRot: 0, beakOpen: 0, ruffle: 0.3 }}
]
},
// FREEZES
'freeze-baby': {
name: 'BABY FREEZE',
duration: 2000,
phases: [
{ t: 0.00, pose: { rootY: 25, rootRot: 0, rootX: 0, spine: [-0.3,-0.2,0], headRot: 0, headBob: 0, wingL: [0.8, -0.5, 0.3], wingR: [0.8, -0.5, 0.3], legL: [0.6, -1.2, 0.8], legR: [-0.6, 0.5, -0.3], tailFan: 0.3, tailRot: 0, beakOpen: 0, ruffle: 0.2 }},
{ t: 0.20, pose: { rootY: 15, rootRot: -0.4, rootX: -8, spine: [0.5,0.8,0.5], headRot: -0.8, headBob: -10, wingL: [1.4, 0.5, 1.0], wingR: [0.2, -1.2, 0], legL: [-1.0, 1.5, -0.8], legR: [0.8, -0.8, 0.6], tailFan: 0.8, tailRot: 0.4, beakOpen: 0.3, ruffle: 0.8 }},
{ t: 0.40, pose: { rootY: 8, rootRot: -0.5, rootX: -10, spine: [0.8,1.2,0.8], headRot: -1.0, headBob: -12, wingL: [1.6, 0.8, 1.2], wingR: [0.1, -1.4, -0.1], legL: [-1.3, 1.8, -1.0], legR: [1.0, -1.0, 0.8], tailFan: 1.0, tailRot: 0.6, beakOpen: 0.4, ruffle: 1.0 }},
{ t: 0.60, pose: { rootY: 8, rootRot: -0.5, rootX: -10, spine: [0.8,1.2,0.8], headRot: -1.0, headBob: -12, wingL: [1.6, 0.8, 1.2], wingR: [0.1, -1.4, -0.1], legL: [-1.3, 1.8, -1.0], legR: [1.0, -1.0, 0.8], tailFan: 1.0, tailRot: 0.6, beakOpen: 0.4, ruffle: 1.0 }},
{ t: 0.80, pose: { rootY: 8, rootRot: -0.5, rootX: -10, spine: [0.8,1.2,0.8], headRot: -1.0, headBob: -12, wingL: [1.6, 0.8, 1.2], wingR: [0.1, -1.4, -0.1], legL: [-1.3, 1.8, -1.0], legR: [1.0, -1.0, 0.8], tailFan: 1.0, tailRot: 0.6, beakOpen: 0.4, ruffle: 1.0 }},
{ t: 1.00, pose: { rootY: 25, rootRot: 0, rootX: 0, spine: [-0.3,-0.2,0], headRot: 0, headBob: 0, wingL: [0.8, -0.5, 0.3], wingR: [0.8, -0.5, 0.3], legL: [0.6, -1.2, 0.8], legR: [-0.6, 0.5, -0.3], tailFan: 0.3, tailRot: 0, beakOpen: 0, ruffle: 0.3 }}
]
},
'freeze-chair': {
name: 'CHAIR FREEZE',
duration: 2200,
phases: [
{ t: 0.00, pose: { rootY: 25, rootRot: 0, rootX: 0, spine: [-0.3,-0.2,0], headRot: 0, headBob: 0, wingL: [0.8, -0.5, 0.3], wingR: [0.8, -0.5, 0.3], legL: [0.6, -1.2, 0.8], legR: [-0.6, 0.5, -0.3], tailFan: 0.3, tailRot: 0, beakOpen: 0, ruffle: 0.2 }},
{ t: 0.15, pose: { rootY: 18, rootRot: 0.3, rootX: 10, spine: [-0.1,0.2,0.4], headRot: 0.4, headBob: -5, wingL: [0.3, -1.0, 0.1], wingR: [1.5, 0.8, 1.2], legL: [0.8, -0.5, 1.0], legR: [-1.2, 1.8, -1.0], tailFan: 0.7, tailRot: -0.3, beakOpen: 0.2, ruffle: 0.7 }},
{ t: 0.30, pose: { rootY: 10, rootRot: 0.5, rootX: 12, spine: [0.2,0.6,0.6], headRot: 0.6, headBob: -8, wingL: [0.1, -1.2, -0.1], wingR: [1.8, 1.2, 1.5], legL: [1.0, -0.3, 1.2], legR: [-1.5, 2.0, -1.2], tailFan: 1.0, tailRot: -0.5, beakOpen: 0.4, ruffle: 1.0 }},
{ t: 0.50, pose: { rootY: 10, rootRot: 0.5, rootX: 12, spine: [0.2,0.6,0.6], headRot: 0.6, headBob: -8, wingL: [0.1, -1.2, -0.1], wingR: [1.8, 1.2, 1.5], legL: [1.0, -0.3, 1.2], legR: [-1.5, 2.0, -1.2], tailFan: 1.0, tailRot: -0.5, beakOpen: 0.4, ruffle: 1.0 }},
{ t: 0.70, pose: { rootY: 10, rootRot: 0.5, rootX: 12, spine: [0.2,0.6,0.6], headRot: 0.6, headBob: -8, wingL: [0.1, -1.2, -0.1], wingR: [1.8, 1.2, 1.5], legL: [1.0, -0.3, 1.2], legR: [-1.5, 2.0, -1.2], tailFan: 1.0, tailRot: -0.5, beakOpen: 0.4, ruffle: 1.0 }},
{ t: 1.00, pose: { rootY: 25, rootRot: 0, rootX: 0, spine: [-0.3,-0.2,0], headRot: 0, headBob: 0, wingL: [0.8, -0.5, 0.3], wingR: [0.8, -0.5, 0.3], legL: [0.6, -1.2, 0.8], legR: [-0.6, 0.5, -0.3], tailFan: 0.3, tailRot: 0, beakOpen: 0, ruffle: 0.3 }}
]
}
};
// Move sequence - choreographed set
const sequence = [
{ id: 'toprock-1', weight: 1 },
{ id: 'toprock-2', weight: 1 },
{ id: 'sixstep', weight: 2 },
{ id: 'cc', weight: 2 },
{ id: 'windmill', weight: 3 },
{ id: 'flare', weight: 3 },
{ id: 'freeze-baby', weight: 2 },
{ id: 'freeze-chair', weight: 2 }
];
let seqIndex = 0;
let lastMoveEnd = 0;
// --- Easing Functions ---
// Weight-aware easings: heavier = more overshoot/settle, lighter = snappier
function easeWeighted(t, weight, type = 'cubic') {
// weight: 0.2 (light) to 1.8 (heavy)
const w = Math.max(0.2, Math.min(1.8, weight));
if (type === 'cubic') {
// Heavy: easeOutBack-ish with settle. Light: easeOutCubic
if (w > 1.0) {
const overshoot = 1.7 * (w - 1.0) / 0.8;
return 1 - Math.pow(1 - t, 3) * (1 + overshoot * t);
} else {
return 1 - Math.pow(1 - t, 3 / w);
}
} else if (type === 'elastic') {
// For impacts/landings
const stiffness = 4 + w * 6;
const damp = 0.8 + (1.8 - w) * 0.3;
return 1 - Math.exp(-t * stiffness) * Math.cos(t * stiffness * 4) * damp;
} else if (type === 'anticipation') {
// Pre-move windup
const ant = 0.15 * w;
if (t < ant) return Math.pow(t / ant, 2) * 0.3;
return 0.3 + 0.7 * easeWeighted((t - ant) / (1 - ant), w, 'cubic');
}
return t;
}
function lerp(a, b, t) { return a + (b - a) * t; }
function lerpArray(a, b, t) { return a.map((v, i) => lerp(v, b[i], t)); }
// --- Pose Interpolation ---
function getPoseAtProgress(move, prog, weight) {
const phases = move.phases;
// Find segment
let i = 0;
while (i < phases.length - 1 && phases[i + 1].t <= prog) i++;
const p0 = phases[i];
const p1 = phases[i + 1];
const localT = (prog - p0.t) / (p1.t - p0.t);
// Choose easing per parameter
const eased = {};
for (const key in p0.pose) {
let et = localT;
// Different easing for different body parts
if (key === 'rootY' || key === 'rootX') et = easeWeighted(localT, weight, 'cubic');
else if (key === 'rootRot') et = easeWeighted(localT, weight * 0.8, 'cubic');
else if (key.includes('leg')) et = easeWeighted(localT, weight * 1.2, 'cubic');
else if (key.includes('wing')) et = easeWeighted(localT, weight * 0.9, 'cubic');
else if (key === 'headBob' || key === 'headRot') et = easeWeighted(localT, weight * 0.7, 'cubic');
else if (key === 'ruffle') et = easeWeighted(localT, weight * 0.5, 'cubic');
else et = easeWeighted(localT, weight, 'cubic');
if (Array.isArray(p0.pose[key])) {
eased[key] = lerpArray(p0.pose[key], p1.pose[key], et);
} else {
eased[key] = lerp(p0.pose[key], p1.pose[key], et);
}
}
return eased;
}
// --- Animation Loop ---
let lastTime = 0;
let moveStartTime = 0;
let currentMove = moves[sequence[0].id];
let moveProgress = 0;
let isTransitioning = false;
let transitionProgress = 0;
let prevPose = {};
function pickNextMove() {
// Weighted random but cycle through categories
const style = params.style;
let pool;
if (style < 0.33) pool = ['windmill', 'flare', 'freeze-baby', 'freeze-chair']; // POWER
else if (style < 0.66) pool = ['toprock-1', 'toprock-2', 'sixstep', 'cc']; // FLOW
else pool = ['sixstep', 'cc', 'windmill', 'flare', 'freeze-baby', 'freeze-chair']; // TECH (mix)
// Avoid repeating same move
let next = pool[Math.floor(Math.random() * pool.length)];
if (next === currentMove.name.toLowerCase().replace(' ', '-')) {
next = pool[(pool.indexOf(next) + 1) % pool.length];
}
return moves[next];
}
function startMove(move) {
currentMove = move;
moveStartTime = performance.now();
moveProgress = 0;
// Capture current pose as transition start
prevPose = { ...bird.pose };
isTransitioning = true;
transitionProgress = 0;
document.getElementById('readout').textContent = move.name;
}
startMove(currentMove);
function animate(time) {
const dt = (time - lastTime) / 1000;
lastTime = time;
// Update move progress
const moveDur = currentMove.duration / params.tempo;
const elapsed = time - moveStartTime;
moveProgress = Math.min(1, elapsed / moveDur);
// Transition from previous pose (first 15% of move)
const transDur = 0.15;
if (isTransitioning) {
transitionProgress = Math.min(1, moveProgress / transDur);
const te = easeWeighted(transitionProgress, params.weight, 'cubic');
for (const key in bird.pose) {
if (Array.isArray(bird.pose[key])) {
bird.pose[key] = lerpArray(prevPose[key], bird.target[key] || bird.pose[key], te);
} else {
bird.pose[key] = lerp(prevPose[key], bird.target[key] || bird.pose[key], te);
}
}
if (transitionProgress >= 1) isTransitioning = false;
}
// Get target pose for current progress
const targetPose = getPoseAtProgress(currentMove, moveProgress, params.weight);
bird.target = targetPose;
if (!isTransitioning) {
// Direct drive with physics feel for root
// Root Y gets gravity/spring
const targetY = targetPose.rootY;
const springK = 30 * params.weight;
const damp = 8 * Math.sqrt(params.weight);
bird.velocity.rootY += (targetY - bird.pose.rootY) * springK * dt - bird.velocity.rootY * damp * dt;
bird.pose.rootY += bird.velocity.rootY * dt;
// Root X
bird.pose.rootX = lerp(bird.pose.rootX, targetPose.rootX, 1 - Math.exp(-15 * dt));
// Root Rot
bird.pose.rootRot = lerp(bird.pose.rootRot, targetPose.rootRot, 1 - Math.exp(-12 * dt));
// Spine
bird.pose.spine = lerpArray(bird.pose.spine, targetPose.spine, 1 - Math.exp(-20 * dt));
// Head
bird.pose.headRot = lerp(bird.pose.headRot, targetPose.headRot, 1 - Math.exp(-25 * dt));
bird.pose.headBob = lerp(bird.pose.headBob, targetPose.headBob, 1 - Math.exp(-20 * dt));
// Wings
bird.pose.wingL = lerpArray(bird.pose.wingL, targetPose.wingL, 1 - Math.exp(-18 * dt));
bird.pose.wingR = lerpArray(bird.pose.wingR, targetPose.wingR, 1 - Math.exp(-18 * dt));
// Legs
bird.pose.legL = lerpArray(bird.pose.legL, targetPose.legL, 1 - Math.exp(-22 * dt));
bird.pose.legR = lerpArray(bird.pose.legR, targetPose.legR, 1 - Math.exp(-22 * dt));
// Tail
bird.pose.tailFan = lerp(bird.pose.tailFan, targetPose.tailFan, 1 - Math.exp(-15 * dt));
bird.pose.tailRot = lerp(bird.pose.tailRot, targetPose.tailRot, 1 - Math.exp(-15 * dt));
// Beak
bird.pose.beakOpen = lerp(bird.pose.beakOpen, targetPose.beakOpen, 1 - Math.exp(-30 * dt));
// Ruffle
bird.pose.ruffle = lerp(bird.pose.ruffle, targetPose.ruffle, 1 - Math.exp(-10 * dt));
}
// Move complete -> pick next
if (moveProgress >= 1) {
startMove(pickNextMove());
}
// Glitch timer
bird.glitchTimer += dt;
if (bird.glitchTimer > 0.1 + Math.random() * 0.5) {
bird.glitchTimer = 0;
bird.glitchOffset = (Math.random() - 0.5) * 8 * params.glitch;
} else {
bird.glitchOffset *= 0.85;
}
// Feather jitter
bird.featherJitter.forEach(f => f.phase += f.speed * dt * params.tempo);
// Draw
drawBird();
requestAnimationFrame(animate);
}
// --- Drawing ---
// Bird space: origin at feet center when standing. Units ~pixels at scale=1
// We'll draw at canvas center
const CX = (W / DPR) / 2;
const CY = (H / DPR) / 2;
const BASE_SCALE = Math.min(W, H) / 600 * 0.8;
// Ink texture cache
let inkPattern = null;
function getInkPattern() {
if (inkPattern) return inkPattern;
const size = 128;
const c = document.createElement('canvas');
c.width = size; c.height = size;
const ictx = c.getContext('2d');
ictx.fillStyle = '#1a1a1a';
ictx.fillRect(0, 0, size, size);
// Add noise
for (let i = 0; i < 2000; i++) {
ictx.fillStyle = `rgba(255,255,255,${Math.random() * 0.03})`;
ictx.fillRect(Math.random() * size, Math.random() * size, 1, 1);
}
// Scratches
ictx.strokeStyle = 'rgba(255,255,255,0.02)';
ictx.lineWidth = 0.5;
for (let i = 0; i < 30; i++) {
ictx.beginPath();
ictx.moveTo(Math.random() * size, 0);
ictx.lineTo(Math.random() * size, size);
ictx.stroke();
}
inkPattern = ctx.createPattern(c, 'repeat');
return inkPattern;
}
function drawBird() {
const s = BASE_SCALE * bird.scale;
ctx.save();
ctx.translate(CX + bird.glitchOffset, CY);
ctx.scale(s, s);
const p = bird.pose;
const w = params.weight;
const g = params.glitch;
const style = params.style;
// Ground line
ctx.save();
ctx.strokeStyle = '#2a2a2a';
ctx.lineWidth = 1.5;
ctx.setLineDash([8, 4]);
ctx.beginPath();
ctx.moveTo(-200, 40 - p.rootY);
ctx.lineTo(200, 40 - p.rootY);
ctx.stroke();
ctx.restore();
// Shadow
ctx.save();
ctx.globalAlpha = 0.15;
ctx.fillStyle = '#000';
ctx.beginPath();
ctx.ellipse(0, 40 - p.rootY, 35, 8, 0, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
// --- Body Parts Drawing Order: Tail -> Legs -> Body -> Wings -> Neck/Head -> Beak ---
// Tail
drawTail(p, g);
// Legs
drawLeg('L', p.legL, p.rootX, p.rootY, p.rootRot, g);
drawLeg('R', p.legR, p.rootX, p.rootY, p.rootRot, g);
// Body
drawBody(p, g);
// Wings
drawWing('L', p.wingL, p.spine, p.rootRot, g);
drawWing('R', p.wingR, p.spine, p.rootRot, g);
// Neck/Head
drawHead(p, g);
// Ruffles / feather particles
if (p.ruffle > 0.3) drawRuffle(p, g);
ctx.restore();
}
function drawTail(p, g) {
ctx.save();
// Tail base at body rear
const tx = -25;
const ty = -p.rootY - 10;
ctx.translate(tx, ty);
ctx.rotate(p.tailRot + p.rootRot * 0.5);
ctx.scale(1, 0.6 + p.tailFan * 0.8);
// Tail feathers - 5 layers
const featherCount = 5;
for (let i = 0; i < featherCount; i++) {
const spread = (i - (featherCount - 1) / 2) * 0.25 * p.tailFan;
const len = 35 + Math.abs(spread) * 10;
ctx.save();
ctx.rotate(spread);
ctx.translate(0, -2);
// Feather shape
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.bezierCurveTo(-6, -len * 0.3, -8, -len * 0.7, 0, -len);
ctx.bezierCurveTo(8, -len * 0.7, 6, -len * 0.3, 0, 0);
ctx.closePath();
// Ink fill with texture
const grad = ctx.createLinearGradient(0, 0, 0, -len);
grad.addColorStop(0, '#1a1a1a');
grad.addColorStop(0.5, '#0d0d0d');
grad.addColorStop(1, '#000');
ctx.fillStyle = grad;
ctx.fill();
// Edge stroke
ctx.strokeStyle = '#000';
ctx.lineWidth = 1.5;
ctx.stroke();
// Central quill
ctx.strokeStyle = '#333';
ctx.lineWidth = 0.8;
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.lineTo(0, -len * 0.9);
ctx.stroke();
ctx.restore();
}
ctx.restore();
}
function drawLeg(side, leg, rootX, rootY, rootRot, g) {
const dir = side === 'L' ? 1 : -1;
const hipX = dir * 10;
const hipY = -rootY + 5;
ctx.save();
ctx.translate(hipX, hipY);
ctx.rotate(rootRot * 0.3);
// Thigh
const thighLen = 28;
const shinLen = 30;
const footLen = 14;
const hipAng = leg[0] * dir;
const kneeAng = leg[1];
const ankleAng = leg[2];
// Thigh
ctx.save();
ctx.rotate(hipAng);
drawLimbSegment(0, 0, thighLen, 8, 5, g);
ctx.translate(0, -thighLen);
// Shin
ctx.rotate(kneeAng);
drawLimbSegment(0, 0, shinLen, 6, 4, g);
ctx.translate(0, -shinLen);
// Foot / toes
ctx.rotate(ankleAng);
drawFoot(dir, footLen, g);
ctx.restore();
ctx.restore();
}
function drawLimbSegment(x, y, len, wTop, wBot, g) {
ctx.beginPath();
ctx.moveTo(-wTop/2, 0);
ctx.lineTo(-wBot/2, -len);
ctx.lineTo(wBot/2, -len);
ctx.lineTo(wTop/2, 0);
ctx.closePath();
// Ink fill
const grad = ctx.createLinearGradient(0, 0, 0, -len);
grad.addColorStop(0, '#2a2a2a');
grad.addColorStop(1, '#0d0d0d');
ctx.fillStyle = grad;
ctx.fill();
// Outline
ctx.strokeStyle = '#000';
ctx.lineWidth = 1.2;
ctx.stroke();
// Joint highlight
ctx.fillStyle = '#333';
ctx.beginPath();
ctx.arc(0, -len, wBot/2 + 1, 0, Math.PI * 2);
ctx.fill();
ctx.stroke();
}
function drawFoot(dir, len, g) {
// Three toes
for (let t = -1; t <= 1; t++) {
const ang = t * 0.35 * dir;
ctx.save();
ctx.rotate(ang);
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.lineTo(0, -len);
ctx.strokeStyle = '#000';
ctx.lineWidth = 2;
ctx.lineCap = 'round';
ctx.stroke();
// Claw
ctx.beginPath();
ctx.moveTo(0, -len);
ctx.lineTo(dir * 3, -len - 5);
ctx.stroke();
ctx.restore();
}
}
function drawBody(p, g) {
ctx.save();
ctx.translate(0, -p.rootY);
ctx.rotate(p.rootRot);
// Body main shape - teardrop
const bodyW = 45;
const bodyH = 65;
const spineCurve = p.spine[0] * 15;
ctx.beginPath();
ctx.moveTo(0, -bodyH/2 + spineCurve);
ctx.bezierCurveTo(
bodyW/2, -bodyH/3 + spineCurve,
bodyW/2 * 1.1, bodyH/3,
0, bodyH/2
);
ctx.bezierCurveTo(
-bodyW/2 * 1.1, bodyH/3,
-bodyW/2, -bodyH/3 + spineCurve,
0, -bodyH/2 + spineCurve
);
ctx.closePath();
// Body fill - charcoal gradient
const grad = ctx.createRadialGradient(0, -10, 5, 0, 0, bodyW);
grad.addColorStop(0, '#2a2a2a');
grad.addColorStop(0.5, '#151515');
grad.addColorStop(1, '#050505');
ctx.fillStyle = grad;
ctx.fill();
// Body outline - rough ink
ctx.strokeStyle = '#000';
ctx.lineWidth = 2;
ctx.lineJoin = 'round';
ctx.stroke();
// Ink texture overlay
ctx.globalCompositeOperation = 'multiply';
ctx.fillStyle = getInkPattern();
ctx.fill();
ctx.globalCompositeOperation = 'source-over';
// Chest highlight
ctx.strokeStyle = '#3a3a3a';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(-10, -5);
ctx.bezierCurveTo(0, 10, 0, 20, -5, 25);
ctx.stroke();
// Spine indication
ctx.strokeStyle = '#222';
ctx.lineWidth = 0.8;
for (let i = 0; i < 3; i++) {
const y = -bodyH/2 + 10 + i * 20 + p.spine[i] * 8;
ctx.beginPath();
ctx.moveTo(-5, y);
ctx.lineTo(5, y);
ctx.stroke();
}
ctx.restore();
}
function drawWing(side, wing, spine, rootRot, g) {
const dir = side === 'L' ? 1 : -1;
const shoulderX = dir * 20;
const shoulderY = -bird.pose.rootY - 25 + spine[0] * 10;
ctx.save();
ctx.translate(shoulderX, shoulderY);
ctx.rotate(rootRot * 0.5);
// Wing segments: upper, lower, hand
const upperLen = 35;
const lowerLen = 40;
const handLen = 30;
const shoulderAng = wing[0] * dir;
const elbowAng = wing[1];
const wristAng = wing[2];
// Upper arm
ctx.save();
ctx.rotate(shoulderAng);
drawWingBone(0, 0, upperLen, 12, 8, g);
ctx.translate(0, -upperLen);
// Lower arm
ctx.rotate(elbowAng);
drawWingBone(0, 0, lowerLen, 10, 6, g);
ctx.translate(0, -lowerLen);
// Hand / primaries
ctx.rotate(wristAng);
drawPrimaries(dir, handLen, g);
ctx.restore();
ctx.restore();
}
function drawWingBone(x, y, len, wTop, wBot, g) {
ctx.beginPath();
ctx.moveTo(-wTop/2, 0);
ctx.lineTo(-wBot/2, -len);
ctx.lineTo(wBot/2, -len);
ctx.lineTo(wTop/2, 0);
ctx.closePath();
const grad = ctx.createLinearGradient(0, 0, 0, -len);
grad.addColorStop(0, '#1f1f1f');
grad.addColorStop(1, '#0a0a0a');
ctx.fillStyle = grad;
ctx.fill();
ctx.strokeStyle = '#000';
ctx.lineWidth = 1;
ctx.stroke();
}
function drawPrimaries(dir, len, g) {
const featherCount = 6;
for (let i = 0; i < featherCount; i++) {
const spread = (i / (featherCount - 1) - 0.5) * 1.2;
const fLen = len * (0.7 + 0.3 * Math.abs(spread) * 2);
const fWidth = 6 * (1 - i * 0.1);
ctx.save();
ctx.rotate(spread * dir);
ctx.translate(0, -2);
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.bezierCurveTo(-fWidth/2, -fLen * 0.4, -fWidth/2, -fLen * 0.8, 0, -fLen);
ctx.bezierCurveTo(fWidth/2, -fLen * 0.8, fWidth/2, -fLen * 0.4, 0, 0);
ctx.closePath();
const grad = ctx.createLinearGradient(0, 0, 0, -fLen);
grad.addColorStop(0, '#1a1a1a');
grad.addColorStop(0.6, '#0d0d0d');
grad.addColorStop(1, '#000');
ctx.fillStyle = grad;
ctx.fill();
ctx.strokeStyle = '#000';
ctx.lineWidth = 1;
ctx.stroke();
// Quill
ctx.strokeStyle = '#2a2a2a';
ctx.lineWidth = 0.6;
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.lineTo(0, -fLen * 0.9);
ctx.stroke();
ctx.restore();
}
}
function drawHead(p, g) {
ctx.save();
// Neck base
const neckBaseX = 0;
const neckBaseY = -p.rootY - 45 + p.spine[0] * 10;
ctx.translate(neckBaseX, neckBaseY);
ctx.rotate(p.rootRot * 0.3 + p.spine[1] * 0.5);
// Neck segments (3)
const neckSegs = 3;
const segLen = 14;
let curAng = p.headRot * 0.3;
for (let i = 0; i < neckSegs; i++) {
const segRot = p.spine[i] * 0.3 + (i === 2 ? p.headRot : 0);
ctx.save();
ctx.rotate(segRot);
drawNeckSegment(0, 0, segLen, 16 - i * 3, 12 - i * 2, g);
ctx.translate(0, -segLen);
ctx.restore();
}
// Head
ctx.translate(0, -segLen);
ctx.rotate(p.headRot);
// Head bob
ctx.translate(0, p.headBob * 0.5);
drawHeadShape(p.beakOpen, g);
ctx.restore();
}
function drawNeckSegment(x, y, len, wTop, wBot, g) {
ctx.beginPath();
ctx.moveTo(-wTop/2, 0);
ctx.lineTo(-wBot/2, -len);
ctx.lineTo(wBot/2, -len);
ctx.lineTo(wTop/2, 0);
ctx.closePath();
const grad = ctx.createLinearGradient(0, 0, 0, -len);
grad.addColorStop(0, '#1f1f1f');
grad.addColorStop(1, '#0a0a0a');
ctx.fillStyle = grad;
ctx.fill();
ctx.strokeStyle = '#000';
ctx.lineWidth = 1;
ctx.stroke();
}
function drawHeadShape(beakOpen, g) {
// Head: rounded triangle
const headW = 28;
const headH = 30;
ctx.beginPath();
ctx.moveTo(0, -headH/2);
ctx.bezierCurveTo(headW/2, -headH/4, headW/2, headH/4, 0, headH/2);
ctx.bezierCurveTo(-headW/2, headH/4, -headW/2, -headH/4, 0, -headH/2);
ctx.closePath();
const grad = ctx.createRadialGradient(0, -5, 2, 0, 0, headW);
grad.addColorStop(0, '#252525');
grad.addColorStop(1, '#080808');
ctx.fillStyle = grad;
ctx.fill();
ctx.strokeStyle = '#000';
ctx.lineWidth = 1.5;
ctx.stroke();
// Eye
const eyeX = 6;
const eyeY = -8;
ctx.save();
ctx.translate(eyeX, eyeY);
// Eye white
ctx.fillStyle = '#1a1a1a';
ctx.beginPath();
ctx.ellipse(0, 0, 5, 6, 0, 0, Math.PI * 2);
ctx.fill();
// Pupil
ctx.fillStyle = '#000';
ctx.beginPath();
ctx.ellipse(1, 1, 2.5, 3.5, 0, 0, Math.PI * 2);
ctx.fill();
// Catch light
ctx.fillStyle = '#fff';
ctx.globalAlpha = 0.6;
ctx.beginPath();
ctx.arc(-1.5, -2, 1.2, 0, Math.PI * 2);
ctx.fill();
ctx.globalAlpha = 1;
ctx.restore();
// Beak
ctx.save();
ctx.translate(0, headH/2 - 2);
ctx.rotate(-0.15);
// Upper beak
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.bezierCurveTo(4, -beakOpen * 4, 6, -8 - beakOpen * 6, 0, -18 - beakOpen * 8);
ctx.bezierCurveTo(-6, -8 - beakOpen * 6, -4, -beakOpen * 4, 0, 0);
ctx.closePath();
ctx.fillStyle = '#2a1a0a';
ctx.fill();
ctx.strokeStyle = '#1a0a00';
ctx.lineWidth = 1;
ctx.stroke();
// Lower beak
if (beakOpen > 0.05) {
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.bezierCurveTo(3, beakOpen * 3, 4, 6 + beakOpen * 5, 0, 12 + beakOpen * 8);
ctx.bezierCurveTo(-4, 6 + beakOpen * 5, -3, beakOpen * 3, 0, 0);
ctx.closePath();
ctx.fillStyle = '#1a1005';
ctx.fill();
ctx.strokeStyle = '#0a0500';
ctx.stroke();
}
ctx.restore();
// Crown feathers
for (let i = -1; i <= 1; i++) {
const cx = i * 6;
const cy = -headH/2 - 3;
ctx.save();
ctx.translate(cx, cy);
ctx.rotate(i * 0.2 + Math.sin(Date.now() * 0.003 + i) * 0.1);
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.lineTo(0, -10);
ctx.strokeStyle = '#1a1a1a';
ctx.lineWidth = 1.5;
ctx.lineCap = 'round';
ctx.stroke();
ctx.restore();
}
}
function drawRuffle(p, g) {
// Feather dust / ruffle particles
const intensity = p.ruffle;
const count = Math.floor(8 * intensity);
ctx.save();
ctx.globalAlpha = 0.4 * intensity;
ctx.strokeStyle = '#444';
ctx.lineWidth = 0.8;
ctx.lineCap = 'round';
bird.featherJitter.forEach((f, i) => {
if (i >= count) return;
const x = (Math.sin(f.phase) - 0.5) * 60;
const y = -p.rootY - 20 + Math.cos(f.phase * 1.3) * 40;
const len = 5 + f.amp * 3;
const ang = Math.sin(f.phase * 0.7) * 0.5;
ctx.save();
ctx.translate(x, y);
ctx.rotate(ang);
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.lineTo(0, -len);
ctx.stroke();
ctx.restore();
});
ctx.restore();
}
// --- Glitch Post-Process ---
function applyGlitch() {
if (params.glitch < 0.05) return;
const g = params.glitch;
// Random scanline shifts
if (Math.random() < 0.02 * g) {
const sliceH = 4 + Math.random() * 12;
const y = Math.random() * H;
const shift = (Math.random() - 0.5) * 30 * g;
ctx.drawImage(canvas, 0, y, W, sliceH, shift, y, W, sliceH);
}
// Color channel offset
if (Math.random() < 0.01 * g) {
ctx.globalCompositeOperation = 'difference';
ctx.fillStyle = `rgba(255,${Math.random()*100},${Math.random()*100},0.05)`;
ctx.fillRect((Math.random()-0.5)*10*g, (Math.random()-0.5)*10*g, W, H);
ctx.globalCompositeOperation = 'source-over';
}
}
// Main loop wrapper
function frame(time) {
animate(time);
applyGlitch();
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
// Keyboard shortcuts
window.addEventListener('keydown', (e) => {
if (e.code === 'Space') {
e.preventDefault();
startMove(pickNextMove());
}
if (e.code === 'KeyR') {
params.tempo = 1.0; params.weight = 1.0; params.glitch = 0.3; params.style = 0.5;
Object.keys(params).forEach(k => updateKnob(k, params[k]));
}
});
})();
</script>
</body>
</html>
Move set & weight timing: Eight moves across toprock (2), downrock (2), power (2), freezes (2). Each move is a keyframed pose sequence with per-parameter easing: root translation uses cubic easing scaled by the weight knob (heavier = more overshoot/settle via easeWeighted with back-style overshoot; lighter = snappier cubic), legs get extra weight multiplier for grounded feel, wings/head stay lighter. Transitions between moves blend 15% of the new move's duration from the previous actual pose, so recovery frames are physical, not interpolated from neutral.
Controls: Four knobs — tempo (0.3–2× global speed), weight (0.2–1.8× physics mass, changes easing character and settle time), glitch (0–1 scanline/color corruption intensity), flavor (POWER/FLOW/TECH biases the random move picker toward power moves, toprock/footwork, or a mix). Drag knobs or use Space for next move, R to reset.
Would fix: The bird's silhouette reads well but the neck/head connection to spine could use an extra joint for smoother S-curves during freezes; also the glitch post-process draws over the whole canvas each frame — a dedicated offscreen buffer for the bird would let glitch affect only the character, not the UI.