Custom Code Experiment •
Dervish
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 whirling dervish 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;
// ---------- Whirling dervish (sema) ----------
const s = Math.sin, c = Math.cos, PI = Math.PI, TAU = PI * 2;
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;
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;
}
// ---------- Physics ----------
const G = 9.81 / 0.83; // gravity in figure units (figure ≈ 1.7 m)
const SKIRT_L = 0.8; // waist to hem
const OMEGA_MAX = 5.4; // ≈ 0.86 turns per second
// Timeline (seconds): bow with arms crossed, begin turning and unfold the arms,
// whirl, slow down and fold the arms again, bow. Then repeat.
const CYCLE = 22;
function omegaAt(tl) {
if (tl < 2.5) return 0;
if (tl < 7) return OMEGA_MAX * smooth((tl - 2.5) / 4.5);
if (tl < 16) return OMEGA_MAX;
if (tl < 19.5) return OMEGA_MAX * (1 - smooth((tl - 16) / 3.5));
return 0;
}
// Arm keyframes: crossed on the chest → raised in front → open (right palm to the sky, left to the earth)
const CROSSED_R = { abd: 0.2, fwd: 0.6, eAbd: -0.98, eFwd: 3.1 };
const CROSSED_L = { abd: 0.2, fwd: 0.6, eAbd: -0.98, eFwd: 3.1 };
const RISE = { abd: 0.35, fwd: 1.9, eAbd: 0.1, eFwd: 0.6 };
const OPEN_R = { abd: 2.25, fwd: 0.25, eAbd: 0.25, eFwd: 0.1 };
const OPEN_L = { abd: 1.35, fwd: 0.2, eAbd: -0.25, eFwd: 0 };
function armsAt(u) { // u: 0 crossed → 1 open
if (u < 0.5) { const w = smooth(u / 0.5); return [blend(CROSSED_R, RISE, w), blend(CROSSED_L, RISE, w)]; }
const w = smooth((u - 0.5) / 0.5); return [blend(RISE, OPEN_R, w), blend(RISE, OPEN_L, w)];
}
const state = { yaw: 0, omega: 0, prevOmega: 0, flare: 0.25, lag: 0, glitchYaw: 0 };
const FLARE_REST = 0.25;
function updateDance(dt, t) {
const tl = ((t % CYCLE) + CYCLE) % CYCLE;
const w = omegaAt(tl);
const alpha = dt > 0 ? (w - state.omega) / dt : 0; // angular acceleration
state.omega = w;
state.yaw += w * dt;
// conical pendulum: the hem rises until cos θ = g / (ω² L); cloth responds with a little lag
const eq = w * w * SKIRT_L > G ? Math.acos(G / (w * w * SKIRT_L)) : 0;
const target = Math.max(FLARE_REST, eq);
state.flare += (target - state.flare) * (1 - Math.exp(-dt / 0.45));
// the hem trails the body, more so while speeding up
const lagTarget = 0.3 * (w / OMEGA_MAX) + 0.08 * alpha;
state.lag += (lagTarget - state.lag) * (1 - Math.exp(-dt / 0.3));
return tl;
}
function poseAt(tl) {
const spinU = state.omega / OMEGA_MAX;
// arms open as the turning begins, close as it ends
let armU;
if (tl < 2.5) armU = 0;
else if (tl < 6) armU = smooth((tl - 2.5) / 3.5);
else if (tl < 16.5) armU = 1;
else if (tl < 19.5) armU = 1 - smooth((tl - 16.5) / 3);
else armU = 0;
const [armR, armL] = armsAt(armU);
// bow at the start and end
const bow = Math.max(smooth(1 - Math.abs(tl - 1.1) / 0.9), smooth(1 - Math.abs(tl - 20.8) / 0.9));
return {
armR, armL, spinU,
lean: 0.35 * bow, headNod: 0.35 * bow,
headTilt: 0.35 * armU, // head rests toward the right shoulder
yaw: state.yaw + state.glitchYaw
};
}
function buildFigure(pose) {
const HIP_H = 0.84;
const pelvisY = FLOOR_Y + HIP_H;
const pelvis = [0, pelvisY, 0];
const hipR = [0.09, pelvisY, 0], hipL = [-0.09, pelvisY, 0];
// the left foot is the pivot; the right foot pushes off once per turn
const f = ((state.yaw / TAU) % 1 + 1) % 1;
const push = pose.spinU > 0.2 && f > 0.7 ? s(PI * (f - 0.7) / 0.3) : 0;
const PIVOT = [-0.07, 0, 0.02];
const ankL = [PIVOT[0], FLOOR_Y + 0.06, PIVOT[2]];
const ankR = [0.14, FLOOR_Y + 0.06 + 0.1 * push, 0.06 - 0.04 * push];
const leg = (hip, target, side) => {
const [knee, ank] = ik(hip, target, THIGH, SHIN, norm([side * 0.2, 0, 1]));
const heel = add(ank, [0, -0.06, -0.03]);
const toe = add(ank, rotY([0, -0.06, 0.14], side * 0.2));
return [hip, knee, ank, heel, toe];
};
const LR = leg(hipR, ankR, 1), LL = leg(hipL, ankL, -1);
const up = rotX([0, 1, 0], pose.lean);
const neck = add(pelvis, mul(up, SPINE));
const shoulder = add(pelvis, mul(up, SPINE - 0.06));
const headOrient = (v) => rotZ(rotX(v, pose.lean + pose.headNod), -pose.headTilt);
const headC = add(neck, mul(headOrient([0, 1, 0]), NECK + HEAD_R));
const arm = (side, A) => {
const lean = (v) => rotX(v, pose.lean);
const elbow = add(shoulder, mul(lean(limbDir(side, A.abd, A.fwd)), UPPER_ARM));
return [shoulder, elbow, add(elbow, mul(lean(limbDir(side, A.abd + A.eAbd, A.fwd + A.eFwd)), FOREARM))];
};
const ar = arm(1, pose.armR), al = arm(-1, pose.armL);
// rotate the whole body about the pivot foot
const place = (p) => { const q = rotY(sub(p, PIVOT), pose.yaw); return add(q, [PIVOT[0], 0, PIVOT[2]]); };
const polys = [];
const line = (pts, w = 2.4) => polys.push({ pts: pts.map(place), color: WHITE, w });
line([pelvis, neck]);
line([add(shoulder, [0.1, 0, 0]), shoulder, add(shoulder, [-0.1, 0, 0])], 2.0);
line(ar); line(al);
for (const L of [LR, LL]) { line(L.slice(0, 3), 2.0); line([L[2], L[3], L[4], L[2]], 1.8); }
// head
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 });
}
// sikke: tall, slightly tapering felt hat with a rounded top
const hy = HEAD_R * 0.5, hh = 0.34;
const hRing = (y, r) => { const pts = []; for (let k = 0; k <= 20; k++) { const a = TAU * k / 20; pts.push(headPt([r * c(a), y, r * s(a)])); } return pts; };
polys.push({ pts: hRing(hy, 0.15), color: WHITE, w: 1.5 });
polys.push({ pts: hRing(hy + hh * 0.5, 0.135), color: WHITE, w: 0.9, dim: true });
polys.push({ pts: hRing(hy + hh, 0.12), color: WHITE, w: 1.4 });
for (let k = 0; k < 8; k++) {
const a = TAU * k / 8;
polys.push({ pts: [headPt([0.15 * c(a), hy, 0.15 * s(a)]), headPt([0.12 * c(a), hy + hh, 0.12 * s(a)]), headPt([0.06 * c(a), hy + hh + 0.035, 0.06 * s(a)])], color: WHITE, w: 0.9, dim: true });
}
// hands: right palm facing up, left palm facing down (small open ovals)
for (const [A, up] of [[ar, 1], [al, -1]]) {
const h = A[2], pts = [];
for (let k = 0; k <= 12; k++) { const a = TAU * k / 12; pts.push(place(add(h, [0.05 * c(a), 0.004 * up, 0.035 * s(a)]))); }
polys.push({ pts, color: WHITE, w: 1.2 });
}
// ---- skirt (tennure) ----
// A cone from the waist whose angle comes from the spin (conical pendulum). At rest it
// hangs in deep pleats; as it flares the pleats pull out and a wave runs around the hem.
const waistY = pelvisY + 0.06, WAIST_R = 0.12;
const th = state.flare, spinU = pose.spinU;
const N = 48;
const skirtPt = (a, frac) => {
const pleat = (1 - smooth((th - FLARE_REST) / 0.5)) * 0.05 * s(a * 12);
const wave = 0.035 * spinU * s(a * 5 - state.yaw * 2.3);
const twist = -state.lag * frac; // hem trails the waist
const aa = a + twist;
const r = WAIST_R + frac * (SKIRT_L * s(th) + pleat * frac);
let y = waistY - frac * SKIRT_L * c(th) + wave * frac * frac;
y = Math.max(y, FLOOR_Y + 0.02);
return [r * c(aa), y, r * s(aa)];
};
const ringAt = (frac) => { const pts = []; for (let k = 0; k <= N; k++) pts.push(place(skirtPt(TAU * k / N, frac))); return pts; };
polys.push({ pts: ringAt(0), color: WHITE, w: 1.6 });
polys.push({ pts: ringAt(0.55), color: WHITE, w: 0.8, dim: true });
polys.push({ pts: ringAt(1), color: WHITE, w: 1.8 });
for (let k = 0; k < 16; k++) {
const a = TAU * k / 16, pts = [];
for (let j = 0; j <= 6; j++) pts.push(place(skirtPt(a, j / 6)));
polys.push({ pts, color: WHITE, w: 0.9, dim: true });
}
// faint shadow of the hem on the floor
{
const pts = [];
for (let k = 0; k <= N; k++) {
const p = skirtPt(TAU * k / N, 1);
pts.push(place([p[0], FLOOR_Y, p[2]]));
}
polys.push({ pts, color: FLOOR, w: 1.0, alpha: 0.5 * spinU });
}
return polys;
}
// Round floor (semahane) with a ring and faint boards
const FLOOR_R = 1.0;
function buildFloor() {
const polys = [];
for (const r of [FLOOR_R, FLOOR_R * 0.92]) {
const ring = [];
for (let k = 0; k <= 56; k++) { const a = TAU * k / 56; ring.push([r * c(a), FLOOR_Y, r * s(a)]); }
polys.push({ pts: ring, color: FLOOR, w: r === FLOOR_R ? 1.1 : 0.7, dim: r !== FLOOR_R });
}
for (let v = -0.8; v <= 0.81; v += 0.2) {
const h = Math.sqrt(FLOOR_R * 0.92 * FLOOR_R * 0.92 - v * v);
polys.push({ pts: [[-h, FLOOR_Y, v], [h, FLOOR_Y, v]], color: FLOOR, w: 0.6, dim: true });
}
return polys;
}
// ---------- Projection ----------
// The camera follows the dancer, so he stays centred and the floor glides beneath him
const CENTER_Y = 0.0;
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.25;
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 glitchYawTarget = 0;
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.05 : 0.15);
const t = now / 1000;
const tl = updateDance(reduced ? dt / 3 : dt, reduced ? t / 3 : t);
if (!reduced && glitch <= 0 && Math.random() < 0.02) {
glitch = 4 + Math.floor(Math.random() * 10);
glitchStrength = 0.4 + Math.random() * 0.6;
glitchYawTarget = Math.random() < 0.5 ? (Math.random() - 0.5) * 1.2 : 0; // skipped frames of the turn
}
const g = glitch > 0;
if (g) glitch--; else glitchYawTarget = 0;
state.glitchYaw = glitchYawTarget;
const view = { yaw: orbit, pitch: 0.26 + Math.sin(t * 0.7) * 0.04 };
const pose = poseAt(tl);
focusX = 0; focusZ = 0;
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>