← Mike Higgins Home
Custom Code Experiment •

MJ

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 moonwalk 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;

  // ---------- Moonwalk & slides ----------
  const s = Math.sin, c = Math.cos, PI = Math.PI, TAU = PI * 2;
  const BPM = 100;
  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 smooth = (x) => { x = Math.min(1, Math.max(0, x)); return x * x * (3 - 2 * x); };

  // Two-bone IK: place the knee so the foot lands exactly on its target
  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))));
    const knee = add(hip, add(mul(u, a * cosA), mul(n, a * sinA)));
    return [knee, add(hip, mul(u, d))];
  }

  // Feet: `o` is the foot's offset along the travel direction (body frame),
  // `heel` is how far the heel is raised (0 = flat, ~0.9 = up on the toe).
  // In the moonwalk the planted foot sits still on its toe while the body glides
  // past it, and the other foot slides flat along the floor — so each foot, relative
  // to the body, moves one way on its toe and slides back the other way flat.
  const STEP = 0.2;                 // body travel per beat
  function glideFoot(phi) {
    phi = ((phi % 1) + 1) % 1;
    if (phi < 0.5) {                // planted on the toe
      return { o: STEP / 2 - 2 * STEP * phi, heel: 0.9 * smooth(phi / 0.07), slide: 0 };
    }
    const q = phi - 0.5;            // sliding flat
    return { o: -STEP / 2 + 2 * STEP * q, heel: 0.9 * (1 - smooth(q / 0.07)), slide: q > 0.04 ? 1 : 0 };
  }

  // Route: a square traced with moonwalks and sidewalks, with a spin + toe stand at each corner
  const C = [[-0.4, 0.4], [-0.4, -0.4], [0.4, -0.4], [0.4, 0.4]];
  const SEGMENTS = [
    { type: 'walk', beats: 4, from: C[0], to: C[1], yaw: 0,  dir: [0, 0, -1] },   // moonwalk
    { type: 'spin', beats: 2, at: C[1], yaw0: 0, turn: TAU },
    { type: 'walk', beats: 4, from: C[1], to: C[2], yaw: 0,  dir: [1, 0, 0] },    // sidewalk
    { type: 'spin', beats: 2, at: C[2], yaw0: 0, turn: TAU + PI },
    { type: 'walk', beats: 4, from: C[2], to: C[3], yaw: PI, dir: [0, 0, -1] },   // moonwalk
    { type: 'spin', beats: 2, at: C[3], yaw0: PI, turn: TAU + PI },
    { type: 'walk', beats: 4, from: C[3], to: C[0], yaw: 0,  dir: [-1, 0, 0] },   // sidewalk
    { type: 'spin', beats: 2, at: C[0], yaw0: 0, turn: TAU }
  ];
  const TOTAL = SEGMENTS.reduce((n, g) => n + g.beats, 0);

  function segmentPose(g, b) {
    if (g.type === 'walk') {
      const u = Math.min(1, Math.max(0, b / g.beats));
      const moon = g.dir[2] !== 0;
      const R = glideFoot(b / 2), L = glideFoot(b / 2 + 0.5);
      const sw = s(b * PI);
      return {
        x: g.from[0] + (g.to[0] - g.from[0]) * u, z: g.from[1] + (g.to[1] - g.from[1]) * u,
        yaw: g.yaw, dx: g.dir[0], dz: g.dir[2],
        fR: R, fL: L, hipH: 0.84 - 0.015 * Math.abs(sw),
        lean: moon ? 0.1 : 0.04, headTurn: moon ? 0 : -g.dir[0] * 0.5, headNod: moon ? 0.05 : 0,
        armR: { abd: 0.18, fwd: moon ? 0.25 * sw : 0.1, eAbd: 0, eFwd: 0.5 },
        armL: { abd: 0.18, fwd: moon ? -0.25 * sw : 0.1, eAbd: 0, eFwd: 0.5 },
        slideR: R.slide, slideL: L.slide
      };
    }
    // spin on the toes, then hit a toe stand
    const spinU = smooth(b / 1.0), stand = smooth((b - 1.0) / 0.15);
    const toes = { o: 0, heel: 0.9 + 0.45 * stand, slide: 0 };
    const out = { abd: 1.3 * (1 - stand), fwd: 0.1, eAbd: 0.2, eFwd: 0.3 };
    return {
      x: g.at[0], z: g.at[1], yaw: g.yaw0 + g.turn * spinU, dx: 0, dz: 0,
      fR: toes, fL: toes, hipH: 0.88 - 0.12 * stand,
      lean: -0.05 * stand, headTurn: 0.6 * stand, headNod: -0.1 * stand,
      armR: blendObj(out, { abd: 2.3, fwd: 0.35, eAbd: 0.15, eFwd: 0 }, stand),
      armL: blendObj(out, { abd: 0.12, fwd: 0.25, eAbd: 0, eFwd: 0.9 }, stand),
      slideR: 0, slideL: 0
    };
  }

  function blendObj(a, b, w) {
    if (typeof a === 'number') return a + (b - a) * w;
    const o = {};
    for (const k in a) o[k] = blendObj(a[k], b[k], w);
    return o;
  }
  const wrap = (a) => ((((a + PI) % TAU) + TAU) % TAU) - PI;

  const FADE = 0.3;
  function poseAt(beat) {
    let m = ((beat % TOTAL) + TOTAL) % TOTAL, i = 0;
    while (m >= SEGMENTS[i].beats) { m -= SEGMENTS[i].beats; i++; }
    const g = SEGMENTS[i];
    const cur = segmentPose(g, m);
    if (m > g.beats - FADE) {
      const next = segmentPose(SEGMENTS[(i + 1) % SEGMENTS.length], 0);
      cur.yaw = wrap(cur.yaw); next.yaw = wrap(next.yaw);
      if (next.yaw - cur.yaw > PI) next.yaw -= TAU;
      if (cur.yaw - next.yaw > PI) next.yaw += TAU;
      const w = smooth((m - (g.beats - FADE)) / FADE);
      const p = blendObj(cur, next, w);
      p.slideR = cur.slideR; p.slideL = cur.slideL;
      return p;
    }
    return cur;
  }

  // Skid marks left by the sliding feet
  const trails = { R: [], L: [] };
  const TRAIL_LIFE = 1.4;

  function buildFigure(pose, t) {
    // everything is built in the body frame (facing +z), then turned by yaw and moved to x/z
    const hipY = FLOOR_Y + pose.hipH;
    const pelvis = [0, hipY, 0];
    const hipR = [0.09, hipY, 0], hipL = [-0.09, hipY, 0];
    const FOOT = 0.16, BALL = 0.11, ANKLE = 0.06;

    const footPts = (side, f) => {
      const base = [side * 0.11 + pose.dx * f.o, 0, pose.dz * f.o];
      const fwd = rotY([0, 0, 1], side * 0.08);
      const ball = [base[0], FLOOR_Y, base[2]];
      const hc = c(f.heel), hs = s(f.heel);
      const heel = add(ball, [ -fwd[0] * BALL * hc, BALL * hs, -fwd[2] * BALL * hc ]);
      const toe = add(ball, mul(fwd, FOOT - BALL));
      const ankle = add(heel, [fwd[0] * 0.02, ANKLE, fwd[2] * 0.02]);
      return { heel, ball, toe, ankle };
    };
    const FR = footPts(1, pose.fR), FL = footPts(-1, pose.fL);
    const [kneeR, ankR] = ik(hipR, FR.ankle, THIGH, SHIN, [0, 0, 1]);
    const [kneeL, ankL] = ik(hipL, FL.ankle, THIGH, SHIN, [0, 0, 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) => rotX(rotY(v, pose.headTurn), pose.lean + pose.headNod);
    const headC = add(neck, mul(rotX(up, pose.headNod), NECK + HEAD_R));

    const arm = (side, A) => {
      const elbow = add(shoulder, mul(limbDir(side, A.abd, A.fwd), UPPER_ARM));
      return [shoulder, elbow, add(elbow, mul(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) => { const q = rotY(p, pose.yaw); return [q[0] + pose.x, q[1], q[2] + pose.z]; };
    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, [0.1, 0, 0]), shoulder, add(shoulder, [-0.1, 0, 0])], 2.0);
    line(ar); line(al);
    line([hipR, kneeR, ankR]); line([hipL, kneeL, ankL]);
    for (const F of [FR, FL]) line([F.ankle, F.heel, F.ball, F.toe], 2.0);

    // 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 });
    }
    // fedora: brim, crown and pinched top, tipped slightly forward
    const hat = (v) => headPt(rotX(add(v, [0, HEAD_R * 0.62, 0]), 0.18));
    const ring = (r, y, pinch = 0) => {
      const pts = [];
      for (let k = 0; k <= 24; k++) {
        const a = TAU * k / 24;
        pts.push(hat([r * c(a), y - pinch * Math.pow(c(a), 2), r * 1.1 * s(a)]));
      }
      return pts;
    };
    polys.push({ pts: ring(0.27, 0), color: WHITE, w: 1.5 });
    polys.push({ pts: ring(0.155, 0.01), color: WHITE, w: 1.1, dim: true });
    polys.push({ pts: ring(0.15, 0.14, 0.03), color: WHITE, w: 1.3 });
    for (let k = 0; k < 8; k++) {
      const a = TAU * k / 8;
      polys.push({ pts: [hat([0.155 * c(a), 0.01, 0.17 * s(a)]), hat([0.15 * c(a), 0.14 - 0.03 * Math.pow(c(a), 2), 0.165 * s(a)])], color: WHITE, w: 1.0, dim: true });
    }
    polys.push({ pts: [hat([0, 0.14, 0.165]), hat([0, 0.1, 0]), hat([0, 0.14, -0.165])], color: WHITE, w: 1.0, dim: true });

    // hands
    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.035 * c(a), 0.035 * s(a), 0]))); }
      polys.push({ pts, color: WHITE, w: 1.2 });
    }

    // record skid marks from feet that are sliding flat
    for (const [key, F, sl] of [['R', FR, pose.slideR], ['L', FL, pose.slideL]]) {
      const tr = trails[key];
      if (sl > 0.5) tr.push({ p: place([(F.heel[0] + F.ball[0]) / 2, FLOOR_Y, (F.heel[2] + F.ball[2]) / 2]), t });
      else if (tr.length && tr[tr.length - 1] !== null) tr.push(null);
      while (tr.length && (tr[0] === null || t - tr[0].t > TRAIL_LIFE)) tr.shift();
    }
    return polys;
  }

  function buildTrails(t) {
    const polys = [];
    for (const key of ['R', 'L']) {
      const tr = trails[key];
      for (let i = 1; i < tr.length; i++) {
        const a = tr[i - 1], b = tr[i];
        if (!a || !b || Math.hypot(a.p[0] - b.p[0], a.p[2] - b.p[2]) > 0.1) continue;
        const life = 1 - (t - b.t) / TRAIL_LIFE;
        polys.push({ pts: [a.p, b.p], color: WHITE, w: 2.2, alpha: 0.6 * life * life });
      }
    }
    return polys;
  }

  // Polished floor: fine grid inside a circle
  const FLOOR_R = 1.5;
  function buildFloor() {
    const polys = [];
    const ring = [];
    for (let k = 0; k <= 48; k++) { const a = TAU * k / 48; ring.push([FLOOR_R * c(a), FLOOR_Y, FLOOR_R * s(a)]); }
    polys.push({ pts: ring, color: FLOOR, w: 1.1 });
    for (let v = -1.25; v <= 1.26; v += 0.25) {
      const h = Math.sqrt(FLOOR_R * FLOOR_R - v * v);
      polys.push({ pts: [[-h, FLOOR_Y, v], [h, FLOOR_Y, v]], color: FLOOR, w: 0.7, dim: true });
      polys.push({ pts: [[v, FLOOR_Y, -h], [v, FLOOR_Y, h]], color: FLOOR, w: 0.7, dim: true });
    }
    return polys;
  }

  // ---------- Projection ----------
  // The camera follows the dancer, so he stays centred and the floor glides beneath him
  const CENTER_Y = 0.08;
  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.27;
    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;

  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.45);

    const t = now / 1000;
    let beat = t * (reduced ? BPM / 3 : BPM) / 60;

    if (!reduced && glitch <= 0 && Math.random() < 0.025) {
      glitch = 4 + Math.floor(Math.random() * 10);
      glitchStrength = 0.4 + Math.random() * 0.6;
      beatShift = Math.random() < 0.5 ? -Math.random() * 0.4 : 0; // the dancer skips back, time keeps flowing
    }
    const g = glitch > 0;
    if (g) glitch--; else beatShift = 0;
    beat += beatShift;

    const view = { yaw: orbit, pitch: 0.22 + Math.sin(t * 0.7) * 0.04 };
    const pose = poseAt(beat);
    focusX = pose.x; focusZ = pose.z;
    const polys = buildFloor().concat(buildTrails(t), buildFigure(pose, t));

    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>