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.
It is a lanky city pigeon rebuilt as a paper-cut b-boy with an oil-slick neck, newsprint feather panels and chalky orange talons. The look is a subway cassette-tape cypher: risograph grain, hand-drawn crowd worms, scuffed floor marks and slightly misregistered ink.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Asphalt Bird Cypher</title>
<style>
:root{
--paper:#f8ebc9; --ink:#151417; --hot:#ee4236; --chalk:#cefff1;
}
*{box-sizing:border-box}
html,body{margin:0;width:100%;height:100%;overflow:hidden;background:#151417}
body{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}
canvas{position:fixed;inset:0;width:100%;height:100%;display:block;touch-action:none;cursor:crosshair}
#hud{
position:fixed;left:clamp(10px,2vw,24px);top:clamp(10px,2vw,22px);
width:min(780px,calc(100vw - 20px));color:var(--paper);pointer-events:none;
text-shadow:0 2px 0 rgba(0,0,0,.45);
}
.card{
pointer-events:auto;border:2px solid var(--ink);background:rgba(17,16,20,.76);
padding:10px 12px 9px;box-shadow:5px 6px 0 rgba(0,0,0,.35);
transform:rotate(-.35deg);
}
.top{display:flex;align-items:baseline;justify-content:space-between;gap:12px;border-bottom:1px dashed rgba(248,235,201,.45);padding-bottom:6px;margin-bottom:8px}
.brand{font-weight:900;letter-spacing:.14em;font-size:12px;color:var(--chalk);white-space:nowrap}
#moveName{font-size:12px;color:var(--paper);opacity:.9;text-align:right;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.rows{display:flex;gap:14px;align-items:center;flex-wrap:wrap;margin-bottom:8px}
label{font-size:11px;letter-spacing:.11em;text-transform:uppercase;white-space:nowrap}
input[type=range]{width:112px;accent-color:var(--hot);vertical-align:middle}
output{display:inline-block;min-width:30px;color:var(--chalk);font-weight:800}
.pads{display:flex;gap:6px;flex-wrap:wrap}
button{
font:700 11px/1 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
letter-spacing:.06em;text-transform:uppercase;color:var(--ink);background:var(--paper);
border:2px solid var(--ink);border-radius:0;padding:7px 8px;cursor:pointer;
box-shadow:2px 2px 0 var(--ink);transition:transform .08s,background .08s,color .08s;
}
button:hover{transform:translate(-1px,-1px);background:var(--chalk)}
button:active,button.hot{transform:translate(2px,2px);background:var(--hot);color:#fff;box-shadow:0 0 0 var(--ink)}
.help{margin-top:8px;font-size:10px;line-height:1.35;color:var(--chalk);opacity:.78}
@media (max-width:680px){
#hud{top:8px;left:8px;width:calc(100vw - 16px)}
.card{padding:8px}
.brand{font-size:10px}.help{font-size:9px}
input[type=range]{width:88px}
button{padding:6px;font-size:10px}
}
</style>
</head>
<body>
<canvas id="stage" aria-label="A paper-cut pigeon breakdancing in an animated subway cassette cypher"></canvas>
<div id="hud">
<div class="card">
<div class="top">
<div class="brand">ASPHALT BIRD // CYPHER <span id="cycleNo">01</span></div>
<div id="moveName">loading the cardboard...</div>
</div>
<div class="rows">
<label>BPM <input id="tempo" type="range" min="70" max="150" value="104"><output id="bpmOut">104</output></label>
<label>WEIGHT <input id="weight" type="range" min="0" max="100" value="72"><output id="weightOut">72</output></label>
</div>
<div class="pads">
<button data-cue="six">1 · 6-step</button>
<button data-cue="back">2 · backspin</button>
<button data-cue="wind">3 · wingmill</button>
<button data-cue="freeze">space · call freeze</button>
<button id="remix">R · remix</button>
<button id="ink">I · ink</button>
</div>
<div class="help">Drag sideways on the floor to scratch time. Click/tap to throw a chalk challenge mark; the bird leans its footwork toward it. Keys: 1/2/3/4 or space, R, I.</div>
</div>
</div>
<script>
(() => {
'use strict';
const TAU = Math.PI * 2;
const PI = Math.PI;
const canvas = document.getElementById('stage');
const ctx = canvas.getContext('2d');
const tempoEl = document.getElementById('tempo');
const weightEl = document.getElementById('weight');
const bpmOut = document.getElementById('bpmOut');
const weightOut = document.getElementById('weightOut');
const moveEl = document.getElementById('moveName');
const cycleEl = document.getElementById('cycleNo');
const remixBtn = document.getElementById('remix');
const inkBtn = document.getElementById('ink');
let bpm = +tempoEl.value;
let weight = +weightEl.value / 100;
const palettes = [
{
name:'NEWSPRINT', bg:'#d8c8a6', bg2:'#a78f73', paper:'#f8ebc9', ink:'#151417',
floor1:'#c7b28c', floor2:'#6f655b', tape:'#2f2b30',
body1:'#d1d0c3', body2:'#737a76', body3:'#2e3032', far:'#4e5455',
oil1:'#19c6b0', oil2:'#7d4bd8', beak:'#ff6b2c', beak2:'#ffd447',
chalk:'#cefff1', hot:'#ee4236', ash:'#77766d'
},
{
name:'MIDNIGHT', bg:'#182331', bg2:'#0a101a', paper:'#d7f5ef', ink:'#080b10',
floor1:'#243447', floor2:'#111827', tape:'#05080d',
body1:'#a9d8dc', body2:'#476274', body3:'#101927', far:'#22384a',
oil1:'#42f5c8', oil2:'#ff4fb8', beak:'#ffb000', beak2:'#fff36b',
chalk:'#ffffff', hot:'#ff3d81', ash:'#60758a'
},
{
name:'ACID RISO', bg:'#efe9b8', bg2:'#b8d88b', paper:'#fff8d8', ink:'#11130d',
floor1:'#c9e05d', floor2:'#537149', tape:'#172514',
body1:'#f3ffb2', body2:'#7baa4b', body3:'#23402e', far:'#42683b',
oil1:'#00d1ff', oil2:'#ff4d00', beak:'#ff2d55', beak2:'#ffea00',
chalk:'#fff7ff', hot:'#ff005d', ash:'#77914b'
}
];
let PAL = palettes[0], palIndex = 0;
let W = 0, H = 0, DPR = 1, S = 1, CX = 0, FLOOR = 0;
let beat = 0, last = performance.now();
let seed = 9137, cycle = 1, routine = null, routineStart = 0, routineEnd = 0;
let cue = null;
let challenge = {active:false, x:0, born:-999};
let pointer = {active:false, id:null, lastX:0, total:0};
let scratchEnergy = 0, scratchPhase = 0;
let shouts = [], particles = [], impactArmed = true, lastLabel = '';
tempoEl.addEventListener('input', () => {
bpm = +tempoEl.value;
bpmOut.textContent = bpm;
});
weightEl.addEventListener('input', () => {
weight = +weightEl.value / 100;
weightOut.textContent = weightEl.value;
});
document.querySelectorAll('[data-cue]').forEach(b => {
b.addEventListener('click', () => triggerCue(b.dataset.cue));
});
remixBtn.addEventListener('click', () => remix());
inkBtn.addEventListener('click', () => cycleInk());
window.addEventListener('resize', resize);
canvas.addEventListener('pointerdown', e => {
e.preventDefault();
pointer.active = true;
pointer.id = e.pointerId;
pointer.lastX = e.clientX;
pointer.total = 0;
scratchEnergy = Math.max(scratchEnergy, .45);
try { canvas.setPointerCapture(e.pointerId); } catch (_) {}
});
canvas.addEventListener('pointermove', e => {
if (!pointer.active || e.pointerId !== pointer.id) return;
const dx = e.clientX - pointer.lastX;
pointer.lastX = e.clientX;
pointer.total += Math.abs(dx);
beat = Math.max(0, beat + dx * .035);
scratchPhase += dx * .012;
scratchEnergy = clamp(scratchEnergy + Math.abs(dx) * .009, 0, 1.4);
});
function endPointer(e){
if (!pointer.active || e.pointerId !== pointer.id) return;
pointer.active = false;
try { canvas.releasePointerCapture(e.pointerId); } catch (_) {}
if (pointer.total < 9) {
challenge.active = true;
challenge.x = clamp((e.clientX - CX) / S, -280, 280);
challenge.born = beat;
spawnChalk(challenge.x, 24);
addShout('CHALK CHALLENGE', .9);
}
}
canvas.addEventListener('pointerup', endPointer);
canvas.addEventListener('pointercancel', e => {
if (e.pointerId === pointer.id) pointer.active = false;
});
window.addEventListener('keydown', e => {
if (e.repeat) return;
const map = {Digit1:'six', Digit2:'back', Digit3:'wind', Digit4:'freeze'};
if (e.code === 'Space') { e.preventDefault(); triggerCue('freeze'); return; }
if (map[e.code]) { triggerCue(map[e.code]); return; }
if (e.code === 'KeyR') remix();
if (e.code === 'KeyI') cycleInk();
});
function clamp(v,a,b){ return Math.max(a, Math.min(b, v)); }
function mix(a,b,t){ return a + (b-a)*t; }
function easeOut(t){ return 1 - Math.pow(1-clamp(t,0,1), 3); }
function easeInOut(t){ t=clamp(t,0,1); return t*t*(3-2*t); }
function roundRectPath(x,y,w,h,r){
r = Math.min(r, w/2, h/2);
ctx.beginPath();
ctx.moveTo(x+r,y);
ctx.lineTo(x+w-r,y);
ctx.quadraticCurveTo(x+w,y,x+w,y+r);
ctx.lineTo(x+w,y+h-r);
ctx.quadraticCurveTo(x+w,y+h,x+w-r,y+h);
ctx.lineTo(x+r,y+h);
ctx.quadraticCurveTo(x,y+h,x,y+h-r);
ctx.lineTo(x,y+r);
ctx.quadraticCurveTo(x,y,x+r,y);
ctx.closePath();
}
function mulberry32(a){
return function(){
a |= 0; a = a + 0x6D2B79F5 | 0;
let t = Math.imul(a ^ a >>> 15, 1 | a);
t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
return ((t ^ t >>> 14) >>> 0) / 4294967296;
};
}
function shuffle(arr,r){
for(let i=arr.length-1;i>0;i--){
const j = Math.floor(r()*(i+1));
[arr[i],arr[j]] = [arr[j],arr[i]];
}
}
function L(h,b,p,pin=0){
return {h:h.slice(), b:b, p:p ? p.slice() : [0,0], pin:pin};
}
function P(o={}){
const q = {
x:0, y:-142, a:0, sx:1, sy:1, head:0, tail:0,
w1:L([65,50],-24), w2:L([-65,45],24),
l1:L([34,142],-16), l2:L([-30,142],16),
shadow:1, impact:0, ghost:0, spin:1, brow:0, sweat:0
};
for (const k in o) {
if (q[k] && typeof q[k] === 'object' && !Array.isArray(q[k]) &&
o[k] && typeof o[k] === 'object' && !Array.isArray(o[k])) {
Object.assign(q[k], o[k]);
} else q[k] = o[k];
}
return q;
}
function lerpAny(a,b,t){
if (typeof a === 'number' && typeof b === 'number') return a + (b-a)*t;
if (Array.isArray(a)) return a.map((v,i) => v + (((b && b[i] !== undefined) ? b[i] : v) - v)*t);
if (a && typeof a === 'object') {
const o = {};
for (const k in a) {
const bv = (b && b[k] !== undefined) ? b[k] : a[k];
o[k] = lerpAny(a[k], bv, t);
}
return o;
}
return t < .5 ? a : b;
}
function clonePose(q){ return lerpAny(q,q,0); }
function worldToLocal(px,py,q){
const c = Math.cos(q.a), s = Math.sin(q.a);
const dx = px - q.x, dy = py - q.y;
return [
(c*dx + s*dy) / q.sx,
(-s*dx + c*dy) / q.sy
];
}
function endpointLocal(limb,q){
const h = limb.h.slice();
if (limb.pin > .001) {
const pl = worldToLocal(limb.p[0], limb.p[1], q);
const m = clamp(limb.pin,0,1);
return [mix(h[0],pl[0],m), mix(h[1],pl[1],m)];
}
return h;
}
function attack(f,w){
const ant = .07 + .12*w;
const amp = .025 + .16*w;
const snap = .24 - .115*w;
const settle = .72 + .13*w;
if (f < ant) return -amp * easeInOut(f/ant);
if (f < ant + snap) {
const z = (f-ant)/snap;
return -amp + (1.08+amp) * (z*z*(3-2*z));
}
if (f < settle) return 1.08 + .012*Math.sin(f*35)*(1-w);
const z = (f-settle)/(1-settle);
return 1.08 - .08*easeOut(z);
}
function hitPulse(f,w){
const ant = .07 + .12*w;
const snap = .24 - .115*w;
const center = ant + snap;
const width = .055 - .025*w;
return Math.exp(-Math.pow((f-center)/width,2));
}
function challengeBias(c){ return clamp((c.challengeX || 0)/260,-1,1); }
function finish(q,c,amount=1){
const w = clamp(c.W,0,1);
q.sx = 1 + (q.sx - 1) * (.55 + .85*w);
q.sy = 1 + (q.sy - 1) * (.50 + 1.05*w);
q.impact = (q.impact || 0) * (.35 + 1.25*w);
q.shadow = (q.shadow || 1) * (.65 + .65*w);
const dx = challengeBias(c) * 20 * amount;
q.x += dx;
for (const k of ['w1','w2','l1','l2']) {
if (q[k].pin > 0) q[k].p[0] += dx * .35;
}
return q;
}
function quantPose(keys,p,c,amount=1){
p = clamp(p,0,1);
const n = keys.length - 1;
const z = p*n;
let i = Math.floor(z);
if (i >= n) i = n-1;
const f = z - i;
const u = attack(f,c.W);
const out = lerpAny(keys[i], keys[Math.min(i+1,n)], u);
const imp = Math.max(keys[i].impact || 0, keys[i+1].impact || 0);
out.impact = hitPulse(f,c.W) * imp;
return finish(out,c,amount);
}
function held(base,p,c){
const q = clonePose(base);
const t = clamp(p,0,1);
const breath = Math.sin(c.beat*TAU*.65) * (1-c.W) * .014;
q.a += breath;
q.head += Math.sin(c.beat*TAU*.45) * .02;
q.sy += Math.sin(c.beat*TAU*.55) * .006;
q.impact = t < .08 ? hitPulse(t/.08,c.W) * .35 : 0;
return finish(q,c,.5);
}
const T0 = P({
x:-35,y:-143,a:-.09,sx:1.02,sy:.98,head:.10,tail:-.12,
w1:L([92,-15],-32), w2:L([-82,-24],30),
l1:L([36,142],-16,[0,0],1), l2:L([-32,142],16,[-78,0],1),
impact:1,shadow:1
});
const T1 = P({
x:24,y:-132,a:.12,sx:.95,sy:1.08,head:-.12,tail:.15,
w1:L([95,18],-36), w2:L([-86,-30],22),
l1:L([42,135],-12,[64,0],1), l2:L([-38,140],20,[-12,0],1),
impact:.95,shadow:1.1
});
const T2 = P({
x:38,y:-151,a:-.04,sx:1.04,sy:.95,head:.18,tail:-.25,
w1:L([80,-38],-30), w2:L([-72,10],35),
l1:L([32,145],-18,[78,0],1), l2:L([-28,145],16,[18,0],1),
impact:.9,shadow:.92
});
const T3 = P({
x:-28,y:-126,a:.20,sx:.92,sy:1.10,head:-.20,tail:.28,
w1:L([85,42],-40), w2:L([-96,-8],38),
l1:L([38,130],-14,[6,0],1), l2:L([-40,130],20,[-70,0],1),
impact:1.15,shadow:1.18
});
const B0 = P({
x:0,y:-112,a:.05,sx:.94,sy:1.13,head:.35,tail:.25,
w1:L([85,32],-25), w2:L([-82,35],25),
l1:L([36,112],-15,[42,0],1), l2:L([-32,112],15,[-45,0],1),
impact:.25,shadow:1.25
});
const B1 = P({
x:0,y:-132,a:-.03,sx:1.02,sy:.96,head:-.08,tail:-.10,
w1:L([88,-10],-28), w2:L([-80,-5],28),
l1:L([34,132],-16,[35,0],1), l2:L([-30,132],16,[-35,0],1),
impact:.15,shadow:1
});
const D1 = P({
x:-8,y:-118,a:-.18,sx:.92,sy:1.14,head:.28,tail:.18,
w1:L([80,-20],-40), w2:L([-75,-30],32),
l1:L([38,118],-14,[35,0],1), l2:L([-36,118],18,[-45,0],1),
impact:.8,shadow:1.2
});