Custom Code Experiment •
Skank
Source Code View HTML / CSS / JavaScript
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>Glitch drum & bass loader</title>
<style>
:root {
box-sizing: border-box;
padding-top: env(safe-area-inset-top, 0px);
padding-bottom: env(safe-area-inset-bottom, 0px);
--page: #EDEDED;
--ring-track: rgba(0, 0, 0, 0.12);
--accent: #FFC21A;
--void: #0A0A0A;
}
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) {
--page: #141414;
--ring-track: rgba(255, 255, 255, 0.12);
}
}
:root[data-theme="dark"] {
--page: #141414;
--ring-track: rgba(255, 255, 255, 0.12);
}
*, *::before, *::after { box-sizing: inherit; }
html { height: 100%; scroll-padding-top: env(safe-area-inset-top, 0px); }
body {
margin: 0;
height: 100%;
background: var(--page);
display: grid;
place-items: center;
}
.orb {
position: relative;
width: min(260px, 70vw);
aspect-ratio: 1;
}
/* Animated yellow ring */
.ring {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
transform: rotate(-90deg);
}
.ring circle { fill: none; stroke-width: 3; }
.ring .track { stroke: var(--ring-track); }
.ring .arc {
stroke: var(--accent);
stroke-linecap: round;
stroke-dasharray: 90 400;
animation: sweep 1.6s cubic-bezier(.6,.1,.3,.9) infinite;
transform-origin: 50% 50%;
}
@keyframes sweep {
0% { stroke-dasharray: 20 400; transform: rotate(0deg); }
50% { stroke-dasharray: 160 400; }
100% { stroke-dasharray: 20 400; transform: rotate(360deg); }
}
/* Circular viewport */
.window {
position: absolute;
inset: 12px;
border-radius: 50%;
overflow: hidden;
background: radial-gradient(circle at 50% 40%, #262626 0%, var(--void) 70%);
box-shadow: inset 0 0 40px rgba(0,0,0,.8), inset 0 0 0 1px rgba(255,255,255,.2);
}
.window canvas { display: block; width: 100%; height: 100%; }
/* spotlight from above */
.window::before {
content: "";
position: absolute;
inset: 0;
background: radial-gradient(ellipse 38% 70% at 50% 0%, rgba(255,255,255,.09), transparent 70%);
pointer-events: none;
}
/* scanlines */
.window::after {
content: "";
position: absolute;
inset: 0;
background: repeating-linear-gradient(to bottom, rgba(0,0,0,0) 0 2px, rgba(0,0,0,.28) 2px 3px);
pointer-events: none;
}
@media (prefers-reduced-motion: reduce) {
.ring .arc { animation-duration: 4s; }
}
</style>
</head>
<body>
<div class="orb" role="status" aria-label="Loading">
<svg class="ring" viewBox="0 0 100 100" aria-hidden="true">
<circle class="track" cx="50" cy="50" r="48"/>
<circle class="arc" cx="50" cy="50" r="48"/>
</svg>
<div class="window">
<canvas id="dancer" aria-hidden="true"></canvas>
</div>
</div>
<script>
(() => {
const canvas = document.getElementById('dancer');
const ctx = canvas.getContext('2d');
const buf = document.createElement('canvas');
const bctx = buf.getContext('2d');
const reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
const WHITE = '#FFFFFF';
const FLOOR = '#8C8C8C';
const GHOST_A = '#8C8C8C', GHOST_B = '#E0E0E0';
let W = 0, H = 0, dpr = 1;
function resize() {
dpr = Math.min(window.devicePixelRatio || 1, 2);
const r = canvas.getBoundingClientRect();
W = Math.round(r.width * dpr); H = Math.round(r.height * dpr);
canvas.width = buf.width = W;
canvas.height = buf.height = H;
}
new ResizeObserver(resize).observe(canvas);
resize();
// ---------- Vector helpers ----------
const add = (a, b) => [a[0]+b[0], a[1]+b[1], a[2]+b[2]];
const mul = (a, k) => [a[0]*k, a[1]*k, a[2]*k];
const rotX = ([x, y, z], a) => [x, y*Math.cos(a) - z*Math.sin(a), y*Math.sin(a) + z*Math.cos(a)];
const rotY = ([x, y, z], a) => [x*Math.cos(a) + z*Math.sin(a), y, -x*Math.sin(a) + z*Math.cos(a)];
const rotZ = ([x, y, z], a) => [x*Math.cos(a) - y*Math.sin(a), x*Math.sin(a) + y*Math.cos(a), z];
// Limb direction: starts pointing down, raised sideways by `abd`, swung forward (+z) by `fwd`
const limbDir = (side, abd, fwd) => rotX(rotZ([0, -1, 0], side * abd), -fwd);
// ---------- Skeleton ----------
const SPINE = 0.72, NECK = 0.1, HEAD_R = 0.17;
const UPPER_ARM = 0.36, FOREARM = 0.34;
const THIGH = 0.44, SHIN = 0.44;
const FLOOR_Y = -1.0;
// ---------- Drum & bass skank ----------
const s = Math.sin, c = Math.cos, PI = Math.PI, TAU = PI * 2;
const BPM = 174;
const sub = (a, b) => [a[0]-b[0], a[1]-b[1], a[2]-b[2]];
const dot = (a, b) => a[0]*b[0] + a[1]*b[1] + a[2]*b[2];
const len = (a) => Math.hypot(a[0], a[1], a[2]);
const norm = (a) => mul(a, 1 / (len(a) || 1));
const clamp = (v, a, b) => Math.min(b, Math.max(a, v));
const smooth = (x) => { x = clamp(x, 0, 1); return x * x * (3 - 2 * x); };
const lerp = (a, b, w) => a + (b - a) * w;
const fract = (x) => ((x % 1) + 1) % 1;
function ik(hip, target, a, b, hint) {
const toT = sub(target, hip);
const d = Math.min(a + b - 1e-4, Math.max(0.05, len(toT)));
const u = norm(toT);
const cosA = (a * a + d * d - b * b) / (2 * a * d);
const sinA = Math.sqrt(Math.max(0, 1 - cosA * cosA));
const n = norm(sub(hint, mul(u, dot(hint, u))));
return [add(hip, add(mul(u, a * cosA), mul(n, a * sinA))), add(hip, mul(u, d))];
}
function blend(a, b, w) {
if (typeof a === 'number') return a + (b - a) * w;
const o = {};
for (const k in a) o[k] = blend(a[k], b[k], w);
return o;
}
// ---------- Gravity ----------
const G = 9.81 / 0.83;
const secPerBeat = () => 60 / (reduced ? BPM / 3 : BPM);
function jump(b, period, height) {
const flight = Math.min(period * 0.75, Math.sqrt(8 * height / G) / secPerBeat());
const ground = period - flight;
const ph = ((b % period) + period) % period;
const takeoff = ground / 2, landing = takeoff + flight;
if (ph >= takeoff && ph <= landing) {
const u = (ph - takeoff) / flight;
return { air: 4 * height * u * (1 - u), flying: true, crouch: 0 };
}
const g = ph < takeoff ? ph + ground / 2 : ph - landing;
return { air: 0, flying: false, crouch: s(PI * g / ground) };
}
// ---------- Track structure (bars of 4 beats) ----------
// 4 bars build-up → 8 bars drop (double-time skank) → 4 bars half-time → repeat
const BUILD = 16, DROP = 32, HALF = 16, TOTAL = BUILD + DROP + HALF;
const section = (beat) => {
const m = ((beat % TOTAL) + TOTAL) % TOTAL;
if (m < BUILD) return ['build', m];
if (m < BUILD + DROP) return ['drop', m - BUILD];
return ['half', m - BUILD - DROP];
};
// Drum pattern for one 16th-note step (global step index)
function drums(step) {
const [sec, b] = section(step / 4);
const st = ((Math.round(step) % 16) + 16) % 16; // step within the bar
const bar = Math.floor(b / 4);
const hit = { kick: false, snare: false, hat: false };
if (sec === 'build') {
if (bar < 3) {
hit.kick = st === 0;
hit.snare = bar === 2 && (st === 4 || st === 12);
hit.hat = st % 2 === 0;
} else { // snare roll into the drop, then a gap
if (st < 8) hit.snare = st % 2 === 0;
else if (st < 12) hit.snare = true;
}
} else if (sec === 'drop') { // classic two-step
hit.kick = st === 0 || st === 10;
hit.snare = st === 4 || st === 12;
hit.hat = st % 2 === 0 || st === 7 || st === 15;
} else { // half-time: snare on beat 3
hit.kick = st === 0 || st === 11;
hit.snare = st === 8;
hit.hat = st % 4 === 2;
}
return hit;
}
// beats since the last hit of an instrument (looks back up to one bar)
function since(beat, type) {
const step = Math.floor(beat * 4);
for (let k = 0; k < 16; k++) if (drums(step - k)[type]) return { dt: beat - (step - k) / 4, step: step - k };
return { dt: 99, step: 0 };
}
// ---------- Poses ----------
const foot = (x, y, z, point = 0) => ({ x, y, z, point });
const base = (o) => Object.assign({ hipH: 0.82, px: 0, air: 0, lean: 0.08, leanSide: 0, yaw: 0, headTurn: 0, headNod: 0, kneeOut: 0.35 }, o);
const FIST = (f, e) => ({ abd: 0.35, fwd: f, eAbd: -0.1, eFwd: e });
// Build-up: bouncing on the beat, a hand pumping in the air, the roll speeds up, crouch… jump into the drop
function build(b) {
const bar = Math.floor(b / 4);
const rate = b < 12 ? 1 : b < 14 ? 2 : 4; // bounce faster through the snare roll
const dip = (1 + c(TAU * b * rate)) / 2;
const pre = smooth((b - 14.5) / 0.5); // wind-up crouch
let air = 0, fly = 0;
if (b >= 15) { // one-beat jump landing exactly on the drop
const u = b - 15, T = secPerBeat(), H = G * T * T / 8;
air = 4 * H * u * (1 - u); fly = s(PI * u);
}
const pump = { abd: 2.5, fwd: 0.35, eAbd: 0.15 * dip, eFwd: 0.35 * dip };
const bothUp = smooth((b - 12) / 2);
const armR = blend(pump, { abd: 2.7, fwd: 0.2, eAbd: 0, eFwd: 0 }, bothUp);
const armL = blend(bar < 2 ? FIST(0.9, 1.4) : pump, { abd: 2.7, fwd: 0.2, eAbd: 0, eFwd: 0 }, bothUp);
const feetY = 0.06;
return base({
fR: foot(0.16, feetY, 0.02), fL: foot(-0.16, feetY, 0.02),
hipH: 0.83 - 0.05 * dip * (1 - pre) - 0.28 * pre * (1 - fly) , air,
lean: 0.08 + 0.3 * pre * (1 - fly), headNod: 0.2 * dip * (1 - pre) + 0.3 * pre - 0.3 * fly,
armR: blend(armR, { abd: 0.5, fwd: -0.6, eAbd: 0, eFwd: 0.3 }, pre * (1 - fly)),
armL: blend(armL, { abd: 0.5, fwd: -0.6, eAbd: 0, eFwd: 0.3 }, pre * (1 - fly))
});
}
// Drop: double-time skank — a hop on every 8th note, kicking legs out, fists pumping
function skank(b) {
const j = jump(b, 0.5, 0.02);
const kR = 0.5 + 0.5 * clamp(c(TAU * b) * 3, -1, 1); // right kicks on the beat, left on the "and"
const kL = 1 - kR;
const side = Math.floor(b / 8) % 2 === 1; // every 2 bars: kicks go out to the side
const kick = (sd, k) => side
? foot(sd * lerp(0.12, 0.46, k), lerp(0.06, 0.14, k), lerp(0.02, 0.08, k), 0.4 * k)
: foot(sd * lerp(0.12, 0.2, k), lerp(0.06, 0.12, k), lerp(0.02, 0.4, k), 0.5 * k);
const land = Math.pow(clamp(1 - b / 0.6, 0, 1), 2); // absorbing the landing from the jump
const nod = Math.pow(1 - fract(b), 3); // head drops on every beat
return base({
fR: kick(1, kR), fL: kick(-1, kL),
hipH: 0.78 - 0.05 * j.crouch - 0.3 * land, air: j.air,
lean: 0.16 + 0.2 * land, headNod: 0.28 * nod,
yaw: 0.22 * (kR - kL), headTurn: -0.12 * (kR - kL),
armR: FIST(0.25 + 0.95 * kL, 1.5 - 0.7 * kL),
armL: FIST(0.25 + 0.95 * kR, 1.5 - 0.7 * kR)
});
}
// Half-time: heavy stomps, wide stance, headbang and fists slamming down on the snare
function halftime(b) {
const f = fract(b / 2), k = s(PI * Math.pow(f, 0.75)); // lift, then slam on the even beats
const right = Math.floor(b / 2) % 2 === 0;
const sn = since(b + BUILD + DROP, 'snare').dt;
const hit = Math.exp(-sn * 3);
const lift = (sd) => foot(sd * 0.24, 0.06 + 0.26 * k, 0.12 * k, 0.2 * k);
return base({
fR: right ? lift(1) : foot(0.24, 0.06, 0.03), fL: right ? foot(-0.24, 0.06, 0.03) : lift(-1),
hipH: 0.76 - 0.14 * hit, lean: 0.2 + 0.15 * hit, headNod: 0.1 + 0.55 * hit,
leanSide: (right ? -1 : 1) * 0.07 * k, kneeOut: 0.5,
armR: { abd: 0.45, fwd: lerp(1.1, 0.35, hit), eAbd: 0, eFwd: lerp(1.4, 0.3, hit) },
armL: { abd: 0.45, fwd: lerp(1.1, 0.35, hit), eAbd: 0, eFwd: lerp(1.4, 0.3, hit) }
});
}
function poseAt(beat) {
const [sec, b] = section(beat);
if (sec === 'build') return build(b); // lands straight into the drop
if (sec === 'drop') {
const p = skank(b);
if (b > DROP - 0.5) return blend(p, halftime(0), smooth((b - (DROP - 0.5)) / 0.5));
return p;
}
const p = halftime(b);
if (b > HALF - 1) return blend(p, build(0), smooth((b - (HALF - 1)) / 1));
return p;
}
function buildFigure(pose) {
const pelvisY = FLOOR_Y + pose.hipH + pose.air;
const pelvis = [pose.px, pelvisY, 0];
const hipR = [pose.px + 0.09, pelvisY, 0], hipL = [pose.px - 0.09, pelvisY, 0];
const ANK = 0.06;
const legPts = (hip, side, F) => {
const target = [F.x, FLOOR_Y + F.y + pose.air, F.z];
const [knee, ank] = ik(hip, target, THIGH, SHIN, norm([side * pose.kneeOut, 0.15, 1]));
const turn = side * 0.2;
return { knee, ank, heel: add(ank, rotY([0, -ANK, -0.03], turn)), toe: add(ank, rotY(rotX([0, -ANK, 0.14], F.point), turn)) };
};
const LR = legPts(hipR, 1, pose.fR), LL = legPts(hipL, -1, pose.fL);
const up = rotZ(rotX([0, 1, 0], pose.lean), -pose.leanSide);
const neck = add(pelvis, mul(up, SPINE));
const shoulder = add(pelvis, mul(up, SPINE - 0.06));
const headOrient = (v) => rotZ(rotX(rotY(v, pose.headTurn), pose.lean + pose.headNod), -pose.leanSide);
const headC = add(neck, mul(headOrient([0, 1, 0]), NECK + HEAD_R));
const torso = (v) => rotZ(rotX(v, pose.lean * 0.5), -pose.leanSide);
const arm = (side, A) => {
const elbow = add(shoulder, mul(torso(limbDir(side, A.abd, A.fwd)), UPPER_ARM));
return [shoulder, elbow, add(elbow, mul(torso(limbDir(side, A.abd + A.eAbd, A.fwd + A.eFwd)), FOREARM))];
};
const ar = arm(1, pose.armR), al = arm(-1, pose.armL);
const place = (p) => rotY(p, pose.yaw);
const polys = [];
const line = (pts, w = 2.4) => polys.push({ pts: pts.map(place), color: WHITE, w });
line([pelvis, neck]);
line([hipR, pelvis, hipL]);
line([add(shoulder, torso([0.1, 0, 0])), shoulder, add(shoulder, torso([-0.1, 0, 0]))], 2.0);
line(ar); line(al);
line([hipR, LR.knee, LR.ank]); line([hipL, LL.knee, LL.ank]);
for (const L of [LR, LL]) line([L.ank, L.heel, L.toe, L.ank], 2.0);
const headPt = (v) => place(add(headC, headOrient(v)));
for (const lat of [-0.5, 0, 0.5]) {
const pts = [], r = HEAD_R * c(lat), y = HEAD_R * s(lat);
for (let k = 0; k <= 18; k++) { const a = TAU * k / 18; pts.push(headPt([r * c(a), y, r * s(a)])); }
polys.push({ pts, color: WHITE, w: 1.2, dim: lat !== 0 });
}
for (let m = 0; m < 4; m++) {
const pts = [], a = PI * m / 4;
for (let k = 0; k <= 18; k++) { const q = TAU * k / 18; pts.push(headPt([HEAD_R * c(q) * c(a), HEAD_R * s(q), HEAD_R * c(q) * s(a)])); }
polys.push({ pts, color: WHITE, w: 1.0, dim: true });
}
// cap, peak forward
{
const pts = [];
for (let k = 0; k <= 18; k++) { const a = PI * k / 18; pts.push(headPt([HEAD_R * 1.06 * c(a), HEAD_R * 0.25, HEAD_R * 1.06 * s(a)])); }
polys.push({ pts, color: WHITE, w: 1.4 });
const peak = [];
for (let k = 0; k <= 10; k++) { const a = PI * k / 10; peak.push(headPt([HEAD_R * 0.95 * c(a), HEAD_R * 0.22, HEAD_R + 0.1 * s(a)])); }
polys.push({ pts: peak, color: WHITE, w: 1.4 });
for (const q of [0.35, 0.7]) {
const arc = [];
for (let k = 0; k <= 12; k++) { const a = PI * k / 12; arc.push(headPt([HEAD_R * 1.04 * c(a) * c(q), HEAD_R * (0.25 + 0.75 * s(q)), HEAD_R * 1.04 * s(a) * c(q)])); }
polys.push({ pts: arc, color: WHITE, w: 1.0, dim: true });
}
}
for (const A of [ar, al]) {
const h = A[2], pts = [];
for (let k = 0; k <= 10; k++) { const a = TAU * k / 10; pts.push(place(add(h, [0.04 * c(a), 0.04 * s(a), 0]))); }
polys.push({ pts, color: WHITE, w: 1.4 });
}
return polys;
}
// ---------- Disco floor driven by the drums ----------
const N = 7, TILE = 0.26, HALF_W = N * TILE / 2;
const hash = (n) => { let x = Math.imul(n ^ 0x9e3779b9, 0x85ebca6b); x ^= x >>> 13; x = Math.imul(x, 0xc2b2ae35); x ^= x >>> 16; return (x >>> 0) / 4294967296; };
let floorBeat = 0;
function buildFloor() {
const beat = floorBeat;
const [sec, b] = section(beat);
const K = since(beat, 'kick'), S = since(beat, 'snare'), Hh = since(beat, 'hat');
const snareIdx = S.step;
const polys = [];
for (let i = 0; i <= N; i++) {
const v = -HALF_W + i * TILE;
polys.push({ pts: [[-HALF_W, FLOOR_Y, v], [HALF_W, FLOOR_Y, v]], color: FLOOR, w: 0.8, dim: true });
polys.push({ pts: [[v, FLOOR_Y, -HALF_W], [v, FLOOR_Y, HALF_W]], color: FLOOR, w: 0.8, dim: true });
}
const riser = sec === 'build' && b >= 12 ? smooth((b - 12) / 3) * 0.35 : 0;
const hatTile = Math.floor(hash(Hh.step) * N * N);
const rowMode = Math.floor(hash(snareIdx + 7) * 2), rowPick = Math.floor(hash(snareIdx + 3) * N);
for (let i = 0; i < N; i++) for (let j = 0; j < N; j++) {
const cx = -HALF_W + (i + 0.5) * TILE, cz = -HALF_W + (j + 0.5) * TILE;
const d = Math.hypot(cx, cz);
let v = 0;
// kick: a ring of light bursting out from the centre
const r = K.dt * 2.2;
v += Math.exp(-K.dt * 2.5) * Math.exp(-Math.pow(d - r, 2) / 0.03);
// snare: a whole row or column flashes
if ((rowMode ? i : j) === rowPick) v += Math.exp(-S.dt * 4);
// hi-hat: one tile flickers
if (i * N + j === hatTile) v += 0.7 * Math.exp(-Hh.dt * 9);
// bass: a slow wave rolling across the floor (wobbling in the half-time)
if (sec !== 'build') {
const rate = sec === 'half' ? 2 : 0.5;
v += 0.22 * (0.5 + 0.5 * s(cx * 3.2 - cz * 1.4 - beat * PI * rate));
}
v += riser * (0.5 + 0.5 * s(beat * TAU * 4 + d * 6));
v = clamp(v, 0, 1);
if (v < 0.06) continue;
for (const [inset, th] of [[0.025, 0], [0.065, 0.45], [0.1, 0.75]]) {
if (v <= th) break;
const x0 = cx - TILE / 2 + inset, x1 = cx + TILE / 2 - inset, z0 = cz - TILE / 2 + inset, z1 = cz + TILE / 2 - inset;
polys.push({ pts: [[x0, FLOOR_Y, z0], [x1, FLOOR_Y, z0], [x1, FLOOR_Y, z1], [x0, FLOOR_Y, z1], [x0, FLOOR_Y, z0]],
color: WHITE, w: 1.3, alpha: v });
}
}
return polys;
}
// ---------- Projection ----------
// The camera follows the dancer, so he stays centred and the floor glides beneath him
const CENTER_Y = 0.12;
let focusX = 0, focusZ = 0;
function project(p, view, jitter) {
let [x, y, z] = p;
x -= focusX; y -= CENTER_Y; z -= focusZ;
if (jitter) { x += (Math.random()-.5)*jitter; y += (Math.random()-.5)*jitter; }
const cy = Math.cos(view.yaw), sy = Math.sin(view.yaw);
const x1 = x * cy + z * sy, z1 = -x * sy + z * cy;
const cx = Math.cos(view.pitch), sx = Math.sin(view.pitch);
const y1 = y * cx - z1 * sx, z2 = y * sx + z1 * cx;
const d = 5, persp = d / (d - z2);
const scale = W * 0.265;
return [W/2 + x1 * scale * persp, H/2 - y1 * scale * persp, z2];
}
function strokeAll(polys, view, color, alpha, ox, oy, jitter) {
for (const poly of polys) {
bctx.beginPath();
let depthSum = 0;
poly.pts.forEach((p, i) => {
const [sx, sy, sz] = project(p, view, jitter);
depthSum += sz;
if (i === 0) bctx.moveTo(sx + ox, sy + oy); else bctx.lineTo(sx + ox, sy + oy);
});
const depth = depthSum / poly.pts.length;
const fade = 0.45 + 0.55 * Math.min(1, Math.max(0, (depth + 1.2) / 2.4));
bctx.globalAlpha = alpha * fade * (poly.dim ? 0.55 : 1) * (poly.alpha ?? 1);
bctx.strokeStyle = color || poly.color;
bctx.lineWidth = poly.w * dpr;
bctx.stroke();
}
bctx.globalAlpha = 1;
}
// ---------- Loop ----------
let glitch = 0, glitchStrength = 0;
let orbit = 0, last = performance.now();
let beatShift = 0, prevStep = null;
const ringEl = document.querySelector('.ring');
function frame(now) {
if (W === 0 || H === 0) {
resize();
if (W === 0 || H === 0) { last = now; requestAnimationFrame(frame); return; }
}
const dt = Math.min(50, now - last) / 1000; last = now;
orbit += dt * (reduced ? 0.15 : 0.4);
const t = now / 1000;
let beat = t * (reduced ? BPM / 3 : BPM) / 60;
// Glitches land on the music: the drop always glitches, snares sometimes do
const step = Math.floor(beat * 4);
if (prevStep !== null && !reduced) {
for (let st = prevStep + 1; st <= step; st++) {
const [sec, b] = section(st / 4);
if (sec === 'drop' && Math.abs(b) < 0.01) {
glitch = 14; glitchStrength = 1; beatShift = 0;
} else if (glitch <= 0 && drums(st).snare && sec !== 'build' && Math.random() < 0.3) {
glitch = 3 + Math.floor(Math.random() * 6);
glitchStrength = 0.4 + Math.random() * 0.5;
beatShift = Math.random() < 0.4 ? -0.25 : 0; // stutter: repeat the last 16th
}
}
}
prevStep = step;
const g = glitch > 0;
if (g) glitch--; else beatShift = 0;
beat += beatShift;
// the yellow ring thumps on the kick
const kEnv = Math.exp(-since(beat, 'kick').dt * 5);
if (ringEl) ringEl.style.transform = `rotate(-90deg) scale(${1 + 0.035 * kEnv})`;
const view = { yaw: orbit, pitch: 0.28 + Math.sin(t * 0.7) * 0.04 };
const pose = poseAt(beat);
focusX = 0; focusZ = 0; floorBeat = beat;
const polys = buildFloor().concat(buildFigure(pose));
bctx.clearRect(0, 0, W, H);
bctx.lineJoin = 'round'; bctx.lineCap = 'round';
bctx.globalCompositeOperation = 'lighter';
if (g) {
const off = (4 + Math.random() * 8) * dpr * glitchStrength;
strokeAll(polys, view, GHOST_A, 0.7, -off, 0, 0.03 * glitchStrength);
strokeAll(polys, view, GHOST_B, 0.6, off, (Math.random()-.5)*2*dpr, 0.03 * glitchStrength);
}
strokeAll(polys, view, null, g ? 0.85 : 1, 0, 0, g ? 0.02 : (reduced ? 0 : 0.004));
bctx.globalCompositeOperation = 'source-over';
ctx.clearRect(0, 0, W, H);
if (g) {
let y = 0;
while (y < H) {
const h = Math.min(H - y, Math.max(2, Math.floor((4 + Math.random() * 30) * dpr)));
const shift = Math.random() < 0.35 ? (Math.random() - .5) * 40 * dpr * glitchStrength : 0;
ctx.drawImage(buf, 0, y, W, h, shift, y, W, h);
y += h;
}
const blocks = Math.floor(Math.random() * 4);
for (let i = 0; i < blocks; i++) {
ctx.fillStyle = [WHITE, GHOST_A, GHOST_B][i % 3];
ctx.globalAlpha = 0.18 + Math.random() * 0.3;
ctx.fillRect(Math.random() * W, Math.random() * H, (10 + Math.random() * 60) * dpr, (1 + Math.random() * 3) * dpr);
}
ctx.globalAlpha = 1;
} else {
ctx.drawImage(buf, 0, 0);
}
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
})();
</script>
</body>
</html>