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.
This is Pip, a pigeon with serious opinions about the cypher. I went for a scuffed record-sleeve look: chalk, ink, vermilion sneakers, and a little bit of theatre.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Pip in the cypher</title>
<style>
:root {
color-scheme: dark;
--ink: #172822;
--paper: #e9e4cf;
--acid: #d8f16a;
--red: #e86443;
--muted: #a9b3a4;
}
* { box-sizing: border-box; }
body {
margin: 0;
background: #17251f;
color: var(--paper);
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}
button, input { font: inherit; }
button { cursor: pointer; }
.page {
width: min(1200px, calc(100% - 40px));
margin: 0 auto;
padding: 25px 0 36px;
}
.masthead {
display: flex;
justify-content: space-between;
align-items: center;
gap: 16px;
border-bottom: 1px solid #68766b;
padding-bottom: 14px;
font-size: 11px;
font-weight: 700;
letter-spacing: .13em;
text-transform: uppercase;
}
.mark { color: var(--acid); }
.masthead-right { color: var(--muted); text-align: right; }
.intro {
display: flex;
justify-content: space-between;
align-items: end;
gap: 20px;
padding: 24px 0 18px;
}
h1 {
margin: 0;
font-family: Impact, "Arial Narrow", sans-serif;
font-size: clamp(42px, 8vw, 91px);
font-weight: 900;
line-height: .83;
letter-spacing: -.025em;
text-transform: uppercase;
}
h1 span { color: var(--acid); }
.intro p {
max-width: 245px;
margin: 0 0 3px;
color: #bbc5b5;
font-size: 11px;
line-height: 1.6;
}
.stage {
position: relative;
overflow: hidden;
background: var(--paper);
color: var(--ink);
border: 1px solid #c0c8ad;
}
canvas {
display: block;
width: 100%;
aspect-ratio: 1000 / 640;
}
.stage-top {
position: absolute;
inset: 18px 20px auto;
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 10px;
pointer-events: none;
font-size: clamp(8px, 1vw, 11px);
font-weight: bold;
letter-spacing: .09em;
text-transform: uppercase;
}
.stage-label {
background: var(--ink);
color: var(--paper);
padding: 8px 10px;
}
.stage-count { text-align: right; line-height: 1.5; }
.stage-bottom {
position: absolute;
inset: auto 20px 16px;
display: flex;
justify-content: space-between;
align-items: end;
pointer-events: none;
font-size: clamp(8px, 1vw, 11px);
font-weight: bold;
letter-spacing: .08em;
text-transform: uppercase;
}
.stage-bottom strong {
display: block;
margin-top: 3px;
font-family: Impact, "Arial Narrow", sans-serif;
font-size: clamp(19px, 3vw, 35px);
line-height: 1;
letter-spacing: .01em;
}
.stamp {
width: 62px;
height: 62px;
display: grid;
place-items: center;
border: 2px solid var(--red);
border-radius: 50%;
color: #b84932;
text-align: center;
line-height: 1.15;
transform: rotate(-14deg);
}
.desk {
display: grid;
grid-template-columns: 1fr auto;
gap: 22px;
padding: 20px;
background: #26392e;
border: 1px solid #607263;
border-top: 0;
}
.control-label {
margin: 0 0 10px;
color: #a9b8a6;
font-size: 10px;
font-weight: bold;
letter-spacing: .13em;
text-transform: uppercase;
}
.moves { display: flex; flex-wrap: wrap; gap: 7px; }
.move, .pause {
min-height: 40px;
padding: 10px 14px;
border: 1px solid #819184;
border-radius: 0;
background: transparent;
color: var(--paper);
font-size: 11px;
font-weight: bold;
letter-spacing: .04em;
text-transform: uppercase;
transition: background .15s, color .15s, border-color .15s;
}
.move:hover, .pause:hover { border-color: var(--acid); color: var(--acid); }
.move.active { background: var(--acid); color: var(--ink); border-color: var(--acid); }
.move.queued { background: var(--red); color: var(--ink); border-color: var(--red); }
button:focus-visible, input:focus-visible {
outline: 3px solid #fff;
outline-offset: 3px;
}
.side-controls {
display: flex;
gap: 18px;
align-items: end;
}
.tempo { min-width: 150px; }
.tempo-head {
display: flex;
justify-content: space-between;
gap: 12px;
margin-bottom: 8px;
color: #a9b8a6;
font-size: 10px;
font-weight: bold;
letter-spacing: .1em;
}
.tempo-head output { color: var(--acid); }
input[type="range"] { width: 100%; margin: 0 0 10px; accent-color: var(--acid); }
.under {
display: flex;
justify-content: space-between;
gap: 14px;
padding-top: 12px;
color: #a5b3a2;
font-size: 10px;
line-height: 1.5;
}
.under p { margin: 0; }
.under b { color: var(--acid); }
@media (max-width: 760px) {
.page { width: min(100% - 24px, 1200px); padding-top: 16px; }
.intro { align-items: start; flex-direction: column; gap: 12px; }
.intro p { max-width: none; }
.desk { grid-template-columns: 1fr; gap: 18px; padding: 15px; }
.side-controls { justify-content: space-between; }
.tempo { flex: 1; max-width: 240px; }
.stage-top { inset: 10px 10px auto; }
.stage-bottom { inset: auto 10px 9px; }
.stamp { width: 45px; height: 45px; border-width: 1px; }
.under { flex-direction: column; gap: 4px; }
}
@media (max-width: 420px) {
.stage-count { display: none; }
.move { padding: 9px 10px; }
}
</style>
</head>
<body>
<main class="page">
<header class="masthead">
<div><span class="mark">●</span> The pigeon underground</div>
<div class="masthead-right">Field recording no. 07 / Est. today</div>
</header>
<section class="intro" aria-labelledby="title">
<h1 id="title">Pip <span>in the</span><br>cypher.</h1>
<p>A small bird with a big floor. Call a move and Pip will pick it up on the next beat.</p>
</section>
<div class="stage">
<canvas id="floor" width="1000" height="640" role="img"
aria-label="An animated pigeon breakdancing on a chalk-marked floor."></canvas>
<div class="stage-top" aria-hidden="true">
<div class="stage-label">Live from the pavement</div>
<div class="stage-count">One bird<br>Infinite rounds</div>
</div>
<div class="stage-bottom" aria-hidden="true">
<div>Now throwing<strong id="moveName">Toprock</strong></div>
<div class="stamp">100%<br>bird<br>made</div>
</div>
</div>
<section class="desk" aria-label="Dance controls">
<div>
<p class="control-label">Call the next move</p>
<div class="moves">
<button class="move active" type="button" data-move="toprock">01 / Toprock</button>
<button class="move" type="button" data-move="footwork">02 / Footwork</button>
<button class="move" type="button" data-move="freeze">03 / Freeze</button>
<button class="move" type="button" data-move="windmill">04 / Windmill</button>
</div>
</div>
<div class="side-controls">
<div class="tempo">
<div class="tempo-head">
<label for="tempo">TEMPO</label>
<output id="tempoValue" for="tempo">110 BPM</output>
</div>
<input id="tempo" type="range" min="70" max="150" value="110" step="1">
</div>
<button class="pause" id="pause" type="button" aria-pressed="false">Pause</button>
</div>
</section>
<footer class="under">
<p id="status" aria-live="polite">The round is running. Pick a move to cut in.</p>
<p><b>TIP</b> / Slower tempo makes the holds hit harder.</p>
</footer>
</main>
<script>
(() => {
"use strict";
const canvas = document.getElementById("floor");
const ctx = canvas.getContext("2d");
const buttons = [...document.querySelectorAll(".move")];
const title = document.getElementById("moveName");
const status = document.getElementById("status");
const tempo = document.getElementById("tempo");
const tempoValue = document.getElementById("tempoValue");
const pauseButton = document.getElementById("pause");
const W = 1000, H = 640, ground = 515;
const ink = "#172822";
const red = "#df6447";
const paper = "#e9e4cf";
function resize() {
const rect = canvas.getBoundingClientRect();
const dpr = Math.min(window.devicePixelRatio || 1, 2);
canvas.width = Math.max(1, Math.round(rect.width * dpr));
canvas.height = Math.max(1, Math.round(rect.width * H / W * dpr));
}
window.addEventListener("resize", resize);
resize();
// All coordinates are relative to the centre of Pip's body.
const neutral = {
x: 500, y: 340, r: 0,
lkx: -38, lky: 119, lfx: -62, lfy: 175,
rkx: 41, rky: 119, rfx: 62, rfy: 175,
lwx: -105, lwy: 35, rwx: 104, rwy: 38,
head: 0
};
const pose = changes => Object.assign({}, neutral, changes);
const leftStep = pose({
x: 473, y: 345, r: -.12,
lkx: -53, lky: 116, lfx: -100, lfy: 170,
rkx: 55, rky: 104, rfx: 113, rfy: 143,
lwx: -125, lwy: -4, rwx: 108, rwy: 65, head: .12
});
const rightStep = pose({
x: 527, y: 345, r: .12,
lkx: -55, lky: 105, lfx: -113, lfy: 142,
rkx: 53, rky: 116, rfx: 100, rfy: 170,
lwx: -108, lwy: 65, rwx: 125, rwy: -4, head: -.12
});
const dip = pose({
y: 365, r: -.06,
lkx: -65, lky: 112, lfx: -86, lfy: 150,
rkx: 65, rky: 112, rfx: 86, rfy: 150,
lwx: -108, lwy: 78, rwx: 110, rwy: 76
});
const lowLeft = pose({
x: 480, y: 405, r: -.28,
lkx: -55, lky: 65, lfx: -91, lfy: 86,
rkx: 49, rky: 45, rfx: 151, rfy: 70,
lwx: -119, lwy: 71, rwx: 96, rwy: -14, head: .18
});
const lowRight = pose({
x: 520, y: 405, r: .28,
lkx: -49, lky: 45, lfx: -151, lfy: 70,
rkx: 55, rky: 65, rfx: 91, rfy: 86,
lwx: -96, lwy: -14, rwx: 119, rwy: 71, head: -.18
});
const tuck = pose({
y: 407, r: .12,
lkx: -64, lky: 64, lfx: -84, lfy: 100,
rkx: 64, rky: 64, rfx: 84, rfy: 100,
lwx: -112, lwy: 95, rwx: 111, rwy: 93
});
const freeze = pose({
x: 493, y: 388, r: .85,
lkx: -65, lky: 49, lfx: -112, lfy: 25,
rkx: -5, rky: 84, rfx: -62, rfy: 106,
lwx: -114, lwy: -60, rwx: 142, rwy: 30, head: -.18
});
const spinShape = pose({
x: 500, y: 405,
lkx: -80, lky: 52, lfx: -160, lfy: 15,
rkx: 60, rky: 37, rfx: 147, rfy: -12,
lwx: -115, lwy: 66, rwx: 115, rwy: 66
});
const spinning = angle => Object.assign({}, spinShape, { r: angle });
// Keyframes deliberately include repeated poses: the spaces between hits
// are as important as the hits themselves.
const moves = {
toprock: {
name: "Toprock", duration: 4,
keys: [
[0, pose({})], [.32, leftStep], [.72, leftStep],
[1, dip], [1.3, rightStep], [1.72, rightStep],
[2, dip], [2.3, leftStep], [2.72, leftStep],
[3, dip], [3.3, rightStep], [3.65, rightStep],
[4, pose({})]
]
},
footwork: {
name: "Footwork", duration: 4,
keys: [
[0, pose({})], [.45, tuck], [.75, tuck],
[1, lowLeft], [1.45, lowLeft], [1.7, tuck],
[2, lowRight], [2.45, lowRight], [2.7, tuck],
[3, lowLeft], [3.35, lowLeft], [3.65, tuck],
[4, pose({})]
]
},
freeze: {
name: "Baby freeze", duration: 4,
keys: [
[0, pose({})], [.55, tuck], [.8, tuck],
[1.2, freeze], [2.85, freeze],
[3.2, tuck], [3.5, tuck], [4, pose({})]
]
},
windmill: {
name: "Windmill", duration: 4,
keys: [
[0, pose({})], [.5, tuck], [.75, spinning(0)],
[1.35, spinning(Math.PI * .65)],
[1.95, spinning(Math.PI * 1.35)],
[2.55, spinning(Math.PI * 2)],
[2.8, Object.assign({}, tuck, {r: Math.PI * 2})],
[3.25, Object.assign({}, tuck, {r: Math.PI * 2})],
[4, Object.assign({}, pose({}), {r: Math.PI * 2})]
]
}
};
const sequence = ["toprock", "footwork", "freeze", "toprock", "windmill"];
let sequenceIndex = 0;
let current = "toprock";
let beat = 0;
let moveStart = 0;
let queued = null;
let queueBeat = 0;
let paused = false;
let lastTime = performance.now();
function mixPose(keys, t) {
let i = 0;
while (i < keys.length - 2 && t > keys[i + 1][0]) i++;
const [aTime, a] = keys[i];
const [bTime, b] = keys[i + 1];
let u = Math.max(0, Math.min(1, (t - aTime) / (bTime - aTime)));
// Smooth takeoff and landing; repeated keys create actual held shapes.
u = u * u * (3 - 2 * u);
const result = {};
for (const key in neutral) result[key] = a[key] + (b[key] - a[key]) * u;
return result;
}
function updateButtons() {
buttons.forEach(button => {
const id = button.dataset.move;
button.classList.toggle("active", id === current && id !== queued);
button.classList.toggle("queued", id === queued);
});
title.textContent = moves[current].name;
}
function startMove(id, at) {
current = id;
moveStart = at;
updateButtons();
}
buttons.forEach(button => {
button.addEventListener("click", () => {
queued = button.dataset.move;
queueBeat = Math.floor(beat + 0.0001) + 1;
updateButtons();
status.textContent = moves[queued].name + " queued for the next beat" +
(paused ? ". Press play to continue." : ".");
});
});
tempo.addEventListener("input", () => {
tempoValue.textContent = tempo.value + " BPM";
});
pauseButton.addEventListener("click", () => {
paused = !paused;
pauseButton.textContent = paused ? "Play" : "Pause";
pauseButton.setAttribute("aria-pressed", String(paused));
status.textContent = paused ? "Round paused." :
(queued ? moves[queued].name + " queued for the next beat." : "The round is running.");
lastTime = performance.now();
});
function line(points, color, width) {
ctx.beginPath();
ctx.moveTo(points[0][0], points[0][1]);
for (let i = 1; i < points.length; i++) ctx.lineTo(points[i][0], points[i][1]);
ctx.strokeStyle = color;
ctx.lineWidth = width;
ctx.lineCap = "round";
ctx.lineJoin = "round";
ctx.stroke();
}
function ellipse(x, y, rx, ry, fill, rotation = 0) {
ctx.beginPath();
ctx.ellipse(x, y, rx, ry, rotation, 0, Math.PI * 2);
ctx.fillStyle = fill;
ctx.fill();
}
function background(beatTime) {
ctx.fillStyle = paper;
ctx.fillRect(0, 0, W, H);
// The backdrop is printed rather than perfectly clean.
ctx.fillStyle = "#dbd8bf";
ctx.beginPath();
ctx.arc(500, 295, 230, 0, Math.PI * 2);
ctx.fill();
ctx.strokeStyle = "#bec9a6";
ctx.lineWidth = 2;
ctx.beginPath();
ctx.arc(500, 295, 204, -.8, 4.4);
ctx.stroke();
ctx.beginPath();
ctx.arc(500, 295, 193, 1.1, 5.9);
ctx.stroke();
ctx.save();
ctx.translate(500, 292);
ctx.rotate(-.14);
ctx.fillStyle = "rgba(49,79,59,.065)";
ctx.font = "900 205px Impact, sans-serif";
ctx.textAlign = "center";
ctx.fillText("PIP", 0, 65);
ctx.restore();
ctx.strokeStyle = "#c7c8ad";
ctx.lineWidth = 1;
for (let i = 0; i < 28; i++) {
const x = 38 + ((i * 173) % 918);
const y = 80 + ((i * 79) % 380);
ctx.beginPath();
ctx.moveTo(x, y);
ctx.lineTo(x + 5 + (i % 3) * 3, y - 2);
ctx.stroke();
}
// Four little beat markers, with a bright downbeat.
for (let i = 0; i < 4; i++) {
const active = Math.floor(beatTime) % 4 === i;
ellipse(79 + i * 17, 350, active ? 5 : 3, active ? 5 : 3,
active ? red : "#aebaa0");
}
ctx.fillStyle = "#d5d4ba";
ctx.fillRect(0, ground, W, H - ground);
ctx.strokeStyle = ink;
ctx.lineWidth = 3;
ctx.beginPath();
ctx.moveTo(0, ground);
ctx.lineTo(W, ground);
ctx.stroke();
ctx.strokeStyle = "#aab59b";
ctx.lineWidth = 2;
ctx.beginPath();
ctx.ellipse(500, 549, 260, 31, 0, 0, Math.PI * 2);
ctx.stroke();
ctx.beginPath();
ctx.ellipse(500, 549, 197, 20, 0, .1, 3.2);
ctx.stroke();
ctx.fillStyle = "#637861";
ctx.font = "bold 11px monospace";
ctx.textAlign = "left";
ctx.fillText("SIDE A / ALL FEATHERS", 28, 470);
ctx.textAlign = "right";
ctx.fillText("33⅓ RPM / NO SKIPS", 972, 470);
}
function wing(x, y, side) {
const sx = side * 47;
ctx.beginPath();
ctx.moveTo(sx, -35);
ctx.quadraticCurveTo(side * 90, -34, x, y);
ctx.quadraticCurveTo(x - side * 17, y + 16, x - side * 32, y + 8);
ctx.quadraticCurveTo(side * 72, 53, side * 37, 42);
ctx.closePath();
ctx.fillStyle = "#527a68";
ctx.fill();
ctx.strokeStyle = ink;
ctx.lineWidth = 5;
ctx.stroke();
line([[side * 62, 8], [x - side * 22, y + 1]], "#9bb89a", 4);
line([[side * 58, 23], [x - side * 29, y + 10]], "#2d5549", 4);
}
function leg(hx, hy, kx, ky, fx, fy, left) {
line([[hx, hy], [kx, ky], [fx, fy - 7]], ink, 15);
line([[hx, hy], [kx, ky], [fx, fy - 7]], "#db9870", 8);
ellipse(kx, ky, 5, 5, "#f0b58a");
// Sneakers are deliberately a little too big.
ctx.save();
ctx.translate(fx, fy - 3);
ctx.rotate(left ? -.08 : .08);
ctx.beginPath();
ctx.moveTo(-20, -8);
ctx.quadraticCurveTo(-12, -16, 2, -12);
ctx.lineTo(13, -8);
ctx.quadraticCurveTo(25, -6, 26, 3);
ctx.lineTo(25, 7);
ctx.lineTo(-22, 7);
ctx.quadraticCurveTo(-25, 0, -20, -8);
ctx.closePath();
ctx.fillStyle = red;
ctx.fill();
ctx.strokeStyle = ink;
ctx.lineWidth = 4;
ctx.stroke();
line([[-20, 5], [24, 5]], "#f4e5c9", 4);
line([[-5, -8], [4, -6]], "#f4e5c9", 2);
line([[-7, -4], [2, -2]], "#f4e5c9", 2);
ctx.restore();
}
function bird(p) {
ctx.save();
// The floor masks the body during the windmill, like a real ground plane.
ctx.beginPath();
ctx.rect(0, 0, W, ground + 3);
ctx.clip();
ctx.translate(p.x, p.y);
ctx.rotate(p.r);
// Tail, behind everything.
ctx.beginPath();
ctx.moveTo(-52, 28);
ctx.lineTo(-105, 73);
ctx.lineTo(-89, 85);
ctx.lineTo(-117, 90);
ctx.lineTo(-89, 103);
ctx.lineTo(-38, 70);
ctx.closePath();
ctx.fillStyle = "#365c51";
ctx.fill();
ctx.strokeStyle = ink;
ctx.lineWidth = 5;
ctx.stroke();
wing(p.lwx, p.lwy, -1);
wing(p.rwx, p.rwy, 1);
leg(-30, 67, p.lkx, p.lky, p.lfx, p.lfy, true);
leg(30, 67, p.rkx, p.rky, p.rfx, p.rfy, false);
// Pear-shaped pigeon body.
ctx.beginPath();
ctx.moveTo(-50, -72);
ctx.bezierCurveTo(-102, -44, -91, 53, -53, 83);
ctx.bezierCurveTo(-16, 111, 63, 98, 78, 49);
ctx.bezierCurveTo(94, 2, 69, -58, 42, -75);
ctx.closePath();
ctx.fillStyle = "#6f9a80";
ctx.fill();
ctx.strokeStyle = ink;
ctx.lineWidth = 6;
ctx.stroke();
ctx.beginPath();
ctx.moveTo(12, -59);
ctx.bezierCurveTo(55, -50, 70, -5, 59, 43);
ctx.bezierCurveTo(51, 73, 22, 85, -2, 79);
ctx.bezierCurveTo(27, 45, 30, -8, 12, -59);
ctx.fillStyle = "#b8cba4";
ctx.fill();
// Iridescent neck stripes.
ctx.save();
ctx.rotate(-.08);
line([[-38, -54], [-17, -42], [5, -39]], "#345e56", 8);
line([[-35, -38], [-14, -28], [7, -27]], "#d2d5a2", 5);
ctx.restore();
ctx.save();
ctx.translate(0, -103);
ctx.rotate(p.head);
ellipse(0, 0, 58, 54, ink);
ellipse(0, -2, 54, 50, "#6f9a80");
ellipse(14, -16, 34, 28, "#83aa8c");
// Beak points right, so orientation remains legible in a spin.
ctx.beginPath();
ctx.moveTo(43, -6);
ctx.lineTo(88, 7);
ctx.lineTo(47, 19);
ctx.closePath();
ctx.fillStyle = "#e6ad53";
ctx.fill();
ctx.strokeStyle = ink;
ctx.lineWidth = 5;
ctx.stroke();
line([[49, 9], [71, 9]], "#a86c38", 2);
ellipse(23, -15, 14, 14, "#e9e4cf");
ellipse(27, -15, 7, 8, ink);
ellipse(29, -18, 2.5, 2.5, "#fff8db");
line([[7, -38], [29, -41]], ink, 4);
ctx.restore();
// A few hand-drawn breast feathers.
line([[-37, 12], [-27, 18], [-17, 12]], "#527a68", 3);
line([[-29, 30], [-19, 36], [-9, 30]], "#527a68", 3);
ctx.restore();
}
function render() {
ctx.setTransform(canvas.width / W, 0, 0, canvas.height / H, 0, 0);
background(beat);
const elapsed = Math.max(0, Math.min(moves[current].duration, beat - moveStart));
const p = mixPose(moves[current].keys, elapsed);
// Shadow compresses as the bird drops; it stays on the floor.
const height = Math.max(0, ground - (p.y + 150));
ellipse(p.x, ground + 8, 85 + height * .25, 10, "rgba(23,40,34,.17)");
bird(p);
}
function frame(now) {
const dt = Math.min((now - lastTime) / 1000, .1);
lastTime = now;
if (!paused) {
beat += dt * Number(tempo.value) / 60;
if (queued && beat >= queueBeat) {
const next = queued;
queued = null;
startMove(next, queueBeat);
status.textContent = moves[next].name + " is on the floor.";
} else if (beat - moveStart >= moves[current].duration) {
const nextStart = moveStart + moves[current].duration;
sequenceIndex = (sequenceIndex + 1) % sequence.length;
startMove(sequence[sequenceIndex], nextStart);
status.textContent = "The round is running. Pick a move to cut in.";
}
}
render();
requestAnimationFrame(frame);
}
updateButtons();
requestAnimationFrame(frame);
})();
</script>
</body>
</html>