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 a pigeon who has clearly been practicing after everyone else left the plaza. I went for a scuffed, ink-heavy dance-battle poster: lavender feathers, iridescent green, orange sneakers, and a floor that behaves like a record.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Pigeon on the Floor</title>
<style>
:root { color-scheme: light; }
* { box-sizing: border-box; }
body {
margin: 0;
background: #182f30;
color: #f5edda;
font-family: Arial, Helvetica, sans-serif;
}
.page { max-width: 1160px; margin: auto; padding: clamp(10px, 2vw, 24px); }
.poster {
position: relative;
width: 100%;
aspect-ratio: 1000 / 650;
overflow: hidden;
background: #f3e9d2;
border: 3px solid #f5edda;
box-shadow: 10px 10px 0 #0c2023;
cursor: pointer;
touch-action: manipulation;
}
canvas { display: block; width: 100%; height: 100%; }
.masthead {
position: absolute;
top: 5%; left: 4%;
color: #173c3b;
pointer-events: none;
transform: rotate(-3deg);
}
.eyebrow {
display: inline-block;
padding: 5px 9px;
background: #ed674c;
color: #fff5d9;
font-size: clamp(9px, 1.25vw, 14px);
font-weight: 900;
letter-spacing: .18em;
}
h1 {
margin: 12px 0 4px;
font-family: Impact, Haettenschweiler, "Arial Narrow Bold", sans-serif;
font-size: clamp(43px, 8.1vw, 104px);
font-weight: 900;
letter-spacing: -.055em;
line-height: .79;
text-transform: uppercase;
text-shadow: 3px 3px 0 #f3e9d2;
}
h1 em { color: #e65c46; font-style: normal; }
.sub { margin-top: 15px; font-size: clamp(9px, 1.2vw, 13px); font-weight: 900; letter-spacing: .15em; }
.stamp {
position: absolute;
right: 3%; top: 4%;
width: 88px; height: 88px;
display: grid; place-items: center;
border: 3px solid #173c3b; border-radius: 50%;
color: #173c3b;
text-align: center;
font-size: 12px; font-weight: 900; line-height: 1.15;
letter-spacing: .08em;
transform: rotate(13deg);
pointer-events: none;
}
.stage-note {
position: absolute;
bottom: 3%; left: 4%; right: 4%;
display: flex; align-items: flex-end; justify-content: space-between;
gap: 12px;
color: #f7eacb; pointer-events: none;
font-weight: 900; letter-spacing: .12em;
}
#moveName { font-size: clamp(14px, 2.8vw, 30px); text-transform: uppercase; }
.stage-note small { font-size: 10px; text-align: right; line-height: 1.5; }
.desk {
display: flex; align-items: center; justify-content: space-between;
gap: 18px; flex-wrap: wrap;
padding: 23px 2px 8px;
}
.intro { min-width: 155px; }
.intro strong { display: block; font-size: 16px; letter-spacing: .15em; }
.intro span { display: block; margin-top: 5px; color: #b6c5b7; font-size: 12px; }
.pads { display: flex; flex-wrap: wrap; gap: 7px; }
button {
appearance: none;
border: 2px solid #d7d5b9;
background: transparent;
color: #f5edda;
padding: 11px 13px;
font: 900 12px Arial, sans-serif;
letter-spacing: .08em;
text-align: left;
cursor: pointer;
transition: transform .12s, background .12s, color .12s;
}
button:hover { transform: translateY(-3px); background: #f5edda; color: #182f30; }
button:focus-visible { outline: 3px solid #ef7154; outline-offset: 3px; }
button.active { background: #e9654b; border-color: #e9654b; color: #fff7df; }
button b { display: block; font-size: 10px; opacity: .7; margin-bottom: 3px; }
.extras { display: flex; gap: 7px; }
.extras button { min-width: 83px; text-align: center; }
footer { color: #aebeb0; font-size: 11px; padding: 8px 2px 16px; letter-spacing: .04em; }
@media (max-width: 650px) {
.poster { aspect-ratio: 4 / 5; }
.masthead { top: 4%; left: 5%; }
h1 { font-size: clamp(43px, 13vw, 72px); }
.sub { max-width: 190px; line-height: 1.5; }
.stamp { width: 61px; height: 61px; font-size: 8px; }
.stage-note small { display: none; }
.desk { align-items: flex-start; }
.pads { width: 100%; }
.pads button { flex: 1 1 calc(50% - 7px); }
}
</style>
</head>
<body>
<div class="page">
<main class="poster" id="poster" aria-label="An animated pigeon breakdances on a painted record. Tap the stage to cheer.">
<canvas id="scene"></canvas>
<div class="masthead">
<span class="eyebrow">THE PLAZA PRESENTS · VOL. 01</span>
<h1>PIGEON<br>ON THE<br><em>FLOOR.</em></h1>
<div class="sub">ONE BIRD. NO REFUNDS. ALL FOOTWORK.</div>
</div>
<div class="stamp">LIVE<br>FROM THE<br>SIDEWALK</div>
<div class="stage-note">
<span id="moveName">01 / TOPROCK</span>
<small>TAP THE FLOOR TO CHEER<br>OR CALL THE NEXT MOVE BELOW ↓</small>
</div>
</main>
<div class="desk">
<div class="intro"><strong>CALL THE MOVE</strong><span>Interrupt the set. The bird will recover.</span></div>
<div class="pads" aria-label="Choose a dance move">
<button data-cue="0"><b>01</b>TOPROCK</button>
<button data-cue="1"><b>02</b>SIX-STEP</button>
<button data-cue="2"><b>03</b>WING FREEZE</button>
<button data-cue="3"><b>04</b>HEADSPIN</button>
</div>
<div class="extras">
<button id="tempo" title="Change playback speed">SPEED<br>1×</button>
<button id="pause" title="Pause or resume">PAUSE<br>Ⅱ</button>
</div>
</div>
<footer>KEYS: 1–4 CALL A MOVE · SPACE PAUSES · ↑ / ↓ CHANGE SPEED · CLICK THE FLOOR TO CHEER</footer>
</div>
<script>
(() => {
const canvas = document.getElementById('scene');
const poster = document.getElementById('poster');
const ctx = canvas.getContext('2d');
const moveName = document.getElementById('moveName');
const pads = [...document.querySelectorAll('[data-cue]')];
const tempoButton = document.getElementById('tempo');
const pauseButton = document.getElementById('pause');
const W = 1000, H = 650, originX = 565, groundY = 512;
const ink = '#193a3c', cream = '#f5ebd4';
const starts = [0, 3.26, 6.65, 8.86];
const names = ['01 / TOPROCK', '02 / SIX-STEP', '03 / WING FREEZE', '04 / HEADSPIN'];
const speeds = [.7, 1, 1.35, 1.7];
let speedIndex = 1, clock = 0, paused = false, last = performance.now();
let particles = [], cheer = 0, lastMove = -1;
// Each pose describes the bird's mass, two sneaker contacts, and two wing tips.
// Close-spaced keys make the drops quick; repeated keys let a freeze actually hold.
const K = [
[0, 0,-114, 0, -48,0, 48,0, -92,-103, 88,-116],
[.35, -14,-119,-.08, -54,0, 48,0, -104,-95, 70,-128],
[.62, -37,-112,-.18, -53,0, 12,-30,-108,-72, 63,-105],
[.84, -36,-108,-.18, -52,0, -13,0, -101,-83, 78,-95],
[1.08,-20,-145, .08, -48,-27, 65,-43,-110,-156,95,-135],
[1.32, 24,-115, .10, -12,0, 78,0, -60,-135, 113,-89],
[1.76, 42,-113, .16, 27,-29, 76,0, -38,-103, 120,-111],
[2.04, 40,-109, .13, 15,0, 74,0, -39,-92, 112,-99],
[2.36, 11,-143,-.10, -45,-37, 58,-28,-103,-151,72,-145],
[2.68,-14,-113,-.10, -62,0, 44,0, -105,-102,80,-112],
[3.02, 0,-116, 0, -49,0, 48,0, -98,-106, 92,-111],
[3.26, -8,-74, -.25, -72,0, 57,0, -101,-4, 100,-70],
[3.58,-55,-69, -.48, -107,0, 34,-15,-132,-2, 74,-101],
[3.88,-17,-74, -.12, -77,0, 79,0, -105,-3, 119,-78],
[4.20, 35,-78, .38, -25,-18,111,0, -55,-82, 126,-5],
[4.48, 62,-71, .49, 10,0, 130,0, -18,-113,137,-8],
[4.80, 20,-74, -.28, -67,0, 89,-22,-100,-3, 112,-92],
[5.14,-54,-69, -.48,-119,0, 13,0, -139,-2, 59,-101],
[5.46,-10,-77, .18, -72,-18,85,0, -104,-92, 112,-4],
[5.80, 46,-77, .40, -5,0, 121,0, -28,-112, 132,-3],
[6.16, 7,-73, -.26, -78,0, 93,-17,-106,-3, 105,-82],
[6.48,-13,-63, -.23, -75,0, 54,0, -104,-3, 77,-62],
[6.65,-28,-76, -.52, -87,0, 46,-37,-112,-2, 80,-99],
[6.88,-32,-125,-1.06,-70,-144, 93,-130,-125,-2,100,-164],
[7.13,-24,-145,-1.24,-45,-220,130,-153,-131,-2,94,-183],
[7.48,-22,-147,-1.28,-49,-219,134,-158,-131,-2,96,-181],
[7.94,-23,-145,-1.24,-44,-220,129,-154,-131,-2,96,-184],
[8.20,-26,-137,-1.12,-55,-197,115,-112,-132,-2,93,-153],
[8.43,-15,-91, -.48,-65,-29, 79,0,-112,-2, 91,-100],
[8.66, 0,-63, .17,-56,0, 58,0,-107,-3, 91,-65],
[8.86, 9,-93, 1.02,-70,-52, 70,-29,-106,-3, 110,-88],
[9.15, 10,-124,2.05,-75,-173,78,-190,-113,-58,116,-50],
[9.43, 4,-125,3.14,-90,-208,84,-202,-101,-136,100,-145],
[9.70, -2,-105,4.48,-77,-169,90,-170,-110,-89, 93,-74],
[9.96, 1,-78, 5.90,-75,-32, 87,-31,-112,-3, 107,-70],
[10.22, 8,-124,8.20,-76,-185,79,-190,-109,-61,108,-66],
[10.48, 2,-125,9.42,-83,-210,86,-203,-103,-133,107,-139],
[10.74,-5,-106,10.76,-82,-172,79,-172,-110,-83,104,-82],
[11.00, 0,-76,12.22,-70,-30, 81,-37,-110,-4, 107,-77],
[11.27, 5,-124,14.45,-80,-177,82,-185,-108,-67,107,-65],
[11.52, 1,-125,15.71,-88,-210,86,-207,-106,-134,108,-137],
[11.79,-3,-103,17.15,-82,-162,82,-167,-111,-73,102,-79],
[12.06, 0,-72,18.65,-57,-19, 75,-22,-103,-4, 105,-83],
[12.26, 0,-63,18.85,-57,0, 58,0, -104,-4, 87,-62],
[12.54, 0,-159,18.85,-95,-92,97,-91,-128,-160,131,-158],
[12.88, 0,-116,18.85,-61,0, 59,0, -111,-125,110,-122],
[13.27, 0,-114,18.85,-48,0, 48,0, -95,-107,91,-111],
[13.65, 0,-105,18.73,-47,0, 49,0, -90,-84, 87,-93],
[14, 0,-114,18.85,-48,0, 48,0, -92,-103,88,-116]
];
const duration = 14;
const bg = document.createElement('canvas');
bg.width = W; bg.height = H;
const b = bg.getContext('2d');
let seed = 21983;
function rand() { seed = (Math.imul(seed, 1664525) + 1013904223) | 0; return (seed >>> 0) / 4294967296; }
function makeBackground() {
b.fillStyle = '#f3e9d2'; b.fillRect(0, 0, W, H);
b.fillStyle = '#e9ddc2';
for (let i = 0; i < 1900; i++) {
b.fillRect(rand()*W, rand()*H, rand()*2+.5, rand()*2+.5);
}
// Offset print plates and the enormous painted "record."
b.save();
b.translate(580, 350);
b.rotate(-.16);
b.fillStyle = '#eecb94';
b.beginPath(); b.ellipse(0, 0, 245, 210, 0, 0, Math.PI*2); b.fill();
b.strokeStyle = '#d65e49'; b.lineWidth = 3;
for (let r of [175, 194, 220]) {
b.beginPath(); b.ellipse(0, 0, r, r*.85, 0, 0, Math.PI*2); b.stroke();
}
b.restore();
b.save();
b.translate(originX, groundY+13);
b.scale(1, .23);
b.fillStyle = '#193a3c';
b.beginPath(); b.arc(0, 0, 405, 0, Math.PI*2); b.fill();
b.strokeStyle = '#4c6960'; b.lineWidth = 3;
for (let r = 125; r < 395; r += 27) {
b.beginPath(); b.arc(0, 0, r, 0, Math.PI*2); b.stroke();
}
b.fillStyle = '#e26149';
b.beginPath(); b.arc(0, 0, 83, 0, Math.PI*2); b.fill();
b.restore();
b.fillStyle = '#173b3a';
b.fillRect(0, 525, W, 125);
b.fillStyle = '#d9664c';
b.fillRect(0, 525, W, 5);
b.fillStyle = '#4b6860';
for (let x = -15; x < W; x += 39) {
b.save(); b.translate(x, 556); b.rotate(-.3);
b.fillRect(0, 0, 2, 67); b.restore();
}
b.fillStyle = '#d2bb98';
for (let i = 0; i < 95; i++) {
b.fillRect(rand()*W, 540+rand()*110, rand()*11+1, 1);
}
b.strokeStyle = '#d75f49'; b.lineWidth = 5;
b.beginPath(); b.moveTo(780, 95); b.lineTo(889, 85); b.lineTo(837, 115); b.stroke();
b.beginPath(); b.moveTo(862, 150); b.lineTo(947, 126); b.stroke();
// Imperfect halftone in one corner.
b.fillStyle = '#d4b998';
for (let y = 265; y < 450; y += 13) {
for (let x = 26; x < 180; x += 13) {
const radius = Math.max(.5, 3.2 - x/80);
b.beginPath(); b.arc(x+(y%2)*2, y, radius, 0, Math.PI*2); b.fill();
}
}
}
makeBackground();
function resize() {
const r = poster.getBoundingClientRect();
const dpr = Math.min(devicePixelRatio || 1, 2);
canvas.width = Math.round(r.width*dpr);
canvas.height = Math.round(r.height*dpr);
}
new ResizeObserver(resize).observe(poster);
resize();
const mix = (a, b, t) => a+(b-a)*t;
const ease = t => t*t*(3-2*t);
const point = (x, y, p) => ({
x: p[0] + x*Math.cos(p[2]) - y*Math.sin(p[2]),
y: p[1] + x*Math.sin(p[2]) + y*Math.cos(p[2])
});
function poseAt(t) {
let i = 0;
while (i < K.length-2 && K[i+1][0] < t) i++;
const a = K[i], z = K[i+1];
const u = Math.max(0, Math.min(1, (t-a[0])/(z[0]-a[0])));
const q = t >= 8.86 && t < 12.07 ? u : ease(u);
const p = a.slice(1).map((v, j) => mix(v, z[j+1], q));
if (t >= 8.86 && t < 12.10) {
// During the power move, the sneakers orbit the body's centre.
// They are not dragged along the ground by the interpolation.
const l = point(-72, 128, p), r = point(65, 133, p);
p[3] = l.x; p[4] = Math.min(5, l.y);
p[5] = r.x; p[6] = Math.min(5, r.y);
const wing = point(-112, 3, p), back = point(113, -17, p);
p[7] = wing.x; p[8] = Math.min(3, wing.y);
p[9] = back.x; p[10] = Math.min(3, back.y);
}
return p;
}
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 sneaker(x, y, angle, flip) {
ctx.save(); ctx.translate(x, y); ctx.rotate(angle); ctx.scale(flip, 1);
ctx.fillStyle = ink; ctx.beginPath();
ctx.moveTo(-22,-13); ctx.quadraticCurveTo(-9,-23,10,-14);
ctx.lineTo(28,-9); ctx.quadraticCurveTo(36,-5,35,3);
ctx.lineTo(-27,3); ctx.quadraticCurveTo(-31,-6,-22,-13); ctx.fill();
ctx.fillStyle = '#f07546'; ctx.beginPath();
ctx.moveTo(-19,-13); ctx.quadraticCurveTo(-8,-21,9,-12);
ctx.lineTo(27,-7); ctx.quadraticCurveTo(32,-5,30,-2);
ctx.lineTo(-25,-2); ctx.quadraticCurveTo(-27,-7,-19,-13); ctx.fill();
line([[-26,2],[32,2]], '#f7e9c9', 5);
line([[-6,-13],[1,-8],[5,-13],[12,-8]], cream, 2);
ctx.restore();
}
function wing(shoulder, tip, rear=false) {
const dx=tip.x-shoulder.x, dy=tip.y-shoulder.y;
const n=Math.hypot(dx,dy)||1, nx=-dy/n, ny=dx/n;
const width=rear ? 23 : 30;
ctx.fillStyle=ink; ctx.beginPath();
ctx.moveTo(shoulder.x+nx*14,shoulder.y+ny*14);
ctx.quadraticCurveTo(shoulder.x+dx*.45+nx*width,shoulder.y+dy*.45+ny*width,
tip.x+nx*8,tip.y+ny*8);
ctx.quadraticCurveTo(tip.x+dx*.10,tip.y+dy*.10,tip.x-nx*10,tip.y-ny*10);
ctx.quadraticCurveTo(shoulder.x+dx*.35-nx*width,shoulder.y+dy*.35-ny*width,
shoulder.x-nx*13,shoulder.y-ny*13);
ctx.closePath(); ctx.fill();
ctx.fillStyle=rear?'#789f91':'#417c78';
ctx.beginPath(); ctx.moveTo(shoulder.x+nx*9,shoulder.y+ny*9);
ctx.quadraticCurveTo(shoulder.x+dx*.50+nx*19,shoulder.y+dy*.50+ny*19,
tip.x+nx*3,tip.y+ny*3);
ctx.quadraticCurveTo(shoulder.x+dx*.8-nx*11,shoulder.y+dy*.8-ny*11,
shoulder.x-nx*8,shoulder.y-ny*8);
ctx.fill();
// Three separate feather ends keep the wing readable as a wing, not an arm.
for(let i=0;i<3;i++){
const f=.67+i*.105;
line([
[shoulder.x+dx*f+nx*12,shoulder.y+dy*f+ny*12],
[shoulder.x+dx*(f+.13)+nx*(25-i*5),shoulder.y+dy*(f+.13)+ny*(25-i*5)]
],ink,4);
}
}
function bird(p, t) {
const [x,y,a,lx,ly,rx,ry,wx,wy,bx,by]=p;
const spin=t>=8.86 && t<12.1;
const hipL=point(-43,43,p), hipR=point(20,49,p);
const front=point(-28,-15,p), rear=point(14,-25,p);
const legAngle=spin ? a : 0;
ctx.save(); ctx.translate(originX,groundY);
const motion=t>=3.26 && t<6.65 ? Math.sin(t*22)*1.4 : 0;
ctx.translate(0,motion);
// The soft shadow stays on the floor as the body leaves it.
ctx.fillStyle='rgba(8,28,29,.28)';
ctx.beginPath(); ctx.ellipse(x,8,Math.max(45,105+(y+110)*.23),12,0,0,Math.PI*2); ctx.fill();
wing(rear,{x:bx,y:by},true);
for(const [hip,fx,fy,flip] of [[hipL,lx,ly,-1],[hipR,rx,ry,1]]) {
ctx.beginPath(); ctx.moveTo(hip.x,hip.y);
ctx.quadraticCurveTo(mix(hip.x,fx,.55)+flip*17,mix(hip.y,fy,.52),fx,fy-8);
ctx.strokeStyle=ink; ctx.lineWidth=17; ctx.lineCap='round'; ctx.stroke();
ctx.strokeStyle='#d3714b'; ctx.lineWidth=10; ctx.stroke();
sneaker(fx,fy,legAngle,flip);
}
ctx.save(); ctx.translate(x,y); ctx.rotate(a);
// Three tail feathers, offset like badly registered print.
ctx.fillStyle=ink;
for (let i=0;i<3;i++) {
ctx.beginPath(); ctx.moveTo(-58,11+i*8);
ctx.quadraticCurveTo(-121,-24+i*21,-145,9+i*19);
ctx.quadraticCurveTo(-109,30+i*15,-68,38);
ctx.fill();
}
ctx.fillStyle='#a97f9d';
ctx.beginPath(); ctx.ellipse(-6,4,81,68,-.12,0,Math.PI*2); ctx.fill();
ctx.lineWidth=5; ctx.strokeStyle=ink; ctx.stroke();
// Belly, chest and the pigeon's oil-slick collar.
ctx.fillStyle='#d9adc0';
ctx.beginPath(); ctx.ellipse(29,22,45,42,-.25,0,Math.PI*2); ctx.fill();
ctx.fillStyle='#2d7273';
ctx.beginPath(); ctx.ellipse(42,-39,44,47,.35,0,Math.PI*2); ctx.fill();
ctx.strokeStyle=ink; ctx.lineWidth=5; ctx.stroke();
ctx.strokeStyle='#ee917d'; ctx.lineWidth=9;
ctx.beginPath(); ctx.arc(39,-34,35,.25,2.25); ctx.stroke();
ctx.strokeStyle='#92bf8b'; ctx.lineWidth=7;
ctx.beginPath(); ctx.arc(43,-39,39,2.8,4.7); ctx.stroke();
ctx.fillStyle='#417e78';
ctx.beginPath(); ctx.ellipse(51,-80,46,43,-.24,0,Math.PI*2); ctx.fill();
ctx.strokeStyle=ink; ctx.lineWidth=5; ctx.stroke();
ctx.fillStyle='#8fc09c';
ctx.beginPath(); ctx.ellipse(35,-95,20,10,-.4,0,Math.PI*2); ctx.fill();
// Beak: two separate shapes leave room for an unmistakable profile.
ctx.fillStyle=ink; ctx.beginPath();
ctx.moveTo(87,-82); ctx.lineTo(116,-74); ctx.lineTo(86,-62); ctx.closePath(); ctx.fill();
ctx.fillStyle='#f3a14e'; ctx.beginPath();
ctx.moveTo(89,-80); ctx.lineTo(111,-74); ctx.lineTo(88,-72); ctx.closePath(); ctx.fill();
ctx.fillStyle=cream; ctx.beginPath(); ctx.ellipse(76,-92,13,16,-.2,0,Math.PI*2); ctx.fill();
ctx.fillStyle=ink; ctx.beginPath(); ctx.arc(80,-92,6,0,Math.PI*2); ctx.fill();
ctx.fillStyle='#fff7df'; ctx.beginPath(); ctx.arc(82,-95,2,0,Math.PI*2); ctx.fill();
ctx.strokeStyle=ink; ctx.lineWidth=3;
ctx.beginPath(); ctx.moveTo(65,-112); ctx.quadraticCurveTo(78,-118,87,-108); ctx.stroke();
// Tiny raised feathers: the pigeon's permanent, unreasonable confidence.
line([[23,-116],[18,-133],[30,-124],[34,-139]],ink,5);
ctx.restore();
wing(front,{x:wx,y:wy},false);
ctx.restore();
}
function burst(count=26) {
cheer=1;
for(let i=0;i<count;i++){
const angle=rand()*Math.PI*2, force=3+rand()*8;
particles.push({
x:originX+(rand()-.5)*95, y:groundY-135+(rand()-.5)*75,
vx:Math.cos(angle)*force, vy:Math.sin(angle)*force-3,
life:40+rand()*42, size:3+rand()*7,
color:['#e65d46','#f5ce82','#4d9990','#193a3c'][i%4]
});
}
}
function frame(now) {
const dt=Math.min((now-last)/1000,.05); last=now;
if(!paused) clock=(clock+dt*speeds[speedIndex])%duration;
cheer=Math.max(0,cheer-dt*1.8);
const move=clock<3.26?0:clock<6.65?1:clock<8.86?2:3;
if(move!==lastMove) {
moveName.textContent=names[move];
pads.forEach((pad,i)=>pad.classList.toggle('active',i===move));
lastMove=move;
}
const cw=canvas.width,ch=canvas.height;
ctx.setTransform(1,0,0,1,0,0); ctx.clearRect(0,0,cw,ch);
const scale=Math.max(cw/W,ch/H);
ctx.setTransform(scale,0,0,scale,(cw-W*scale)/2,(ch-H*scale)/2);
ctx.drawImage(bg,0,0);
const pulse=Math.pow(Math.max(0,Math.sin(clock*Math.PI*4)),12);
ctx.save(); ctx.translate(originX,groundY+8); ctx.scale(1,.23);
ctx.strokeStyle=`rgba(245,226,173,${.18+pulse*.55+cheer*.35})`;
ctx.lineWidth=5+pulse*5+cheer*8;
ctx.beginPath(); ctx.arc(0,0,134+cheer*65,0,Math.PI*2); ctx.stroke();
ctx.restore();
// Fast arcs behind the tumbling bird, but never on top of its silhouette.
if(clock>=8.86 && clock<12.1) {
ctx.strokeStyle='rgba(222,91,68,.55)';
ctx.lineWidth=5; ctx.lineCap='round';
for(let i=0;i<3;i++){
ctx.beginPath();
ctx.arc(originX+2,groundY-108,129+i*19,clock*6+i*.7,clock*6+i*.7+1.25);
ctx.stroke();
}
}
bird(poseAt(clock),clock);
for(let i=particles.length-1;i>=0;i--){
const f=particles[i];
if(!paused){ f.x+=f.vx; f.y+=f.vy; f.vy+=.23; f.vx*=.985; f.life--; }
if(f.life<=0){particles.splice(i,1);continue;}
ctx.save();ctx.globalAlpha=Math.min(1,f.life/22);
ctx.translate(f.x,f.y);ctx.rotate(f.life*.14);
ctx.fillStyle=f.color;ctx.fillRect(-f.size/2,-f.size/2,f.size,f.size*.48);
ctx.restore();
}
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
function cue(i) {
clock=starts[i];
lastMove=-1;
burst(35);
}
pads.forEach(pad=>pad.addEventListener('click',()=>cue(Number(pad.dataset.cue))));
poster.addEventListener('pointerdown',()=>burst(34));
function setSpeed(n) {
speedIndex=(n+speeds.length)%speeds.length;
tempoButton.innerHTML=`SPEED<br>${speeds[speedIndex]}×`;
}
function togglePause() {
paused=!paused;
pauseButton.innerHTML=paused?'PLAY<br>▶':'PAUSE<br>Ⅱ';
}
tempoButton.addEventListener('click',()=>setSpeed(speedIndex+1));
pauseButton.addEventListener('click',togglePause);
window.addEventListener('keydown',e=>{
if(['1','2','3','4'].includes(e.key)) cue(Number(e.key)-1);
else if(e.code==='Space'){e.preventDefault();togglePause();}
else if(e.key==='ArrowUp'){e.preventDefault();setSpeed(speedIndex+1);}
else if(e.key==='ArrowDown'){e.preventDefault();setSpeed(speedIndex-1);}
});
})();
</script>
</body>
</html>