← Mike Higgins Home
Custom Code Experiment •

Insect Loader

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 bee loader</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Chakra+Petch:wght@500&display=swap" rel="stylesheet">
<style>
  :root {
    box-sizing: border-box;
    padding-top: env(safe-area-inset-top, 0px);
    padding-bottom: env(safe-area-inset-bottom, 0px);
    --page: #EDEDED;
    --ink: #111111;
    --ring-track: rgba(0, 0, 0, 0.12);
    --pollen: #FFC21A;
    --comb: #0A0A0A;
  }
  @media (prefers-color-scheme: dark) {
    :root:not([data-theme="light"]) {
      --page: #141414;
      --ink: #F2F2F2;
      --ring-track: rgba(255, 255, 255, 0.12);
    }
  }
  :root[data-theme="dark"] {
    --page: #141414;
    --ink: #F2F2F2;
    --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);
    color: var(--ink);
    font-family: "Chakra Petch", "Segoe UI", system-ui, sans-serif;
    display: grid;
    place-items: center;
  }

  .loader {
    display: grid;
    place-items: center;
  }

  .orb {
    position: relative;
    width: min(260px, 70vw);
    aspect-ratio: 1;
  }

  /* Progress ring around the circle */
  .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(--pollen);
    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); }
  }

  /* The circular viewport */
  .window {
    position: absolute;
    inset: 12px;
    border-radius: 50%;
    overflow: hidden;
    background:
      radial-gradient(circle at 50% 45%, #262626 0%, var(--comb) 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%; }
  /* scanlines + honeycomb texture */
  .window::before {
    content: "";
    position: absolute;
    inset: 0;
    opacity: .09;
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='28' height='48.5' viewBox='0 0 28 48.5'%3E%3Cpath d='M14 0 L28 8.08 V24.25 L14 32.33 L0 24.25 V8.08 Z M14 32.33 V48.5' fill='none' stroke='%23FFFFFF' stroke-width='1'/%3E%3C/svg%3E");
    pointer-events: none;
  }
  .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="loader" role="status" aria-label="Loading">
    <div class="orb">
      <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="bee" aria-hidden="true"></canvas>
      </div>
    </div>
  </div>

<script>
(() => {
  const canvas = document.getElementById('bee');
  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 YELLOW = '#FFFFFF';
  const AMBER  = '#5E5E5E';
  const TAIL   = '#BDBDBD';
  const WING   = '#D6D6D6';
  const MAG = '#8C8C8C', CYAN = '#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();

  // ---------- Geometry (bee points along +x = head) ----------
  const statics = []; // {pts:[[x,y,z]...], color, w}

  function ellipsoid(cx, rx, ry, rz, lat, lon, colorFn, cy = 0) {
    const seg = 28;
    for (let i = 1; i < lat; i++) {
      const t = -Math.PI / 2 + Math.PI * i / lat;
      const x = cx + rx * Math.sin(t), f = Math.cos(t);
      const pts = [];
      for (let k = 0; k <= seg; k++) {
        const a = 2 * Math.PI * k / seg;
        pts.push([x, cy + ry * f * Math.cos(a), rz * f * Math.sin(a)]);
      }
      statics.push({ pts, color: colorFn(x), w: 1.8 });
    }
    for (let j = 0; j < lon; j++) {
      const a = 2 * Math.PI * j / lon;
      const pts = [];
      for (let k = 0; k <= seg; k++) {
        const t = -Math.PI / 2 + Math.PI * k / seg;
        const f = Math.cos(t);
        pts.push([cx + rx * Math.sin(t), cy + ry * f * Math.cos(a), rz * f * Math.sin(a)]);
      }
      statics.push({ pts, color: YELLOW, w: 1.0, dim: true });
    }
  }

  // Abdomen with bumblebee stripes
  ellipsoid(-0.62, 0.78, 0.56, 0.56, 14, 12, x => {
    if (x < -1.12) return TAIL;          // white bumblebee tail
    const band = Math.floor((x + 1.12) / 0.3);
    return band % 2 === 0 ? AMBER : YELLOW;
  }, -0.05);
  // Thorax (fuzzy, full yellow)
  ellipsoid(0.38, 0.40, 0.44, 0.44, 7, 10, () => YELLOW, 0.02);
  // Head (darker)
  ellipsoid(0.98, 0.26, 0.30, 0.30, 6, 8, () => AMBER, 0.0);

  // Eyes
  for (const s of [-1, 1]) {
    const pts = [];
    for (let k = 0; k <= 16; k++) {
      const a = 2 * Math.PI * k / 16;
      pts.push([1.08 + 0.09 * Math.cos(a), 0.06 + 0.13 * Math.sin(a), s * 0.25]);
    }
    statics.push({ pts, color: YELLOW, w: 1.4 });
  }
  // Fuzz: short radial hairs on thorax and abdomen
  (() => {
    let seed = 7; const rnd = () => (seed = (seed * 16807) % 2147483647) / 2147483647;
    const hair = (cx, cy, rx, ry, rz, n, len, color) => {
      for (let i = 0; i < n; i++) {
        const t = (rnd() - .5) * Math.PI * 0.9, a = rnd() * Math.PI * 2;
        const f = Math.cos(t);
        const nx = Math.sin(t), ny = f * Math.cos(a), nz = f * Math.sin(a);
        const p = [cx + rx * nx, cy + ry * ny, rz * nz];
        statics.push({ pts: [p, [p[0] + nx*len, p[1] + ny*len, p[2] + nz*len]], color, w: 0.8, dim: true });
      }
    };
    hair(0.38, 0.02, 0.40, 0.44, 0.44, 90, 0.09, YELLOW);
    hair(-0.62, -0.05, 0.78, 0.56, 0.56, 110, 0.06, YELLOW);
  })();
  // Stinger
  statics.push({ pts: [[-1.38, -0.05, 0], [-1.62, -0.12, 0]], color: YELLOW, w: 1.6 });
  // Antennae
  for (const s of [-1, 1]) {
    statics.push({ pts: [[1.12, 0.22, s*0.10], [1.35, 0.55, s*0.22], [1.55, 0.62, s*0.34]], color: YELLOW, w: 1.3 });
  }
  // Legs (three pairs, jointed)
  for (const s of [-1, 1]) {
    for (let i = 0; i < 3; i++) {
      const x = 0.55 - i * 0.22;
      statics.push({ pts: [
        [x, -0.30, s*0.18],
        [x - 0.05, -0.52, s*0.42],
        [x - 0.22 - i*0.05, -0.78, s*0.46]
      ], color: YELLOW, w: 1.1 });
    }
  }

  // Wings: built each frame so they flap
  function wing(side, fore, flap) {
    const root = [0.40, 0.40, side * 0.10];
    const len = fore ? 0.95 : 0.65, wid = fore ? 0.32 : 0.24;
    const sweep = fore ? 0.35 : 0.75; // angle back from +z
    const outline = [], vein = [];
    const seg = 22;
    for (let k = 0; k <= seg; k++) {
      const a = 2 * Math.PI * k / seg;
      outline.push([len/2 + (len/2) * Math.cos(a), (wid) * Math.sin(a)]);
    }
    vein.push([0,0],[len*0.9, wid*0.1]);
    vein.push(null, [len*0.35, 0], [len*0.6, wid*0.7]);
    vein.push(null, [len*0.35, 0], [len*0.65, -wid*0.6]);
    const map = ([u, v]) => {
      // u along wing span, v across chord; lay in x-z plane then lift by flap
      const dz = u * Math.cos(sweep), dx = -u * Math.sin(sweep) + v * 0.4;
      // rotate the span around the body axis to flap
      const liftY = dz * Math.sin(flap);
      const liftZ = dz * Math.cos(flap);
      return [root[0] + dx, root[1] + liftY, root[2] + side * liftZ];
    };
    const polys = [];
    polys.push({ pts: outline.map(map), color: WING, w: 1.1, wing: true });
    let cur = [];
    for (const p of vein) {
      if (p === null) { if (cur.length) polys.push({ pts: cur, color: WING, w: 0.8, wing: true, dim: true }); cur = []; continue; }
      cur.push(map(p));
    }
    if (cur.length) polys.push({ pts: cur, color: WING, w: 0.8, wing: true, dim: true });
    return polys;
  }

  // ---------- Projection ----------
  function project(p, rotY, tilt, jitter) {
    let [x, y, z] = p;
    if (jitter) { x += (Math.random()-.5)*jitter; y += (Math.random()-.5)*jitter; }
    // rotate Y
    const cy = Math.cos(rotY), sy = Math.sin(rotY);
    let x1 = x * cy + z * sy, z1 = -x * sy + z * cy;
    // tilt X
    const cx = Math.cos(tilt), sx = Math.sin(tilt);
    let y1 = y * cx - z1 * sx, z2 = y * sx + z1 * cx;
    const d = 4.2, persp = d / (d - z2);
    const scale = W * 0.25;
    return [W/2 + x1 * scale * persp, H/2 - (y1 - 0.02) * scale * persp, z2];
  }

  function strokeAll(polys, rotY, tilt, color, alpha, ox, oy, jitter, hover) {
    for (const poly of polys) {
      bctx.beginPath();
      let depthSum = 0;
      poly.pts.forEach((p, i) => {
        const [sx, sy, sz] = project([p[0], p[1] + hover, p[2]], rotY, tilt, 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; // -1.5..1.5
      const fade = 0.5 + 0.5 * Math.min(1, Math.max(0, (depth + 1.2) / 2.4));
      bctx.globalAlpha = alpha * fade * (poly.dim ? 0.55 : 1) * (poly.wing ? 0.8 : 1);
      bctx.strokeStyle = color || poly.color;
      bctx.lineWidth = poly.w * dpr;
      bctx.stroke();
    }
    bctx.globalAlpha = 1;
  }

  // ---------- Glitch state ----------
  let glitch = 0, glitchStrength = 0;
  let rot = 0, last = performance.now();
  let frozenRot = null;

  function frame(now) {
    // Skip drawing while the canvas has no size (hidden, not yet laid out, etc.)
    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;
    const speed = reduced ? 0.5 : 1.6;
    rot += dt * speed;

    if (!reduced) {
      if (glitch <= 0 && Math.random() < 0.025) {
        glitch = 4 + Math.floor(Math.random() * 10);
        glitchStrength = 0.4 + Math.random() * 0.6;
        if (Math.random() < 0.4) frozenRot = rot + (Math.random() - .5) * 1.2;
      }
    }
    const g = glitch > 0;
    if (g) glitch--; else frozenRot = null;

    const t = now / 1000;
    const flap = Math.sin(t * (reduced ? 6 : 38)) * 0.55 + 0.25;
    const hover = Math.sin(t * 2.2) * 0.06;
    const rotY = frozenRot ?? rot;
    const tilt = 0.32 + Math.sin(t * 0.9) * 0.08;

    const polys = statics.concat(
      wing(1, true, flap), wing(1, false, flap * 0.9),
      wing(-1, true, flap), wing(-1, false, flap * 0.9)
    );

    // --- render bee to buffer ---
    bctx.clearRect(0, 0, W, H);
    bctx.lineJoin = 'round'; bctx.lineCap = 'round';
    bctx.globalCompositeOperation = 'lighter';
    const baseJit = reduced ? 0 : 0.006;
    if (g) {
      const off = (4 + Math.random() * 8) * dpr * glitchStrength;
      strokeAll(polys, rotY, tilt, MAG, 0.75, -off, 0, 0.03 * glitchStrength, hover);
      strokeAll(polys, rotY, tilt, CYAN, 0.75, off, (Math.random()-.5)*2*dpr, 0.03 * glitchStrength, hover);
    }
    strokeAll(polys, rotY, tilt, null, g ? 0.85 : 1, 0, 0, g ? 0.02 : baseJit, hover);
    bctx.globalCompositeOperation = 'source-over';

    // --- composite to screen, slicing when glitching ---
    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;
      }
      // occasional block noise
      const blocks = Math.floor(Math.random() * 4);
      for (let i = 0; i < blocks; i++) {
        ctx.fillStyle = [YELLOW, MAG, CYAN][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>