27:18Gemini 4 Hydrogen: Cheaper Tokens, More Expensive Answers?
Nov 13, 2026 · YouTube
18:40ERNIE 5.5 Flash Preview vs GLM 5.3 Flash: Baidu's new model is very impressive!
Nov 11, 2026 · YouTube
36:22Fable 5.2 robbing me blind, plus: Nex AGI's N2.6 vs GLM 5.3 Flash
Nov 11, 2026 · YouTube
14:03Gemini 4 Carbon: Can it handle my challenges? (First look with one-shot coding tests)
Nov 10, 2026 · YouTube
47:51Ranking major U.S. Open-Weights Models. (Where does Ripple 65B fit in?)
Nov 6, 2026 · YouTube
10:57"Crouton Alpha": the latest super-efficient coding model by Meta?
Nov 3, 2026 · YouTube
38:58Testing Particle-1: a post-trained DeepSeek V4.1 Flash specializing in coding and efficiency
Oct 28, 2026 · YouTube
22:14GPT-6.1 Terra Pro vs Terra: How Much Better Is Pro?
Oct 27, 2026 · YouTube
29:55Kimi K3.1 Pro Looked Incredible. Then I Kept Testing.
Oct 26, 2026 · YouTube
31:28Is this company reselling GPT Sol 6.1 at a loss (for non-agentic/text-only)?🤔 Is this sustainable?
Oct 20, 2026 · YouTube
17:58Nemotron 3.5 Super VL vs Opus 5.5: Testing NVIDIA’s new coding juggernaut
Oct 15, 2026 · YouTube
16:04Testing a *massive* new open-weights model 🤯: Muse Spark Open (vs. Nemotron, Opus 5.5, and more)
Oct 15, 2026 · YouTube
16:43Gemini 4 Argon: Great 🐈 Animation, but a broken escalator. (Coding challenge vs Opus 5.5, Sol 6.1)
Oct 13, 2026 · YouTube
16:37Meta's new "Muse Wonder" AI is impressive... but can it compete with Anthropic's models?
Oct 12, 2026 · YouTube
11:12WOW... Haiku 5.5 is very impressive. RIP OpenAI?
Oct 7, 2026 · YouTube
22:47Mistral Large 4 Is Cheap. But Is It Good Enough to Compete?
Oct 6, 2026 · YouTube
22:02Is Pareto 26.10 Preview Just Routing to GPT Sol? I Tested It Against Sol 6.1 Pro
Oct 1, 2026 · YouTube
11:22I Tested Sonnet 5.5 Against Opus 5.5. Is It Actually Cheaper?
Sep 28, 2026 · YouTube
30:00Did Elon Musk's team "train the gay away"... and make Grok 4.7 worse?
Sep 26, 2026 · YouTube
19:21I Tested the Stealth "Space Bunny Alpha" Model Before It Went Offline
Sep 23, 2026 · YouTube
10:58So... how much better is Claude Opus 5.5 vs. GPT-6 Sol, really?
Sep 23, 2026 · YouTube
9:38Claude Opus 5.5... the BEST new model for coding!! 🤩
Sep 22, 2026 · YouTube
29:00Grok 4.7: improved, but GLM 5.3 FlashX beat it anyway (benchmark against Gemini, Sol, etc.)
Sep 22, 2026 · YouTube
You make short animated pieces for the web.
Build me a single HTML page: a bird, breakdancing.
data: URIs are fine.localStorage, sessionStorage, cookies or indexedDB — sandboxed frame, they throw.html block.Two sentences: what the bird is, and the look you went for.
The single fenced html block.
Three bullets.
The bird is a stubborn street sparrow, a b-boy who treats every hydrant, awning and curb as a cypher. The look is gritty city-night poster art: cream linework, a red bandana, and just enough feathers to read as bird before breaker.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>B-Boy Sparrow</title>
<style>
:root{
--ui:#f0dfbf;
--uiborder:rgba(240,223,191,.18);
}
*{ box-sizing:border-box; }
html,body{
height:100%;
margin:0;
overflow:hidden;
background:#0d1215;
}
body{
font-family:'Courier New',Courier,monospace;
color:var(--ui);
}
#c{
position:fixed;
inset:0;
display:block;
}
header{
position:fixed;
top:16px;
left:20px;
z-index:5;
pointer-events:none;
}
header h1{
font-size:clamp(16px,2.6vw,28px);
letter-spacing:.16em;
margin:0;
text-transform:uppercase;
color:rgba(240,223,191,.92);
text-shadow:0 0 12px rgba(255,120,60,.25);
}
header p{
margin:5px 0 0;
font-size:11px;
letter-spacing:.38em;
opacity:.6;
}
#ui{
position:fixed;
left:50%;
bottom:18px;
transform:translateX(-50%);
display:flex;
flex-wrap:wrap;
align-items:center;
justify-content:center;
gap:10px;
padding:10px 14px;
border:1px solid var(--uiborder);
background:rgba(10,14,14,.55);
border-radius:999px;
backdrop-filter:blur(6px);
z-index:5;
max-width:calc(100vw - 20px);
}
.ctl{
display:flex;
align-items:center;
gap:7px;
font-size:11px;
letter-spacing:.12em;
text-transform:uppercase;
white-space:nowrap;
}
input[type=range]{
width:90px;
accent-color:#e6553a;
background:transparent;
}
button{
background:transparent;
border:1px solid var(--uiborder);
color:var(--ui);
padding:5px 10px;
border-radius:999px;
cursor:pointer;
font:inherit;
letter-spacing:.12em;
text-transform:uppercase;
}
button:hover{
border-color:rgba(255,220,180,.5);
background:rgba(255,220,180,.08);
}
button.active{
border-color:#e6553a;
color:#ffd5b0;
box-shadow:0 0 10px rgba(230,85,58,.35);
}
#bpmout{
min-width:72px;
text-align:right;
opacity:.85;
}
</style>
</head>
<body>
<canvas id="c"></canvas>
<header>
<h1>B-BOY SPARROW</h1>
<p>CONCRETE IS A CYPHER</p>
</header>
<div id="ui">
<div class="ctl">
<label for="bpm">tempo</label>
<input id="bpm" type="range" min="45" max="170" value="100">
<span id="bpmout">100 BPM</span>
</div>
<button id="modebtn">routine: full set</button>
<button id="pinbtn">pause</button>
<button id="palbtn">paint: sodium</button>
</div>
<script>
'use strict';
const $ = id => document.getElementById(id);
const canvas = $('c');
const ctx = canvas.getContext('2d');
let W = 0, H = 0, cx = 0, groundY = 0, DPR = 1;
function resize() {
DPR = Math.min(window.devicePixelRatio || 1, 2);
W = window.innerWidth;
H = window.innerHeight;
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);
cx = W / 2;
groundY = H * 0.72;
}
window.addEventListener('resize', resize);
resize();
const TORSO = 52, NECK = 22, TAIL = 30;
const UARM = 40, LARM = 32;
const ULEG = 38, LLEG = 34;
const clamp = (v, a, b) => Math.max(a, Math.min(b, v));
const EASE = {
linear: u => u,
inOutCubic: u => u < .5 ? 4 * u * u * u : 1 - Math.pow(-2 * u + 2, 3) / 2,
outCubic: u => 1 - Math.pow(1 - u, 3),
anticipate: u => {
const a = .12;
return u < a ? -u : -a + (1 + a) * (u - a) / (1 - a);
},
overshoot: u => {
const c = 1.70158;
const c3 = c + 1;
return 1 + c3 * Math.pow(u - 1, 3) + c * Math.pow(u - 1, 2);
},
hold: () => 0
};
const P_TOP = [0,-58,0.04,0.14,-36,-62,36,-62,-16,0,16,0];
const P_SQUAT = [0,-42,0.18,0.24,-30,-40,30,-40,-16,0,16,0];
const P_KICK_R = [-6,-56,0.10,0.14,-46,-54,30,-62,-18,0,32,-26];
const P_KICK_L = [6,-56,-0.10,0.14,-30,-62,46,-54,-32,-26,18,0];
const P_DOWN = [0,-12,1.20,1.00,6,0,90,0,-38,0,28,0];
const P_STEP_L = [5,-10,1.28,1.05,10,0,88,0,10,-32,30,0];
const P_STEP_R = [-5,-10,1.12,0.95,8,0,92,0,-36,0,-8,-32];
const P_VKICK = [0,-24,1.35,1.05,8,0,86,0,-48,-30,48,-30];
const P_FREEZE_PREP = [-8,-14,0.85,0.85,12,0,70,-8,-24,0,26,0];
const P_FREEZE = [-12,-16,0.80,0.90,10,0,62,-6,20,-38,-28,0];
const P_RECOVER = [2,-36,0.40,0.20,-16,-44,30,-42,-14,0,18,0];
const MODES = {
all: {
dur: 16,
seq: [
{b:0, p:P_TOP, e:'inOutCubic'},
{b:0.5,p:P_SQUAT, e:'anticipate'},
{b:1, p:P_KICK_R, e:'overshoot'},
{b:1.5,p:P_SQUAT, e:'inOutCubic'},
{b:2, p:P_KICK_L, e:'overshoot'},
{b:2.5,p:P_SQUAT, e:'inOutCubic'},
{b:3, p:P_TOP, e:'outCubic'},
{b:3.4,p:P_SQUAT, e:'anticipate'},
{b:4, p:P_DOWN, e:'outCubic'},
{b:4.5,p:P_DOWN, e:'hold'},
{b:5.5,p:P_STEP_L, e:'inOutCubic'},
{b:6.5,p:P_STEP_R, e:'inOutCubic'},
{b:7.5,p:P_VKICK, e:'overshoot'},
{b:8.5,p:P_FREEZE_PREP,e:'anticipate'},
{b:9.5,p:P_FREEZE, e:'outCubic'},
{b:11.5,p:P_FREEZE, e:'hold'},
{b:12.5,p:P_RECOVER, e:'outCubic'},
{b:13.5,p:P_TOP, e:'overshoot'},
{b:15.5,p:P_TOP, e:'hold'}
]
},
toprock: {
dur: 4,
seq: [
{b:0, p:P_TOP, e:'inOutCubic'},
{b:0.5,p:P_SQUAT, e:'anticipate'},
{b:1, p:P_KICK_R, e:'overshoot'},
{b:1.5,p:P_SQUAT, e:'inOutCubic'},
{b:2, p:P_KICK_L, e:'overshoot'},
{b:2.5,p:P_SQUAT, e:'inOutCubic'},
{b:3, p:P_TOP, e:'outCubic'}
]
},
footwork: {
dur: 11,
seq: [
{b:0, p:P_SQUAT, e:'inOutCubic'},
{b:0.5,p:P_DOWN, e:'outCubic'},
{b:1, p:P_DOWN, e:'hold'},
{b:2, p:P_STEP_L, e:'inOutCubic'},
{b:3, p:P_STEP_R, e:'inOutCubic'},
{b:4, p:P_VKICK, e:'overshoot'},
{b:5, p:P_FREEZE_PREP,e:'anticipate'},
{b:6, p:P_FREEZE, e:'outCubic'},
{b:8, p:P_FREEZE, e:'hold'},
{b:9, p:P_RECOVER, e:'outCubic'},
{b:10, p:P_SQUAT, e:'overshoot'}
]
}
};
function mix(a, b, t) {
const r = [];
for (let i = 0; i < a.length; i++) r.push(a[i] + (b[i] - a[i]) * t);
return r;
}
let beatDur = 60 / 100;
let mode = 'all';
let sceneTime = 0;
function getPose(time) {
const data = MODES[mode];
const dur = data.dur;
const seq = data.seq;
const beat = time / beatDur;
const bt = ((beat % dur) + dur) % dur;
let idx = 0;
for (let i = 0; i < seq.length; i++) {
const a = seq[i];
const next = seq[(i + 1) % seq.length];
const end = (i + 1 === seq.length) ? dur : next.b;
if (bt >= a.b && bt < end) { idx = i; break; }
}
const a = seq[idx];
const b = seq[(idx + 1) % seq.length];
const len = (b.b - a.b + dur) % dur;
const dt = (bt - a.b + dur) % dur;
const u = clamp(dt / len, 0, 1);
const f = EASE[b.e] || EASE.inOutCubic;
return mix(a.p, b.p, f(u));
}
const PALETTES = [
{
name:'sodium',
bgTop:'#0d1215', bgBottom:'#1b2426',
light:'rgba(255,168,80,.06)', floor:'#0a0f10',
grid:'rgba(255,185,115,.07)', ring:'rgba(255,190,100,.22)',
cardboard:'#24332e', cardline:'rgba(255,220,170,.14)',
line:'#f0dfbf', bodyFill:'#242b2c',
texture:'rgba(240,223,191,.22)', far:'#6f665a',
shadow:'rgba(0,0,0,.45)', shoe:'#111416',
beak:'#eea14e', red:'#e6463c', white:'#fff6e8', black:'#050707'
},
{
name:'neon mint',
bgTop:'#03171c', bgBottom:'#0a2624',
light:'rgba(110,255,180,.05)', floor:'#031014',
grid:'rgba(120,255,190,.07)', ring:'rgba(120,255,190,.20)',
cardboard:'#123039', cardline:'rgba(140,255,190,.15)',
line:'#b9f2d8', bodyFill:'#0f2930',
texture:'rgba(185,242,216,.2)', far:'#3d777a',
shadow:'rgba(0,0,0,.5)', shoe:'#061216',
beak:'#f4c95d', red:'#ff6b9a', white:'#ecfff3', black:'#020a0c'
},
{
name:'smoke velvet',
bgTop:'#15111f', bgBottom:'#271725',
light:'rgba(255,150,200,.05)', floor:'#0b0911',
grid:'rgba(255,190,210,.08)', ring:'rgba(255,160,200,.22)',
cardboard:'#2f1e2c', cardline:'rgba(255,210,225,.15)',
line:'#f5d9b8', bodyFill:'#241a2b',
texture:'rgba(245,217,184,.2)', far:'#74586e',
shadow:'rgba(0,0,0,.5)', shoe:'#120c14',
beak:'#ffbe5c', red:'#57e6e6', white:'#fff2e4', black:'#080408'
}
];
let paletteIdx = 0;
function drawBackground(time, palette) {
const g = ctx.createLinearGradient(0, 0, 0, H);
g.addColorStop(0, palette.bgTop);
g.addColorStop(1, palette.bgBottom);
ctx.fillStyle = g;
ctx.fillRect(0, 0, W, H);
ctx.save();
ctx.globalCompositeOperation = 'lighter';
const rg = ctx.createRadialGradient(cx, H * 0.10, 20, cx, H * 0.15, W * 0.7);
rg.addColorStop(0, palette.light);
rg.addColorStop(1, 'rgba(0,0,0,0)');
ctx.fillStyle = rg;
ctx.fillRect(0, 0, W, H);
ctx.restore();
ctx.fillStyle = palette.floor;
ctx.fillRect(0, groundY, W, H - groundY + 2);
ctx.strokeStyle = palette.grid;
ctx.lineWidth = 1;
ctx.beginPath();
for (let i = -6; i <= 6; i++) {
ctx.moveTo(cx + i * 48, groundY);
ctx.lineTo(cx + i * 150, H);
}
for (let j = 1; j < 6; j++) {
const y = groundY + (H - groundY) * j / 6;
ctx.moveTo(0, y);
ctx.lineTo(W, y);
}
ctx.stroke();
const beat = time / beatDur;
const ph = (beat % 1 + 1) % 1;
for (let i = 0; i < 3; i++) {
const rr = 34 + ((ph + i) / 3) * 130;
ctx.strokeStyle = palette.ring;
ctx.globalAlpha = 0.22 * (1 - (ph + i) / 3);
ctx.beginPath();
ctx.ellipse(cx, groundY + 5, rr, rr * 0.18, 0, 0, Math.PI * 2);
ctx.stroke();
}
ctx.globalAlpha = 1;
ctx.save();
ctx.translate(cx, groundY);
ctx.rotate(0.035 * Math.sin(time * 0.7));
ctx.fillStyle = palette.cardboard;
ctx.fillRect(-125, -7, 250, 14);
ctx.strokeStyle = palette.cardline;
ctx.lineWidth = 1.5;
ctx.strokeRect(-125, -7, 250, 14);
ctx.beginPath();
ctx.moveTo(-40, -7); ctx.lineTo(-30, 7);
ctx.moveTo(35, -7); ctx.lineTo(45, 7);
ctx.stroke();
ctx.restore();
}
function drawShadow(palette) {
ctx.save();
ctx.translate(cx, groundY);
ctx.scale(1, 0.12);
ctx.fillStyle = palette.shadow;
ctx.beginPath();
ctx.arc(0, 0, 68, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
function getParts(p) {
const hx = cx + p[0], hy = groundY + p[1];
const ta = p[2], na = p[3];
const shX = hx + Math.sin(ta) * TORSO;
const shY = hy - Math.cos(ta) * TORSO;
const headX = shX + Math.sin(na) * NECK;
const headY = shY - Math.cos(na) * NECK;
const tailX = hx - Math.sin(ta + 0.22) * TAIL;
const tailY = hy + Math.cos(ta + 0.22) * TAIL;
return {
hip: {x:hx, y:hy},
shoulder: {x:shX, y:shY},
head: {x:headX, y:headY},
tail: {x:tailX, y:tailY}
};
}
function ik(ox, oy, tx, ty, l1, l2, dir) {
let dx = tx - ox, dy = ty - oy;
let d = Math.hypot(dx, dy);
if (d < 0.001) {
return {x:ox, y:oy - l1, angle:0, tx:tx, ty:ty};
}
const max = l1 + l2 - 0.1;
if (d > max) {
tx = ox + dx / d * max;
ty = oy + dy / d * max;
dx = tx - ox;
dy = ty - oy;
d = max;
}
const base = Math.atan2(dy, dx);
const cosa = clamp((l1 * l1 + d * d - l2 * l2) / (2 * l1 * d), -1, 1);
const a = Math.acos(cosa);
const kx = ox + l1 * Math.cos(base + dir * a);
const ky = oy + l1 * Math.sin(base + dir * a);
return {
x:kx, y:ky,
angle:Math.atan2(ty - ky, tx - kx),
tx:tx, ty:ty
};
}
function strokeSeg(x1, y1, x2, y2, w, color) {
ctx.strokeStyle = color;
ctx.lineWidth = w;
ctx.lineCap = 'round';
ctx.beginPath();
ctx.moveTo(x1, y1);
ctx.lineTo(x2, y2);
ctx.stroke();
}
function drawWing(origin, target, dir, far, palette) {
const k = ik(origin.x, origin.y, target.x, target.y, UARM, LARM, dir);
const col = far ? palette.far : palette.line;
strokeSeg(origin.x, origin.y, k.x, k.y, far ? 5 : 7, col);
strokeSeg(k.x, k.y, k.tx, k.ty, far ? 3.5 : 4.5, col);
ctx.save();
ctx.translate(k.tx, k.ty);
ctx.strokeStyle = col;
ctx.lineWidth = far ? 2 : 2.5;
const a = k.angle;
for (let i = -1; i <= 1; i++) {
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.lineTo(Math.cos(a + i * 0.5) * 14, Math.sin(a + i * 0.5) * 14);
ctx.stroke();
}
ctx.restore();
ctx.fillStyle = col;
ctx.beginPath();
ctx.arc(origin.x, origin.y, 3, 0, Math.PI * 2);
ctx.fill();
}
function drawLeg(origin, target, dir, far, palette) {
const k = ik(origin.x, origin.y, target.x, target.y, ULEG, LLEG, dir);
const col = far ? palette.far : palette.line;
strokeSeg(origin.x, origin.y, k.x, k.y, far ? 4.5 : 5.5, col);
strokeSeg(k.x, k.y, k.tx, k.ty, far ? 3 : 4, col);
ctx.save();
ctx.translate(k.tx, k.ty);
ctx.rotate(k.angle);
ctx.fillStyle = palette.shoe;
ctx.strokeStyle = col;
ctx.lineWidth = 1.2;
ctx.beginPath();
ctx.ellipse(0, 0, 9, 5, 0, 0, Math.PI * 2);
ctx.fill();
ctx.stroke();
ctx.beginPath();
ctx.moveTo(6, 0);
ctx.lineTo(9, 0);
ctx.stroke();
ctx.restore();
}
function drawTail(parts, palette) {
ctx.fillStyle = palette.bodyFill;
ctx.strokeStyle = palette.line;
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(parts.hip.x - 7, parts.hip.y + 2);
ctx.quadraticCurveTo(parts.tail.x + 6, parts.tail.y - 4, parts.tail.x + 2, parts.tail.y);
ctx.quadraticCurveTo(parts.tail.x - 6, parts.tail.y + 4, parts.hip.x + 7, parts.hip.y + 4);
ctx.closePath();
ctx.fill();
ctx.stroke();
}
function drawTorso(parts, palette) {
ctx.save();
const mx = (parts.hip.x + parts.shoulder.x) / 2;
const my = (parts.hip.y + parts.shoulder.y) / 2;
ctx.translate(mx, my);
ctx.rotate(Math.atan2(parts.shoulder.y - parts.hip.y, parts.shoulder.x - parts.hip.x));
ctx.fillStyle = palette.bodyFill;
ctx.strokeStyle = palette.line;
ctx.lineWidth = 2.2;
ctx.beginPath();
ctx.ellipse(0, 0, TORSO * 0.52, 17, 0, 0, Math.PI * 2);
ctx.fill();
ctx.stroke();
ctx.strokeStyle = palette.texture;
ctx.lineWidth = 1.2;
ctx.beginPath();
ctx.moveTo(-10, -8); ctx.lineTo(0, -12);
ctx.moveTo(-14, 2); ctx.lineTo(-3, -2);
ctx.moveTo(12, 5); ctx.lineTo(20, 0);
ctx.stroke();
ctx.restore();
}
function drawHead(parts, palette) {
const x = parts.head.x, y = parts.head.y;
ctx.fillStyle = palette.bodyFill;
ctx.strokeStyle = palette.line;
ctx.lineWidth = 2;
ctx.beginPath();
ctx.arc(x, y, 13, 0, Math.PI * 2);
ctx.fill();
ctx.stroke();
ctx.fillStyle = palette.beak;
ctx.strokeStyle = palette.line;
ctx.lineWidth = 1.5;
ctx.beginPath();
ctx.moveTo(x + 8, y - 5);
ctx.lineTo(x + 20, y + 1);
ctx.lineTo(x + 8, y + 7);
ctx.closePath();
ctx.fill();
ctx.stroke();
ctx.fillStyle = palette.white;
ctx.strokeStyle = palette.line;
ctx.lineWidth = 1;
ctx.beginPath();
ctx.arc(x + 3, y - 4, 4.6, 0, Math.PI * 2);
ctx.fill();
ctx.stroke();
ctx.fillStyle = palette.black;
ctx.beginPath();
ctx.arc(x + 3.8, y - 4, 2.2, 0, Math.PI * 2);
ctx.fill();
ctx.strokeStyle = palette.red;
ctx.lineWidth = 3.5;
ctx.lineCap = 'round';
ctx.beginPath();
ctx.arc(x, y, 13.5, Math.PI * 1.15, Math.PI * 1.85);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(x - 13, y - 8);
ctx.lineTo(x - 20, y - 14);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(x - 13, y - 7);
ctx.lineTo(x - 19, y - 3);
ctx.stroke();
}
function drawBird(palette, p) {
const parts = getParts(p);
const lh = {x: cx + p[4], y: groundY + p[5]};
const rh = {x: cx + p[6], y: groundY + p[7]};
const lf = {x: cx + p[8], y: groundY + p[9]};
const rf = {x: cx + p[10], y: groundY + p[11]};
drawTail(parts, palette);
drawLeg(parts.hip, lf, -1, true, palette);
drawWing(parts.shoulder, lh, -1, true, palette);
drawTorso(parts, palette);
drawHead(parts, palette);
drawLeg(parts.hip, rf, 1, false, palette);
drawWing(parts.shoulder, rh, 1, false, palette);
}
const bpmEl = $('bpm');
const bpmOut = $('bpmout');
const modeBtn = $('modebtn');
const pinBtn = $('pinbtn');
const palBtn = $('palbtn');
let bpm = 100;
let pinned = false;
let lastTs = null;
bpmEl.addEventListener('input', () => {
bpm = +bpmEl.value;
bpmOut.textContent = bpm + ' BPM';
});
const modeNames = {all:'full set', toprock:'top rock', footwork:'footwork'};
modeBtn.addEventListener('click', () => {
const keys = Object.keys(MODES);
const i = keys.indexOf(mode);
mode = keys[(i + 1) % keys.length];
modeBtn.textContent = 'routine: ' + (modeNames[mode] || mode);
sceneTime = 0;
});
pinBtn.addEventListener('click', () => {
pinned = !pinned;
pinBtn.classList.toggle('active', pinned);
pinBtn.textContent = pinned ? 'unpause' : 'pause';
});
palBtn.addEventListener('click', () => {
paletteIdx = (paletteIdx + 1) % PALETTES.length;
const pal = PALETTES[paletteIdx];
palBtn.textContent = 'paint: ' + pal.name;
document.documentElement.style.setProperty('--ui', pal.line);
document.documentElement.style.setProperty('--uiborder', pal.ring);
});
function render(ts) {
if (lastTs === null) lastTs = ts;
const dt = Math.min(0.05, (ts - lastTs) / 1000);
lastTs = ts;
if (!pinned) sceneTime += dt;
beatDur = 60 / bpm;
const palette = PALETTES[paletteIdx];
ctx.clearRect(0, 0, W, H);
drawBackground(sceneTime, palette);
drawShadow(palette);
const pose = getPose(sceneTime);
drawBird(palette, pose);
requestAnimationFrame(render);
}
requestAnimationFrame(render);
</script>
</body>
</html>