27:18Gemini 4 Hydrogen: Cheaper Tokens, More Expensive Answers?
Nov 13, 2026 · YouTube
18:40ERNIE 5.5 Flash Preview vs GLM 5.3 Flash: Baidu's new model is very impressive!
Nov 11, 2026 · YouTube
36:22Fable 5.2 robbing me blind, plus: Nex AGI's N2.6 vs GLM 5.3 Flash
Nov 11, 2026 · YouTube
14:03Gemini 4 Carbon: Can it handle my challenges? (First look with one-shot coding tests)
Nov 10, 2026 · YouTube
47:51Ranking major U.S. Open-Weights Models. (Where does Ripple 65B fit in?)
Nov 6, 2026 · YouTube
10:57"Crouton Alpha": the latest super-efficient coding model by Meta?
Nov 3, 2026 · YouTube
38:58Testing Particle-1: a post-trained DeepSeek V4.1 Flash specializing in coding and efficiency
Oct 28, 2026 · YouTube
22:14GPT-6.1 Terra Pro vs Terra: How Much Better Is Pro?
Oct 27, 2026 · YouTube
29:55Kimi K3.1 Pro Looked Incredible. Then I Kept Testing.
Oct 26, 2026 · YouTube
31:28Is this company reselling GPT Sol 6.1 at a loss (for non-agentic/text-only)?🤔 Is this sustainable?
Oct 20, 2026 · YouTube
17:58Nemotron 3.5 Super VL vs Opus 5.5: Testing NVIDIA’s new coding juggernaut
Oct 15, 2026 · YouTube
16:04Testing a *massive* new open-weights model 🤯: Muse Spark Open (vs. Nemotron, Opus 5.5, and more)
Oct 15, 2026 · YouTube
16:43Gemini 4 Argon: Great 🐈 Animation, but a broken escalator. (Coding challenge vs Opus 5.5, Sol 6.1)
Oct 13, 2026 · YouTube
16:37Meta's new "Muse Wonder" AI is impressive... but can it compete with Anthropic's models?
Oct 12, 2026 · YouTube
11:12WOW... Haiku 5.5 is very impressive. RIP OpenAI?
Oct 7, 2026 · YouTube
22:47Mistral Large 4 Is Cheap. But Is It Good Enough to Compete?
Oct 6, 2026 · YouTube
22:02Is Pareto 26.10 Preview Just Routing to GPT Sol? I Tested It Against Sol 6.1 Pro
Oct 1, 2026 · YouTube
11:22I Tested Sonnet 5.5 Against Opus 5.5. Is It Actually Cheaper?
Sep 28, 2026 · YouTube
30:00Did Elon Musk's team "train the gay away"... and make Grok 4.7 worse?
Sep 26, 2026 · YouTube
19:21I Tested the Stealth "Space Bunny Alpha" Model Before It Went Offline
Sep 23, 2026 · YouTube
10:58So... how much better is Claude Opus 5.5 vs. GPT-6 Sol, really?
Sep 23, 2026 · YouTube
9:38Claude Opus 5.5... the BEST new model for coding!! 🤩
Sep 22, 2026 · YouTube
29:00Grok 4.7: improved, but GLM 5.3 FlashX beat it anyway (benchmark against Gemini, Sol, etc.)
Sep 22, 2026 · YouTube
You make short animated pieces for the web.
Build me a single HTML page: a bird, breakdancing.
data: URIs are fine.localStorage, sessionStorage, cookies or indexedDB — sandboxed frame, they throw.html block.Two sentences: what the bird is, and the look you went for.
The single fenced html block.
Three bullets.
A barrel-chested city pigeon in coral high-tops, with an iridescent neck and a sweatband that cannot keep up. The look is a misregistered photocopy of a rooftop battle flyer: ink-heavy cut-paper shapes, halftone acid yellow, and a chalked cipher on concrete.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Pigeon Breaks — Rooftop Session 04</title>
<style>
:root{
--ink:#17151d;
--panel:#121117;
--paper:#f0e8d7;
--muted:#aaa4a0;
--acid:#d9f45b;
--coral:#ff655d;
--line:rgba(240,232,215,.19);
}
*{box-sizing:border-box}
html,body{width:100%;height:100%;margin:0;overflow:hidden;background:var(--ink)}
body{
color:var(--paper);
font-family:Arial,Helvetica,sans-serif;
-webkit-font-smoothing:antialiased;
}
button,input{font:inherit}
button{touch-action:manipulation}
.poster{position:relative;width:100%;height:100%;isolation:isolate}
canvas{position:absolute;inset:0;width:100%;height:100%;display:block}
.masthead{
position:absolute;
z-index:2;
top:0;
left:0;
right:0;
padding:24px 30px;
display:flex;
justify-content:space-between;
align-items:flex-start;
pointer-events:none;
}
.eyebrow,.label,.onair,.brand>p,.footline,.sticker{
font-size:10px;
font-weight:800;
letter-spacing:.18em;
text-transform:uppercase;
}
.eyebrow{display:flex;align-items:center;gap:9px;color:var(--acid)}
.eyebrow:before{content:"";width:25px;height:3px;background:var(--acid)}
h1{
margin:19px 0 13px;
font-family:Impact,Haettenschmidt,"Arial Narrow Bold",sans-serif;
font-size:clamp(66px,8.4vw,132px);
font-weight:900;
line-height:.74;
letter-spacing:-.035em;
text-transform:uppercase;
transform:scaleX(.88);
transform-origin:left center;
}
h1 em{
color:var(--coral);
font-style:normal;
text-shadow:3px 3px 0 var(--ink),5px 5px 0 rgba(240,232,215,.22);
}
.brand>p{margin:0;color:rgba(240,232,215,.67)}
.onair{
display:grid;
grid-template-columns:8px auto;
gap:5px 9px;
align-items:center;
padding-top:3px;
text-align:right;
}
.onair i{
width:8px;height:8px;border-radius:50%;background:var(--coral);
box-shadow:0 0 0 4px rgba(255,101,93,.13);
animation:live 1.4s steps(2,end) infinite;
}
.onair span{color:rgba(240,232,215,.63)}
.onair b{grid-column:1/3;color:var(--paper);font-size:12px;letter-spacing:.12em}
@keyframes live{50%{opacity:.35}}
.sticker{
position:absolute;
z-index:2;
left:35px;
top:52%;
padding:12px 15px;
color:var(--ink);
background:var(--paper);
border:2px solid var(--ink);
box-shadow:4px 4px 0 var(--coral);
line-height:1.45;
transform:rotate(-7deg);
pointer-events:none;
}
.controls{
position:absolute;
z-index:4;
left:0;
right:0;
bottom:0;
padding:14px 24px 11px;
background:rgba(18,17,23,.93);
border-top:1px solid var(--line);
backdrop-filter:blur(13px);
-webkit-backdrop-filter:blur(13px);
}
.control-grid{
max-width:1480px;
margin:auto;
display:grid;
grid-template-columns:155px minmax(420px,1fr) 190px;
gap:24px;
align-items:end;
}
.label{display:block;margin-bottom:8px;color:var(--muted);font-size:9px}
.label small{font-size:8px;color:#777279;letter-spacing:.1em}
.now strong{
display:block;
max-width:150px;
overflow:hidden;
color:var(--acid);
font-family:Impact,Haettenschmidt,"Arial Narrow Bold",sans-serif;
font-size:21px;
font-weight:400;
letter-spacing:.035em;
white-space:nowrap;
text-overflow:ellipsis;
}
.beats{display:flex;gap:5px;margin-top:9px}
.beats i{display:block;width:21px;height:3px;background:#403c45;transition:background .08s,transform .08s}
.beats i.on{background:var(--coral);transform:scaleY(2)}
.pads{display:grid;grid-template-columns:repeat(4,1fr);gap:7px}
.move{
position:relative;
min-width:0;
height:47px;
padding:0 10px;
display:flex;
align-items:center;
gap:8px;
color:var(--paper);
background:#24212b;
border:1px solid #45404b;
border-radius:2px;
cursor:pointer;
font-size:10px;
font-weight:900;
letter-spacing:.07em;
text-align:left;
transition:transform .12s,background .12s,color .12s,border-color .12s;
}
.move:hover{transform:translateY(-2px);border-color:var(--paper)}
.move:focus-visible,.sound:focus-visible,input:focus-visible{outline:2px solid var(--acid);outline-offset:3px}
.move .num{color:#827c87;font-size:8px}
.move .key{
margin-left:auto;
color:#77717c;
font-size:8px;
border:1px solid #514b57;
padding:2px 4px;
}
.move.active{color:var(--ink);background:var(--acid);border-color:var(--acid)}
.move.active .num,.move.active .key{color:var(--ink)}
.move.active .key{border-color:rgba(23,21,29,.35)}
.move.queued{color:var(--ink);background:var(--coral);border-color:var(--coral)}
.move.queued .num,.move.queued .key{color:var(--ink)}
.utility{display:grid;grid-template-columns:1fr 68px;gap:10px;align-items:end}
.tempo{display:block;min-width:0}
.tempo .label{display:flex;justify-content:space-between;margin-bottom:10px}
.tempo output{color:var(--acid);letter-spacing:.04em}
input[type="range"]{
display:block;
width:100%;
height:18px;
margin:0;
accent-color:var(--acid);
cursor:ew-resize;
}
.sound{
height:37px;
padding:0 7px;
color:var(--paper);
background:transparent;
border:1px solid #4b4650;
border-radius:2px;
cursor:pointer;
font-size:8px;
font-weight:900;
letter-spacing:.09em;
}
.sound b{display:block;margin-top:3px;color:var(--coral);font-size:10px}
.sound[aria-pressed="true"]{border-color:var(--acid)}
.sound[aria-pressed="true"] b{color:var(--acid)}
.footline{
max-width:1480px;
margin:10px auto 0;
padding-top:8px;
display:flex;
justify-content:space-between;
gap:20px;
color:#716c76;
border-top:1px solid rgba(240,232,215,.07);
font-size:8px;
letter-spacing:.11em;
}
#queueText{color:#aaa3ad}
@media(max-width:1000px){
.control-grid{grid-template-columns:120px minmax(360px,1fr) 160px;gap:14px}
.sticker{display:none}
.move{padding:0 7px;gap:5px;font-size:9px}
.move .key{display:none}
}
@media(max-width:720px){
.masthead{padding:16px}
h1{font-size:52px;margin-top:13px}
.brand>p{display:none}
.eyebrow{font-size:8px}
.onair{font-size:8px}
.onair b{font-size:9px}
.controls{padding:10px 12px 8px}
.control-grid{grid-template-columns:1fr;gap:9px}
.now{display:none}
.move-wrap .label{margin-bottom:6px}
.move{height:42px;justify-content:center;text-align:center}
.move .num{display:none}
.utility{grid-template-columns:1fr 82px;align-items:center}
.tempo .label{margin-bottom:3px}
.sound{height:34px}
.sound b{display:inline;margin-left:5px}
.footline{margin-top:7px;padding-top:6px}
.footline span:last-child{display:none}
}
@media(max-height:650px){
h1{font-size:48px}
.brand>p,.sticker,.footline{display:none}
.controls{padding-top:9px}
}
</style>
</head>
<body>
<main class="poster">
<canvas id="stage" role="img" aria-label="An animated pigeon in high-top sneakers performs a breakdancing set on a rooftop."></canvas>
<header class="masthead">
<div class="brand">
<div class="eyebrow">Rooftop session № 04</div>
<h1>Pigeon<br><em>Breaks</em></h1>
<p>One bird. All floor.</p>
</div>
<div class="onair">
<i></i><span>Live cipher</span>
<b id="topMove">TOP ROCK</b>
</div>
</header>
<div class="sticker" aria-hidden="true">Featherweight<br>division<br>★★★★★</div>
<footer class="controls" id="controls">
<div class="control-grid">
<div class="now">
<span class="label">Now serving</span>
<strong id="moveName">TOP ROCK</strong>
<div class="beats" aria-hidden="true">
<i></i><i></i><i></i><i></i>
</div>
</div>
<div class="move-wrap">
<span class="label">Call the next move <small>(keys 1–4)</small></span>
<div class="pads">
<button class="move active" data-move="toprock" aria-label="Call top rock">
<span class="num">01</span><span>TOP ROCK</span><span class="key">1</span>
</button>
<button class="move" data-move="footwork" aria-label="Call six-step footwork">
<span class="num">02</span><span>SIX-STEP</span><span class="key">2</span>
</button>
<button class="move" data-move="windmill" aria-label="Call windmill">
<span class="num">03</span><span>WINDMILL</span><span class="key">3</span>
</button>
<button class="move" data-move="freeze" aria-label="Call a one-wing freeze">
<span class="num">04</span><span>FREEZE</span><span class="key">4</span>
</button>
</div>
</div>
<div class="utility">
<label class="tempo">
<span class="label"><span>Tempo</span><output id="bpmOut">104 BPM</output></span>
<input id="tempo" type="range" min="80" max="140" value="104" step="1" aria-label="Tempo in beats per minute">
</label>
<button class="sound" id="sound" aria-pressed="false" aria-label="Toggle generated drum beat">
SOUND <b id="soundState">OFF</b>
</button>
</div>
</div>
<div class="footline">
<span id="queueText">Call a move — it cuts in on a beat.</span>
<span>Space: freeze · M: sound · No pigeons were interpolated</span>
</div>
</footer>
</main>
<script>
(() => {
"use strict";
const canvas = document.getElementById("stage");
const ctx = canvas.getContext("2d");
const footer = document.getElementById("controls");
const TAU = Math.PI * 2;
const INK = "#17151d";
const PAPER = "#f0e8d7";
const ACID = "#d9f45b";
const CORAL = "#ff655d";
const reduceMotion = window.matchMedia &&
window.matchMedia("(prefers-reduced-motion: reduce)").matches;
const clamp = (v,a,b) => Math.max(a,Math.min(b,v));
const wrapAngle = a => Math.atan2(Math.sin(a),Math.cos(a));
let W = 0, H = 0, DPR = 1;
let groundY = 0, birdX = 0, birdScale = 1;
let skyline = [];
function seeded(seed) {
return function() {
seed |= 0;
seed = seed + 0x6D2B79F5 | 0;
let t = Math.imul(seed ^ seed >>> 15, 1 | seed);
t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
return ((t ^ t >>> 14) >>> 0) / 4294967296;
};
}
const stars = [];
{
const r = seeded(1983);
for (let i=0;i<55;i++) stars.push({x:r(),y:r()*.68,s:.5+r()*1.5,a:.15+r()*.5});
}
const grainCanvas = document.createElement("canvas");
grainCanvas.width = grainCanvas.height = 96;
{
const g = grainCanvas.getContext("2d");
const image = g.createImageData(96,96);
const r = seeded(404);
for (let i=0;i<image.data.length;i+=4) {
const light = r() > .5;
image.data[i] = light ? 255 : 0;
image.data[i+1] = light ? 255 : 0;
image.data[i+2] = light ? 255 : 0;
image.data[i+3] = Math.floor(r()*42);
}
g.putImageData(image,0,0);
}
const grain = ctx.createPattern(grainCanvas,"repeat");
function resize() {
W = window.innerWidth;
H = window.innerHeight;
DPR = Math.min(window.devicePixelRatio || 1,2);
canvas.width = Math.round(W*DPR);
canvas.height = Math.round(H*DPR);
canvas.style.width = W+"px";
canvas.style.height = H+"px";
ctx.setTransform(DPR,0,0,DPR,0,0);
const mobile = W < 720;
const footerH = footer.offsetHeight || (mobile ? 150 : 110);
groundY = H-footerH-(mobile ? 34 : 42);
groundY = clamp(groundY,H*.43,H*.79);
birdX = mobile ? W*.5 : W*.625;
const fitHeight = (groundY-88)/220;
const fitWidth = W/(mobile ? 390 : 850);
birdScale = Math.max(.54,Math.min(mobile ? 1.1 : 1.48,fitHeight,fitWidth));
const r = seeded(74);
skyline = [];
let x = -25;
while (x < W+40) {
const w = 45+r()*95;
const h = 45+r()*145;
const windows = [];
for (let yy=17;yy<h-12;yy+=22) {
for (let xx=12;xx<w-8;xx+=19) {
if (r()>.57) windows.push({x:xx,y:yy,a:.12+r()*.38});
}
}
skyline.push({x,w,h,windows});
x += w+5+r()*12;
}
}
window.addEventListener("resize",resize);
// ---------- Pose rig and choreography ----------
const BASE = Object.freeze({
x:0,y:-112,a:-.08,
hx:0,hy:0,ha:0,
lx:-32,ly:-14,la:-.05,
rx:30,ry:-14,ra:.05,
lwx:-45,lwy:-72,
rwx:42,rwy:-66,
tail:0,spread:0
});
function P(o={}) { return Object.assign({},BASE,o); }
function K(t,o,e="smooth") { return {t,p:P(o),e}; }
const EASE = {
linear:u=>u,
smooth:u=>u*u*(3-2*u),
in:u=>u*u*u,
out:u=>1-Math.pow(1-u,3),
pop:u=>{
const c=1.35;
return 1+(c+1)*Math.pow(u-1,3)+c*Math.pow(u-1,2);
}
};
function mix(a,b,e) {
const out = {};
for (const k in BASE) out[k] = a[k]+(b[k]-a[k])*e;
return out;
}
function sampleKeys(keys,t) {
if (t<=keys[0].t) return Object.assign({},keys[0].p);
const last = keys[keys.length-1];
if (t>=last.t) return Object.assign({},last.p);
for (let i=0;i<keys.length-1;i++) {
const a=keys[i], b=keys[i+1];
if (t>=a.t && t<=b.t) {
const u=(t-a.t)/(b.t-a.t);
return mix(a.p,b.p,(EASE[b.e]||EASE.smooth)(u));
}
}
return Object.assign({},last.p);
}
function bodyPoint(p,x,y) {
const c=Math.cos(p.a), s=Math.sin(p.a);
return {x:p.x+x*c-y*s,y:p.y+x*s+y*c};
}
const TOPROCK = [
K(0,{x:-10,y:-105,a:-.12,lx:-44,rx:18,lwx:-20,lwy:-145,rwx:-42,rwy:-112}),
K(.45,{x:-16,y:-120,a:.05,lx:-44,rx:8,ry:-52,ra:-.35,lwx:-60,lwy:-152,rwx:38,rwy:-140}, "out"),
K(1,{x:8,y:-104,a:.12,lx:-44,rx:44,lwx:20,lwy:-80,rwx:-45,rwy:-105}, "in"),
K(1.45,{x:14,y:-121,a:-.04,lx:-10,ly:-50,la:.35,rx:44,lwx:-55,lwy:-105,rwx:60,rwy:-120}, "out"),
K(2,{x:24,y:-105,a:-.14,lx:8,rx:44,lwx:-35,lwy:-150,rwx:15,rwy:-155}, "in"),
K(2.5,{x:18,y:-119,a:.06,lx:8,rx:-10,ry:-46,ra:-.5,lwx:-75,lwy:-100,rwx:72,rwy:-100}, "out"),
K(3,{x:0,y:-104,a:.12,lx:8,rx:-28,lwx:-20,lwy:-150,rwx:65,rwy:-85}, "in"),
K(3.5,{x:-8,y:-120,a:-.08,lx:-48,ly:-48,la:.45,rx:-28,lwx:-70,lwy:-85,rwx:15,rwy:-155}, "out"),
K(4,{x:-18,y:-106,a:-.16,lx:-52,rx:-12,lwx:-25,lwy:-140,rwx:-55,rwy:-90}, "in"),
K(4.5,{x:-20,y:-122,a:.04,lx:-52,rx:12,ry:-60,ra:-.6,lwx:-75,lwy:-135,rwx:45,rwy:-155}, "out"),
K(5,{x:-28,y:-116,a:-.22,lx:-52,rx:72,ry:-58,ra:-.2,lwx:-70,lwy:-80,rwx:-10,rwy:-160,spread:.5}, "out"),
K(5.5,{x:0,y:-108,a:.1,lx:-20,ly:-40,la:.4,rx:38,lwx:-30,lwy:-150,rwx:65,rwy:-110}, "in"),
K(6,{x:8,y:-100,a:.18,lx:-24,rx:38,lwx:15,lwy:-85,rwx:-45,rwy:-125}, "in"),
K(6.5,{x:4,y:-126,a:0,lx:-20,ly:-35,rx:35,ry:-38,lwx:-85,lwy:-145,rwx:85,rwy:-140,spread:1}, "out"),
K(7,{x:0,y:-96,a:-.08,lx:-48,rx:48,lwx:-70,lwy:-60,rwx:68,rwy:-60,spread:.3}, "in"),
K(7.35,{x:-5,y:-118,a:-.12,lx:-48,rx:48,lwx:-40,lwy:-140,rwx:35,rwy:-145}, "pop"),
K(8,{})
];
const F_START = P({
x:10,y:-48,a:1.35,
lx:-48,ly:-14,la:.1,
rx:20,ry:-45,ra:1.2,
lwx:-20,lwy:-25,
rwx:62,rwy:-6
});
const DROP = [
K(0,{}),
K(.45,{x:0,y:-124,a:-.15,lx:-38,rx:38,lwx:-70,lwy:-155,rwx:68,rwy:-150,spread:.5},"out"),
K(.9,{x:0,y:-70,a:.2,lx:-42,rx:42,lwx:-65,lwy:-55,rwx:70,rwy:-55},"in"),
K(1.35,{x:12,y:-60,a:.55,lx:-35,rx:30,ry:-35,ra:.4,lwx:-45,lwy:-85,rwx:68,rwy:-6},"out"),
K(2.1,{x:18,y:-48,a:1.05,lx:-28,ly:-30,la:.4,rx:12,ra:.8,lwx:-30,lwy:-35,rwx:68,rwy:-6},"in"),
K(2.8,{x:8,y:-45,a:1.4,lx:-50,la:.2,rx:10,ry:-55,ra:1,lwx:0,lwy:-20,rwx:62,rwy:-6}),
K(3.4,F_START),
K(4,F_START)
];
const W_START = P({
x:0,y:-48,a:4.2,
lx:-45,ly:-38,la:3.7,
rx:35,ry:-52,ra:4.2,
lwx:-50,lwy:-12,
rwx:45,rwy:-6
});
const FOOTWORK = [
K(0,F_START),
K(.5,{x:15,y:-52,a:1.5,lx:55,la:.1,rx:12,ry:-55,ra:1.2,lwx:-30,lwy:-35,rwx:62,rwy:-6},"out"),
K(1,{x:22,y:-50,a:1.7,lx:62,rx:38,ra:.2,lwx:-35,lwy:-25,rwx:62,rwy:-6},"in"),
K(1.5,{x:15,y:-47,a:1.95,lx:20,ly:-70,la:1.2,rx:28,lwx:-15,lwy:-18,rwx:62,rwy:-6},"out"),
K(2,{x:0,y:-50,a:2.1,lx:-38,la:.8,rx:12,lwx:-25,lwy:-30,rwx:62,rwy:-6},"in"),
K(2.5,{x:-12,y:-48,a:2.25,lx:-48,rx:-45,ry:-65,ra:1.8,lwx:-25,lwy:-35,rwx:62,rwy:-6},"out"),
K(3,{x:-20,y:-50,a:2.4,lx:-5,ly:-60,la:1.4,rx:-62,ra:2.2,lwx:-30,lwy:-40,rwx:62,rwy:-6},"in"),
K(3.5,{x:-18,y:-48,a:2.55,lx:-20,ly:-35,la:1.7,rx:-55,ra:2.2,lwx:-62,lwy:-6,rwx:15,rwy:-30},"out"),
K(4,{x:-10,y:-50,a:2.7,lx:-55,la:2.5,rx:5,ry:-55,ra:2,lwx:-62,lwy:-6,rwx:25,rwy:-40},"in"),
K(4.5,{x:0,y:-48,a:2.85,lx:-42,rx:48,ry:-40,ra:2.4,lwx:-62,lwy:-6,rwx:25,rwy:-40},"out"),
K(5,{x:12,y:-50,a:3,lx:-20,ly:-50,la:2.4,rx:55,ra:2.8,lwx:-62,lwy:-6,rwx:0,rwy:-20},"in"),
K(5.5,{x:18,y:-52,a:3.25,lx:40,la:2.9,rx:35,ry:-45,ra:2.8,lwx:-62,lwy:-6,rwx:0,rwy:-20},"out"),
K(6,{x:10,y:-56,a:3.45,lx:15,rx:45,ra:3,lwx:-30,lwy:-15,rwx:50,rwy:-15},"in"),
K(6.5,{x:0,y:-48,a:3.7,lx:-20,ly:-35,la:3.4,rx:35,ra:3.5,lwx:-45,lwy:-6,rwx:45,rwy:-25}),
K(7,{x:-5,y:-45,a:4,lx:-45,ly:-45,la:3.5,rx:35,ry:-60,ra:4,lwx:-55,lwy:-20,rwx:40,rwy:-6}),
K(7.5,{x:0,y:-48,a:4.2,lx:-45,ly:-38,la:3.7,rx:35,ry:-52,ra:4.2,lwx:-50,lwy:-12,rwx:45,rwy:-6}),
K(8,W_START)
];
const WIND_IN = [
K(0,W_START),
K(.45,{x:-8,y:-42,a:4.45,lx:-35,ly:-25,la:4,rx:30,ry:-38,ra:4.4,lwx:-52,lwy:-6,rwx:48,rwy:-6},"in"),
K(1,{x:0,y:-56,a:5,lx:-55,ly:-70,la:4.5,rx:50,ry:-75,ra:5,lwx:-60,lwy:-20,rwx:55,rwy:-10,spread:1},"out")
];
const FZ_START = P({
x:0,y:-50,a:4.2,
lx:-38,ly:-14,la:4,
rx:38,ry:-14,ra:4,
lwx:-50,lwy:-6,
rwx:50,rwy:-6
});
function spinFrame(t) {
const turn=(t-1)/3;
const angle=5+TAU*turn-.18*Math.sin(TAU*turn);
const p=P({
x:4*Math.sin(TAU*turn),
y:-50-6*Math.pow(Math.cos(Math.PI*turn),2),
a:angle,
ha:-.12*Math.sin(TAU*turn*2),
tail:.12*Math.sin(TAU*turn),
spread:1
});
const scissors=Math.sin(TAU*turn*2);
const lf=bodyPoint(p,-58+scissors*12,120);
const rf=bodyPoint(p,58+scissors*12,120);
p.lx=lf.x; p.ly=Math.min(lf.y,-12); p.la=angle+.15;
p.rx=rf.x; p.ry=Math.min(rf.y,-12); p.ra=angle-.15;
const lw=bodyPoint(p,-72,30);
const rw=bodyPoint(p,72,30);
p.lwx=lw.x; p.lwy=Math.min(lw.y,-6);
p.rwx=rw.x; p.rwy=Math.min(rw.y,-6);
return p;
}
function windmillPose(t) {
if (t<1) return sampleKeys(WIND_IN,Math.max(0,t));
if (t<7) return spinFrame(t);
const from=spinFrame(7);
from.a-=4*Math.PI;
from.la-=4*Math.PI;
from.ra-=4*Math.PI;
return mix(from,FZ_START,EASE.smooth(clamp(t-7,0,1)));
}
const FREEZE_POSE = P({
x:0,y:-108,a:3.12,hx:-4,hy:3,ha:-.2,
lx:62,ly:-205,la:2.7,
rx:-58,ry:-198,ra:3.5,
lwx:-72,lwy:-105,
rwx:38,rwy:-6,
spread:.65
});
const RECOVER_START = P({
x:0,y:-92,a:3.35,hx:-3,hy:2,ha:-.15,
lx:45,ly:-165,la:2.9,
rx:-40,ry:-155,ra:3.6,
lwx:-62,lwy:-85,
rwx:38,rwy:-6,
spread:.35
});
const FREEZE = [
K(0,FZ_START),
K(.4,{x:-6,y:-43,a:4,lx:-40,rx:40,la:4,ra:4,lwx:-50,lwy:-6,rwx:50,rwy:-6},"in"),
K(.85,{x:8,y:-82,a:3.55,lx:48,ly:-145,la:3.2,rx:-24,ry:-130,ra:3.8,lwx:-60,lwy:-60,rwx:38,rwy:-6,spread:.7},"out"),
K(1.2,FREEZE_POSE,"in"),
K(1.5,Object.assign({},FREEZE_POSE,{y:-104,x:1}),"pop"),
K(2.4,Object.assign({},FREEZE_POSE,{y:-106,x:1,lx:65,rx:-62})),
K(3.4,Object.assign({},FREEZE_POSE,{y:-104,x:-1,lx:59,ly:-202,rx:-61,ry:-201})),
K(4.2,Object.assign({},FREEZE_POSE,{y:-106})),
K(4.7,Object.assign({},FREEZE_POSE,{y:-100,a:3.18,lx:54,ly:-185,rx:-48,ry:-181}),"in"),
K(5.3,RECOVER_START,"out"),
K(6,RECOVER_START)
];
const RECOVER = [
K(0,RECOVER_START),
K(.45,{x:0,y:-80,a:3.75,lx:35,ly:-125,la:3.3,rx:-30,ry:-120,ra:3.8,lwx:-55,lwy:-55,rwx:38,rwy:-6},"in"),
K(.9,{x:5,y:-48,a:4.4,lx:-42,la:4.2,rx:30,ry:-45,ra:4.4,lwx:-50,lwy:-6,rwx:45,rwy:-6},"out"),
K(1.4,{x:-5,y:-58,a:5.5,lx:-40,rx:40,la:5.7,ra:5.9,lwx:-60,lwy:-30,rwx:60,rwy:-30}),
K(1.9,{x:0,y:-72,a:6.1,lx:-42,rx:42,la:6.1,ra:6.15,lwx:-68,lwy:-58,rwx:68,rwy:-58},"in"),
K(2.25,{x:0,y:-124,a:6.2,lx:-34,rx:34,la:6.18,ra:6.2,lwx:-72,lwy:-165,rwx:72,rwy:-160,spread:.8},"out"),
K(2.6,{x:0,y:-104,a:6.15,lx:-40,rx:40,la:6.15,ra:6.2,lwx:15,lwy:-80,rwx:-45,rwy:-120},"in"),
K(3.05,{x:0,y:-117,a:6.25,lx:-36,rx:36,la:6.2,ra:6.25,lwx:-60,lwy:-140,rwx:60,rwy:-140},"pop"),
K(3.45,{x:0,y:-108,a:6.2,lx:-34,rx:32,la:6.2,ra:6.23,lwx:-35,lwy:-90,rwx:35,rwy:-90}),
K(4,{a:TAU-.08})
];
const MOVES = {
toprock:{
label:"TOP ROCK",keys:TOPROCK,duration:8,
impacts:[[1,.2],[2,.2],[3,.2],[4,.25],[5.5,.3],[6,.22],[7,.65]]
},
drop:{
label:"THE DROP",keys:DROP,duration:4,
impacts:[[.9,.45],[1.35,.55],[2.8,.35]]
},
footwork:{
label:"SIX-STEP",keys:FOOTWORK,duration:8,
impacts:[[.5,.18],[1,.18],[1.5,.16],[2,.18],[2.5,.16],[3,.2],[3.5,.22],[4,.18],[4.5,.16],[5,.2],[5.5,.16],[6,.25]]
},
windmill:{
label:"WINDMILL",pose:windmillPose,duration:8,
impacts:[[1,.5],[2.45,.25],[4,.42],[5.5,.25],[7,.55]]
},
freeze:{
label:"ONE-WING FREEZE",keys:FREEZE,duration:6,
impacts:[[.85,.35],[1.2,1]]
},
recover:{
label:"RECOVERY",keys:RECOVER,duration:4,
impacts:[[.9,.35],[1.9,.45],[2.6,.3]]
}
};
const NEXT = {
toprock:"drop",
drop:"footwork",
footwork:"windmill",
windmill:"freeze",
freeze:"recover",
recover:"toprock"
};
function rawPose(name,t) {
const m=MOVES[name];
return m.pose ? m.pose(t) : sampleKeys(m.keys,t);
}
let bpm=104;
let beat=0;
let current="toprock";
let moveStart=0;
let lastLocal=0;
let lastPose=P();
let transition=null;
let pending=null;
function setMove(name,start,withTransition) {
if (withTransition && lastPose) {
transition={from:Object.assign({},lastPose),start,duration:.7};
} else {
transition=null;
}
current=name;
moveStart=start;
lastLocal=0;
}
function requestMove(name) {
if (!MOVES[name]) return;
let at=pending ? pending.at : Math.floor(beat)+1;
if (!pending && at-beat<.18) at+=1;
pending={name,at};
document.getElementById("queueText").textContent=
MOVES[name].label+" CALLED · CUTTING IN ON THE NEXT BEAT";
}
function currentPose() {
const local=Math.max(0,beat-moveStart);
const raw=rawPose(current,local);
if (!transition) return raw;
const q=(beat-transition.start)/transition.duration;
if (q>=1) {
transition=null;
return raw;
}
const from=transition.from;
raw.a=from.a+wrapAngle(raw.a-from.a);
raw.la=from.la+wrapAngle(raw.la-from.la);
raw.ra=from.ra+wrapAngle(raw.ra-from.ra);
const eased=EASE.smooth(clamp(q,0,1));
const p=mix(from,raw,eased);
const arc=Math.sin(clamp(q,0,1)*Math.PI);
if (Math.hypot(raw.lx-from.lx,raw.ly-from.ly)>12) p.ly-=arc*16;
if (Math.hypot(raw.rx-from.rx,raw.ry-from.ry)>12) p.rx-=0,p.ry-=arc*16;
p.y-=arc*4;
return p;
}
// ---------- Impact dust and secondary motion ----------
const particles=[];
const rings=[];
let shake=0;
let impactPulse=0;
let shout=null;
function spawnImpact(strength,p,name) {
let x=p.x;
if (name==="toprock") x=(p.lx+p.rx)*.5;
if (name==="freeze" || name==="recover") x=p.rwx;
const count=Math.floor(3+strength*13);
for (let i=0;i<count;i++) {
particles.push({
x:x+(Math.random()-.5)*20,
y:-2,
vx:(Math.random()-.5)*(45+strength*75),
vy:-20-Math.random()*(35+strength*70),
life:.45+Math.random()*.45,
max:.9,
size:1.5+Math.random()*3.5,
rot:Math.random()*TAU,
vr:(Math.random()-.5)*8,
feather:strength>.7 && i===0,
front:Math.random()>.5
});
}
if (strength>.4) rings.push({x,life:1,strength});
shake=Math.max(shake,strength*6);
impactPulse=Math.max(impactPulse,strength);
if (strength>.6) {
const words={
toprock:"KRRK!",drop:"BOMP!",windmill:"FWIP!",
freeze:"HIT!",recover:"UP!"
};
shout={
text:words[name]||"TK!",
x:p.x+(name==="freeze"?85:-90),
y:p.y-85,
life:1,
rot:(Math.random()-.5)*.18
};
}
}
function checkImpacts(previous,now,p) {
for (const hit of MOVES[current].impacts) {
if (hit[0]>previous && hit[0]<=now) {
spawnImpact(hit[1],p,current);
}
}
}
let previousRoot=null;
let headSpring=0,headVelocity=0;
let tailSpring=0,tailVelocity=0;
let bandSpring=0,bandVelocity=0;
let chainSwing=0;
function secondaryMotion(p,dt) {
if (!previousRoot || dt<=0) {
previousRoot={x:p.x,y:p.y,a:p.a};
return;
}
const vx=(p.x-previousRoot.x)/dt;
const vy=(p.y-previousRoot.y)/dt;
const av=wrapAngle(p.a-previousRoot.a)/dt;
previousRoot={x:p.x,y:p.y,a:p.a};
const headTarget=clamp(-vy*.0014-av*.025,-.22,.22);
headVelocity+=(headTarget-headSpring)*72*dt;
headVelocity*=Math.exp(-10*dt);
headSpring+=headVelocity*dt;
const tailTarget=clamp(av*.04+vx*.0007,-.32,.32);
tailVelocity+=(tailTarget-tailSpring)*55*dt;
tailVelocity*=Math.exp(-8*dt);
tailSpring+=tailVelocity*dt;
const bandTarget=clamp(-av*.06-vx*.0015,-.65,.65);
bandVelocity+=(bandTarget-bandSpring)*48*dt;
bandVelocity*=Math.exp(-7*dt);
bandSpring+=bandVelocity*dt;
chainSwing=clamp(chainSwing+(vx*.0005+av*.012-chainSwing)*dt*7,-.5,.5);
p.ha+=headSpring;
p.tail+=tailSpring;
}
function updateFX(dt) {
for (const p of particles) {
p.life-=dt;
p.vy+=185*dt;
p.x+=p.vx*dt;
p.y+=p.vy*dt;
p.vx*=Math.exp(-1.8*dt);
p.rot+=p.vr*dt;
if (p.y>0) {p.y=0;p.vy*=-.18;}
}
for (let i=particles.length-1;i>=0;i--) {
if (particles[i].life<=0) particles.splice(i,1);
}
for (const r of rings) r.life-=dt*1.7;
for (let i=rings.length-1;i>=0;i--) {
if (rings[i].life<=0) rings.splice(i,1);
}
if (shout) {
shout.life-=dt*1.35;
if (shout.life<=0) shout=null;
}
shake*=Math.exp(-12*dt);
impactPulse*=Math.exp(-8*dt);
}
// ---------- Generated backbeat ----------
let audioCtx=null;
let master=null;
let noiseBuffer=null;
let soundOn=false;
let lastAudioStep=-1;
function initAudio() {
if (audioCtx) return;
const AC=window.AudioContext||window.webkitAudioContext;
if (!AC) return;
audioCtx=new AC();
master=audioCtx.createGain();
master.gain.value=.24;
master.connect(audioCtx.destination);
noiseBuffer=audioCtx.createBuffer(1,audioCtx.sampleRate,audioCtx.sampleRate);
const data=noiseBuffer.getChannelData(0);
for (let i=0;i<data.length;i++) data[i]=Math.random()*2-1;
}
function kick(t) {
const o=audioCtx.createOscillator();
const g=audioCtx.createGain();
o.frequency.setValueAtTime(145,t);
o.frequency.exponentialRampToValueAtTime(44,t+.13);
g.gain.setValueAtTime(.75,t);
g.gain.exponentialRampToValueAtTime(.001,t+.19);
o.connect(g);g.connect(master);
o.start(t);o.stop(t+.2);
}
function noiseHit(t,duration,frequency,type,volume) {
const src=audioCtx.createBufferSource();
const filter=audioCtx.createBiquadFilter();
const gain=audioCtx.createGain();
src.buffer=noiseBuffer;
filter.type=type;
filter.frequency.value=frequency;
gain.gain.setValueAtTime(volume,t);
gain.gain.exponentialRampToValueAtTime(.001,t+duration);
src.connect(filter);filter.connect(gain);gain.connect(master);
src.start(t);src.stop(t+duration+.01);
}
function scheduleStep(step) {
if (!soundOn || !audioCtx || audioCtx.state!=="running") return;
const t=audioCtx.currentTime+.006;
const s=((step%16)+16)%16;
if (s===0 || s===6 || s===10) kick(t);
if (s===4 || s===12) {
noiseHit(t,.15,1500,"bandpass",.45);
kick(t);
}
if (s%2===0) noiseHit(t,s===14?.11:.035,6500,"highpass",s===14?.16:.09);
}
async function toggleSound() {
try {
initAudio();
if (!audioCtx) return;
if (audioCtx.state==="suspended") await audioCtx.resume();
soundOn=!soundOn;
lastAudioStep=Math.floor(beat*4);
const button=document.getElementById("sound");
button.setAttribute("aria-pressed",String(soundOn));
document.getElementById("soundState").textContent=soundOn?"ON":"OFF";
} catch(e) {
document.getElementById("queueText").textContent="AUDIO IS UNAVAILABLE IN THIS FRAME · THE DANCE GOES ON";
}
}
// ---------- Drawing ----------
function pathFillStroke(fill,stroke=INK,width=4) {
ctx.fillStyle=fill;
ctx.fill();
ctx.lineWidth=width;
ctx.strokeStyle=stroke;
ctx.stroke();
}
function drawBackground() {
const sky=ctx.createLinearGradient(0,0,0,H);
sky.addColorStop(0,"#2b253c");
sky.addColorStop(.62,"#211d2d");
sky.addColorStop(1,"#17151d");
ctx.fillStyle=sky;
ctx.fillRect(-12,-12,W+24,H+24);
for (const s of stars) {
const x=s.x*W,y=s.y*Math.max(groundY,1);
ctx.globalAlpha=s.a;
ctx.strokeStyle=PAPER;
ctx.lineWidth=1;
ctx.beginPath();
ctx.moveTo(x-s.s*2,y);ctx.lineTo(x+s.s*2,y);
ctx.moveTo(x,y-s.s*2);ctx.lineTo(x,y+s.s*2);
ctx.stroke();
}
ctx.globalAlpha=1;
const pulse=Math.exp(-(beat%1)*7);
const R=136*birdScale*(1+pulse*.012+impactPulse*.015);
const sx=birdX;
const sy=groundY-124*birdScale;
ctx.save();
ctx.beginPath();
ctx.arc(sx,sy,R,0,TAU);
ctx.clip();
ctx.fillStyle=ACID;
ctx.fillRect(sx-R,sy-R,R*2,R*2);
ctx.fillStyle="rgba(23,21,29,.19)";
const gap=Math.max(10,13*birdScale);
for (let y=sy-R;y<sy+R;y+=gap) {
for (let x=sx-R;x<sx+R;x+=gap) {
if (y>sy-R*.15 && x>sx-R*.75+(y-sy)*.25) {
ctx.beginPath();
ctx.arc(x,y,1.4+((y-sy+R)/(R*2))*2.2,0,TAU);
ctx.fill();
}
}
}
ctx.restore();
ctx.fillStyle="#191720";
for (const b of skyline) {
const top=groundY+5-b.h;
ctx.fillRect(b.x,top,b.w,b.h);
ctx.fillRect(b.x+b.w*.18,top-5,b.w*.55,5);
for (const w of b.windows) {
ctx.globalAlpha=w.a;
ctx.fillStyle=ACID;
ctx.fillRect(b.x+w.x,top+w.y,5,8);
}
ctx.globalAlpha=1;
ctx.fillStyle="#191720";
}
// Water tower and aerials.
if (W>760) {
const x=W*.86,y=groundY-160;
ctx.strokeStyle="#191720";
ctx.fillStyle="#191720";
ctx.lineWidth=7;
ctx.beginPath();
ctx.moveTo(x-24,y+45);ctx.lineTo(x-36,groundY);
ctx.moveTo(x+24,y+45);ctx.lineTo(x+36,groundY);
ctx.stroke();
ctx.fillRect(x-34,y,68,46);
ctx.beginPath();
ctx.moveTo(x-39,y);ctx.lineTo(x,y-24);ctx.lineTo(x+39,y);ctx.closePath();ctx.fill();
}
ctx.fillStyle="#211e28";
ctx.fillRect(-10,groundY,W+20,H-groundY+20);
ctx.fillStyle=PAPER;
ctx.globalAlpha=.7;
ctx.fillRect(-10,groundY,W+20,2);
ctx.globalAlpha=1;
ctx.strokeStyle="rgba(240,232,215,.07)";
ctx.lineWidth=1;
for (let i=-4;i<=4;i++) {
ctx.beginPath();
ctx.moveTo(birdX+i*35*birdScale,groundY);
ctx.lineTo(birdX+i*150*birdScale,H);
ctx.stroke();
}
ctx.save();
ctx.strokeStyle="rgba(217,244,91,.45)";
ctx.lineWidth=2;
ctx.setLineDash([13*birdScale,10*birdScale]);
ctx.beginPath();
ctx.ellipse(birdX,groundY+25*birdScale,205*birdScale,43*birdScale,0,0,TAU);
ctx.stroke();
ctx.setLineDash([]);
ctx.restore();
// Gaffer-tape marks around the cipher.
ctx.fillStyle="rgba(240,232,215,.32)";
for (const side of [-1,1]) {
ctx.save();
ctx.translate(birdX+side*205*birdScale,groundY+8*birdScale);
ctx.rotate(side*.15);
ctx.fillRect(-14,-2,28,4);
ctx.fillRect(-2,-11,4,22);
ctx.restore();
}
ctx.save();
ctx.globalAlpha=.19;
ctx.fillStyle=grain;
ctx.fillRect(0,0,W,H);
ctx.restore();
}
function drawShoe(x,y,angle,far) {
ctx.save();
ctx.translate(x,y);
ctx.rotate(angle);
ctx.beginPath();
ctx.moveTo(-9,-13);
ctx.lineTo(5,-13);
ctx.lineTo(8,-5);
ctx.lineTo(18,0);
ctx.lineTo(19,9);
ctx.lineTo(16,13);
ctx.lineTo(-18,13);
ctx.lineTo(-19,7);
ctx.lineTo(-11,1);
ctx.closePath();
pathFillStroke(far?"#c54f50":CORAL,INK,4);
ctx.fillStyle=PAPER;
ctx.beginPath();
ctx.moveTo(-18,7);ctx.lineTo(18,7);ctx.lineTo(16,13);ctx.lineTo(-18,13);ctx.closePath();
ctx.fill();
ctx.strokeStyle=INK;ctx.lineWidth=2;ctx.stroke();
ctx.strokeStyle=PAPER;
ctx.lineWidth=2;
for (let i=0;i<3;i++) {
ctx.beginPath();
ctx.moveTo(-6+i*4,-8+i*2);
ctx.lineTo(3+i*2,-6+i*2);
ctx.stroke();
}
ctx.restore();
}
function kneeFor(hip,foot,bend) {
let dx=foot.x-hip.x,dy=foot.y-hip.y;
let d=Math.max(1,Math.hypot(dx,dy));
let l1=45,l2=50;
if (d>94) {
const stretch=d/94;
l1*=stretch;l2*=stretch;
}
d=Math.min(d,l1+l2-1);
const ux=dx/Math.hypot(dx,dy),uy=dy/Math.hypot(dx,dy);
const a=(l1*l1-l2*l2+d*d)/(2*d);
const h=Math.sqrt(Math.max(0,l1*l1-a*a));
return {
x:hip.x+ux*a-uy*h*bend,
y:hip.y+uy*a+ux*h*bend
};
}
function drawLeg(hip,foot,angle,near) {
const knee=kneeFor(hip,foot,near?1:-1);
ctx.lineCap="round";
ctx.lineJoin="round";
ctx.beginPath();
ctx.moveTo(hip.x,hip.y);
ctx.lineTo(knee.x,knee.y);
ctx.lineTo(foot.x,foot.y);
ctx.strokeStyle=INK;ctx.lineWidth=10;ctx.stroke();
ctx.strokeStyle=near?"#e99270":"#aa675f";
ctx.lineWidth=4;ctx.stroke();
ctx.beginPath();
ctx.arc(knee.x,knee.y,3.3,0,TAU);
ctx.fillStyle=near?"#e99270":"#aa675f";
ctx.fill();
drawShoe(foot.x,foot.y,angle,!near);
}
function drawWing(S,T,near,spread) {
const dx=T.x-S.x,dy=T.y-S.y;
const d=Math.max(1,Math.hypot(dx,dy));
const ux=dx/d,uy=dy/d;
const nx=-uy,ny=ux;
const bend=(near?-1:1)*Math.min(27,d*.24);
const E={
x:(S.x+T.x)*.5+nx*bend,
y:(S.y+T.y)*.5+ny*bend
};
ctx.beginPath();
ctx.moveTo(S.x+nx*11,S.y+ny*11);
ctx.quadraticCurveTo(E.x+nx*(20+spread*5),E.y+ny*(20+spread*5),T.x,T.y);
ctx.lineTo(T.x-ux*17+nx*(7+spread*4),T.y-uy*17+ny*(7+spread*4));
ctx.lineTo(T.x-ux*12,T.y-uy*12);
ctx.lineTo(T.x-ux*20-nx*(8+spread*4),T.y-uy*20-ny*(8+spread*4));
ctx.quadraticCurveTo(E.x-nx*18,E.y-ny*18,S.x-nx*10,S.y-ny*10);
ctx.closePath();
pathFillStroke(near?"#686b7d":"#464858",INK,near?4.5:4);
ctx.strokeStyle=near?"#a9abb2":"#707180";
ctx.lineWidth=2;
ctx.beginPath();
ctx.moveTo(S.x,S.y);
ctx.quadraticCurveTo(E.x,E.y,T.x-ux*8,T.y-uy*8);
ctx.stroke();
for (const f of [.42,.62,.78]) {
const bx=S.x+(T.x-S.x)*f+nx*bend*Math.sin(f*Math.PI);
const by=S.y+(T.y-S.y)*f+ny*bend*Math.sin(f*Math.PI);
ctx.beginPath();
ctx.moveTo(bx-nx*8,by-ny*8);
ctx.lineTo(bx+nx*9+ux*8,by+ny*9+uy*8);
ctx.stroke();
}
if (spread>.15) {
ctx.fillStyle=near?"#686b7d":"#464858";
ctx.strokeStyle=INK;
ctx.lineWidth=2;
for (let i=-1;i<=1;i++) {
const base={x:T.x-ux*13+nx*i*5,y:T.y-uy*13+ny*i*5};
const tip={x:T.x+ux*(5+spread*9)+nx*i*(5+spread*4),y:T.y+uy*(5+spread*9)+ny*i*(5+spread*4)};
ctx.beginPath();
ctx.moveTo(base.x-nx*3,base.y-ny*3);
ctx.lineTo(tip.x,tip.y);
ctx.lineTo(base.x+nx*3,base.y+ny*3);
ctx.closePath();ctx.fill();ctx.stroke();
}
}
}
function drawTail(p) {
ctx.save();
ctx.translate(p.x,p.y);
ctx.rotate(p.a);
ctx.translate(-35,27);
ctx.rotate(p.tail+tailSpring-.12);
for (let i=-1;i<=1;i++) {
ctx.beginPath();
ctx.moveTo(0,i*7-7);
ctx.lineTo(-70,i*13-12);
ctx.lineTo(-91,i*14+2);
ctx.lineTo(-18,i*8+12);
ctx.closePath();
pathFillStroke(i===0?"#777988":"#555767",INK,3.5);
ctx.strokeStyle="rgba(240,232,215,.35)";
ctx.lineWidth=1.5;
ctx.beginPath();ctx.moveTo(-12,i*8);ctx.lineTo(-72,i*13);ctx.stroke();
}
ctx.restore();
}
function drawBody(p) {
ctx.save();
ctx.translate(p.x,p.y);
ctx.rotate(p.a);
ctx.scale(1+impactPulse*.035,1-impactPulse*.045);
ctx.save();
ctx.translate(3,2);
ctx.beginPath();ctx.ellipse(0,0,52,63,0,0,TAU);
ctx.strokeStyle=CORAL;ctx.globalAlpha=.65;ctx.lineWidth=7;ctx.stroke();
ctx.restore();
const g=ctx.createLinearGradient(-40,-55,45,55);
g.addColorStop(0,"#d8d4ca");
g.addColorStop(.55,"#aaa9aa");
g.addColorStop(1,"#858793");
ctx.beginPath();ctx.ellipse(0,0,52,63,0,0,TAU);
pathFillStroke(g,INK,5);
ctx.beginPath();ctx.ellipse(13,16,29,38,-.18,0,TAU);
ctx.fillStyle="#d9d5cc";ctx.fill();
ctx.strokeStyle="rgba(23,21,29,.35)";
ctx.lineWidth=2;
for (let i=0;i<3;i++) {
ctx.beginPath();
ctx.arc(10,2+i*13,18-i*2,.2,2.25);
ctx.stroke();
}
ctx.fillStyle=INK;
ctx.font="900 10px Arial";
ctx.textAlign="center";
ctx.fillText("04",-20,26);
ctx.restore();
}
function drawNeck(p,H) {
const A=bodyPoint(p,24,-40);
const dx=H.x-A.x,dy=H.y-A.y;
const d=Math.max(1,Math.hypot(dx,dy));
const nx=-dy/d,ny=dx/d;
const g=ctx.createLinearGradient(A.x,A.y,H.x,H.y);
g.addColorStop(0,"#7457a5");
g.addColorStop(.48,"#3ba99e");
g.addColorStop(1,"#7ad6b5");
ctx.beginPath();
ctx.moveTo(A.x+nx*20,A.y+ny*20);
ctx.quadraticCurveTo((A.x+H.x)*.5+nx*8,(A.y+H.y)*.5+ny*8,H.x+nx*20,H.y+ny*20);
ctx.lineTo(H.x-nx*20,H.y-ny*20);
ctx.quadraticCurveTo((A.x+H.x)*.5-nx*5,(A.y+H.y)*.5-ny*5,A.x-nx*20,A.y-ny*20);
ctx.closePath();
pathFillStroke(g,INK,4);
ctx.strokeStyle="rgba(240,232,215,.42)";
ctx.lineWidth=2;
for (let i=1;i<4;i++) {
const q=i/5;
const x=A.x+dx*q,y=A.y+dy*q;
ctx.beginPath();
ctx.moveTo(x-nx*12,y-ny*12);
ctx.lineTo(x+nx*11+dx/d*5,y+ny*11+dy/d*5);
ctx.stroke();
}
}
function drawChain(p) {
ctx.save();
ctx.translate(p.x,p.y);
ctx.rotate(p.a);
const swing=chainSwing*24;
ctx.strokeStyle="#f2c34e";
ctx.lineWidth=3;
ctx.beginPath();
ctx.moveTo(8,-40);
ctx.quadraticCurveTo(28+swing,-9,43,-38);
ctx.stroke();
ctx.save();
ctx.translate(29+swing,-10);
ctx.rotate(chainSwing*.6);
ctx.beginPath();
ctx.moveTo(0,-7);ctx.lineTo(7,0);ctx.lineTo(0,8);ctx.lineTo(-7,0);ctx.closePath();
pathFillStroke("#f2c34e",INK,2);
ctx.restore();
ctx.restore();
}
function drawHead(p,H) {
const angle=p.a*.28+p.ha+headSpring;
ctx.save();
ctx.translate(H.x,H.y);
ctx.rotate(angle);
// Sweatband ties, driven by their own damped spring.
ctx.strokeStyle=INK;
ctx.lineWidth=9;
ctx.lineCap="round";
for (const side of [-1,1]) {
ctx.beginPath();
ctx.moveTo(-22,-13);
ctx.quadraticCurveTo(-39,-19+bandSpring*13*side,-52-bandSpring*14,-7+bandSpring*20*side);
ctx.stroke();
ctx.strokeStyle=CORAL;ctx.lineWidth=5;ctx.stroke();
ctx.strokeStyle=INK;ctx.lineWidth=9;
}
// Beak.
ctx.beginPath();
ctx.moveTo(22,-6);ctx.lineTo(53,1);ctx.lineTo(25,12);ctx.lineTo(16,5);ctx.closePath();
pathFillStroke("#f3bd55",INK,4);
ctx.strokeStyle="rgba(23,21,29,.45)";
ctx.lineWidth=1.5;
ctx.beginPath();ctx.moveTo(29,3);ctx.lineTo(46,2);ctx.stroke();
ctx.save();
ctx.translate(2,2);
ctx.beginPath();ctx.ellipse(0,0,32,31,0,0,TAU);
ctx.strokeStyle=CORAL;ctx.globalAlpha=.6;ctx.lineWidth=6;ctx.stroke();
ctx.restore();
const hg=ctx.createLinearGradient(-20,-25,20,25);
hg.addColorStop(0,"#d5d2ca");
hg.addColorStop(1,"#92949e");
ctx.beginPath();ctx.ellipse(0,0,32,31,0,0,TAU);
pathFillStroke(hg,INK,4.5);
ctx.fillStyle="#777a89";
ctx.beginPath();ctx.arc(-8,-10,23,3.35,5.85);ctx.lineTo(-8,-10);ctx.fill();
// Sweatband.
ctx.strokeStyle=INK;ctx.lineWidth=11;
ctx.beginPath();ctx.moveTo(-28,-14);ctx.quadraticCurveTo(0,-21,28,-12);ctx.stroke();
ctx.strokeStyle=CORAL;ctx.lineWidth=7;ctx.stroke();
ctx.strokeStyle=ACID;ctx.lineWidth=2;
ctx.beginPath();ctx.moveTo(-23,-15);ctx.quadraticCurveTo(0,-20,23,-13);ctx.stroke();
// One extremely unimpressed eye.
ctx.beginPath();ctx.ellipse(12,-6,9,8,0,0,TAU);
ctx.fillStyle=PAPER;ctx.fill();ctx.strokeStyle=INK;ctx.lineWidth=2.5;ctx.stroke();
ctx.beginPath();ctx.arc(15,-5,4,0,TAU);ctx.fillStyle=INK;ctx.fill();
ctx.beginPath();ctx.arc(16,-7,1.1,0,TAU);ctx.fillStyle=PAPER;ctx.fill();
ctx.strokeStyle=INK;ctx.lineWidth=3;
ctx.beginPath();ctx.moveTo(4,-12);ctx.lineTo(20,-10);ctx.stroke();
ctx.lineWidth=2.5;
for (let i=-1;i<=1;i++) {
ctx.beginPath();
ctx.moveTo(-8+i*7,-28);
ctx.lineTo(-12+i*8,-37-Math.abs(i)*2);
ctx.stroke();
}
ctx.restore();
}
function drawContact(x,wing=false) {
ctx.save();
ctx.translate(x,1);
ctx.strokeStyle=wing?CORAL:ACID;
ctx.globalAlpha=.6+impactPulse*.25;
ctx.lineWidth=2;
ctx.beginPath();
ctx.moveTo(-11,0);ctx.lineTo(-18,4);
ctx.moveTo(10,0);ctx.lineTo(17,3);
ctx.stroke();
ctx.restore();
}
function drawRig(p) {
const hipFar=bodyPoint(p,-18,48);
const hipNear=bodyPoint(p,18,50);
const shoulderFar=bodyPoint(p,-10,-12);
const shoulderNear=bodyPoint(p,12,-8);
const H=bodyPoint(p,34+p.hx,-62+p.hy);
if (p.ly>=-17) drawContact(p.lx);
if (p.ry>=-17) drawContact(p.rx);
if (p.lwy>=-8) drawContact(p.lwx,true);
if (p.rwy>=-8) drawContact(p.rwx,true);
drawLeg(hipFar,{x:p.lx,y:p.ly},p.la,false);
drawWing(shoulderFar,{x:p.lwx,y:p.lwy},false,p.spread);
drawTail(p);
drawLeg(hipNear,{x:p.rx,y:p.ry},p.ra,true);
drawBody(p);
drawNeck(p,H);
drawChain(p);
drawWing(shoulderNear,{x:p.rwx,y:p.rwy},true,p.spread);
drawHead(p,H);
}
function drawFX(front) {
for (const r of rings) {
if (front) continue;
const q=1-r.life;
ctx.save();
ctx.globalAlpha=r.life*.5;
ctx.strokeStyle=ACID;
ctx.lineWidth=2;
ctx.beginPath();
ctx.ellipse(r.x,1,15+q*65*r.strength,3+q*12,0,0,TAU);
ctx.stroke();
ctx.restore();
}
for (const p of particles) {
if (p.front!==front) continue;
ctx.save();
ctx.translate(p.x,p.y);
ctx.rotate(p.rot);
ctx.globalAlpha=clamp(p.life/p.max,0,1)*.75;
if (p.feather) {
ctx.fillStyle=PAPER;
ctx.strokeStyle=INK;
ctx.lineWidth=1.5;
ctx.beginPath();
ctx.moveTo(0,-7);ctx.quadraticCurveTo(7,-1,0,8);ctx.quadraticCurveTo(-6,0,0,-7);
ctx.fill();ctx.stroke();
} else {
ctx.fillStyle=Math.random()>.5?PAPER:ACID;
ctx.fillRect(-p.size*.5,-p.size*.5,p.size,p.size);
}
ctx.restore();
}
}
function drawWorld(p) {
ctx.save();
ctx.translate(birdX,groundY);
ctx.scale(birdScale,birdScale);
const lift=clamp((-p.y-45)/100,0,1);
ctx.save();
ctx.globalAlpha=.34-lift*.14;
ctx.fillStyle="#08070b";
ctx.beginPath();
ctx.ellipse(p.x*.75+8,4,84-lift*25,12-lift*3,0,0,TAU);
ctx.fill();
ctx.restore();
if (current==="windmill" && beat-moveStart>1 && beat-moveStart<7) {
ctx.save();
ctx.strokeStyle="rgba(217,244,91,.45)";
ctx.lineWidth=3;
ctx.lineCap="round";
for (let i=0;i<4;i++) {
const a=p.a+i*1.55;
ctx.beginPath();
ctx.arc(p.x,p.y,91+i*8,a,a+.58);
ctx.stroke();
}
ctx.restore();
}
drawFX(false);
drawRig(p);
drawFX(true);
if (shout) {
ctx.save();
ctx.translate(shout.x,shout.y);
ctx.rotate(shout.rot);
ctx.globalAlpha=clamp(shout.life*2,0,1);
const s=1+(1-shout.life)*.18;
ctx.scale(s,s);
ctx.font="900 29px Impact, Arial Black, sans-serif";
ctx.textAlign="center";
ctx.lineWidth=7;
ctx.strokeStyle=INK;
ctx.strokeText(shout.text,0,0);
ctx.fillStyle=ACID;
ctx.fillText(shout.text,0,0);
ctx.restore();
}
ctx.restore();
}
// ---------- Interface and main clock ----------
const moveButtons=Array.from(document.querySelectorAll(".move"));
const beatDots=Array.from(document.querySelectorAll(".beats i"));
const moveName=document.getElementById("moveName");
const topMove=document.getElementById("topMove");
moveButtons.forEach(button=>{
button.addEventListener("click",()=>requestMove(button.dataset.move));
});
document.getElementById("sound").addEventListener("click",toggleSound);
const tempo=document.getElementById("tempo");
tempo.addEventListener("input",()=>{
bpm=Number(tempo.value);
document.getElementById("bpmOut").textContent=bpm+" BPM";
});
window.addEventListener("keydown",event=>{
if (event.repeat) return;
const map={Digit1:"toprock",Digit2:"footwork",Digit3:"windmill",Digit4:"freeze"};
if (map[event.code]) requestMove(map[event.code]);
if (event.code==="KeyM") toggleSound();
if (event.code==="Space") {
const tag=document.activeElement && document.activeElement.tagName;
if (tag!=="INPUT" && tag!=="BUTTON") {
event.preventDefault();
requestMove("freeze");
}
}
});
function updateUI(local) {
const m=MOVES[current];
moveName.textContent=m.label;
topMove.textContent=pending ? "NEXT: "+MOVES[pending.name].label : m.label;
const activeBeat=((Math.floor(beat)%4)+4)%4;
beatDots.forEach((dot,i)=>dot.classList.toggle("on",i===activeBeat));
moveButtons.forEach(button=>{
const name=button.dataset.move;
button.classList.toggle("active",name===current && !(pending&&pending.name===name));
button.classList.toggle("queued",!!pending&&pending.name===name);
button.setAttribute("aria-pressed",String(name===current));
});
}
let lastTime=performance.now();
function frame(now) {
let dt=Math.min(.05,Math.max(0,(now-lastTime)/1000));
lastTime=now;
beat+=dt*bpm/60;
const audioStep=Math.floor(beat*4);
if (audioStep!==lastAudioStep) {
if (lastAudioStep<0 || audioStep-lastAudioStep>4) lastAudioStep=audioStep-1;
for (let s=lastAudioStep+1;s<=audioStep;s++) scheduleStep(s);
lastAudioStep=audioStep;
}
if (pending && beat>=pending.at) {
const name=pending.name;
const at=pending.at;
pending=null;
setMove(name,at,true);
document.getElementById("queueText").textContent="CALL ANOTHER MOVE · OR LET THE SET RUN";
}
let local=beat-moveStart;
if (local>=MOVES[current].duration) {
const nextStart=moveStart+MOVES[current].duration;
setMove(NEXT[current],nextStart,false);
local=beat-moveStart;
}
const p=currentPose();
checkImpacts(lastLocal,local,p);
lastLocal=local;
secondaryMotion(p,dt);
updateFX(dt);
lastPose=Object.assign({},p);
ctx.setTransform(DPR,0,0,DPR,0,0);
ctx.clearRect(0,0,W,H);
ctx.fillStyle=INK;
ctx.fillRect(0,0,W,H);
const sx=reduceMotion?0:(Math.random()-.5)*shake;
const sy=reduceMotion?0:(Math.random()-.5)*shake;
ctx.save();
ctx.translate(sx,sy);
drawBackground();
drawWorld(p);
ctx.restore();
updateUI(local);
requestAnimationFrame(frame);
}
document.addEventListener("visibilitychange",()=>{
lastTime=performance.now();
});
resize();
requestAnimationFrame(frame);
})();
</script>
</body>
</html>