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.
The bird is a broad-billed shoebill in oversized vermilion court shoes, built like a tiny navy-blue breakdancer. I went for a risograph sports-poster look: cream paper, cobalt and coral, chunky ink outlines, halftone sunlight, and spring-loaded movement.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Wing Break — a breaking shoebill</title>
<style>
:root {
--paper: #f4ead1;
--ink: #17233a;
--accent: #ef5138;
--blue: #2852b7;
}
* { box-sizing: border-box; }
html { color-scheme: light; }
body {
margin: 0;
min-height: 100vh;
color: var(--ink);
background:
radial-gradient(circle at 1px 1px, rgba(23, 35, 58, .09) 1px, transparent 0) 0 0 / 14px 14px,
var(--paper);
font-family: Arial, Helvetica, sans-serif;
}
button, input, select { font: inherit; }
button, select { color: inherit; }
.page {
width: min(1280px, calc(100% - 40px));
margin: 0 auto;
padding: 28px 0 36px;
}
.masthead {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(230px, 360px);
gap: 32px;
align-items: end;
margin-bottom: 24px;
}
.eyebrow {
display: flex;
align-items: center;
gap: 9px;
margin: 0 0 12px;
font-size: 11px;
font-weight: 900;
letter-spacing: .16em;
text-transform: uppercase;
}
.live-dot {
width: 9px;
height: 9px;
border-radius: 50%;
background: var(--accent);
box-shadow: 0 0 0 4px color-mix(in srgb, var(--accent) 18%, transparent);
animation: livePulse 1s steps(2, end) infinite;
}
@keyframes livePulse {
50% { opacity: .35; transform: scale(.75); }
}
h1 {
margin: 0;
font-family: Impact, Haettenschweiler, "Arial Narrow Bold", sans-serif;
font-size: clamp(58px, 9vw, 112px);
font-weight: 900;
line-height: .78;
letter-spacing: -.045em;
text-transform: uppercase;
}
h1 span {
color: var(--accent);
-webkit-text-stroke: 2px var(--ink);
}
.intro {
border-left: 4px solid var(--ink);
padding: 6px 0 6px 18px;
}
.intro strong {
display: block;
margin-bottom: 10px;
font-family: Impact, Haettenschweiler, "Arial Narrow Bold", sans-serif;
font-size: 27px;
line-height: 1;
letter-spacing: .015em;
text-transform: uppercase;
}
.intro p {
margin: 0;
max-width: 31ch;
font-size: 14px;
font-weight: 700;
line-height: 1.45;
}
.stage {
position: relative;
isolation: isolate;
width: 100%;
aspect-ratio: 12 / 7;
overflow: hidden;
border: 3px solid var(--ink);
background: #e8ddc0;
box-shadow: 9px 9px 0 var(--ink);
cursor: pointer;
}
.stage::after {
content: "";
position: absolute;
inset: 9px;
z-index: 4;
border: 1px solid rgba(23, 35, 58, .25);
pointer-events: none;
}
canvas {
position: absolute;
inset: 0;
display: block;
width: 100%;
height: 100%;
outline: none;
}
canvas:focus-visible {
box-shadow: inset 0 0 0 5px var(--accent);
}
.hud {
position: absolute;
z-index: 5;
pointer-events: none;
}
.round-stamp {
top: 24px;
left: 25px;
display: grid;
width: 78px;
height: 78px;
place-content: center;
transform: rotate(-4deg);
border: 3px solid var(--ink);
background: var(--paper);
box-shadow: 4px 4px 0 var(--ink);
text-align: center;
font-size: 10px;
font-weight: 900;
letter-spacing: .12em;
}
.round-stamp b {
display: block;
font-family: Impact, sans-serif;
font-size: 35px;
line-height: .9;
}
.move-card {
top: 24px;
right: 25px;
width: min(310px, 39%);
padding: 12px 15px 13px;
border: 3px solid var(--ink);
background: rgba(244, 234, 209, .94);
box-shadow: 5px 5px 0 var(--ink);
transform: rotate(1deg);
}
.move-card .phase {
display: block;
margin-bottom: 4px;
color: var(--accent);
font-size: 10px;
font-weight: 900;
letter-spacing: .19em;
}
.move-card strong {
display: block;
overflow: hidden;
font-family: Impact, Haettenschweiler, "Arial Narrow Bold", sans-serif;
font-size: clamp(20px, 3vw, 35px);
line-height: .95;
letter-spacing: .025em;
text-overflow: ellipsis;
text-transform: uppercase;
white-space: nowrap;
}
.move-card .meta {
display: block;
margin-top: 7px;
font-size: 9px;
font-weight: 900;
letter-spacing: .12em;
text-transform: uppercase;
}
.stage-bottom {
right: 25px;
bottom: 24px;
left: 25px;
display: flex;
align-items: end;
justify-content: space-between;
gap: 20px;
}
.beat-box {
display: flex;
align-items: center;
gap: 9px;
padding: 8px 11px;
border: 2px solid var(--ink);
background: var(--paper);
box-shadow: 3px 3px 0 var(--ink);
}
.beat-label {
margin-right: 4px;
font-size: 9px;
font-weight: 900;
letter-spacing: .14em;
}
.beat-lamp {
width: 9px;
height: 9px;
border: 2px solid var(--ink);
border-radius: 50%;
background: transparent;
}
.beat-lamp.on {
background: var(--accent);
transform: scale(1.35);
box-shadow: 0 0 0 3px rgba(239, 81, 56, .18);
}
.floor-hint {
padding: 8px 11px;
border: 2px solid var(--ink);
background: var(--ink);
color: var(--paper);
font-size: 9px;
font-weight: 900;
letter-spacing: .14em;
text-transform: uppercase;
transform: rotate(-1deg);
}
.status-line {
min-height: 25px;
margin: 17px 2px 0;
font-size: 11px;
font-weight: 900;
letter-spacing: .12em;
text-transform: uppercase;
}
.status-line::before {
content: "↳";
margin-right: 8px;
color: var(--accent);
font-size: 17px;
}
.controls {
display: grid;
grid-template-columns: 1.1fr 1.3fr 1.35fr .75fr;
gap: 0;
margin-top: 22px;
border: 3px solid var(--ink);
background: var(--paper);
box-shadow: 6px 6px 0 var(--ink);
}
.control-block {
min-width: 0;
padding: 18px 20px 19px;
border-right: 2px solid var(--ink);
}
.control-block:last-child { border-right: 0; }
.control-label {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin-bottom: 12px;
font-size: 10px;
font-weight: 900;
letter-spacing: .15em;
text-transform: uppercase;
}
.control-label output {
color: var(--accent);
font-family: Impact, sans-serif;
font-size: 19px;
letter-spacing: .04em;
}
.tempo-row {
display: grid;
grid-template-columns: 1fr auto;
align-items: center;
gap: 13px;
}
input[type="range"] {
--fill: 36%;
width: 100%;
height: 7px;
margin: 0;
border: 1px solid var(--ink);
border-radius: 0;
appearance: none;
background: linear-gradient(to right, var(--accent) var(--fill), rgba(23, 35, 58, .14) var(--fill));
cursor: ew-resize;
}
input[type="range"]::-webkit-slider-thumb {
width: 15px;
height: 23px;
border: 2px solid var(--ink);
border-radius: 0;
appearance: none;
background: var(--paper);
box-shadow: 2px 2px 0 var(--ink);
}
input[type="range"]::-moz-range-thumb {
width: 13px;
height: 21px;
border: 2px solid var(--ink);
border-radius: 0;
background: var(--paper);
box-shadow: 2px 2px 0 var(--ink);
}
.bpm-bumps {
display: flex;
align-items: end;
gap: 3px;
height: 24px;
}
.bpm-bumps i {
display: block;
width: 3px;
background: var(--ink);
}
.bpm-bumps i:nth-child(1) { height: 8px; }
.bpm-bumps i:nth-child(2) { height: 14px; }
.bpm-bumps i:nth-child(3) { height: 22px; background: var(--accent); }
.bpm-bumps i:nth-child(4) { height: 16px; }
.bpm-bumps i:nth-child(5) { height: 10px; }
.segmented {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 6px;
}
.style-button {
min-height: 43px;
padding: 7px 8px;
border: 2px solid var(--ink);
border-radius: 0;
background: transparent;
box-shadow: 2px 2px 0 var(--ink);
font-size: 10px;
font-weight: 900;
letter-spacing: .07em;
cursor: pointer;
text-transform: uppercase;
}
.style-button:hover { background: rgba(23, 35, 58, .06); }
.style-button[aria-pressed="true"] {
background: var(--accent);
color: var(--paper);
transform: translate(2px, 2px);
box-shadow: none;
}
.call-row {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 7px;
}
select,
.call-button,
.pause-button {
min-height: 43px;
border: 2px solid var(--ink);
border-radius: 0;
background: var(--paper);
font-size: 10px;
font-weight: 900;
letter-spacing: .08em;
text-transform: uppercase;
}
select {
width: 100%;
min-width: 0;
padding: 0 30px 0 10px;
cursor: pointer;
}
.call-button,
.pause-button {
padding: 0 14px;
box-shadow: 3px 3px 0 var(--ink);
cursor: pointer;
}
.call-button {
background: var(--accent);
color: var(--paper);
}
.call-button.called {
transform: translate(3px, 3px);
box-shadow: none;
}
.pause-button {
width: 100%;
background: var(--ink);
color: var(--paper);
}
.transport {
display: flex;
flex-direction: column;
justify-content: space-between;
gap: 12px;
}
.key-hint {
margin: 0;
color: rgba(23, 35, 58, .65);
font-size: 8px;
font-weight: 900;
letter-spacing: .1em;
line-height: 1.5;
text-transform: uppercase;
}
.page-footer {
display: flex;
justify-content: space-between;
gap: 20px;
margin: 22px 2px 0;
font-size: 10px;
font-weight: 800;
letter-spacing: .09em;
text-transform: uppercase;
}
.page-footer span:last-child { color: var(--accent); }
@media (max-width: 900px) {
.masthead { grid-template-columns: 1fr; gap: 20px; }
.intro { max-width: 440px; }
.controls { grid-template-columns: 1fr 1fr; }
.control-block:nth-child(2) { border-right: 0; }
.control-block:nth-child(-n+2) { border-bottom: 2px solid var(--ink); }
}
@media (max-width: 560px) {
.page { width: calc(100% - 20px); padding-top: 18px; }
h1 { font-size: clamp(50px, 19vw, 76px); }
.stage { box-shadow: 5px 5px 0 var(--ink); }
.round-stamp { top: 16px; left: 17px; width: 58px; height: 58px; }
.round-stamp b { font-size: 27px; }
.move-card { top: 16px; right: 17px; width: 57%; padding: 8px 10px; box-shadow: 3px 3px 0 var(--ink); }
.move-card strong { font-size: 19px; }
.move-card .meta { font-size: 7px; }
.stage-bottom { right: 17px; bottom: 16px; left: 17px; }
.floor-hint { display: none; }
.controls { grid-template-columns: 1fr; }
.control-block { border-right: 0; border-bottom: 2px solid var(--ink); }
.control-block:nth-child(2) { border-bottom: 2px solid var(--ink); }
.control-block:last-child { border-bottom: 0; }
.page-footer { flex-direction: column; gap: 6px; }
}
</style>
</head>
<body>
<main class="page">
<header class="masthead">
<div>
<p class="eyebrow"><span class="live-dot"></span>Live from the nest · Floor 04</p>
<h1>Wing <span>Break</span></h1>
</div>
<div class="intro">
<strong>A shoebill with a timing problem.</strong>
<p>Pick the pocket, call the move, and let the big bird put it on the floor.</p>
</div>
</header>
<section class="stage" id="stage" aria-label="Animated breakdancing shoebill">
<canvas id="danceCanvas" width="1200" height="700" tabindex="0"
aria-label="A colorful shoebill performs windmills, freezes, kicks, and backspins on a poster-like dance floor.">
Your browser needs canvas support to show the dancing bird.
</canvas>
<div class="hud round-stamp">Round<b>04</b></div>
<div class="hud move-card" aria-live="polite">
<span class="phase" id="phaseName">Ready</span>
<strong id="moveName">Broken Step</strong>
<span class="meta" id="moveMeta">Boom bap · take 01</span>
</div>
<div class="hud stage-bottom">
<div class="beat-box">
<span class="beat-label">Beat</span>
<i class="beat-lamp"></i>
<i class="beat-lamp"></i>
<i class="beat-lamp"></i>
<i class="beat-lamp"></i>
</div>
<div class="floor-hint">Click the floor to call a move</div>
</div>
</section>
<p class="status-line" id="statusLine" aria-live="polite">Pick a pocket. Call a move.</p>
<section class="controls" aria-label="Dance controls">
<div class="control-block">
<label class="control-label" for="tempo">
Tempo
<output id="bpmValue" for="tempo">104 BPM</output>
</label>
<div class="tempo-row">
<input id="tempo" type="range" min="72" max="148" value="104" step="1">
<span class="bpm-bumps" aria-hidden="true"><i></i><i></i><i></i><i></i><i></i></span>
</div>
</div>
<div class="control-block">
<span class="control-label">Style / pocket</span>
<div class="segmented" role="group" aria-label="Dance style">
<button class="style-button" type="button" data-style="bump" aria-pressed="true">Boom bap</button>
<button class="style-button" type="button" data-style="house" aria-pressed="false">House</button>
<button class="style-button" type="button" data-style="loco" aria-pressed="false">Loco crew</button>
</div>
</div>
<div class="control-block">
<label class="control-label" for="moveSelect">Next move</label>
<div class="call-row">
<select id="moveSelect">
<option value="auto">Auto / surprise me</option>
<option value="footwork">Broken step</option>
<option value="windmill">Windmill</option>
<option value="freeze">Wing freeze</option>
<option value="backspin">Backspin</option>
<option value="kick">Swing kick</option>
</select>
<button class="call-button" id="callButton" type="button">Call it</button>
</div>
</div>
<div class="control-block transport">
<button class="pause-button" id="pauseButton" type="button" aria-pressed="false">Pause</button>
<p class="key-hint">Space: call move<br>P: pause</p>
</div>
</section>
<footer class="page-footer">
<span>Every move lands, holds, and gets up.</span>
<span>That is the whole secret.</span>
</footer>
</main>
<script>
(() => {
"use strict";
const W = 1200;
const H = 700;
const FLOOR = 528;
const TAU = Math.PI * 2;
const canvas = document.getElementById("danceCanvas");
const ctx = canvas.getContext("2d");
const backdrop = document.createElement("canvas");
backdrop.width = W;
backdrop.height = H;
const stage = document.getElementById("stage");
const phaseName = document.getElementById("phaseName");
const moveName = document.getElementById("moveName");
const moveMeta = document.getElementById("moveMeta");
const statusLine = document.getElementById("statusLine");
const tempo = document.getElementById("tempo");
const bpmValue = document.getElementById("bpmValue");
const moveSelect = document.getElementById("moveSelect");
const callButton = document.getElementById("callButton");
const pauseButton = document.getElementById("pauseButton");
const styleButtons = [...document.querySelectorAll("[data-style]")];
const beatLamps = [...document.querySelectorAll(".beat-lamp")];
const palettes = {
bump: {
sky: "#e8ddc0", sun: "#ef5138", sun2: "#f4c64f",
body: "#2852b7", bodyDark: "#1e347d", wing: "#28a89d", wingFar: "#1d7b78",
head: "#5e7892", leg: "#ef7542", legFar: "#a64b3f",
shoe: "#ef5138", sole: "#f4c64f", ink: "#17233a"
},
house: {
sky: "#ded8ee", sun: "#ed4b91", sun2: "#39c2b0",
body: "#6244a8", bodyDark: "#3f2b78", wing: "#ed4b91", wingFar: "#a83972",
head: "#666eaa", leg: "#39c2b0", legFar: "#27867f",
shoe: "#f4c64f", sole: "#17233a", ink: "#17233a"
},
loco: {
sky: "#e8edc6", sun: "#d5eb3f", sun2: "#f07a42",
body: "#197d82", bodyDark: "#11585f", wing: "#7b4aab", wingFar: "#573679",
head: "#4f9b98", leg: "#f07a42", legFar: "#a84c3c",
shoe: "#7b4aab", sole: "#d5eb3f", ink: "#17233a"
}
};
const BASE = {
x: 600, y: 350, r: 0, sx: 1, sy: 1,
hx: 67, hy: -87, hr: 0, beak: 0, brow: 0,
wingF: Math.PI + .05, wingFL: 88,
wingN: Math.PI - .05, wingNL: 94,
farKneeX: -28, farKneeY: 102, farFootX: -37, farFootY: 174, farToe: -.08,
nearKneeX: 27, nearKneeY: 104, nearFootX: 37, nearFootY: 174, nearToe: .08,
tail: -.08
};
const P = (o = {}) => Object.assign({}, BASE, o);
const F = (at, phase, p, ease = "settle", hit = 0) => ({ at, phase, p, ease, hit });
const clips = {
footwork: {
id: "footwork",
name: "Broken Step",
note: "feet / cross / lock",
beats: 2.55,
frames: [
F(0, "Ready", P(), "load"),
F(.45, "Load", P({
x: 596, y: 374, r: -.03, sx: 1.08, sy: .9,
hx: 61, hy: -81, farKneeX: -38, farFootX: -12,
nearKneeX: 30, nearFootX: 20, wingF: Math.PI + .2, wingN: Math.PI - .18
}), "strike"),
F(.9, "Step", P({
x: 632, y: 349, r: -.06, sx: .97, sy: 1.04,
hx: 73, hy: -88, farKneeX: 5, farKneeY: 95, farFootX: 56,
nearKneeX: 12, nearFootX: -2, wingF: Math.PI + .35, wingN: Math.PI - .3
}), "hold", .8),
F(1.22, "Lock", P({
x: 618, y: 356, r: -.04, sx: 1.02, sy: .98,
farKneeX: 3, farFootX: 28, nearKneeX: 20, nearFootX: 14
}), "recover", .3),
F(1.58, "Catch", P({
x: 611, y: 365, r: -.03, sx: 1.05, sy: .95,
nearKneeX: 35, nearFootX: 37
}), "settle"),
F(2.15, "Recover", P({
x: 603, y: 354, r: -.01, sx: 1.01, sy: .99,
farFootX: -30, nearFootX: 30
}), "settle"),
F(2.55, "Ready", P(), "settle")
]
},
windmill: {
id: "windmill",
name: "Windmill",
note: "body / scissor / recover",
beats: 3.85,
frames: [
F(0, "Ready", P(), "load"),
F(.55, "Load", P({
x: 620, y: 384, r: .08, sx: 1.08, sy: .87,
farKneeX: -45, farFootX: -55, nearKneeX: 44, nearFootX: 52,
wingF: 1.2, wingN: .6
}), "strike"),
F(1.05, "Drop", P({
x: 610, y: 365, r: 1.25, sx: 1.05, sy: .94,
farKneeX: -90, farKneeY: 40, farFootX: -115, farFootY: 110, farToe: -.45,
nearKneeX: 85, nearKneeY: 65, nearFootX: 150, nearFootY: 40, nearToe: .15,
wingF: 1.7, wingN: 2.0
}), "snap", 1),
F(1.55, "Scissor", P({
x: 600, y: 360, r: 1.46, sx: 1.06, sy: .93,
farKneeX: 100, farKneeY: 30, farFootX: 160, farFootY: 10, farToe: .18,
nearKneeX: -90, nearKneeY: 40, nearFootX: -120, nearFootY: 110, nearToe: -.3,
wingF: 1.55, wingN: 2.2
}), "snap", .65),
F(2.05, "Scissor", P({
x: 590, y: 350, r: 1.65, sx: 1.04, sy: .94,
farKneeX: -90, farKneeY: 40, farFootX: -120, farFootY: 110, farToe: -.3,
nearKneeX: 100, nearKneeY: 25, nearFootX: 175, nearFootY: -30, nearToe: .22,
wingF: 1.9, wingN: 1.4
}), "snap", .65),
F(2.55, "Catch", P({
x: 610, y: 360, r: 1.25, sx: 1.03, sy: .96,
farKneeX: -90, farKneeY: 40, farFootX: -115, farFootY: 110,
nearKneeX: 85, nearKneeY: 65, nearFootX: 150, nearFootY: 40,
wingF: 1.7, wingN: 2.0
}), "recover", .4),
F(3.15, "Recover", P({
x: 610, y: 377, r: .65, sx: 1.05, sy: .93,
farKneeX: -35, farFootX: -28, farFootY: 165,
nearKneeX: 50, nearFootX: 112, nearFootY: 126,
wingF: 2.55, wingN: 2.35
}), "settle"),
F(3.85, "Ready", P(), "settle")
]
},
freeze: {
id: "freeze",
name: "Wing Freeze",
note: "load / plant / hold",
beats: 4.05,
frames: [
F(0, "Ready", P(), "load"),
F(.6, "Load", P({
x: 600, y: 384, r: -.08, sx: 1.08, sy: .9,
wingF: 1.05, wingN: 2.6, farFootX: -45, nearFootX: 48
}), "strike"),
F(1.2, "Drop", P({
x: 635, y: 456, r: -.18, sx: 1.1, sy: .9,
hx: 57, hy: -72, hr: .08, wingF: 2.4, wingN: 1.02, wingNL: 106,
farKneeX: -92, farKneeY: 30, farFootX: -125, farFootY: -18, farToe: -.5,
nearKneeX: 42, nearKneeY: 72, nearFootX: 45, nearFootY: 78, nearToe: .1
}), "snap", 1),
F(1.85, "Hold", P({
x: 635, y: 456, r: -.18, sx: 1.1, sy: .9,
hx: 57, hy: -72, hr: .08, wingF: 2.4, wingN: 1.03, wingNL: 106,
farKneeX: -92, farKneeY: 30, farFootX: -125, farFootY: -18,
nearKneeX: 42, nearKneeY: 72, nearFootX: 45, nearFootY: 78
}), "hold"),
F(2.4, "Hold", P({
x: 633, y: 455, r: -.17, sx: 1.1, sy: .92,
wingN: 1.035, farFootX: -130, farFootY: -25
}), "hold"),
F(2.85, "Shift", P({
x: 629, y: 449, r: -.15, sx: 1.08, sy: .94,
wingN: 1.1, farFootX: -124, farFootY: -12
}), "recover"),
F(3.5, "Recover", P({
x: 615, y: 378, r: -.08, sx: 1.04, sy: .96,
wingN: 2.7, farKneeX: -55, farKneeY: 60, farFootX: -70, farFootY: 120,
nearKneeX: 32, nearFootX: 42
}), "settle"),
F(4.05, "Ready", P(), "settle")
]
},
backspin: {
id: "backspin",
name: "Backspin",
note: "edge / turn / land",
beats: 3.45,
frames: [
F(0, "Ready", P(), "load"),
F(.55, "Load", P({
x: 620, y: 386, sx: 1.1, sy: .88,
farKneeX: -20, farFootX: -22, nearKneeX: 22, nearFootX: 26
}), "strike"),
F(.95, "Turn", P({
x: 640, y: 390, sx: .12, sy: 1.12,
hx: 58, hy: -86, farFootX: -18, nearFootX: 22,
wingF: 2.65, wingN: .55
}), "snap", 1),
F(1.45, "Turn", P({
x: 622, y: 351, sx: -.72, sy: 1.04,
farKneeX: -10, farFootX: 15, nearKneeX: 5, nearFootX: -5,
wingF: 2.35, wingN: .8
}), "snap", .8),
F(1.95, "Turn", P({
x: 590, y: 381, sx: -.25, sy: 1.1,
farFootX: -10, nearFootX: 16, wingF: 2.7, wingN: .5
}), "snap", .8),
F(2.45, "Land", P({
x: 575, y: 360, sx: .84, sy: .98,
farFootX: -34, nearFootX: 35, wingF: 3.0, wingN: 3.05
}), "recover", .7),
F(2.9, "Set", P({
x: 585, y: 354, sx: 1, sy: .99,
farFootX: -31, nearFootX: 32
}), "settle"),
F(3.45, "Ready", P(), "settle")
]
},
kick: {
id: "kick",
name: "Swing Kick",
note: "load / snap / catch",
beats: 2.8,
frames: [
F(0, "Ready", P(), "load"),
F(.55, "Load", P({
x: 590, y: 380, r: .05, sx: 1.07, sy: .9,
farFootX: -34, nearKneeX: 42, nearFootX: 44
}), "strike"),
F(1.05, "Kick", P({
x: 635, y: 350, r: -.18, sx: .94, sy: 1.07,
nearKneeX: 75, nearKneeY: 20, nearFootX: 120, nearFootY: -45, nearToe: .25,
farFootX: -30, wingF: Math.PI + .2, wingN: 2.45
}), "snap", 1),
F(1.4, "Hold", P({
x: 635, y: 350, r: -.18, sx: .94, sy: 1.07,
nearKneeX: 75, nearKneeY: 20, nearFootX: 120, nearFootY: -45,
farFootX: -30, wingF: Math.PI + .2, wingN: 2.45
}), "hold"),
F(2.05, "Catch", P({
x: 620, y: 365, r: -.12, sx: 1.05, sy: .94,
nearKneeX: 40, nearFootX: 48, nearFootY: 166,
wingN: 2.8
}), "recover", .5),
F(2.8, "Ready", P(), "settle")
]
}
};
const styles = {
bump: { label: "Boom bap", moves: ["footwork", "windmill", "freeze", "backspin"] },
house: { label: "House", moves: ["footwork", "backspin", "kick", "freeze"] },
loco: { label: "Loco crew", moves: ["kick", "windmill", "backspin", "freeze"] }
};
const ease = {
load: t => t * t,
strike: t => 1 - Math.pow(1 - t, 6),
snap: t => 1 - Math.pow(1 - t, 9),
hold: t => t,
recover: t => {
const c1 = 1.15, c3 = c1 + 1;
return 1 + c3 * Math.pow(t - 1, 3) + c1 * Math.pow(t - 1, 2);
},
settle: t => 1 - Math.pow(1 - t, 3),
wind: t => t < .5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2
};
const clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
function lerpPose(a, b, t) {
const out = {};
for (const key of Object.keys(BASE)) out[key] = a[key] + (b[key] - a[key]) * t;
return out;
}
function sampleClip(clip, beat) {
const frames = clip.frames;
let i = 0;
while (i < frames.length - 1 && beat >= frames[i + 1].at) i++;
const a = frames[i];
const z = frames[i + 1] || a;
const raw = z.at === a.at ? 1 : clamp((beat - a.at) / (z.at - a.at), 0, 1);
const shaped = (ease[a.ease] || ease.settle)(raw);
return {
pose: lerpPose(a.p, z.p, shaped),
phase: a.phase,
segment: raw
};
}
let currentStyle = "bump";
let currentBpm = 104;
let paused = false;
let playlist = styles[currentStyle].moves.slice();
let pendingPlaylist = null;
let queuedMove = null;
let clipIndex = 0;
let currentClip = clips[playlist[0]];
let clipBeat = 0;
let lastImpactBeat = 0;
let totalBeats = 0;
let takeCount = 1;
let currentPhase = "Ready";
let currentPose = P();
let lastBeatLamp = -1;
const state = {
particles: [],
rings: [],
shake: 0,
impulse: 0,
bodyVX: 0,
bodyVY: 0,
prevRoot: { x: 600, y: 350 },
prevR: 0,
headSpring: { x: 0, y: 0, vx: 0, vy: 0 }
};
function flashStatus(message) {
statusLine.textContent = message;
}
function transformPoint(p, x, y) {
const lx = x * p.sx;
const ly = y * p.sy;
const c = Math.cos(p.r);
const s = Math.sin(p.r);
return {
x: p.x + lx * c - ly * s,
y: p.y + lx * s + ly * c
};
}
function findContact(p) {
const pal = palettes[currentStyle];
const contacts = [
transformPoint(p, p.farFootX, p.farFootY),
transformPoint(p, p.nearFootX, p.nearFootY),
transformPoint(p, -8 + Math.cos(p.wingN) * p.wingNL, -12 + Math.sin(p.wingN) * p.wingNL)
];
contacts.sort((a, b) => Math.abs(a.y - FLOOR) - Math.abs(b.y - FLOOR));
return contacts[0];
}
function triggerImpact(pose, power) {
const pal = palettes[currentStyle];
const contact = findContact(pose);
state.shake = Math.max(state.shake, 4 + power * 5);
state.impulse = Math.max(state.impulse, .7 + power * .3);
state.rings.push({ x: contact.x, y: Math.min(contact.y, FLOOR + 2), life: .45, max: .45, power });
const count = 5 + Math.round(power * 8);
for (let i = 0; i < count; i++) {
const angle = -Math.PI * (.15 + Math.random() * .7);
const speed = 50 + Math.random() * (85 + power * 65);
state.particles.push({
x: contact.x + (Math.random() - .5) * 16,
y: contact.y - Math.random() * 8,
vx: Math.cos(angle) * speed,
vy: Math.sin(angle) * speed - 25,
life: .4 + Math.random() * .35,
max: .75,
size: 3 + Math.random() * 6,
rot: Math.random() * TAU,
spin: (Math.random() - .5) * 10,
color: Math.random() > .5 ? pal.accent || pal.sun : pal.sole
});
}
}
function applySecondaryMotion(p, dt) {
const instantVX = (p.x - state.prevRoot.x) / Math.max(dt, .001);
const instantVY = (p.y - state.prevRoot.y) / Math.max(dt, .001);
state.bodyVX += (instantVX - state.bodyVX) * .16;
state.bodyVY += (instantVY - state.bodyVY) * .16;
const targetX = clamp(-state.bodyVX * .012, -7, 7);
const targetY = clamp(-state.bodyVY * .008, -5, 5);
const spring = state.headSpring;
spring.vx += (targetX - spring.x) * 38 * dt;
spring.vy += (targetY - spring.y) * 38 * dt;
spring.vx *= Math.exp(-8 * dt);
spring.vy *= Math.exp(-8 * dt);
spring.x += spring.vx * dt;
spring.y += spring.vy * dt;
p.hx += spring.x;
p.hy += spring.y;
p.hr += clamp(-state.bodyVX * .0008, -.08, .08);
p.wingN += clamp((state.prevR - p.r) * .14, -.12, .12);
p.sx *= 1 + state.impulse * .025;
p.sy *= 1 - state.impulse * .035;
state.prevRoot = { x: p.x, y: p.y };
state.prevR = p.r;
}
function advanceClip(overshoot) {
let nextId;
if (queuedMove) {
nextId = queuedMove;
queuedMove = null;
flashStatus(`${clips[nextId].name} is in.`);
} else if (pendingPlaylist) {
playlist = pendingPlaylist.slice();
pendingPlaylist = null;
clipIndex = 0;
nextId = playlist[0];
} else {
clipIndex = (clipIndex + 1) % playlist.length;
nextId = playlist[clipIndex];
}
currentClip = clips[nextId];
clipBeat = overshoot;
lastImpactBeat = 0;
takeCount++;
}
function update(dt) {
const beatDelta = dt * currentBpm / 60;
totalBeats += beatDelta;
const oldBeat = clipBeat;
clipBeat += beatDelta;
for (const frame of currentClip.frames) {
if (frame.hit && oldBeat < frame.at && clipBeat >= frame.at) {
triggerImpact(frame.p, frame.hit);
}
}
lastImpactBeat = clipBeat;
if (clipBeat >= currentClip.beats) {
advanceClip(clipBeat - currentClip.beats);
}
const sample = sampleClip(currentClip, Math.min(clipBeat, currentClip.beats));
currentPose = sample.pose;
currentPhase = sample.phase;
applySecondaryMotion(currentPose, dt);
state.shake *= Math.exp(-9 * dt);
state.impulse *= Math.exp(-8 * dt);
for (const p of state.particles) {
p.life -= dt;
p.vy += 430 * dt;
p.x += p.vx * dt;
p.y += p.vy * dt;
p.rot += p.spin * dt;
if (p.y > FLOOR + 2) {
p.y = FLOOR + 2;
p.vy *= -.24;
p.vx *= .78;
}
}
state.particles = state.particles.filter(p => p.life > 0);
for (const ring of state.rings) ring.life -= dt;
state.rings = state.rings.filter(r => r.life > 0);
}
function roundedRect(c, x, y, w, h, r) {
c.beginPath();
c.moveTo(x + r, y);
c.lineTo(x + w - r, y);
c.quadraticCurveTo(x + w, y, x + w, y + r);
c.lineTo(x + w, y + h - r);
c.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
c.lineTo(x + r, y + h);
c.quadraticCurveTo(x, y + h, x, y + h - r);
c.lineTo(x, y + r);
c.quadraticCurveTo(x, y, x + r, y);
c.closePath();
}
function drawSpeaker(c, x, y, w, h, pal) {
c.save();
c.fillStyle = pal.ink;
roundedRect(c, x, y, w, h, 4);
c.fill();
c.strokeStyle = pal.sky;
c.lineWidth = 4;
c.stroke();
c.fillStyle = pal.sun2;
c.beginPath();
c.arc(x + w / 2, y + h * .35, w * .29, 0, TAU);
c.fill();
c.strokeStyle = pal.ink;
c.lineWidth = 5;
c.stroke();
c.fillStyle = pal.sky;
c.beginPath();
c.arc(x + w / 2, y + h * .35, w * .12, 0, TAU);
c.fill();
c.fillStyle = pal.sun;
c.beginPath();
c.arc(x + w / 2, y + h * .74, w * .17, 0, TAU);
c.fill();
c.strokeStyle = pal.ink;
c.lineWidth = 4;
c.stroke();
c.fillStyle = pal.paper || "#f4ead1";
c.fillRect(x + 12, y + 10, w - 24, 8);
c.restore();
}
function buildBackdrop() {
const pal = palettes[currentStyle];
const b = backdrop.getContext("2d");
b.clearRect(0, 0, W, H);
b.fillStyle = pal.sky;
b.fillRect(0, 0, W, H);
b.save();
b.translate(805, 225);
b.strokeStyle = pal.sun2;
b.lineWidth = 8;
for (let i = 0; i < 24; i++) {
b.rotate(TAU / 24);
b.beginPath();
b.moveTo(170, 0);
b.lineTo(205, 0);
b.stroke();
}
b.restore();
b.fillStyle = pal.sun;
b.beginPath();
b.arc(805, 225, 162, 0, TAU);
b.fill();
b.save();
b.beginPath();
b.arc(805, 225, 158, 0, TAU);
b.clip();
b.fillStyle = "rgba(23,35,58,.18)";
for (let y = 70; y < 390; y += 14) {
for (let x = 640; x < 970; x += 14) {
b.beginPath();
b.arc(x, y, 2.25, 0, TAU);
b.fill();
}
}
b.restore();
b.save();
b.translate(-20, 270);
b.rotate(-.11);
b.fillStyle = pal.body;
b.fillRect(0, 0, 145, 275);
b.fillStyle = pal.sun2;
b.fillRect(105, 0, 40, 275);
b.restore();
b.save();
b.translate(43, 480);
b.rotate(-Math.PI / 2);
b.fillStyle = pal.ink;
b.font = '900 20px Impact, "Arial Black", sans-serif';
b.fillText("BIRD BATTLE / LIVE", 0, 0);
b.restore();
b.fillStyle = pal.ink;
b.font = '900 13px Arial, sans-serif';
b.fillText("NO MUSIC TOYS. JUST FEET.", 150, 82);
b.fillText("NEST ATHLETIC DEPT. © 2025", 868, 65);
drawSpeaker(b, 38, 365, 120, 163, pal);
drawSpeaker(b, 1042, 365, 120, 163, pal);
b.fillStyle = pal.ink;
b.fillRect(0, FLOOR, W, H - FLOOR);
b.fillStyle = pal.sun;
b.fillRect(0, FLOOR, W, 6);
b.strokeStyle = "rgba(244,234,209,.17)";
b.lineWidth = 2;
for (let x = -350; x <= 1550; x += 130) {
b.beginPath();
b.moveTo(600, 395);
b.lineTo(x, H);
b.stroke();
}
for (const y of [575, 632, 695]) {
b.beginPath();
b.moveTo(0, y);
b.lineTo(W, y);
b.stroke();
}
b.strokeStyle = pal.sole;
b.lineWidth = 4;
b.beginPath();
b.ellipse(600, 585, 175, 35, 0, 0, TAU);
b.stroke();
b.fillStyle = pal.sky;
b.font = '900 11px Arial, sans-serif';
b.fillText("KEEP IT CLEAN / KEEP IT MEAN", 475, 684);
b.save();
b.translate(1120, 42);
b.rotate(Math.PI / 2);
b.fillStyle = pal.ink;
b.font = '900 12px Arial, sans-serif';
b.fillText("FEET FIRST / HEAD SECOND", 0, 0);
b.restore();
b.save();
b.translate(180, 505);
b.rotate(-.08);
b.fillStyle = pal.sun2;
b.fillRect(-12, -15, 112, 30);
b.strokeStyle = pal.ink;
b.lineWidth = 3;
b.strokeRect(-12, -15, 112, 30);
b.fillStyle = pal.ink;
b.font = '900 11px Arial, sans-serif';
b.fillText("TAPE THIS", 2, 4);
b.restore();
}
function drawLights(time) {
const pal = palettes[currentStyle];
const pulse = Math.exp(-(totalBeats % 1) * 5);
ctx.save();
ctx.fillStyle = `rgba(255,255,255,${.055 + pulse * .045})`;
ctx.beginPath();
ctx.moveTo(525, -20);
ctx.lineTo(250, FLOOR + 42);
ctx.lineTo(790, FLOOR + 42);
ctx.closePath();
ctx.fill();
ctx.fillStyle = `rgba(255,255,255,${.035 + pulse * .025})`;
ctx.beginPath();
ctx.moveTo(700, -20);
ctx.lineTo(920, FLOOR + 38);
ctx.lineTo(1100, FLOOR + 38);
ctx.closePath();
ctx.fill();
ctx.fillStyle = pal.ink;
ctx.beginPath();
ctx.arc(525, 4, 10, 0, TAU);
ctx.arc(700, 4, 10, 0, TAU);
ctx.fill();
const bx = 1095;
for (let i = 0; i < 5; i++) {
const h = 10 + Math.abs(Math.sin(totalBeats * 2.1 + i * .72)) * 29;
ctx.fillStyle = i === 2 ? pal.sun : pal.ink;
ctx.fillRect(bx + i * 10, 112 - h, 6, h);
}
ctx.restore();
}
function drawFloorAndShadow() {
const pal = palettes[currentStyle];
const pulse = Math.exp(-(totalBeats % 1) * 6);
const horizontal = Math.abs(Math.sin(currentPose.r));
ctx.save();
ctx.fillStyle = pal.sun;
ctx.globalAlpha = .09 + pulse * .1;
ctx.beginPath();
ctx.ellipse(600, FLOOR + 22, 220 + pulse * 15, 37, 0, 0, TAU);
ctx.fill();
ctx.globalAlpha = .24;
ctx.fillStyle = "#000";
ctx.beginPath();
ctx.ellipse(
currentPose.x,
FLOOR + 7,
91 + horizontal * 45 + Math.abs(currentPose.sx) * 13,
12 + horizontal * 7,
0, 0, TAU
);
ctx.fill();
ctx.restore();
}
function drawMotion() {
const pal = palettes[currentStyle];
const p = currentPose;
ctx.save();
ctx.strokeStyle = pal.ink;
ctx.fillStyle = pal.ink;
ctx.lineWidth = 4;
ctx.lineCap = "round";
ctx.globalAlpha = currentPhase === "Ready" ? .12 : .34;
if (currentClip.id === "windmill") {
const spin = ((p.r % TAU) + TAU) % TAU;
ctx.translate(p.x, p.y);
for (let i = 0; i < 3; i++) {
ctx.beginPath();
ctx.arc(0, 0, 125 + i * 13, spin - .95, spin + .28);
ctx.stroke();
}
} else if (currentClip.id === "backspin") {
ctx.translate(p.x, p.y);
ctx.rotate(-.08);
ctx.beginPath();
ctx.ellipse(0, 0, 142, 42, 0, 0, TAU);
ctx.stroke();
ctx.beginPath();
ctx.ellipse(0, 0, 119, 28, 0, 0, TAU);
ctx.stroke();
} else if (currentClip.id === "freeze") {
const contact = findContact(p);
for (let i = 0; i < 6; i++) {
const a = -.9 + i * .34;
ctx.beginPath();
ctx.moveTo(contact.x + Math.cos(a) * 15, contact.y + Math.sin(a) * 8);
ctx.lineTo(contact.x + Math.cos(a) * (28 + i * 2), contact.y + Math.sin(a) * 17);
ctx.stroke();
}
} else if (currentClip.id === "kick" || currentClip.id === "footwork") {
const foot = transformPoint(p, p.nearFootX, p.nearFootY);
for (let i = 0; i < 3; i++) {
ctx.beginPath();
ctx.moveTo(foot.x - 65 - i * 12, foot.y - 16 + i * 14);
ctx.lineTo(foot.x - 24 - i * 8, foot.y - 16 + i * 14);
ctx.stroke();
}
}
ctx.restore();
}
function drawWing(angle, length, fill, far = false) {
const c = ctx;
c.save();
c.translate(-8, -12);
c.rotate(angle);
c.globalAlpha = far ? .8 : 1;
c.beginPath();
c.moveTo(0, -5);
c.quadraticCurveTo(length * .38, -28, length, -2);
c.quadraticCurveTo(length * .67, 12, length * .58, 12);
c.quadraticCurveTo(length * .32, 18, 0, 9);
c.closePath();
c.fillStyle = fill;
c.fill();
c.strokeStyle = "#17233a";
c.lineWidth = 5;
c.lineJoin = "round";
c.stroke();
c.strokeStyle = "rgba(23,35,58,.48)";
c.lineWidth = 3;
for (let i = 0; i < 3; i++) {
c.beginPath();
c.moveTo(12, 1 + i * 2);
c.quadraticCurveTo(length * .52, -10 + i * 9, length * .86, -1 + i * 5);
c.stroke();
}
c.restore();
}
function drawLeg(hipX, hipY, kneeX, kneeY, footX, footY, toe, far) {
const pal = palettes[currentStyle];
const c = ctx;
c.save();
c.globalAlpha = far ? .72 : 1;
c.lineCap = "round";
c.lineJoin = "round";
c.beginPath();
c.moveTo(hipX, hipY);
c.lineTo(kneeX, kneeY);
c.lineTo(footX, footY);
c.strokeStyle = pal.ink;
c.lineWidth = 14;
c.stroke();
c.strokeStyle = far ? pal.legFar : pal.leg;
c.lineWidth = 8;
c.stroke();
c.beginPath();
c.arc(kneeX, kneeY, 5.5, 0, TAU);
c.fillStyle = far ? pal.legFar : pal.leg;
c.fill();
c.strokeStyle = pal.ink;
c.lineWidth = 2.5;
c.stroke();
c.translate(footX, footY);
c.rotate(toe);
c.beginPath();
c.moveTo(-18, -8);
c.lineTo(7, -10);
c.quadraticCurveTo(25, -7, 21, 4);
c.lineTo(12, 9);
c.lineTo(-18, 8);
c.closePath();
c.fillStyle = pal.shoe;
c.fill();
c.strokeStyle = pal.ink;
c.lineWidth = 4;
c.lineJoin = "round";
c.stroke();
c.beginPath();
c.moveTo(-16, 7);
c.lineTo(17, 6);
c.strokeStyle = pal.sole;
c.lineWidth = 5;
c.stroke();
c.strokeStyle = "#f4ead1";
c.lineWidth = 2;
for (let i = 0; i < 3; i++) {
c.beginPath();
c.moveTo(-2 + i * 5, -5);
c.lineTo(2 + i * 5, 0);
c.stroke();
}
c.restore();
}
function drawTail() {
const pal = palettes[currentStyle];
const c = ctx;
c.save();
c.translate(-66, -14);
c.rotate(Math.PI + currentPose.tail);
const feathers = [
{ y: -13, len: 88, color: pal.wingFar },
{ y: 0, len: 104, color: pal.wing },
{ y: 13, len: 78, color: pal.sun }
];
for (const f of feathers) {
c.beginPath();
c.moveTo(0, f.y - 5);
c.quadraticCurveTo(f.len * .48, f.y - 13, f.len, f.y - 4);
c.lineTo(f.len - 7, f.y + 5);
c.quadraticCurveTo(f.len * .42, f.y + 12, 0, f.y + 6);
c.closePath();
c.fillStyle = f.color;
c.fill();
c.strokeStyle = pal.ink;
c.lineWidth = 4;
c.stroke();
}
c.restore();
}
function drawTorso() {
const pal = palettes[currentStyle];
const c = ctx;
c.beginPath();
c.moveTo(-74, -28);
c.bezierCurveTo(-54, -62, 11, -71, 58, -43);
c.bezierCurveTo(88, -25, 91, 19, 58, 45);
c.bezierCurveTo(20, 66, -44, 60, -74, 27);
c.closePath();
c.fillStyle = pal.body;
c.fill();
c.strokeStyle = pal.ink;
c.lineWidth = 6;
c.lineJoin = "round";
c.stroke();
c.beginPath();
c.moveTo(34, -34);
c.bezierCurveTo(67, -26, 78, 5, 55, 34);
c.bezierCurveTo(38, 48, 22, 47, 13, 42);
c.bezierCurveTo(43, 20, 49, -8, 34, -34);
c.closePath();
c.fillStyle = "rgba(244,234,209,.92)";
c.fill();
c.strokeStyle = "rgba(23,35,58,.35)";
c.lineWidth = 4;
c.lineCap = "round";
for (let i = 0; i < 3; i++) {
c.beginPath();
c.moveTo(-53 + i * 8, -19 + i * 12);
c.quadraticCurveTo(-35 + i * 8, -13 + i * 12, -24 + i * 8, -19 + i * 12);
c.stroke();
}
}
function drawHead() {
const pal = palettes[currentStyle];
const c = ctx;
const p = currentPose;
c.save();
c.translate(p.hx, p.hy);
c.rotate(p.hr);
c.beginPath();
c.moveTo(-35, -18);
c.bezierCurveTo(-34, -40, 10, -45, 31, -25);
c.bezierCurveTo(47, -10, 38, 23, 16, 32);
c.bezierCurveTo(-14, 42, -40, 20, -35, -18);
c.closePath();
c.fillStyle = pal.head;
c.fill();
c.strokeStyle = pal.ink;
c.lineWidth = 6;
c.stroke();
c.strokeStyle = pal.ink;
c.lineWidth = 4;
c.lineCap = "round";
for (let i = 0; i < 3; i++) {
c.beginPath();
c.moveTo(-28 + i * 6, -20 - i * 3);
c.lineTo(-48 - i * 3, -31 - i * 4);
c.stroke();
}
c.beginPath();
c.moveTo(24, -16);
c.bezierCurveTo(54, -22, 86, -13, 104, 5);
c.bezierCurveTo(94, 12, 91, 26, 72, 27);
c.lineTo(28, 20);
c.quadraticCurveTo(20, 2, 24, -16);
c.closePath();
c.fillStyle = pal.sole;
c.fill();
c.strokeStyle = pal.ink;
c.lineWidth = 5;
c.lineJoin = "round";
c.stroke();
c.beginPath();
c.moveTo(30, 11);
c.quadraticCurveTo(58, 15, 91, 9);
c.strokeStyle = pal.ink;
c.lineWidth = 3;
c.stroke();
c.fillStyle = pal.ink;
c.beginPath();
c.ellipse(79, 1, 4, 2.3, -.15, 0, TAU);
c.fill();
const blinkCycle = totalTime % 5.7;
const blink = blinkCycle > 5.48 ? clamp((blinkCycle - 5.48) / .2, 0, 1) : 0;
c.beginPath();
c.ellipse(9, -8, 11, 11 * (1 - blink), 0, 0, TAU);
c.fillStyle = "#f4ead1";
c.fill();
c.strokeStyle = pal.ink;
c.lineWidth = 3;
c.stroke();
if (blink < .85) {
c.beginPath();
c.arc(12, -8, 5.2, 0, TAU);
c.fillStyle = pal.ink;
c.fill();
c.beginPath();
c.arc(14, -10, 1.7, 0, TAU);
c.fillStyle = "#f4ead1";
c.fill();
}
c.beginPath();
c.moveTo(-4, -24);
c.quadraticCurveTo(11, -31 + p.brow * 3, 25, -22 + p.brow * 4);
c.strokeStyle = pal.ink;
c.lineWidth = 5;
c.stroke();
c.restore();
}
function drawBird(time) {
const pal = palettes[currentStyle];
const p = currentPose;
const shakeX = Math.sin(time * 83) * state.shake;
const shakeY = Math.cos(time * 67) * state.shake * .55;
ctx.save();
ctx.translate(p.x + shakeX, p.y + shakeY);
ctx.rotate(p.r);
ctx.scale(p.sx, p.sy);
drawWing(p.wingF, p.wingFL, pal.wingFar, true);
drawLeg(-20, 29, p.farKneeX, p.farKneeY, p.farFootX, p.farFootY, p.farToe, true);
drawTail();
drawTorso();
drawLeg(23, 31, p.nearKneeX, p.nearKneeY, p.nearFootX, p.nearFootY, p.nearToe, false);
drawWing(p.wingN, p.wingNL, pal.wing, false);
ctx.beginPath();
ctx.moveTo(37, -26);
ctx.quadraticCurveTo(57, -63, p.hx, p.hy + 12);
ctx.strokeStyle = pal.ink;
ctx.lineWidth = 35;
ctx.lineCap = "round";
ctx.stroke();
ctx.strokeStyle = pal.head;
ctx.lineWidth = 27;
ctx.stroke();
drawHead();
ctx.restore();
}
function drawParticles() {
const pal = palettes[currentStyle];
ctx.save();
for (const ring of state.rings) {
const age = 1 - ring.life / ring.max;
ctx.globalAlpha = (1 - age) * .65;
ctx.strokeStyle = pal.sole;
ctx.lineWidth = 5 * (1 - age) + 1;
ctx.beginPath();
ctx.ellipse(ring.x, ring.y, 12 + age * 62, 4 + age * 13, 0, 0, TAU);
ctx.stroke();
}
for (const p of state.particles) {
ctx.globalAlpha = clamp(p.life / p.max, 0, 1);
ctx.save();
ctx.translate(p.x, p.y);
ctx.rotate(p.rot);
ctx.fillStyle = p.color;
ctx.fillRect(-p.size / 2, -p.size / 2, p.size, p.size);
ctx.restore();
}
ctx.restore();
}
function draw(time) {
ctx.setTransform(canvas.width / W, 0, 0, canvas.height / H, 0, 0);
ctx.clearRect(0, 0, W, H);
ctx.drawImage(backdrop, 0, 0, W, H);
drawLights(time);
drawFloorAndShadow();
drawMotion();
drawBird(time);
drawParticles();
}
function updateHud() {
const phase = currentPhase.toUpperCase();
if (phaseName.textContent !== phase) phaseName.textContent = phase;
if (moveName.textContent !== currentClip.name.toUpperCase()) moveName.textContent = currentClip.name.toUpperCase();
const meta = `${styles[currentStyle].label} · take ${String(takeCount).padStart(2, "0")}`;
if (moveMeta.textContent !== meta) moveMeta.textContent = meta;
const lamp = Math.floor(totalBeats) % 4;
if (lamp !== lastBeatLamp) {
beatLamps.forEach((el, i) => el.classList.toggle("on", i === lamp));
lastBeatLamp = lamp;
}
}
function applyPalette() {
const pal = palettes[currentStyle];
document.documentElement.style.setProperty("--accent", pal.sun);
document.documentElement.style.setProperty("--blue", pal.body);
}
function setStyle(name) {
if (!styles[name]) return;
currentStyle = name;
applyPalette();
buildBackdrop();
pendingPlaylist = styles[name].moves.slice();
styleButtons.forEach(button => {
button.setAttribute("aria-pressed", String(button.dataset.style === name));
});
flashStatus(`${styles[name].label} pocket loaded for the next phrase.`);
}
function callMove() {
let id = moveSelect.value;
if (id === "auto") {
const choices = Object.keys(clips).filter(move => move !== currentClip.id);
id = choices[Math.floor(Math.random() * choices.length)];
}
if (!clips[id]) return;
queuedMove = id;
flashStatus(`${clips[id].name} queued for the next phrase.`);
callButton.textContent = "Called!";
callButton.classList.add("called");
window.setTimeout(() => {
callButton.textContent = "Call it";
callButton.classList.remove("called");
}, 650);
}
function togglePause() {
paused = !paused;
pauseButton.textContent = paused ? "Resume" : "Pause";
pauseButton.setAttribute("aria-pressed", String(paused));
stage.classList.toggle("is-paused", paused);
flashStatus(paused ? "Music held. The bird is waiting." : "Back in the pocket.");
}
styleButtons.forEach(button => {
button.addEventListener("click", () => setStyle(button.dataset.style));
});
tempo.addEventListener("input", () => {
currentBpm = Number(tempo.value);
bpmValue.textContent = `${currentBpm} BPM`;
const percent = ((currentBpm - 72) / (148 - 72)) * 100;
tempo.style.setProperty("--fill", `${percent}%`);
});
callButton.addEventListener("click", callMove);
pauseButton.addEventListener("click", togglePause);
canvas.addEventListener("click", callMove);
document.addEventListener("keydown", event => {
const tag = event.target.tagName;
if (["INPUT", "SELECT", "BUTTON"].includes(tag)) return;
if (event.code === "Space") {
event.preventDefault();
callMove();
} else if (event.key.toLowerCase() === "p") {
event.preventDefault();
togglePause();
}
});
function resize() {
const rect = canvas.getBoundingClientRect();
if (!rect.width || !rect.height) return;
const dpr = Math.min(window.devicePixelRatio || 1, 2);
canvas.width = Math.round(rect.width * dpr);
canvas.height = Math.round(rect.height * dpr);
}
if ("ResizeObserver" in window) {
new ResizeObserver(resize).observe(stage);
} else {
window.addEventListener("resize", resize);
}
let totalTime = 0;
let lastTime = performance.now();
function frame(now) {
const dt = Math.min(.05, (now - lastTime) / 1000);
lastTime = now;
totalTime += dt;
if (!paused) update(dt);
draw(totalTime);
updateHud();
requestAnimationFrame(frame);
}
applyPalette();
buildBackdrop();
resize();
requestAnimationFrame(frame);
})();
</script>
</body>
</html>