← Mike Higgins Home
Custom Code Experiment •

Wolf

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

  // ---------- Wolf ----------
  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;
  const fract = (x) => ((x % 1) + 1) % 1;

  function ik(root, target, a, b, hint) {
    const toT = sub(target, root);
    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(root, add(mul(u, a * cosA), mul(n, a * sinA)));
  }

  // ---------- Timeline (seconds) ----------
  // trot → slow to a stop → raise the head and howl → set off again
  const CYCLE = 12;
  const V = 1.9;                // trotting speed (units/s)
  const STRIDE_T = 0.6;         // seconds per full stride
  const DUTY = 0.55;            // fraction of the stride each paw is on the ground
  function speedAt(tl) {
    if (tl < 1) return V * smooth(tl);
    if (tl < 7) return V;
    if (tl < 8) return V * (1 - smooth(tl - 7));
    return 0;
  }
  function howlAt(tl) {
    if (tl < 8.2) return 0;
    if (tl < 9.2) return smooth(tl - 8.2);
    if (tl < 10.9) return 1;
    return 1 - smooth((tl - 10.9) / 0.8);
  }

  // Trot: diagonal pairs move together (left-fore with right-hind, right-fore with left-hind)
  const LEGS = [
    { key: 'LF', front: true,  side: -1, off: 0 },
    { key: 'RH', front: false, side: 1,  off: 0 },
    { key: 'RF', front: true,  side: 1,  off: 0.5 },
    { key: 'LH', front: false, side: -1, off: 0.5 }
  ];
  // Where a paw is relative to its hip/shoulder: on the ground it slides back exactly as fast
  // as the floor moves (so it stays planted), in the air it swings forward and lifts
  function pawAt(phase, off, speed) {
    const q = fract(phase + off);
    const S = speed * STRIDE_T * DUTY, lift = 0.13 * clamp(speed / V, 0, 1);
    if (q < DUTY) return { x: S / 2 - S * (q / DUTY), y: 0, swing: 0 };
    const u = (q - DUTY) / (1 - DUTY);
    return { x: -S / 2 + S * smooth(u), y: lift * s(PI * u), swing: s(PI * u) };
  }

  function buildWolf(st) {
    const Y0 = FLOOR_Y;
    const hw = st.howl, run = clamp(st.speed / V, 0, 1);
    const bob = 0.02 * run * c(4 * PI * st.phase);
    const hipY = Y0 + 0.62 + bob - 0.06 * hw, shY = Y0 + 0.67 + bob + 0.04 * hw;
    const HIP_X = -0.45, SH_X = 0.36;
    const polys = [];
    const line = (pts, w = 1.6, dim = false) => polys.push({ pts, color: WHITE, w, dim });

    // --- body: cross-section rings along a spine, joined by long lines ---
    const XS  = [-0.6, -0.47, -0.3, -0.12, 0.06, 0.24, 0.4, 0.52];
    const RY  = [0.07, 0.13, 0.14, 0.115, 0.13, 0.175, 0.19, 0.15];
    const RZ  = [0.06, 0.12, 0.13, 0.11, 0.12, 0.14, 0.15, 0.12];
    const OFF = [0.02, 0.0, -0.01, 0.015, 0.0, -0.04, -0.06, -0.03];
    const ringPts = XS.map((x, i) => {
      const w = (x - HIP_X) / (SH_X - HIP_X);
      const cy = lerp(hipY, shY, clamp(w, -0.2, 1.3)) + OFF[i];
      const pts = [];
      for (let k = 0; k <= 16; k++) { const a = TAU * k / 16; pts.push([x, cy + RY[i] * c(a), RZ[i] * s(a)]); }
      return pts;
    });
    ringPts.forEach((r, i) => line(r, i === 0 || i === ringPts.length - 1 ? 1.4 : 1.0, i % 2 === 1));
    for (let k = 0; k < 16; k += 2) line(ringPts.map(r => r[k]), k === 0 || k === 8 ? 1.6 : 1.0, !(k === 0 || k === 8));

    // --- neck and head ---
    const neckA = lerp(0.18, 1.0, hw), headA = lerp(-0.3, 1.1, hw) + 0.03 * run * s(TAU * st.phase * 2);
    const neckBase = [0.5, shY + 0.02, 0];
    const neckEnd = add(neckBase, [0.3 * c(neckA), 0.3 * s(neckA), 0]);
    for (let i = 0; i <= 3; i++) {
      const p = add(neckBase, mul(sub(neckEnd, neckBase), i / 3)), r = lerp(0.12, 0.085, i / 3);
      const f = norm(sub(neckEnd, neckBase)), n = [-f[1], f[0], 0];
      const pts = [];
      for (let k = 0; k <= 12; k++) { const a = TAU * k / 12; pts.push(add(p, add(mul(n, r * c(a)), [0, 0, r * 0.85 * s(a)]))); }
      line(pts, 1.0, true);
    }
    line([neckBase, neckEnd], 1.4);
    const hf = [c(headA), s(headA), 0], hn = [-s(headA), c(headA), 0];
    const hp = (u, v, w) => add(neckEnd, add(add(mul(hf, u), mul(hn, v)), [0, 0, w]));
    // skull tapering into the muzzle
    const HU = [-0.02, 0.07, 0.15, 0.23, 0.31], HR = [0.085, 0.095, 0.07, 0.048, 0.034], HV = [0.01, 0.01, -0.01, -0.025, -0.035];
    const headRings = HU.map((u, i) => {
      const pts = [];
      for (let k = 0; k <= 12; k++) { const a = TAU * k / 12; pts.push(hp(u, HV[i] + HR[i] * c(a), HR[i] * 0.9 * s(a))); }
      return pts;
    });
    headRings.forEach((r, i) => line(r, 1.2, i === 1 || i === 3));
    for (const k of [0, 3, 6, 9]) line(headRings.map(r => r[k]), 1.2);
    // lower jaw drops open for the howl
    const jaw = 0.42 * hw;
    const jawPt = (u, w) => { const base = hp(0.08, -0.06, w); const d = u - 0.08; return add(base, add(mul(hf, d * c(jaw)), mul(hn, -d * s(jaw)))); };
    line([jawPt(0.08, 0.05), jawPt(0.3, 0.02), jawPt(0.3, -0.02), jawPt(0.08, -0.05)], 1.3);
    // nose
    line([hp(0.33, -0.01, 0.015), hp(0.345, -0.03, 0), hp(0.33, -0.01, -0.015)], 1.6);
    // ears
    for (const sd of [-1, 1]) line([hp(-0.01, 0.07, sd * 0.03), hp(-0.04, 0.2, sd * 0.07), hp(0.06, 0.08, sd * 0.07)], 1.5);
    // eyes
    for (const sd of [-1, 1]) line([hp(0.14, 0.035, sd * 0.055), hp(0.17, 0.03, sd * 0.05)], 1.8);

    // --- tail: bushy, swinging with the trot, lowered while howling ---
    const tailBase = [-0.6, hipY + 0.03, 0];
    let tp = tailBase, ang = lerp(-2.55, -1.95, hw) + 0.15 * run;
    const tailPts = [tailBase], TR = [0.035, 0.05, 0.062, 0.066, 0.06, 0.045, 0.02];
    for (let i = 1; i < 7; i++) {
      ang -= 0.07;
      const sway = 0.035 * run * s(TAU * st.phase + i * 0.5) * i / 6;
      tp = add(tp, [0.095 * c(ang), 0.095 * s(ang), 0]);
      tailPts.push([tp[0], tp[1], sway]);
    }
    line(tailPts, 1.4);
    tailPts.forEach((p, i) => {
      if (i === 0) return;
      const d = norm(sub(p, tailPts[i - 1])), n = [-d[1], d[0], 0], pts = [];
      for (let k = 0; k <= 10; k++) { const a = TAU * k / 10; pts.push(add(p, add(mul(n, TR[i] * c(a)), [0, 0, TR[i] * s(a)]))); }
      line(pts, 0.9, true);
    });

    // --- legs ---
    for (const L of LEGS) {
      const paw = pawAt(st.phase, L.off, st.speed);
      const rootX = L.front ? SH_X : HIP_X;
      const root = [rootX, (L.front ? shY - 0.08 : hipY - 0.03), L.side * 0.1];
      const pawP = [rootX + 0.03 + paw.x, Y0 + 0.025 + paw.y, L.side * 0.11];
      let hip2, mid;
      if (L.front) {
        // shoulder → elbow (bends back) → wrist → paw; the wrist folds back in the swing
        const wr = add(pawP, [0.02 - 0.08 * paw.swing, 0.09, 0]);
        mid = ik(root, wr, 0.26, 0.28, [-1, 0, 0]);
        hip2 = wr;
      } else {
        // hip → knee (bends forward) → hock → paw; the hock sits behind and above the paw
        const hock = add(pawP, [-0.06 - 0.05 * paw.swing, 0.17 - 0.02 * paw.swing, 0]);
        mid = ik(root, hock, 0.3, 0.31, [1, 0, 0]);
        hip2 = hock;
      }
      line([root, mid, hip2, pawP], 1.8);
      // paw
      const pts = [];
      for (let k = 0; k <= 10; k++) { const a = TAU * k / 10; pts.push(add(pawP, [0.04 + 0.045 * c(a), -0.015, 0.035 * s(a)])); }
      line(pts, 1.2);
    }
    return polys;
  }

  // ---------- Floor: scrolls under the wolf, with fading paw prints ----------
  const FLOOR_R = 1.25, GRID = 0.3;
  function buildFloor(travelled, prints, t) {
    const polys = [];
    const ring = [];
    for (let k = 0; k <= 56; k++) { const a = TAU * k / 56; ring.push([FLOOR_R * c(a), FLOOR_Y, FLOOR_R * s(a)]); }
    polys.push({ pts: ring, color: FLOOR, w: 1.0 });
    const shift = ((travelled % GRID) + GRID) % GRID;
    for (let x = -FLOOR_R - shift + GRID; x < FLOOR_R; x += GRID) {
      const h = Math.sqrt(Math.max(0, FLOOR_R * FLOOR_R - x * x));
      polys.push({ pts: [[x, FLOOR_Y, -h], [x, FLOOR_Y, h]], color: FLOOR, w: 0.7, dim: true });
    }
    for (let z = -1.2; z <= 1.21; z += GRID) {
      const h = Math.sqrt(Math.max(0, FLOOR_R * FLOOR_R - z * z));
      polys.push({ pts: [[-h, FLOOR_Y, z], [h, FLOOR_Y, z]], color: FLOOR, w: 0.7, dim: true });
    }
    for (const p of prints) {
      const x = p.fx - travelled, age = t - p.t;
      if (Math.hypot(x, p.z) > FLOOR_R - 0.05) continue;
      const a = Math.max(0, 1 - age / 2.5);
      const pad = [];
      for (let k = 0; k <= 10; k++) { const q = TAU * k / 10; pad.push([x + 0.03 * c(q), FLOOR_Y + 0.002, p.z + 0.028 * s(q)]); }
      polys.push({ pts: pad, color: WHITE, w: 1.0, alpha: 0.7 * a });
      for (const [dx, dz] of [[0.055, -0.03], [0.07, -0.01], [0.07, 0.01], [0.055, 0.03]]) {
        polys.push({ pts: [[x + dx, FLOOR_Y + 0.002, p.z + dz], [x + dx + 0.012, FLOOR_Y + 0.002, p.z + dz]], color: WHITE, w: 1.6, alpha: 0.7 * a });
      }
    }
    return polys;
  }

  // Wireframe moon, drawn flat in the sky while the wolf howls
  function drawMoon(ctx2, a, ox, oy) {
    if (a <= 0.01) return;
    const cx = W * 0.3 + ox, cy = H * 0.27 + oy, r = W * 0.085;
    ctx2.save();
    ctx2.globalAlpha = a;
    ctx2.strokeStyle = WHITE;
    ctx2.lineWidth = 1.4 * dpr;
    ctx2.beginPath(); ctx2.arc(cx, cy, r, 0, TAU); ctx2.stroke();
    ctx2.globalAlpha = a * 0.55;
    ctx2.lineWidth = 1 * dpr;
    for (const k of [-0.6, -0.2, 0.2, 0.6]) {           // latitude lines
      const yy = cy + k * r, hw = Math.sqrt(1 - k * k) * r;
      ctx2.beginPath(); ctx2.ellipse(cx, yy, hw, hw * 0.18, 0, 0, TAU); ctx2.stroke();
    }
    for (const [dx, dy, rr] of [[-0.3, -0.25, 0.18], [0.3, 0.2, 0.13], [0.05, 0.45, 0.1]]) {
      ctx2.beginPath(); ctx2.ellipse(cx + dx * r, cy + dy * r, rr * r, rr * r * 0.8, 0, 0, TAU); ctx2.stroke();
    }
    ctx2.restore();
  }

  // ---------- Projection ----------
  // The camera follows the dancer, so he stays centred and the floor glides beneath him
  const CENTER_Y = -0.42;
  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.28;
    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 travelled = 0, gaitPhase = 0, prevTl = null;
  const prints = [];
  const lastQ = {};

  function frame(now) {
    if (W === 0 || H === 0) {
      resize();
      if (W === 0 || H === 0) { last = now; requestAnimationFrame(frame); return; }
    }
    const rdt = Math.min(50, now - last) / 1000; last = now;
    const dt = reduced ? rdt / 3 : rdt;
    orbit += rdt * (reduced ? 0.08 : 0.3);
    const t = now / 1000;
    const tl = ((t * (reduced ? 1 / 3 : 1)) % CYCLE + CYCLE) % CYCLE;

    const speed = speedAt(tl), howl = howlAt(tl);
    gaitPhase += dt / STRIDE_T;
    travelled += speed * dt;

    // paw prints: stamp one each time a paw touches down
    for (const L of LEGS) {
      const q = fract(gaitPhase + L.off);
      if (lastQ[L.key] !== undefined && q < lastQ[L.key] && speed > 0.4) {
        const p = pawAt(gaitPhase, L.off, speed);
        const rootX = L.front ? 0.36 : -0.45;
        prints.push({ fx: rootX + 0.03 + p.x + travelled, z: L.side * 0.11, t });
      }
      lastQ[L.key] = q;
    }
    while (prints.length && t - prints[0].t > 2.5) prints.shift();

    // the howl always glitches at its peak; otherwise the usual random glitches
    if (!reduced && prevTl !== null && prevTl < 9.5 && tl >= 9.5) { glitch = 14; glitchStrength = 1; }
    else if (!reduced && glitch <= 0 && Math.random() < 0.02) { glitch = 4 + Math.floor(Math.random() * 10); glitchStrength = 0.4 + Math.random() * 0.6; }
    prevTl = tl;
    const g = glitch > 0;
    if (g) glitch--;
    const shX = 0, shY = 0;

    const view = { yaw: orbit, pitch: 0.22 + Math.sin(t * 0.7) * 0.04 };
    focusX = 0; focusZ = 0;
    const polys = buildFloor(travelled, prints, t).concat(buildWolf({ phase: gaitPhase, speed, howl }));

    bctx.clearRect(0, 0, W, H);
    bctx.lineJoin = 'round'; bctx.lineCap = 'round';
    bctx.globalCompositeOperation = 'lighter';
    drawMoon(bctx, howl, 0, 0);
    if (g) {
      const off = (4 + Math.random() * 8) * dpr * glitchStrength;
      strokeAll(polys, view, GHOST_A, 0.7, -off + shX, shY, 0.03 * glitchStrength);
      strokeAll(polys, view, GHOST_B, 0.6, off + shX, shY + (Math.random()-.5)*2*dpr, 0.03 * glitchStrength);
    }
    strokeAll(polys, view, null, g ? 0.85 : 1, shX, shY, 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>