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 small, long-beaked songbird rendered as a glowing skeletal wireframe — ribs of light, no feathers — so every joint reads as a decision rather than a texture. The look is neon-on-void with a faint perspective grid and a floor line, and the bird's hue shifts per move so you can tell a headspin from a flare at a glance.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>BIRD BREAKS</title>
<style>
*{box-sizing:border-box}
html,body{margin:0;height:100%;overflow:hidden;background:#07070c}
body{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;color:#9fe8ff}
canvas{position:fixed;inset:0;display:block}
#bird{pointer-events:none}
#title{position:fixed;top:16px;left:18px;z-index:10;font-size:12px;letter-spacing:.34em;
text-transform:uppercase;color:rgba(120,220,255,.55);pointer-events:none}
#beat{position:fixed;bottom:24px;left:50%;transform:translateX(-50%) scale(.35);
width:64px;height:64px;border-radius:50%;border:1px solid rgba(120,220,255,.2);
z-index:10;pointer-events:none;transition:transform .06s linear,box-shadow .06s linear}
#ui{position:fixed;left:0;right:0;bottom:0;padding:14px 18px 18px;display:flex;gap:16px;
align-items:center;flex-wrap:wrap;font-size:11px;letter-spacing:.14em;text-transform:uppercase;
background:linear-gradient(transparent,rgba(7,7,12,.92));z-index:10;user-select:none}
.grp{display:flex;align-items:center;gap:9px}
button{background:rgba(120,220,255,.07);border:1px solid rgba(120,220,255,.32);color:#9fe8ff;
padding:6px 11px;border-radius:3px;cursor:pointer;font:inherit;font-size:10px;
letter-spacing:.14em;text-transform:uppercase;transition:.15s}
button:hover{background:rgba(120,220,255,.18);border-color:rgba(120,220,255,.75)}
button.on{background:rgba(120,220,255,.22);border-color:rgba(120,220,255,.9);color:#fff}
input[type=range]{-webkit-appearance:none;appearance:none;height:3px;border-radius:2px;
background:rgba(120,220,255,.28);outline:none;width:104px;cursor:pointer}
input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;width:12px;height:12px;
border-radius:50%;background:#9fe8ff;box-shadow:0 0 8px #9fe8ff}
input[type=range]::-moz-range-thumb{width:12px;height:12px;border:0;border-radius:50%;
background:#9fe8ff;box-shadow:0 0 8px #9fe8ff}
select{background:rgba(120,220,255,.07);border:1px solid rgba(120,220,255,.32);color:#9fe8ff;
padding:5px 9px;border-radius:3px;font:inherit;font-size:10px;letter-spacing:.14em;
text-transform:uppercase;outline:none;cursor:pointer}
.val{color:#5a8fa8;min-width:36px;text-align:right}
</style>
</head>
<body>
<canvas id="bg"></canvas>
<canvas id="bird"></canvas>
<div id="title">BIRD BREAKS</div>
<div id="beat"></div>
<div id="ui">
<div class="grp">
<button data-set="all" class="on">Routine</button>
<button data-set="headspin">Headspin</button>
<button data-set="windmill">Windmill</button>
<button data-set="flare">Flare</button>
<button data-set="freeze">Freeze</button>
</div>
<div class="grp">
<label for="bpm">Tempo</label>
<input id="bpm" type="range" min="70" max="190" value="112">
<span class="val" id="bpmv">112</span>
</div>
<div class="grp">
<label for="trail">Trail</label>
<input id="trail" type="range" min="0" max="1" value="0.16" step="0.01">
</div>
<div class="grp">
<label for="style">Style</label>
<select id="style">
<option value="neon">Neon</option>
<option value="ink">Ink</option>
<option value="pixel">Pixel</option>
</select>
</div>
</div>
<script>
(() => {
'use strict';
/* ---------------- math ---------------- */
const TAU = Math.PI * 2;
const clamp = (v,a,b) => v<a?a:(v>b?b:v);
const lerp = (a,b,t) => a+(b-a)*t;
const easeInOut = t => t<0.5 ? 4*t*t*t : 1-Math.pow(-2*t+2,3)/2;
const easeOut = t => 1-Math.pow(1-t,3);
const smoothstep = t => t*t*(3-2*t);
/* ---------------- canvas ---------------- */
const bgCv = document.getElementById('bg');
const bgCtx = bgCv.getContext('2d');
const bdCv = document.getElementById('bird');
const ctx = bdCv.getContext('2d');
let W=0, H=0, DPR=1, floorY=0, scale=1;
function resize(){
DPR = Math.min(window.devicePixelRatio || 1, 2);
W = window.innerWidth; H = window.innerHeight;
for (const cv of [bgCv, bdCv]){
cv.width = Math.floor(W*DPR);
cv.height = Math.floor(H*DPR);
cv.style.width = W+'px';
cv.style.height = H+'px';
}
bgCtx.setTransform(DPR,0,0,DPR,0,0);
ctx.setTransform(DPR,0,0,DPR,0,0);
floorY = H - Math.max(58, H*0.11);
scale = Math.min(W/820, H/520);
drawBG();
}
window.addEventListener('resize', resize);
/* ---------------- background ---------------- */
function drawBG(){
const g = bgCtx.createRadialGradient(W*0.5, H*0.42, 0, W*0.5, H*0.5, Math.max(W,H)*0.85);
g.addColorStop(0, 'rgba(26,42,74,0.55)');
g.addColorStop(1, 'rgba(7,7,12,0)');
bgCtx.fillStyle = '#07070c';
bgCtx.fillRect(0,0,W,H);
bgCtx.fillStyle = g;
bgCtx.fillRect(0,0,W,H);
const gs = Math.max(18, Math.min(W,H)/26);
bgCtx.strokeStyle = 'rgba(120,180,255,0.055)';
bgCtx.lineWidth = 1;
bgCtx.beginPath();
for (let x=0; x<=W; x+=gs){ bgCtx.moveTo(x+0.5,0); bgCtx.lineTo(x+0.5,H); }
for (let y=0; y<=H; y+=gs){ bgCtx.moveTo(0,y+0.5); bgCtx.lineTo(W,y+0.5); }
bgCtx.stroke();
const fg = bgCtx.createLinearGradient(0, floorY-40, 0, floorY+40);
fg.addColorStop(0, 'rgba(120,220,255,0)');
fg.addColorStop(0.5, 'rgba(120,220,255,0.22)');
fg.addColorStop(1, 'rgba(120,220,255,0)');
bgCtx.fillStyle = fg;
bgCtx.fillRect(0, floorY-40, W, 80);
}
/* ---------------- model ---------------- */
const M = { torso:64, neck:34, head:26, beak:30, tail:82, wing:52, leg:46 };
function buildPose(p){
const hip = {x:0, y:0};
const a0 = p.a0;
const chest = { x: Math.cos(a0)*M.torso, y: Math.sin(a0)*M.torso };
const neckAng = a0 + (p.a1 !== undefined ? p.a1 : -0.15);
const neck = { x: chest.x + Math.cos(neckAng)*M.neck, y: chest.y + Math.sin(neckAng)*M.neck };
const headAng = neckAng + (p.a2 !== undefined ? p.a2 : -0.1);
const headBase = { x: neck.x + Math.cos(headAng)*M.head, y: neck.y + Math.sin(headAng)*M.head };
const headTip = { x: headBase.x + Math.cos(headAng)*14, y: headBase.y + Math.sin(headAng)*14 };
const beakAng = headAng + (p.beak !== undefined ? p.beak : 1.7);
const beakTip = { x: headTip.x + Math.cos(beakAng)*M.beak, y: headTip.y + Math.sin(beakAng)*M.beak };
const tailAng = a0 + (p.tail !== undefined ? p.tail : 4.0);
const tailTip = { x: Math.cos(tailAng)*M.tail, y: Math.sin(tailAng)*M.tail };
const bones = [];
bones.push({from:hip, to:chest, w0:17, w1:12});
bones.push({from:chest, to:neck, w0:12, w1:10});
bones.push({from:neck, to:headBase,w0:10, w1:9});
bones.push({from:headBase, to:headTip, w0:9, w1:8});
bones.push({from:headTip, to:beakTip, w0:8, w1:5});
bones.push({from:hip, to:tailTip, w0:12, w1:4});
const perp = { x: -Math.sin(a0), y: Math.cos(a0) };
const shL = { x: chest.x + perp.x*16, y: chest.y + perp.y*16 };
const shR = { x: chest.x - perp.x*16, y: chest.y - perp.y*16 };
function wing(shoulder, w1, w2, w3){
const segs = [];
let cur = shoulder;
const base = a0;
const angles = [base+w1, base+w1+w2, base+w1+w2+w3];
for (let i=0;i<3;i++){
const nx = { x: cur.x + Math.cos(angles[i])*M.wing, y: cur.y + Math.sin(angles[i])*M.wing };
segs.push({ from:cur, to:nx, w0: 10-i*1.6, w1: 10-(i+1)*1.6 });
cur = nx;
}
return segs;
}
bones.push(...wing(shL, p.w1!==undefined?p.w1:0.5, p.w2!==undefined?p.w2:0.35, p.w3!==undefined?p.w3:0.25));
bones.push(...wing(shR, p.w1!==undefined?p.w1:0.5, p.w2!==undefined?p.w2:0.35, p.w3!==undefined?p.w3:0.25));
function leg(hipX, l1, l2, l3){
const segs = [];
let cur = { x: hipX, y: 0 };
const angles = [l1, l1+l2, l1+l2+l3];
for (let i=0;i<3;i++){
const nx = { x: cur.x + Math.cos(angles[i])*M.leg, y: cur.y + Math.sin(angles[i])*M.leg };
segs.push({ from:cur, to:nx, w0: 11-i*1.6, w1: 11-(i+1)*1.6 });
cur = nx;
}
return { segs, foot: cur };
}
const legL = leg(-15, p.l1!==undefined?p.l1:Math.PI/2, p.l2||0, p.l3||0);
const legR = leg( 15, p.r1!==undefined?p.r1:Math.PI/2, p.r2||0, p.r3||0);
bones.push(...legL.segs);
bones.push(...legR.segs);
let contact;
if (p.contact === 'head') contact = headTip;
else if (p.contact === 'leftfoot') contact = legL.foot;
else contact = legL.foot.y < legR.foot.y ? legL.foot : legR.foot;
return { bones, contact, head: headTip, hip, chest, beakTip };
}
/* ---------------- base poses ---------------- */
function standPose(sway){
return {
a0: -Math.PI/2 + sway,
a1: -0.15,
a2: -0.10,
beak: 1.7,
tail: 4.0,
w1: 0.5, w2: 0.35, w3: 0.25,
l1: Math.PI/2 + 0.05, l2: -0.05, l3: 0.05,
r1: Math.PI/2 - 0.05, r2: 0.05, r3: -0.05,
contact: 'foot'
};
}
/* ---------------- phases ---------------- */
const PHASES = {};
PHASES.stand = { beats:2, fn:(t,b)=>{
const sway = Math.sin(b*Math.PI*2)*0.05;
return standPose(sway);
}};
PHASES.dip = { beats:2, fn:(t,b)=>{
const e = easeInOut(t);
const p = standPose(0);
p.a0 = -Math.PI/2 + e*0.55;
p.a1 = -0.15 + e*0.20;
p.tail= 4.0 + e*0.55;
p.w1 = 0.5 + e*0.85;
p.w2 = 0.35 + e*0.45;
p.l1 = Math.PI/2 - e*0.55;
p.l2 = -0.05 + e*0.65;
p.l3 = 0.05 - e*0.65;
p.r1 = Math.PI/2 - e*0.55;
p.r2 = 0.05 - e*0.65;
p.r3 = -0.05 + e*0.65;
return p;
}};
PHASES.headspin = { beats:10, fn:(t,b)=>{
let spin;
if (t < 0.22) spin = easeInOut(t/0.22) * Math.PI;
else if (t < 0.78) spin = Math.PI + easeInOut((t-0.22)/0.56) * 4*Math.PI;
else spin = 5*Math.PI + easeInOut((t-0.78)/0.22) * Math.PI;
const p = standPose(0);
p.spin = spin;
p.contact = 'head';
p.hue = 192;
const midAmt = smoothstep(clamp((t-0.14)/0.10,0,1)) * smoothstep(clamp((0.72-t)/0.10,0,1));
const c = Math.sin(((t-0.22)/0.56)*Math.PI*4) * 0.75;
p.l1 = lerp(Math.PI/2+0.05, Math.PI/2 + c, midAmt);
p.l2 = lerp(-0.05, 0.45, midAmt);
p.l3 = lerp( 0.05, -0.45, midAmt);
p.r1 = lerp(Math.PI/2-0.05, Math.PI/2 - c, midAmt);
p.r2 = lerp( 0.05, -0.45, midAmt);
p.r3 = lerp(-0.05, 0.45, midAmt);
p.w1 = lerp(0.5, 1.05, midAmt);
p.w2 = lerp(0.35, 0.55, midAmt);
p.w3 = lerp(0.25, 0.30, midAmt);
return p;
}};
PHASES.windmill = { beats:8, fn:(t,b)=>{
const entry = clamp(t/0.20, 0, 1);
const spinT = clamp((t-0.20)/0.60, 0, 1);
const exit = clamp((t-0.80)/0.20, 0, 1);
const p = standPose(0);
p.contact = 'leftfoot';
p.hue = 285;
if (t < 0.20){
const e = easeInOut(entry);
p.a0 = -Math.PI/2 + e*0.55;
p.l1 = Math.PI/2 - e*0.45;
p.l2 = -0.05 + e*0.5;
p.l3 = 0.05 - e*0.5;
p.r1 = Math.PI/2 - e*0.45;
p.r2 = 0.05 - e*0.5;
p.r3 = -0.05 + e*0.5;
p.w1 = 0.5 + e*0.5;
} else if (t < 0.80){
const ang = spinT * Math.PI * 3;
p.a0 = ang;
p.l1 = Math.PI/2;
p.l2 = 0.15;
p.l3 = -0.15;
p.r1 = Math.PI/2 + Math.sin(ang*2)*0.85;
p.r2 = Math.sin(ang*2)*0.35;
p.r3 = -Math.sin(ang*2)*0.35;
p.w1 = 0.9 + Math.sin(ang)*0.35;
p.w2 = 0.45;
p.w3 = 0.25;
} else {
const e = easeInOut(exit);
const ang = Math.PI*3;
p.a0 = lerp(ang, -Math.PI/2, e);
p.l1 = lerp(Math.PI/2, Math.PI/2+0.05, e);
p.l2 = lerp(0.15, -0.05, e);
p.l3 = lerp(-0.15, 0.05, e);
p.r1 = lerp(Math.PI/2 + Math.sin(ang*2)*0.85, Math.PI/2-0.05, e);
p.r2 = lerp(Math.sin(ang*2)*0.35, 0.05, e);
p.r3 = lerp(-Math.sin(ang*2)*0.35, -0.05, e);
p.w1 = lerp(0.9 + Math.sin(ang)*0.35, 0.5, e);
p.w2 = lerp(0.45, 0.35, e);
p.w3 = lerp(0.25, 0.25, e);
}
return p;
}};
PHASES.freeze = { beats:4, fn:(t,b)=>{
const p = standPose(0);
p.hue = 45;
p.a0 = -Math.PI/2 + 0.42;
p.a1 = -0.08;
p.a2 = -0.05;
p.tail= 4.6;
p.w1 = 1.25;
p.w2 = 0.62;
p.w3 = 0.30;
p.l1 = Math.PI/2 + 0.30;
p.l2 = -0.30;
p.l3 = 0.30;
p.r1 = Math.PI/2 - 0.62;
p.r2 = 0.85;
p.r3 = -0.85;
p.contact = 'leftfoot';
const m = Math.sin(t*Math.PI*2)*0.035;
p.a0 += m;
p.w1 += Math.sin(t*Math.PI*2 + 1)*0.03;
p.r2 += Math.sin(t*Math.PI*2 + 2)*0.05;
return p;
}};
PHASES.flare = { beats:8, fn:(t,b)=>{
const p = standPose(0);
p.hue = 12;
const f = Math.sin(t*Math.PI*17);
p.l1 = Math.PI/2 + f*0.55;
p.l2 = f*0.32;
p.l3 = -f*0.32;
p.r1 = Math.PI/2 - f*0.55;
p.r2 = -f*0.32;
p.r3 = f*0.32;
p.w1 = 0.62 + Math.abs(f)*0.22;
p.w2 = 0.40;
p.w3 = 0.22;
p.a0 = -Math.PI/2 + Math.sin(t*Math.PI*4)*0.05;
return p;
}};
/* ---------------- timeline ---------------- */
const MOVE_SETS = {
all: ['stand','dip','headspin','windmill','freeze','flare','stand'],
headspin: ['headspin'],
windmill: ['windmill'],
flare: ['flare'],
freeze: ['freeze']
};
let bpm = 112;
let beatDur = 60/bpm;
let currentSet = 'all';
let timeline = null;
let time = 0;
let trailAmt = 0.16;
let style = 'neon';
function rebuildTimeline(){
const names = MOVE_SETS[currentSet];
const tl = [];
let total = 0;
for (const n of names){
const ph = PHASES[n];
tl.push({ name:n, start:total, dur: ph.beats * beatDur, fn: ph.fn });
total += ph.beats * beatDur;
}
timeline = { tl, total };
}
/* ---------------- rendering ---------------- */
const STYLES = {
neon: { passes:2, lw:1.0, glow:0.30, hueShift:0 },
ink: { passes:1, lw:1.7, glow:0.0, hueShift:0 },
pixel: { passes:2, lw:2.4, glow:0.16, hueShift:0 }
};
function drawTapered(from, to, w0, w1, color, alpha, steps){
ctx.globalAlpha = alpha;
ctx.strokeStyle = color;
const n = steps;
for (let i=0;i<n;i++){
const f0 = i/n, f1 = (i+1)/n;
const x0 = from.x + (to.x-from.x)*f0;
const y0 = from.y + (to.y-from.y)*f0;
const x1 = from.x + (to.x-from.x)*f1;
const y1 = from.y + (to.y-from.y)*f1;
const w = (w0 + (w1-w0)*(f0+f1)*0.5) * STYLES[style].lw;
ctx.lineWidth = Math.max(0.6, w);
ctx.beginPath();
ctx.moveTo(x0,y0);
ctx.lineTo(x1,y1);
ctx.stroke();
}
ctx.globalAlpha = 1;
}
function drawSkeleton(bp, pose){
const hue = (pose.hue !== undefined ? pose.hue : 175) + STYLES[style].hueShift;
const col = `hsl(${hue}, 92%, 63%)`;
const colDim = `hsl(${hue}, 92%, 55%)`;
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
const bones = bp.bones;
const steps = 6;
// soft halo pass
if (STYLES[style].passes >= 2){
for (const b of bones){
drawTapered(b.from, b.to, b.w0*2.4, b.w1*2.4, colDim, STYLES[style].glow, steps);
}
}
// core pass
for (const b of bones){
drawTapered(b.from, b.to, b.w0, b.w1, col, 0.95, steps);
}
// bright core
for (const b of bones){
drawTapered(b.from, b.to, b.w0*0.42, b.w1*0.42, '#ffffff', 0.55, steps);
}
}
/* ---------------- main loop ---------------- */
let last = performance.now();
function frame(now){
const dt = Math.min((now - last)/1000, 0.05);
last = now;
time += dt;
// trail fade
const fade = lerp(0.90, 0.34, trailAmt);
ctx.globalAlpha = fade;
ctx.fillStyle = '#07070c';
ctx.fillRect(0,0,W,H);
ctx.globalAlpha = 1;
const total = timeline.total;
const tt = time % total;
let ph = timeline.tl[0], local = 0;
for (const p of timeline.tl){
if (tt >= p.start && tt < p.start + p.dur){
ph = p; local = (tt - p.start)/p.dur; break;
}
}
const beatInPhase = (tt - ph.start)/beatDur;
const b = beatInPhase - Math.floor(beatInPhase);
const pose = ph.fn(local, b);
const bp = buildPose(pose);
ctx.save();
ctx.translate(W/2, floorY);
ctx.rotate(pose.spin !== undefined ? pose.spin : 0);
ctx.translate(-bp.contact.x*scale, -bp.contact.y*scale);
ctx.scale(scale, scale);
drawSkeleton(bp, pose);
ctx.restore();
// beat ring
const gb = (time * bpm / 60) % 1;
const pulse = Math.max(0, 1 - gb*5);
const beatEl = document.getElementById('beat');
beatEl.style.transform = `translateX(-50%) scale(${0.35 + pulse*0.65})`;
beatEl.style.boxShadow = `0 0 ${14 + pulse*46}px rgba(120,220,255,${0.06 + pulse*0.5})`;
requestAnimationFrame(frame);
}
/* ---------------- UI wiring ---------------- */
const bpmInput = document.getElementById('bpm');
const bpmVal = document.getElementById('bpmv');
const trailInput = document.getElementById('trail');
const styleSel = document.getElementById('style');
bpmInput.addEventListener('input', () => {
bpm = parseInt(bpmInput.value, 10);
beatDur = 60/bpm;
bpmVal.textContent = bpm;
rebuildTimeline();
});
trailInput.addEventListener('input', () => {
trailAmt = parseFloat(trailInput.value);
});
styleSel.addEventListener('change', () => {
style = styleSel.value;
});
document.querySelectorAll('button[data-set]').forEach(btn => {
btn.addEventListener('click', () => {
currentSet = btn.dataset.set;
document.querySelectorAll('button[data-set]').forEach(b => b.classList.remove('on'));
btn.classList.add('on');
rebuildTimeline();
});
});
/* ---------------- boot ---------------- */
rebuildTimeline();
resize();
requestAnimationFrame(frame);
})();
</script>
</body>
</html>
stand → dip → headspin → windmill → freeze → flare → stand, each a self-contained phrase that begins and ends in the same standing pose, so transitions never pop. Weight comes from three things: a two-beat dip that coils the torso and sweeps the wings back before anything happens (anticipation), contact-point solving so the bird is always planted on a foot or its crown rather than floating, and asymmetric easing — easeInOut for the big rotations, easeOut for recoveries, plus a smoothstep-blended window where the headspin's legs switch from standing to cycling. The headspin is a single accumulated spin angle (0 → 6π) rotating the whole rig around the head contact, so it reads as one committed rotation rather than a wiggle; the flare is a 17π sine on the knee angles, which is what makes it feel fast rather than interpolated.