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 Bleep, a small, overconfident house pigeon in borrowed high-tops, dancing like he has a whole basement to win. I made him a tactile cut-paper screen print: warm paper, blue-black contour ink, cobalt feathers, vermilion shoes, and one dramatic red sun.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta name="theme-color" content="#eee5d5">
<title>No brakes. Just wing. — Bleep</title>
<style>
:root {
--paper: #eee5d5;
--white: #faf5e9;
--ink: #172f37;
--red: #ef513b;
--blue: #365bd0;
--line: rgba(23,47,55,.25);
--mono: "Courier New", Courier, monospace;
--display: Impact, Haettenschweiler, "Arial Narrow Bold", sans-serif;
}
* { box-sizing: border-box; }
html { background: var(--paper); }
body {
margin: 0;
color: var(--ink);
background:
radial-gradient(.65px at 3px 3px, rgba(23,47,55,.15) .6px, transparent .8px) 0 0 / 7px 7px,
var(--paper);
font-family: Arial, Helvetica, sans-serif;
}
body::before {
content: "";
position: fixed;
inset: 0 auto 0 0;
width: 7px;
background: var(--red);
z-index: 10;
}
button, input { font: inherit; }
button { color: inherit; }
button, label, input { -webkit-tap-highlight-color: transparent; }
button { cursor: pointer; }
button:focus-visible, input:focus-visible {
outline: 3px solid var(--blue);
outline-offset: 4px;
}
button:disabled { cursor: not-allowed; opacity: .5; }
::selection { color: var(--white); background: var(--blue); }
.page { max-width: 1320px; margin: auto; padding: 0 36px 24px 43px; }
.masthead {
min-height: 82px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 24px;
border-bottom: 1px solid var(--ink);
}
.wordmark { display: flex; align-items: center; gap: 12px; }
.wordmark svg { width: 37px; height: 37px; flex: none; }
.wordmark strong {
display: block;
font-size: 14px;
letter-spacing: .1em;
}
.wordmark small {
display: block;
margin-top: 3px;
font: 9px var(--mono);
letter-spacing: .12em;
}
.edition {
display: flex;
align-items: center;
gap: 28px;
font: 10px/1.6 var(--mono);
letter-spacing: .06em;
}
.edition .status {
display: flex;
align-items: center;
gap: 8px;
}
.status-dot {
width: 7px;
height: 7px;
border-radius: 50%;
background: var(--red);
box-shadow: 0 0 0 4px rgba(239,81,59,.1);
}
#sound {
border: 0;
border-bottom: 1px solid var(--ink);
border-radius: 0;
background: transparent;
padding: 8px 0;
font: 10px var(--mono);
letter-spacing: .06em;
}
#sound[aria-pressed="true"] { color: var(--red); border-color: var(--red); }
#sound .sound-icon { font-size: 16px; margin-left: 12px; }
.intro {
display: flex;
justify-content: space-between;
align-items: flex-end;
gap: 30px;
padding: 25px 0 27px;
}
.eyebrow {
margin: 0 0 10px;
font: 10px var(--mono);
letter-spacing: .12em;
text-transform: uppercase;
}
h1 {
margin: 0;
font: clamp(54px, 7.1vw, 104px)/.84 var(--display);
letter-spacing: -.015em;
text-transform: uppercase;
}
h1 span { color: var(--red); }
.lede {
max-width: 255px;
margin: 0 0 3px;
padding-left: 18px;
border-left: 3px solid var(--ink);
font-size: 13px;
line-height: 1.65;
}
.lede strong { font-weight: 700; }
.layout {
display: grid;
grid-template-columns: minmax(0, 1fr) 282px;
border: 1.5px solid var(--ink);
}
.stage {
position: relative;
min-width: 0;
height: clamp(510px, 49vw, 650px);
overflow: hidden;
border-right: 1.5px solid var(--ink);
background: var(--paper);
isolation: isolate;
}
canvas { position: absolute; inset: 0; width: 100%; height: 100%; display: block; }
.stage.paused canvas { cursor: grab; touch-action: none; }
.stage.dragging canvas { cursor: grabbing; }
.stage-top {
position: absolute;
inset: 20px 20px auto;
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
pointer-events: none;
font: 9px var(--mono);
letter-spacing: .08em;
}
.stage-top .floor-state {
display: flex;
align-items: center;
gap: 7px;
background: rgba(250,245,233,.85);
border: 1px solid var(--ink);
padding: 8px 10px;
}
.floor-state::before {
content: "";
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--red);
}
.paused .floor-state::before { background: var(--blue); border-radius: 0; }
.reposition {
position: absolute;
left: 50%;
top: 58px;
transform: translateX(-50%) rotate(-2deg);
padding: 9px 14px;
border: 1px solid var(--ink);
background: var(--blue);
color: white;
font: bold 10px var(--mono);
letter-spacing: .08em;
white-space: nowrap;
opacity: 0;
pointer-events: none;
transition: opacity .15s;
}
.paused .reposition { opacity: 1; }
.move-card {
position: absolute;
bottom: 20px;
left: 22px;
padding: 2px 14px 2px 12px;
border-left: 4px solid var(--red);
background: rgba(250,245,233,.92);
pointer-events: none;
}
#move-number {
display: block;
margin-bottom: 4px;
font: 9px var(--mono);
letter-spacing: .08em;
}
.move-card h2 {
margin: 0;
font: 36px/.98 var(--display);
letter-spacing: .015em;
text-transform: uppercase;
}
.move-card p {
max-width: 245px;
margin: 7px 0 0;
font: 10px/1.5 var(--mono);
}
.tape-label {
position: absolute;
right: 19px;
bottom: 20px;
width: 143px;
padding: 10px 11px;
border: 1px solid var(--ink);
background: rgba(250,245,233,.92);
transform: rotate(2deg);
pointer-events: none;
}
.tape-label::before {
content: "";
position: absolute;
top: -5px;
left: 52px;
width: 35px;
height: 12px;
background: rgba(239,81,59,.55);
transform: rotate(-8deg);
}
.tape-row {
display: flex;
justify-content: space-between;
gap: 7px;
padding: 4px 0;
border-bottom: 1px solid var(--line);
font: 9px var(--mono);
}
.tape-row:last-child { border: 0; }
#contact { font-size: 8px; max-width: 92px; text-align: right; }
.deck { padding: 23px 20px 19px; display: flex; flex-direction: column; }
.deck-heading {
display: flex;
justify-content: space-between;
font: 9px var(--mono);
letter-spacing: .08em;
}
.deck h2 {
margin: 15px 0 20px;
font: 43px/.9 var(--display);
text-transform: uppercase;
letter-spacing: .01em;
}
.tempo-label {
display: flex;
justify-content: space-between;
align-items: center;
font: 10px var(--mono);
letter-spacing: .06em;
}
#tempo-value { font: 31px/.9 var(--display); letter-spacing: .02em; }
.tempo-label small { font: 8px var(--mono); margin-left: 3px; }
#tempo {
display: block;
width: 100%;
height: 20px;
margin: 14px 0 4px;
appearance: none;
background: transparent;
cursor: ew-resize;
}
#tempo::-webkit-slider-runnable-track { height: 4px; background: var(--ink); }
#tempo::-moz-range-track { height: 4px; background: var(--ink); }
#tempo::-webkit-slider-thumb {
appearance: none;
width: 17px;
height: 22px;
margin-top: -9px;
background: var(--red);
border: 1.5px solid var(--ink);
border-radius: 0;
}
#tempo::-moz-range-thumb {
width: 17px;
height: 22px;
background: var(--red);
border: 1.5px solid var(--ink);
border-radius: 0;
}
.tempo-marks { display: flex; justify-content: space-between; font: 8px var(--mono); opacity: .65; }
.rule {
height: 1px;
margin: 19px 0 12px;
background: var(--ink);
}
.set-label {
display: flex;
justify-content: space-between;
margin-bottom: 9px;
font: 9px var(--mono);
letter-spacing: .05em;
}
.cues { display: grid; gap: 5px; }
.cue {
width: 100%;
min-height: 35px;
display: grid;
grid-template-columns: 25px 1fr 12px;
align-items: center;
gap: 8px;
padding: 8px 10px;
border: 1px solid var(--ink);
border-radius: 0;
background: transparent;
text-align: left;
font-size: 12px;
transition: background .12s, color .12s;
}
.cue:hover { background: rgba(54,91,208,.1); }
.cue b { font: 9px var(--mono); }
.cue i { font-style: normal; opacity: .5; }
.cue.active { background: var(--ink); color: var(--white); }
.cue.active b { color: #ff927d; }
.full-cue { min-height: 29px; grid-template-columns: 25px 1fr 12px; font: 9px var(--mono); }
.transport { display: grid; grid-template-columns: 1fr 43px; gap: 6px; margin-top: 12px; }
.transport button { min-height: 43px; border: 1.5px solid var(--ink); border-radius: 0; }
#call {
background: var(--red);
font: 12px var(--mono);
letter-spacing: .025em;
}
#call span { margin-left: 14px; font-size: 19px; }
#pause { background: transparent; font: 17px var(--mono); }
#pause:hover, #call:hover { filter: brightness(1.06); }
#restart {
margin-top: 7px;
padding: 6px 0;
border: 0;
border-bottom: 1px solid var(--line);
border-radius: 0;
background: transparent;
text-align: left;
font: 9px var(--mono);
}
.tape-control {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
margin-top: 17px;
cursor: pointer;
font: 9px var(--mono);
}
.switch {
position: relative;
width: 29px;
height: 16px;
flex: none;
background: var(--ink);
}
.switch::after {
content: "";
position: absolute;
width: 10px;
height: 10px;
top: 3px;
left: 3px;
background: var(--white);
transition: transform .15s;
}
.tape-control input { position: absolute; opacity: 0; }
.tape-control input:checked + .switch::after { transform: translateX(13px); background: var(--red); }
.tape-control input:focus-visible + .switch { outline: 3px solid var(--blue); outline-offset: 4px; }
.keys { margin-top: 16px; color: rgba(23,47,55,.75); font: 9px/1.7 var(--mono); }
kbd {
padding: 2px 4px;
border: 1px solid var(--ink);
font: 8px var(--mono);
color: var(--ink);
}
.deck-note {
margin: auto 0 0;
padding-top: 17px;
font: 8px/1.6 var(--mono);
letter-spacing: .04em;
}
.deck-note span { color: var(--red); }
.footer {
display: flex;
justify-content: space-between;
gap: 20px;
padding-top: 14px;
font: 8px/1.5 var(--mono);
letter-spacing: .08em;
}
.footer span:last-child { text-align: right; }
@media (max-width: 900px) {
.page { padding: 0 20px 20px 27px; }
.layout { grid-template-columns: 1fr; }
.stage { height: clamp(500px, 76vw, 650px); border-right: 0; border-bottom: 1.5px solid var(--ink); }
.deck { display: grid; grid-template-columns: 1fr 1fr; column-gap: 25px; }
.deck-heading, .deck h2, .tempo-box, .set-list, .transport-box, .extras, .deck-note { grid-column: 1; }
.deck h2 { margin-bottom: 0; }
.tempo-box { align-self: center; }
.extras { grid-column: 2; grid-row: 3 / span 2; align-self: center; }
.deck-note { padding-top: 0; }
.cues { grid-template-columns: 1fr 1fr; }
.full-cue { grid-column: 1 / -1; }
.edition > span:first-child { display: none; }
}
@media (max-width: 540px) {
.page { padding-left: 20px; padding-right: 14px; }
.masthead { min-height: 70px; }
.wordmark small { font-size: 8px; }
.edition { gap: 13px; }
.intro { display: block; padding: 24px 0; }
h1 { font-size: 65px; }
.lede { margin-top: 21px; max-width: 290px; font-size: 12px; }
.stage { height: 480px; }
.stage-top { inset: 14px 12px auto; font-size: 8px; }
.move-card { left: 13px; bottom: 15px; max-width: 180px; }
.move-card h2 { font-size: 29px; }
.move-card p { font-size: 9px; max-width: 170px; }
.tape-label { right: 10px; bottom: 15px; width: 112px; padding: 7px; }
.tape-row { font-size: 8px; }
#contact { font-size: 7px; max-width: 72px; }
.deck { display: flex; padding: 20px 17px; }
.deck h2 { font-size: 45px; }
.cues { grid-template-columns: 1fr 1fr; }
.footer { font-size: 7px; }
}
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after { transition-duration: 0s !important; }
}
</style>
</head>
<body>
<main class="page">
<header class="masthead">
<div class="wordmark">
<svg viewBox="0 0 40 40" aria-hidden="true">
<path fill="#ef513b" d="M0 0h40v40H0z"/>
<path fill="#172f37" d="M10 26c3-11 13-15 22-10-3 0-4 2-4 4 0 5-6 9-12 8l4 4-14-6Z"/>
<circle cx="25" cy="20" r="1.5" fill="#faf5e9"/>
</svg>
<div>
<strong>PERCH / PRACTICE</strong>
<small>PAPER PIGEON RADIO</small>
</div>
</div>
<div class="edition">
<span>MOTION STUDY 004<br>LOOP NO. 01</span>
<span class="status"><i class="status-dot"></i>DRAWN LIVE</span>
<button id="sound" type="button" aria-pressed="false" title="Toggle a synthesized breakbeat (M)">
SOUND OFF <span class="sound-icon" aria-hidden="true">◌</span>
</button>
</div>
</header>
<section class="intro">
<div>
<p class="eyebrow">An unnecessarily serious study in small-bird choreography</p>
<h1>No brakes.<br><span>Just wing.</span></h1>
</div>
<p class="lede">
Meet <strong>Bleep</strong>: a pigeon in borrowed kicks,
treating a blank dance floor like the main event.
Watch the feet. Ignore the beak.
</p>
</section>
<div class="layout">
<section class="stage" id="stage" aria-label="Bleep, an animated paper pigeon breakdancing">
<canvas id="canvas" role="img" aria-label="A cream paper pigeon with cobalt wings, orange beak, and red high-tops performing top rock, footwork, air flares, and freezing poses on a red dance circle."></canvas>
<div class="stage-top">
<span>BLEEP / CUT-OUT CREW</span>
<span class="floor-state" id="floor-state">FLOOR IS LIVE</span>
</div>
<div class="reposition">PAUSED — DRAG TO REPOSE</div>
<div class="move-card" aria-live="polite" aria-atomic="true">
<span id="move-number">01 / 04 — TOP ROCK</span>
<h2 id="move-title">Clockwork.</h2>
<p id="move-description">The chest leads. The feet follow. The beat is not optional.</p>
</div>
<div class="tape-label" aria-label="Dance timing">
<div class="tape-row"><span>BEAT</span><b id="beat-count">01 / 04</b></div>
<div class="tape-row"><span>TEMPO</span><b id="tape-tempo">108 BPM</b></div>
<div class="tape-row"><span>CONTACT</span><b id="contact">BOTH FEET</b></div>
</div>
</section>
<aside class="deck" aria-label="Dance controls">
<div class="deck-heading"><span>THE CUE SHEET</span><span>004 / ∞</span></div>
<h2>You run<br>the floor.</h2>
<div class="tempo-box">
<label class="tempo-label" for="tempo">
<span>HOUSE TEMPO</span>
<span><output id="tempo-value" for="tempo">108</output><small>BPM</small></span>
</label>
<input id="tempo" type="range" min="80" max="148" value="108" step="1">
<div class="tempo-marks"><span>80 / SLOW HEAT</span><span>148 / FULL SEND</span></div>
</div>
<div class="rule"></div>
<div class="set-list">
<div class="set-label"><span>SELECT A ROUTINE</span><span>↘</span></div>
<div class="cues" role="group" aria-label="Choreography routines">
<button class="cue full-cue active" type="button" data-cue="full" aria-pressed="true">
<b>∞</b><span>Full set / call the whole crew</span><i>↗</i>
</button>
<button class="cue" type="button" data-cue="top" aria-pressed="false">
<b>01</b><span>Top rock</span><i>↗</i>
</button>
<button class="cue" type="button" data-cue="ground" aria-pressed="false">
<b>02</b><span>Six-step</span><i>↗</i>
</button>
<button class="cue" type="button" data-cue="flare" aria-pressed="false">
<b>03</b><span>Air flare</span><i>↗</i>
</button>
<button class="cue" type="button" data-cue="freeze" aria-pressed="false">
<b>04</b><span>Freezes</span><i>↗</i>
</button>
</div>
</div>
<div class="transport-box">
<div class="transport">
<button id="call" type="button" title="Call the next move (Space)">CALL NEXT <span>↗</span></button>
<button id="pause" type="button" aria-pressed="false" aria-label="Pause dance" title="Pause or resume (P)">Ⅱ</button>
</div>
<button id="restart" type="button">RE-CENTRE & START OVER ↺</button>
</div>
<div class="extras">
<label class="tape-control">
<span>SHOW MOVE TAPE</span>
<input id="tape" type="checkbox" checked>
<i class="switch" aria-hidden="true"></i>
</label>
<p class="keys"><kbd>SPACE</kbd> call next <kbd>P</kbd> hold<br><kbd>1–4</kbd> choose a move <kbd>← →</kbd> tempo</p>
</div>
<p class="deck-note"><span>● NO AUTO-PILOT.</span> FOUR MOVES.<br>ONE VERY COMMITTED BIRD.</p>
</aside>
</div>
<footer class="footer">
<span>NO SPRITES. NO ASSETS. JUST BAD HABITS.</span>
<span>PAUSE, THEN DRAG THE FLOOR TO FIND A NEW BALANCE.</span>
</footer>
</main>
<script>
(() => {
"use strict";
const $ = id => document.getElementById(id);
const canvas = $("canvas");
const ctx = canvas.getContext("2d");
const stage = $("stage");
const tempoInput = $("tempo");
const tapeInput = $("tape");
const reduceMotion = matchMedia("(prefers-reduced-motion: reduce)").matches;
const W = 1000, H = 680, FLOOR = 500;
const INK = "#172f37", PAPER = "#faf5e9", RED = "#ef513b";
const BLUE = "#365bd0", TAU = Math.PI * 2;
const rad = n => n * Math.PI / 180;
const lerp = (a, b, t) => a + (b - a) * t;
const clamp = (n, a, b) => Math.max(a, Math.min(b, n));
const smooth = t => { t = clamp(t, 0, 1); return t * t * (3 - 2 * t); };
const mixPoint = (a, b, t) => ({ x: lerp(a.x, b.x, t), y: lerp(a.y, b.y, t) });
let bpm = reduceMotion ? 90 : 108;
tempoInput.value = bpm;
$("tempo-value").textContent = bpm;
$("tape-tempo").textContent = bpm + " BPM";
if (reduceMotion) tapeInput.checked = false;
let cssW = 1000, cssH = 650, dpr = 1;
function resize() {
cssW = stage.clientWidth || 1000;
cssH = stage.clientHeight || 650;
dpr = Math.min(devicePixelRatio || 1, 2);
canvas.width = Math.round(cssW * dpr);
canvas.height = Math.round(cssH * dpr);
}
new ResizeObserver(resize).observe(stage);
resize();
// Printed-paper and halftone tiles are generated once, never fetched.
let seed = 72831;
function random() {
seed = (Math.imul(seed, 1664525) + 1013904223) | 0;
return (seed >>> 0) / 4294967296;
}
const grainTile = document.createElement("canvas");
grainTile.width = grainTile.height = 128;
const grainCtx = grainTile.getContext("2d");
const grain = grainCtx.createImageData(128, 128);
for (let i = 0; i < grain.data.length; i += 4) {
const dark = random() > .48;
grain.data[i] = dark ? 23 : 255;
grain.data[i + 1] = dark ? 42 : 247;
grain.data[i + 2] = dark ? 48 : 233;
grain.data[i + 3] = Math.floor(random() * 23);
}
grainCtx.putImageData(grain, 0, 0);
const dotTile = document.createElement("canvas");
dotTile.width = dotTile.height = 10;
const dotCtx = dotTile.getContext("2d");
dotCtx.fillStyle = "rgba(15,37,49,.27)";
dotCtx.beginPath();
dotCtx.arc(2, 2, .9, 0, TAU);
dotCtx.arc(7, 6, .65, 0, TAU);
dotCtx.fill();
const paperPattern = ctx.createPattern(grainTile, "repeat");
const dotPattern = ctx.createPattern(dotTile, "repeat");
const backdrop = document.createElement("canvas");
backdrop.width = W; backdrop.height = H;
const bg = backdrop.getContext("2d");
function buildBackdrop() {
bg.fillStyle = PAPER;
bg.fillRect(0, 0, W, H);
bg.save();
bg.globalAlpha = .035;
bg.fillStyle = BLUE;
bg.beginPath();
bg.moveTo(0, 0); bg.lineTo(400, 0); bg.lineTo(0, 410);
bg.closePath(); bg.fill();
bg.beginPath();
bg.moveTo(W, H); bg.lineTo(650, H); bg.lineTo(W, 235);
bg.closePath(); bg.fill();
bg.restore();
bg.strokeStyle = RED;
bg.lineWidth = 1.4;
bg.globalAlpha = .45;
bg.beginPath(); bg.arc(506, 292, 224, 0, TAU); bg.stroke();
bg.beginPath(); bg.arc(506, 292, 232, -.2, Math.PI * 1.5); bg.stroke();
bg.globalAlpha = 1;
bg.fillStyle = RED;
bg.beginPath(); bg.arc(506, 292, 207, 0, TAU); bg.fill();
bg.save();
bg.fillStyle = PAPER;
bg.globalAlpha = .22;
bg.font = "900 350px Arial, sans-serif";
bg.fillText("04", 563, 420);
bg.restore();
bg.save();
bg.translate(0, 352);
bg.rotate(-.17);
bg.globalAlpha = .94;
bg.fillStyle = PAPER;
bg.fillRect(-80, 0, 1180, 27);
bg.globalAlpha = .25;
bg.fillStyle = INK;
bg.fillRect(-80, 29, 1180, 3);
bg.restore();
// A paper stage, not a shiny 3D room.
bg.fillStyle = "#d8d7c4";
bg.strokeStyle = INK;
bg.lineWidth = 1.3;
bg.beginPath(); bg.ellipse(510, 499, 292, 53, 0, 0, TAU);
bg.fill(); bg.globalAlpha = .3; bg.stroke();
bg.globalAlpha = .16;
bg.setLineDash([3, 8]);
bg.beginPath(); bg.ellipse(510, 495, 329, 70, 0, 0, TAU); bg.stroke();
bg.setLineDash([]);
bg.save();
bg.globalAlpha = .12;
bg.lineWidth = 1;
for (let i = 0; i < 12; i++) {
const x = i * 100 - 80;
bg.beginPath(); bg.moveTo(500, 480); bg.lineTo(x, H); bg.stroke();
}
bg.restore();
function star(x, y, r) {
bg.save();
bg.translate(x, y);
bg.fillStyle = INK;
bg.beginPath();
for (let i = 0; i < 8; i++) {
const a = i * Math.PI / 4, rr = i % 2 ? r * .18 : r;
if (i === 0) bg.moveTo(Math.cos(a) * rr, Math.sin(a) * rr);
else bg.lineTo(Math.cos(a) * rr, Math.sin(a) * rr);
}
bg.closePath(); bg.fill();
bg.restore();
}
star(212, 285, 15);
star(793, 293, 10);
star(768, 183, 6);
bg.globalAlpha = .38;
bg.fillStyle = INK;
bg.font = "9px 'Courier New', monospace";
bg.save();
bg.translate(798, 399);
bg.rotate(Math.PI / 2);
bg.fillText("FOOTWORK IS A LANGUAGE", 0, 0);
bg.restore();
bg.globalAlpha = .2;
bg.fillStyle = INK;
bg.font = "9px 'Courier New', monospace";
bg.fillText("GROUND / TRUTH", 58, 579);
bg.fillText("WING / OPTIONAL", 806, 579);
bg.globalAlpha = 1;
bg.strokeStyle = INK;
bg.lineWidth = 1.5;
bg.strokeRect(14, 14, W - 28, H - 28);
bg.strokeStyle = RED;
bg.lineWidth = 3;
for (const [x, y, sx, sy] of [[14,14,1,1],[986,14,-1,1],[14,666,1,-1],[986,666,-1,-1]]) {
bg.beginPath(); bg.moveTo(x, y + 18 * sy); bg.lineTo(x, y);
bg.lineTo(x + 18 * sx, y); bg.stroke();
}
bg.globalAlpha = .15;
bg.fillStyle = paperPattern;
bg.fillRect(0, 0, W, H);
}
buildBackdrop();
function P(x, y, lean, stance, wing, extra = {}) {
const left = {
x: stance === "L" ? 486 : stance === "R" ? 427 : 486,
y: FLOOR, lock: stance === "L" || stance === "B"
};
const right = {
x: stance === "R" ? 544 : stance === "L" ? 577 : 544,
y: FLOOR, lock: stance === "R" || stance === "B"
};
return {
x, y, lean, stance, wing, sx: 1,
headX: -47, headY: -91, headRot: -.08, headOpen: .04,
hand: null, impact: 0, ease: "settle",
label: stance === "L" ? "LEFT FOOT" :
stance === "R" ? "RIGHT FOOT" :
stance === "B" ? "BOTH FEET" : "IN THE AIR",
feet: [left, right], ...extra
};
}
const home = () => P(500, 295, 0, "B", .12, { headY: -91 });
function makeTop() {
const poses = [
P(500,295,0,"B",.12,{impact:.25}),
P(491,301,-12,"L",-.30,{impact:.75}),
P(499,289,8,"R",.32,{impact:.65}),
P(515,298,-9,"R",.58,{impact:.8,headX:-42,headOpen:.23}),
P(506,281,14,"L",.66,{impact:1,headX:-53,headY:-97,headRot:-.2}),
P(499,294,-11,"L",-.18,{impact:.7}),
P(488,282,12,"R",-.36,{impact:.75,headX:-39,headY:-85,headRot:.15}),
P(502,296,0,"B",.22,{impact:.4})
];
return poses;
}
function makeGround() {
const poses = [];
const specs = [
[466,374,-9,"L",440,558,416],
[479,366,7,"L",468,503,416],
[501,376,-10,"L",492,550,430],
[483,360,9,"L",471,492,418],
[500,370,-8,"R",414,501,590],
[520,365,9,"R",488,542,590],
[526,357,-8,"R",485,542,579],
[501,331,0,"B",486,544,0]
];
specs.forEach((s,i) => {
const [x,y,lean,stance,lx,rx,handX] = s;
poses.push(P(x,y,lean,stance,-.35,{
feet: [{x:lx,y:FLOOR,lock:stance==="L"||stance==="B"},
{x:rx,y:FLOOR,lock:stance==="R"||stance==="B"}],
hand: i < 7 ? {x:handX,y:FLOOR,side:handX<500?-1:1} : null,
headX: -48, headY: -78, headRot: lean * .003,
impact: i === 0 || i === 3 || i === 6 ? .8 : .35,
label: i < 7 ? "FOOT + WING" : "WEIGHT UP"
}));
});
return poses;
}
function makeFlare() {
return Array.from({length:4}, (_,i) => {
const a = i * Math.PI / 2;
return P(531,334,-10,"R",.9,{
sx: Math.cos(a),
feet: [
{x:531 + Math.cos(a)*83, y:420 + Math.sin(a)*70, lock:false},
{x:544,y:FLOOR,lock:true}
],
headX:-38, headY:-87, headRot:-.23, headOpen:.32,
impact:i === 0 ? .8 : 0,
label:"RIGHT TOE / FLARE"
});
});
}
function makeFreeze() {
return [
P(465,403,8,"-",-.8,{
beats:2, ease:"drop",
feet:[{x:495,y:423,lock:false},{x:544,y:408,lock:false}],
hand:{x:420,y:FLOOR,side:-1},
headX:-37,headY:67,headRot:-.22,headOpen:.26,
impact:.65,label:"HEAD + WING",freezeLabel:"Baby freeze."
}),
P(465,403,8,"-",-.8,{
beats:4,headX:-37,headY:67,headRot:-.22,headOpen:.26,
hand:{x:420,y:FLOOR,side:-1},
feet:[{x:495,y:423,lock:false},{x:544,y:408,lock:false}],
label:"HEAD + WING",freezeLabel:"Baby freeze."
}),
P(492,358,-16,"R",-.72,{
beats:2,ease:"drop",impact:.6,
feet:[{x:509,y:393,lock:false},{x:548,y:FLOOR,lock:true}],
hand:{x:424,y:FLOOR,side:-1},
headX:-43,headY:-70,headRot:-.31,headOpen:.08,
label:"KNEE + WING",freezeLabel:"Chair freeze."
}),
P(492,358,-16,"R",-.72,{
beats:2,headX:-43,headY:-70,headRot:-.31,
hand:{x:424,y:FLOOR,side:-1},
feet:[{x:509,y:393,lock:false},{x:548,y:FLOOR,lock:true}],
label:"KNEE + WING",freezeLabel:"Chair freeze."
}),
P(502,295,0,"B",.15,{beats:2,headX:-47,headY:-91,headRot:-.08})
];
}
const info = {
top: {
title:"Clockwork.", number:"01",
description:"The chest leads. The feet follow. The beat is not optional."
},
ground: {
title:"Low six.", number:"02",
description:"Circle the floor. Keep one piece of the bird in contact."
},
flare: {
title:"One-foot orbit.", number:"03",
description:"One toe in place. Everything else is up for grabs."
},
freeze: {
title:"Baby freeze.", number:"04",
description:"Head and wing planted. Four beats of absolutely nothing."
}
};
const order = ["top", "ground", "flare", "freeze"];
let cue = "full", move = "top", paused = false;
let timeline = null, segmentIndex = -1;
let beats = 0, simTime = 0;
let manual = { active:false, dragging:false, x:0, y:0 };
function updateCueUI() {
document.querySelectorAll("[data-cue]").forEach(button => {
const active = button.dataset.cue === cue;
button.classList.toggle("active", active);
button.setAttribute("aria-pressed", String(active));
});
}
function setMoveHUD() {
const m = info[move];
$("move-number").textContent = `${m.number} / 04 — ${order.map(k=>info[k].title.replace(".","").toUpperCase()).indexOf(m.title.replace(".","").toUpperCase())+1}`;
// Keep the numbering explicit rather than deriving it from a display title.
$("move-number").textContent = `${{top:"01",ground:"02",flare:"03",freeze:"04"}[move]} / 04 — ${{top:"TOP ROCK",ground:"SIX-STEP",flare:"AIR FLARE",freeze:"FREEZES"}[move]}`;
$("move-title").textContent = m.title;
$("move-description").textContent = m.description;
if (paused) $("move-description").textContent = "MANUAL BALANCE. DRAG THE BIRD; PRESS P TO LET THE MUSIC LEAD AGAIN.";
}
function setRecoveryHUD(next) {
$("move-number").textContent = "RECOVERY / RE-PLANT";
$("move-title").textContent = "Re-centre.";
$("move-description").textContent = `Follow the hands. Bring the feet home. Next: ${info[next].title.toLowerCase()}`;
}
function setFrozenHUD(label) {
$("move-title").textContent = label;
$("move-description").textContent = move === "flare"
? info.flare.description
: label === "Chair freeze."
? "Knee and wing loaded. Stay long enough to make it look intentional."
: info.freeze.description;
}
function setTimeline(kind, value) {
timeline = {kind, value, duration:0, segments:[]};
segmentIndex = -1;
beats = 0;
}
function beginMove(name) {
move = name;
const poses = ({
top:makeTop, ground:makeGround, flare:makeFlare, freeze:makeFreeze
})[name]();
setTimeline("move", poses);
let from = home(), end = 0;
poses.forEach(to => {
timeline.segments.push({
from, to, start:end, end:end+(to.beats || 2), hit:!!to.impact
});
end += to.beats || 2;
from = {...to, feet:to.feet.map(f=>({...f}))};
});
timeline.duration = end;
setMoveHUD();
}
function beginRecovery(next, quick = false) {
if (!next) return;
setTimeline("recovery", {
from:{
...home(),
sx:rig.x.sx,
headX:rig.hx.value, headY:rig.hy.value, headRot:rig.ha.value,
feet:feet.map(f=>({x:f.x,y:f.y})),
hand:plant ? {...plant} : null
},
to:home(),
duration:quick ? 1.5 : 1.25,
next
});
setRecoveryHUD(next);
}
const rig = {
x:{value:500,v:0}, y:{value:295,v:0}, lean:{value:0,v:0},
sx:{value:1,v:0}, wing:{value:.12,v:0},
hx:{value:-47,v:0}, hy:{value:-91,v:0}, ha:{value:-.08,v:0},
mouth:{value:.04,v:0}
};
const feet = [
{x:486,y:FLOOR,vx:0,vy:0,ax:486,ay:FLOOR,lock:true},
{x:544,y:FLOOR,vx:0,vy:0,ax:544,ay:FLOOR,lock:true}
];
const kick = {value:0,v:0};
let plant = null;
const particles = [], tape = [];
let lastTape = 0;
function spring(s, target, dt, stiffness, damping) {
s.v += (target - s.value) * stiffness * dt;
s.v *= Math.exp(-damping * dt);
s.value += s.v * dt;
}
function dust(x, y, amount, color = INK) {
const n = Math.round(3 + amount*6);
for (let i=0; i<n; i++) {
particles.push({
x:x+(random()-.5)*35,y:y-2,
vx:(random()-.5)*100,vy:-15-random()*65,
life:.3+random()*.25,max:.55,size:2+random()*4,
rot:random()*TAU,spin:(random()-.5)*8,color
});
}
if (particles.length > 80) particles.splice(0,particles.length-80);
}
function impact(goal) {
rig.y.v -= 30;
kick.v -= 1.9;
if (goal.hand) dust(goal.hand.x, goal.hand.y, .35, RED);
else feet.forEach((f,i) => {
if (goal.feet[i].lock) dust(f.ax, FLOOR, .55, i ? RED : BLUE);
});
}
function weightEase(t, kind) {
if (kind === "hold") return 1;
if (kind === "drop") {
if (t < .22) return -.055 * smooth(t/.22);
const u = (t-.22)/.78;
return -.055 + 1.055 * (1-Math.pow(1-u,3));
}
return smooth(t);
}
function samplePose() {
let result;
if (timeline.kind === "move") {
let index = timeline.segments.findIndex(s => beats < s.end);
if (index < 0) index = timeline.segments.length-1;
const s = timeline.segments[index];
if (index !== segmentIndex) {
segmentIndex = index;
if (s.hit) impact(s.to);
}
const raw = clamp((beats-s.start)/(s.end-s.start),0,1);
const u = weightEase(raw, s.to.ease);
const a = s.from, b = s.to;
result = {
...b,
x:lerp(a.x,b.x,u), y:lerp(a.y,b.y,u),
lean:lerp(a.lean,b.lean,u), sx:lerp(a.sx,b.sx,u),
wing:lerp(a.wing,b.wing,u),
headX:lerp(a.headX,b.headX,u), headY:lerp(a.headY,b.headY,u),
headRot:lerp(a.headRot,b.headRot,u), headOpen:lerp(a.headOpen,b.headOpen,u),
hand:null, feet:[]
};
for (let i=0; i<2; i++) {
const f0=a.feet[i], f1=b.feet[i];
const settle = smooth(clamp(raw/.3,0,1));
const travel = f1.lock ? settle : smooth(raw);
const dx=f1.x-f0.x, dy=f1.y-f0.y;
const lift = !f1.lock && f0.y>FLOOR-8 && f1.y>FLOOR-8
? Math.min(38, 12+Math.hypot(dx,dy)*.12) : 0;
result.feet.push({
x:lerp(f0.x,f1.x,travel),
y:lerp(f0.y,f1.y,travel)-Math.sin(Math.PI*raw)*lift,
lock:f1.lock
});
}
const handU=smooth(raw);
if (a.hand && b.hand) {
result.hand={
x:lerp(a.hand.x,b.hand.x,handU),
y:FLOOR,side:b.hand.side
};
} else result.hand=b.hand;
if (move === "freeze" && b.freezeLabel) {
const title=b.freezeLabel;
if ($("move-title").textContent !== title) setFrozenHUD(title);
}
} else {
const r=timeline.value, p=clamp(beats/r.duration,0,1);
const u=smooth(p), a=r.from, b=r.to;
result={...b,
x:lerp(a.x,b.x,u),
y:lerp(a.y,b.y,u)+8*Math.sin(Math.PI*clamp(p/.28,0,1))
-5*Math.sin(Math.PI*clamp((p-.65)/.35,0,1)),
lean:lerp(a.lean,b.lean,u),sx:lerp(a.sx,b.sx,u),
wing:lerp(a.wing,b.wing,u),
headX:lerp(a.headX,b.headX,u),headY:lerp(a.headY,b.headY,u),
headRot:lerp(a.headRot,b.headRot,u),headOpen:lerp(a.headOpen,b.headOpen,u),
hand:null,feet:[]
};
for (let i=0;i<2;i++) {
const f0=a.feet[i],f1=b.feet[i];
result.feet.push({
x:lerp(f0.x,f1.x,u),
y:lerp(f0.y,f1.y,u),
lock:p>.25
});
}
if (a.hand && p<.38) result.hand=mixPoint(a.hand,{x:500,y:FLOOR},smooth(p/.38));
}
if (paused && manual.active) {
result.x=clamp(result.x+manual.x*.65,425,580);
result.y=clamp(result.y+manual.y*.65,255,456);
result.lean=clamp(result.lean+manual.x*.065,-33,33);
const anchor=result.x<500?0:1;
result.feet[anchor].lock=true;
result.label="MANUAL BALANCE";
}
return result;
}
function updateRig(dt, goal) {
spring(rig.x,goal.x,dt,145,22);
spring(rig.y,goal.y,dt,140,22);
spring(rig.lean,goal.lean,dt,82,14);
spring(rig.sx,goal.sx,dt,20,12);
spring(rig.wing,goal.wing,dt,66,10.5);
spring(rig.hx,goal.headX,dt,105,15);
spring(rig.hy,goal.headY,dt,100,14);
spring(rig.ha,goal.headRot,dt,90,13);
spring(rig.mouth,goal.headOpen,dt,110,19);
spring(kick,0,dt,85,9);
for (let i=0;i<2;i++) {
const f=feet[i], g=goal.feet[i];
f.ax=g.x; f.ay=g.y;
if (g.lock) {
f.x=lerp(f.x,f.ax,1-Math.exp(-28*dt));
f.y=lerp(f.y,f.ay,1-Math.exp(-32*dt));
f.vx=f.vy=0;
} else {
spring({value:f.x,v:f.vx},f.ax,dt,145,21);
f.x+=(f.vx)*dt;
spring({value:f.y,v:f.vy},f.ay,dt,145,21);
f.y+=(f.vy)*dt;
}
}
plant=goal.hand;
for (let i=particles.length-1;i>=0;i--) {
const p=particles[i];
p.life-=dt;
p.x+=p.vx*dt; p.y+=p.vy*dt; p.vy+=230*dt; p.rot+=p.spin*dt;
if (p.life<=0) particles.splice(i,1);
}
if (tapeInput.checked && simTime-lastTape>.045) {
tape.push({x:rig.x.value,y:rig.y.value+130,age:0});
lastTape=simTime;
}
for (let i=tape.length-1;i>=0;i--) {
tape[i].age+=dt;
if (tape[i].age>.85) tape.splice(i,1);
}
const text=goal.label;
if ($("contact").textContent!==text) $("contact").textContent=text;
const beat=(Math.floor(beats)%4)+1;
const beatText=`0${beat} / 04`;
if ($("beat-count").textContent!==beatText) $("beat-count").textContent=beatText;
}
function bodyPosition() {
return {
x:rig.x.value+clamp(rig.x.v*.012,-4,4),
y:rig.y.value+clamp(rig.y.v*.014,-6,6)
};
}
function worldPoint(x,y) {
const b=bodyPosition(), a=rad(rig.lean.value);
x*=rig.sx.value;
return {
x:b.x+x*Math.cos(a)-y*Math.sin(a),
y:b.y+x*Math.sin(a)+y*Math.cos(a)
};
}
function localPoint(x,y) {
const b=bodyPosition(), a=-rad(rig.lean.value);
const dx=x-b.x,dy=y-b.y;
return {x:(dx*Math.cos(a)-dy*Math.sin(a))/rig.sx.value,
y:dx*Math.sin(a)+dy*Math.cos(a)};
}
function paint(c, path, fill, width=3.4, dots=false) {
const p=new Path2D(path);
c.fillStyle=fill; c.fill(p);
if (dots) {
c.save(); c.clip(p); c.fillStyle=dotPattern;
c.fillRect(-220,-220,500,500); c.restore();
}
if (width) {
c.strokeStyle=INK; c.lineWidth=width; c.lineJoin="round"; c.lineCap="round";
c.stroke(p);
}
}
function line(c,path,color,width) {
const p=new Path2D(path);
c.strokeStyle=color; c.lineWidth=width; c.lineCap="round"; c.lineJoin="round";
c.stroke(p);
}
const bodyPath = `
M -27 -39
C -47 -23 -51 14 -37 40
C -25 64 6 72 31 56
C 56 40 62 10 47 -17
C 31 -44 1 -53 -27 -39 Z`;
const wingPath = `
M -37 -15
C -19 -40 20 -34 43 0
C 60 24 55 47 32 57
C 2 52 -27 33 -39 8
C -45 -3 -45 -7 -37 -15 Z`;
function drawWing(c, rear=false) {
c.save();
c.translate(-14,-6);
c.rotate(-rig.wing.value*.26);
c.scale(1+Math.abs(rig.wing.value)*.10,1-Math.abs(rig.wing.value)*.12);
c.translate(14,6);
c.globalAlpha=rear?.65:1;
paint(c,wingPath,rear?"#244697":BLUE,rear?3:4,true);
c.globalAlpha=rear?.42:1;
line(c,"M -23 -2 Q 9 8 39 38",PAPER,2.3);
line(c,"M -16 15 Q 9 24 35 47",PAPER,2);
line(c,"M -5 29 Q 14 35 28 50",PAPER,1.5);
line(c,"M -21 -3 Q -7 -18 9 -12",INK,2.3);
c.globalAlpha=1;
c.fillStyle=PAPER;
c.beginPath(); c.arc(-24,-8,2.6,0,TAU); c.fill();
c.strokeStyle=INK; c.lineWidth=1.2; c.stroke();
c.restore();
}
function drawTail(c) {
c.save();
const flutter=rig.y.v*.035;
c.translate(28,34);
c.rotate(.03+flutter);
paint(c,"M 0 -9 C 35 -17 83 -8 120 13 L 133 38 C 88 28 43 25 2 15 Z","#dae0e8",3.2);
paint(c,"M 1 2 C 40 1 80 16 125 44 L 106 60 C 63 39 32 32 2 22 Z",BLUE,3.4,true);
paint(c,"M 1 17 C 34 21 72 38 105 66 L 78 76 C 45 53 21 38 0 29 Z",PAPER,3.2);
line(c,"M 17 9 L 108 29",INK,1.5);
line(c,"M 14 25 L 91 59",INK,1.5);
c.restore();
}
function drawNeckAndHead(c) {
const hx=rig.hx.value,hy=rig.hy.value;
const neck=`M -31 -27 C -45 -53 ${hx+9} ${hy-26} ${hx+7} ${hy+12}`;
line(c,neck,INK,49);
line(c,neck,PAPER,39);
line(c,`M -23 -25 Q -28 -49 ${hx+20} ${hy-17}`, "#dedbcb",4);
// A tiny red neckerchief is the only thing he packed.
c.save();
c.translate(-29,-31); c.rotate(-.35);
paint(c,"M -9 -4 L 10 -7 L 13 6 L -9 8 Z",RED,2.1);
c.restore();
c.save();
c.translate(hx,hy); c.rotate(rig.ha.value);
line(c,"M 2 -27 L 6 -39 L 14 -30 M 10 -29 L 19 -38 L 21 -26",INK,3);
paint(c,`
M -29 -10
C -32 -27 -12 -35 11 -31
C 30 -28 37 -8 27 13
C 18 30 -9 32 -25 17
C -32 11 -35 1 -29 -10 Z`,PAPER,3.8);
c.fillStyle="#e5e0d0";
c.beginPath(); c.ellipse(18,5,12,19,-.3,0,TAU); c.fill();
const open=clamp(rig.mouth.value,0,1);
paint(c,`M -26 -8 L -50 -1 L -26 7 Z`,"#ff8a36",2.8);
if (open>.07) {
paint(c,`M -27 2 L -45 5 L -28 10 Z`,"#b9362c",1.9);
} else {
line(c,"M -45 0 L -27 1",INK,1.4);
}
c.fillStyle=PAPER;
c.beginPath(); c.arc(-9,-10,12,0,TAU); c.fill();
c.strokeStyle=INK; c.lineWidth=2.2; c.stroke();
c.fillStyle=INK;
c.beginPath(); c.arc(-11,-10,4.3,0,TAU); c.fill();
c.fillStyle=PAPER;
c.beginPath(); c.arc(-12.5,-11.7,1.4,0,TAU); c.fill();
line(c,"M -24 -22 Q -11 -29 1 -24",INK,2.6);
line(c,"M -23 8 L -15 10",RED,3.5);
c.restore();
}
function solveKnee(start,end,bend) {
const dx=end.x-start.x,dy=end.y-start.y;
const d=Math.max(1,Math.min(Math.hypot(dx,dy),225.5));
const a=Math.atan2(dy,dx);
const offset=Math.acos(clamp((101*101+d*d-126*126)/(2*101*d),-1,1))*bend;
return {
x:start.x+Math.cos(a+offset)*101,
y:start.y+Math.sin(a+offset)*101
};
}
function drawLeg(c,index,rear=false) {
const f=feet[index], start=worldPoint(index===0?-15:17,43);
const end={x:f.x,y:f.y-11};
const knee=solveKnee(start,end,index===0?1:-1);
const path=`M ${start.x} ${start.y} Q ${knee.x} ${knee.y} ${end.x} ${end.y}`;
c.save(); c.globalAlpha=rear?.7:1;
line(c,path,INK,11);
line(c,path,rear?"#c4c7bf":PAPER,7);
c.translate(f.x,f.y+2);
if (rear) c.globalAlpha=.72;
paint(c,`
M -13 -10 L 7 -10 L 12 -22 L 21 -19 L 23 3
C 5 8 -12 8 -25 3 L -24 -4 Z`,RED,2.8);
line(c,"M -23 3 Q -2 8 22 3",PAPER,2.7);
line(c,"M 9 -12 L 15 -10 M 5 -5 L 12 -3",PAPER,1.5);
line(c,"M -16 3 L -25 0 M -16 5 L -25 6",INK,1.7);
c.restore();
}
function drawSupportWing() {
if (!plant) return;
const h=localPoint(plant.x,plant.y);
const side=plant.side;
const s={x:side<0?-10:15,y:-15};
const elbow={x:(s.x+h.x)*.5+(h.y-s.y)*.12,
y:(s.y+h.y)*.5+12};
const path=`M ${s.x} ${s.y} Q ${elbow.x} ${elbow.y} ${h.x} ${h.y}`;
line(ctx,path,INK,33);
line(ctx,path,BLUE,24);
line(ctx,path,"#244697",8);
line(ctx,`M ${lerp(s.x,h.x,.45)} ${lerp(s.y,h.y,.45)-5} Q ${lerp(s.x,h.x,.8)} ${lerp(s.y,h.y,.8)} ${h.x-3} ${h.y-2}`,PAPER,2.1);
ctx.save(); ctx.translate(h.x,h.y); ctx.rotate(side*.3);
paint(ctx,"M -11 -5 L 4 -11 L 14 -2 L 4 6 L -10 5 Z",PAPER,2.4);
line(ctx,"M -9 4 L -15 9 M -2 6 L 1 11 M 5 4 L 11 8",INK,2);
paint(ctx,"M -8 -7 L 8 -9 L 10 -3 L -8 -1 Z",RED,1.6);
ctx.restore();
}
function drawShadowAndContact() {
const lowest=Math.min(feet[0].y,feet[1].y);
const lift=clamp((FLOOR-lowest)/210,0,1);
const x=rig.x.value+rig.lean.value*32;
ctx.save();
ctx.globalAlpha=.16*(1-lift*.55);
ctx.fillStyle=INK;
ctx.beginPath(); ctx.ellipse(x,502,88-lift*30,12-lift*4,0,0,TAU); ctx.fill();
ctx.globalAlpha=.13;
feet.forEach((f,i)=>{
if (goalFeet[i].lock) {
ctx.beginPath(); ctx.ellipse(f.x,502,25,4,0,0,TAU); ctx.fill();
}
});
ctx.restore();
}
function drawTape() {
if (!tapeInput.checked || tape.length<2) return;
ctx.save();
ctx.lineWidth=2;
ctx.setLineDash([2,7]);
for (let i=1;i<tape.length;i++) {
const a=tape[i-1],b=tape[i];
ctx.globalAlpha=(1-b.age/.85)*.24;
ctx.strokeStyle=BLUE;
ctx.beginPath(); ctx.moveTo(a.x+3,b.y-4); ctx.lineTo(b.x+3,b.y-4); ctx.stroke();
}
ctx.setLineDash([]);
ctx.restore();
}
function drawFlareArc() {
if (move!=="flare" || timeline.kind!=="move") return;
const t=clamp(beats/timeline.duration,0,1);
const lift=Math.sin(t*TAU);
if (lift<-.2) return;
ctx.save();
ctx.strokeStyle=BLUE; ctx.lineWidth=2;
ctx.globalAlpha=.3;
ctx.setLineDash([3,5]);
ctx.beginPath();
ctx.ellipse(536,445,77,41,-.12,Math.PI,TAU);
ctx.stroke();
ctx.restore();
}
const bodyShape=new Path2D(bodyPath);
let goalFeet=[{lock:true},{lock:true}];
function draw() {
ctx.setTransform(dpr,0,0,dpr,0,0);
ctx.fillStyle=PAPER; ctx.fillRect(0,0,cssW,cssH);
const scale=Math.max(cssW/W,cssH/H);
ctx.save();
ctx.translate((cssW-W*scale)/2,(cssH-H*scale)/2);
ctx.scale(scale,scale);
ctx.lineCap="round"; ctx.lineJoin="round";
ctx.drawImage(backdrop,0,0);
drawTape();
drawShadowAndContact();
drawFlareArc();
for (const p of particles) {
ctx.save();
ctx.translate(p.x,p.y); ctx.rotate(p.rot);
ctx.globalAlpha=clamp(p.life/p.max,0,1);
ctx.fillStyle=p.color;
ctx.fillRect(-p.size/2,-p.size/2,p.size,p.size);
ctx.restore();
}
drawSupportWing();
ctx.save();
const b=bodyPosition();
ctx.translate(b.x,b.y);
ctx.rotate(rig.lean.value+kick.value);
ctx.scale(rig.sx.value,1);
drawWing(ctx,true);
drawTail(ctx);
ctx.restore();
drawLeg(ctx,0,true);
ctx.save();
ctx.translate(b.x,b.y);
ctx.rotate(rig.lean.value+kick.value);
ctx.scale(rig.sx.value,1);
// A tiny misregistered impression beneath the paper silhouette.
ctx.save(); ctx.translate(1.7,1.4);
ctx.globalAlpha=.15; ctx.fillStyle=RED; ctx.fill(bodyShape); ctx.restore();
paint(ctx,bodyPath,PAPER,4,false);
ctx.fillStyle="#e0dccb";
ctx.beginPath(); ctx.ellipse(-28,4,14,26,-.13,0,TAU); ctx.fill();
line(ctx,"M 11 43 Q 25 42 34 32",INK,1.7);
line(ctx,"M 18 49 Q 29 47 35 40",INK,1.3);
line(ctx,"M -5 -31 Q 8 -33 18 -25",INK,1.6);
drawNeckAndHead(ctx);
drawWing(ctx);
ctx.restore();
drawLeg(ctx,1);
// A physical tape-strip beat ruler, sitting on the paper floor.
ctx.save();
ctx.translate(435,548);
for (let i=0;i<16;i++) {
const active=Math.floor(beats)%16===i;
ctx.fillStyle=active?RED:INK;
ctx.globalAlpha=active?1:.25;
ctx.fillRect(i*9,0,active?6:3,active?7:3);
}
ctx.globalAlpha=.6; ctx.fillStyle=INK;
ctx.font="8px 'Courier New', monospace";
ctx.fillText("4 × 4 / KEEP IT MOVING",0,20);
ctx.restore();
ctx.restore();
}
function setPaused(value) {
paused=value;
if (!paused) manual.active=false;
stage.classList.toggle("paused",paused);
$("pause").textContent=paused?"▶":"Ⅱ";
$("pause").setAttribute("aria-pressed",String(paused));
$("pause").setAttribute("aria-label",paused?"Resume dance":"Pause dance");
$("floor-state").textContent=paused?"HOLDING POSITION":"FLOOR IS LIVE";
setMoveHUD();
if (timeline.kind==="recovery") setRecoveryHUD(timeline.value.next);
}
function nextInSet() {
const i=order.indexOf(move);
return order[(i+1)%order.length];
}
function callNext() {
const next=nextInSet();
cue="full"; updateCueUI(); setPaused(false);
beginRecovery(next,true);
}
function chooseCue(value) {
cue=value; updateCueUI(); setPaused(false);
beginRecovery(value==="full"?"top":value,true);
}
function restart() {
setPaused(false);
beginRecovery(cue==="full"?"top":cue,true);
}
document.querySelectorAll("[data-cue]").forEach(button => {
button.addEventListener("click",()=>chooseCue(button.dataset.cue));
});
$("call").addEventListener("click",callNext);
$("pause").addEventListener("click",()=>setPaused(!paused));
$("restart").addEventListener("click",restart);
tempoInput.addEventListener("input",()=>{
bpm=Number(tempoInput.value);
$("tempo-value").textContent=bpm;
$("tape-tempo").textContent=bpm+" BPM";
});
function manualMove(e) {
const rect=canvas.getBoundingClientRect();
manual.x=clamp((e.clientX-rect.left)/rect.width*W-500,-100,100);
manual.y=clamp((e.clientY-rect.top)/rect.height*H-365,-60,70);
}
canvas.addEventListener("pointerdown",e=>{
if (!paused) return;
manual.active=true; manual.dragging=true;
stage.classList.add("dragging");
canvas.setPointerCapture(e.pointerId);
manualMove(e);
});
canvas.addEventListener("pointermove",e=>{
if (manual.dragging) manualMove(e);
});
function releasePointer() {
manual.dragging=false;
stage.classList.remove("dragging");
}
canvas.addEventListener("pointerup",releasePointer);
canvas.addEventListener("pointercancel",releasePointer);
tapeInput.addEventListener("change",()=>{if(!tapeInput.checked)tape.length=0;});
// A tiny, entirely synthesized four-on-the-floor break loop.
let soundOn=false, audio=null, master=null, noiseBuffer=null;
let nextAudioTime=0, audioStep=0;
function makeAudio() {
const AudioContext=window.AudioContext||window.webkitAudioContext;
if (!AudioContext) return false;
audio=new AudioContext();
master=audio.createGain();
master.gain.value=0;
master.connect(audio.destination);
noiseBuffer=audio.createBuffer(1,Math.floor(audio.sampleRate*.6),audio.sampleRate);
const data=noiseBuffer.getChannelData(0);
for(let i=0;i<data.length;i++) data[i]=Math.random()*2-1;
return true;
}
function tone(time,freq,endFreq,duration,volume,type="sine") {
const o=audio.createOscillator(),g=audio.createGain();
o.type=type;
o.frequency.setValueAtTime(freq,time);
o.frequency.exponentialRampToValueAtTime(endFreq,time+duration);
g.gain.setValueAtTime(volume,time);
g.gain.exponentialRampToValueAtTime(.0001,time+duration);
o.connect(g); g.connect(master);
o.start(time); o.stop(time+duration+.02);
}
function noise(time,duration,volume,cutoff) {
const s=audio.createBufferSource(),f=audio.createBiquadFilter(),g=audio.createGain();
s.buffer=noiseBuffer; f.type="highpass"; f.frequency.value=cutoff;
g.gain.setValueAtTime(volume,time);
g.gain.exponentialRampToValueAtTime(.0001,time+duration);
s.connect(f); f.connect(g); g.connect(master);
s.start(time); s.stop(time+duration+.01);
}
function playStep(time,step) {
if ([0,3,8,10,14].includes(step)) tone(time,145,43,.22,.65);
if (step===4 || step===12) {
noise(time,.16,.25,1000);
tone(time,175,85,.075,.10,"triangle");
}
if (step%2===0 || step===7 || step===15) noise(time,.035,step%4===2?.065:.043,7000);
if ([0,3,7,8,11,14].includes(step)) {
const f=[55,55,65.41,55,49,65.41][Math.floor(step/3)%6];
tone(time,f,f*.88,.21,.12,"triangle");
}
}
function pumpAudio() {
if (!soundOn || !audio || audio.state!=="running") return;
const stepLength=60/bpm/4;
if (nextAudioTime<audio.currentTime) nextAudioTime=audio.currentTime+.025;
while(nextAudioTime<audio.currentTime+.1) {
playStep(nextAudioTime,audioStep);
nextAudioTime+=stepLength;
audioStep=(audioStep+1)%16;
}
}
$("sound").addEventListener("click",async()=>{
try {
if (!audio && !makeAudio()) {
$("sound").textContent="NO AUDIO";
$("sound").disabled=true;
return;
}
soundOn=!soundOn;
if (soundOn) {
await audio.resume();
nextAudioTime=audio.currentTime+.04;
audioStep=0;
}
master.gain.setTargetAtTime(soundOn?.3:0,audio.currentTime,.035);
$("sound").setAttribute("aria-pressed",String(soundOn));
$("sound").innerHTML=`SOUND ${soundOn?"ON":"OFF"} <span class="sound-icon" aria-hidden="true">${soundOn?"≋":"◌"}</span>`;
} catch (_) {
$("sound").textContent="AUDIO UNAVAILABLE";
}
});
document.addEventListener("keydown",e=>{
if (e.target.closest("button,input,textarea,select,[contenteditable]")) return;
if (e.code==="Space") { e.preventDefault(); callNext(); }
else if (e.key.toLowerCase()==="p") setPaused(!paused);
else if (e.key.toLowerCase()==="m") $("sound").click();
else if (/^[1-4]$/.test(e.key)) chooseCue(order[Number(e.key)-1]);
else if (e.key==="ArrowLeft" || e.key==="ArrowRight") {
e.preventDefault();
tempoInput.value=clamp(Number(tempoInput.value)+(e.key==="ArrowRight"?2:-2),80,148);
tempoInput.dispatchEvent(new Event("input"));
}
});
document.addEventListener("visibilitychange",()=>{
if (!audio) return;
if (document.hidden) audio.suspend();
else if (soundOn) audio.resume().catch(()=>{});
});
beginMove("top");
let previous=performance.now();
function frame(now) {
const dt=clamp((now-previous)/1000,0,.035);
previous=now;
if (!paused) {
simTime+=dt;
beats+=dt/(60/bpm);
if (beats>=timeline.duration) {
if (timeline.kind==="recovery") {
beginMove(timeline.value.next);
} else {
const i=order.indexOf(move);
beginRecovery(cue==="full"?order[(i+1)%order.length]:move);
}
}
}
goalFeet=samplePose();
updateRig(dt,goalFeet);
draw();
pumpAudio();
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
})();
</script>
</body>
</html>