• 01 - Critter Models 02
  • 02 - VantaJS Shaders
  • 03 - App Bundle 02
  • 04 - Cathedrals
  • 05 - App Bundle 01
  • 06 - Retroscopic
  • 07 - Games
  • 08 - Voronoi
  • 09 - Terrain
  • 10 - Critter Models 01
  • 11 - Chromatophores
  • 12 - Conformal Maps
  • 13 - Echo Logix
  • 14 - Music
  • 15 - Colour Theory
MIKE'S NOTEBOOK
Notes
Number:
Project:
01Critter Models 02
└ Locust└ Butterfly└ Cockroach└ Spider└ Scorpion└ Dragon└ Brazilian Treehopper└ Tardigrade└ Diatoms└ Bumblebee└ Wasp└ Hammerhead Worm└ Giraffe Weevil└ Trilobite Beetle
02VantaJS Shaders
└ Clouds└ Waves└ Cells└ Rings└ Halo└ Globe└ Net└ Dots└ Birds└ Trunk└ Topology└ About VantaJS
03App Bundle 02
└ Gutindex└ Insect Builder└ OpenLayers
04Cathedrals
└ Lattice└ Lost└ Lucent└ Lumen└ Glass└ Disco└ Hyperion└ Latent
05App Bundle 01
└ Robohash└ PrismJS└ Horoscope└ Binary Jazz└ Morse Code└ URL Extractor└ Entity Escaper└ Phonetic Alphabet└ Toki Pona└ Semaphore└ Cut Up Machine└ Text Mixer└ Currency Freaks└ Host IO└ CrUX API└ Logo Dev└ Web Speech API└ GEO DB
06Retroscopic
└ Galaxy└ Ocean└ Slime└ 24-Cell└ Unicorn└ Polyp└ Oneiric 1└ Oneiric 2└ Node└ Torus Knot└ GLSL Grid└ Liquid Crystal└ Silky└ Butterfly└ Ocean 2└ Icosahedron└ Luma└ Swarm└ Broken Solid└ Hypersphere└ Constraints└ Psycho Killer
01 / 03

HYPERSPHERE - RETROSCOPIC CRT SHADER SOURCE CODE

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no, viewport-fit=cover" />
  <title>Hypersphere</title>
  <style>
    :root { color-scheme: dark; }
    html, body {
      margin: 0;
      height: 100%;
      background: #000;
      overflow: hidden;
    }
    #stage {
      position: fixed;
      inset: 0;
      filter: grayscale(1) sepia(1) hue-rotate(80deg) saturate(5) brightness(1.15) contrast(1.2);
      z-index: 2;
    }
    canvas {
      display: block;
      width: 100vw;
      height: 100vh;
      touch-action: none;
      image-rendering: optimizeQuality;
    }
    /* Retroscopic Overlay */
    #scope {
      position: fixed;
      inset: 0;
      z-index: 4;
      pointer-events: none;
      display: block;
    }
    #scope>div {
      position: absolute;
      inset: 0;
    }
    #scope .glow {
      background: radial-gradient(circle at 50% 46%, rgba(30, 255, 130, .08), rgba(0, 0, 0, 0) 62%);
      animation: nvflick 3.6s steps(24) infinite;
    }
    #scope .scan {
      background: repeating-linear-gradient(to bottom, rgba(0, 0, 0, 0) 0 2px, rgba(0, 25, 8, .45) 2px 3px);
      animation: nvscan 7s linear infinite;
    }
    #scope .vig {
      box-shadow: inset 0 0 200px 50px rgba(0, 0, 0, .8);
    }
    #scope .mask {
      background: radial-gradient(circle at 50% 50%, rgba(0, 0, 0, 0) 54%, rgba(0, 0, 0, .5) 72%, rgba(0, 0, 0, .94) 100%);
    }
    @keyframes nvscan {
      from { background-position: 0 0; }
      to { background-position: 0 240px; }
    }
    @keyframes nvflick {
      0%, 100% { opacity: .55; }
      40% { opacity: .9; }
      62% { opacity: .4; }
      80% { opacity: .75; }
    }
  </style>
</head>
<body>
  <div id="scope">
    <div class="glow"></div>
    <div class="scan"></div>
    <div class="vig"></div>
    <div class="mask"></div>
  </div>
  <div id="stage">
    <canvas id="canvas"></canvas>
  </div>
  <script>
    (() => {
      const canvas = document.getElementById('canvas');
      const ctx = canvas.getContext('2d', { alpha: false });

      const setSize = () => {
        const dpr = Math.max(1, Math.min(3, window.devicePixelRatio || 1));
        const w = Math.floor(canvas.clientWidth || window.innerWidth);
        const h = Math.floor(canvas.clientHeight || window.innerHeight);
        canvas.width = Math.floor(w * dpr);
        canvas.height = Math.floor(h * dpr);
        ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
        cx = w / 2;
        cy = h / 2;
        baseScale = Math.min(w, h) * 0.2375;
      };

      let cx = 0, cy = 0, baseScale = 300, dynamicScale = 1;
      const MAX_POINTS_DESKTOP = 1800;
      const MAX_POINTS_MOBILE  = 900;
      const TRAIL_FADE = 0.045;
      const LINE_ALPHA = 0.65;
      const REDUCED_MOTION = matchMedia('(prefers-reduced-motion: reduce)').matches;

      const isMobile = /Mobi|Android/i.test(navigator.userAgent) ||
                       Math.min(window.innerWidth, window.innerHeight) < 768;
      const COUNT = REDUCED_MOTION ? 250 : (isMobile ? MAX_POINTS_MOBILE : MAX_POINTS_DESKTOP);

      const STAR_COUNT = (Math.min(window.innerWidth, window.innerHeight) < 768 ? 140 : 280);
      const stars = new Array(STAR_COUNT).fill(0).map(() => ({
        x: Math.random(),
        y: Math.random(),
        r: Math.random() * 1.2 + 0.3,
        p: Math.random() * Math.PI * 2
      }));
      function drawStars(time){
        const w = cx * 2, h = cy * 2;
        ctx.save();
        ctx.globalCompositeOperation = 'source-over';
        for (let i=0; i<STAR_COUNT; i++){
          const s = stars[i];
          const tw = 0.5 + 0.5 * Math.sin(time*0.001 + s.p);
          ctx.globalAlpha = 0.15 + 0.45 * tw;
          ctx.beginPath();
          ctx.arc(s.x * w, s.y * h, s.r, 0, Math.PI*2);
          ctx.fillStyle = `hsla(${(200 + 120*Math.sin(s.p))%360}, 100%, 70%, 1)`;
          ctx.fill();
        }
        ctx.restore();
      }

      const gauss = (() => {
        let spare, hasSpare = false;
        return () => {
          if (hasSpare) { hasSpare = false; return spare; }
          let u=0, v=0, s=0;
          while (s===0 || s>=1) { u = Math.random()*2-1; v = Math.random()*2-1; s = u*u + v*v; }
          const mul = Math.sqrt(-2*Math.log(s)/s);
          spare = v * mul;
          hasSpare = true;
          return u * mul;
        };
      })();
      function randS3() {
        let x = gauss(), y = gauss(), z = gauss(), w = gauss();
        const m = Math.hypot(x,y,z,w) || 1;
        return [x/m, y/m, z/m, w/m];
      }

      const nodes = new Array(COUNT);
      const prev  = new Array(COUNT);
      const hues  = new Array(COUNT);
      const curr  = new Array(COUNT);
      const buddies = new Uint32Array(COUNT);
      const isComet = new Uint8Array(COUNT);
      const cometTrails = new Array(COUNT);

      for (let i=0; i<COUNT; i++) {
        const v = randS3();
        nodes[i] = v;
        prev[i]  = [0, 0];
        const h1 = Math.atan2(v[1], v[0]);
        const h2 = Math.atan2(v[3], v[2]);
        let hue = ((h1 + h2) / (Math.PI*2));
        hue = (hue - Math.floor(hue)) * 360;
        hues[i] = hue;
        buddies[i] = (i * 73 + 97) % COUNT;
      }

      const COMET_COUNT = Math.max(12, Math.round(COUNT * 0.06));
      for (let k=0; k<COMET_COUNT; k++){
        const idx = Math.floor(Math.random()*COUNT);
        if (isComet[idx]) { k--; continue; }
        isComet[idx] = 1;
        cometTrails[idx] = [];
      }

      let t0 = performance.now();
      let cxTilt = 0, cyTilt = 0;
      window.addEventListener('pointermove', (e) => {
        const w = canvas.clientWidth || window.innerWidth;
        const h = canvas.clientHeight || window.innerHeight;
        const nx = (e.clientX / w) * 2 - 1;
        const ny = (e.clientY / h) * 2 - 1;
        cxTilt = nx * 0.35;
        cyTilt = ny * 0.35;
      }, { passive: true });

      window.addEventListener('resize', setSize, { passive: true });
      setSize();

      ctx.globalCompositeOperation = 'lighter';
      ctx.lineCap = 'round';
      ctx.lineJoin = 'round';

      function fadeFrame() {
        ctx.save();
        ctx.globalCompositeOperation = 'source-over';
        ctx.fillStyle = `rgba(0,0,0,${TRAIL_FADE})`;
        ctx.fillRect(0,0,canvas.width,canvas.height);
        ctx.restore();
      }

      function rot4(v, ax, ay, az, aw, au, avv) {
        let x=v[0], y=v[1], z=v[2], w=v[3];
        let c, s, x1,y1,z1,w1;

        c = Math.cos(ax); s = Math.sin(ax); x1 = c*x - s*y; y1 = s*x + c*y; x = x1; y = y1;
        c = Math.cos(ay); s = Math.sin(ay); z1 = c*z - s*w; w1 = s*z + c*w; z = z1; w = w1;
        c = Math.cos(az); s = Math.sin(az); x1 = c*x - s*z; z1 = s*x + c*z; x = x1; z = z1;
        c = Math.cos(aw); s = Math.sin(aw); y1 = c*y - s*w; w1 = s*y + c*w; y = y1; w = w1;
        c = Math.cos(au); s = Math.sin(au); x1 = c*x - s*w; w1 = s*x + c*w; x = x1; w = w1;
        c = Math.cos(avv); s = Math.sin(avv); y1 = c*y - s*z; z1 = s*y + c*z; y = y1; z = z1;

        return [x,y,z,w];
      }

      function projectTo2D(v4) {
        let x=v4[0], y=v4[1], z=v4[2], w=v4[3];

        const wCam = 1.6;
        const f4   = 3.8;
        const denom4 = (wCam - w);
        const s4 = f4 / (denom4 <= 0.05 ? 0.05 : denom4);
        let x3 = x * s4, y3 = y * s4, z3 = z * s4;

        const zCam = 3.2;
        const f3   = baseScale * dynamicScale;
        const denom3 = (zCam - z3);
        const s3 = f3 / (denom3 <= 0.05 ? 0.05 : denom3);

        const sx = cx + x3 * s3;
        const sy = cy + y3 * s3;
        const scale = s3;
        return [sx, sy, scale];
      }

      function frame(now) {
        const dt = Math.max(0.001, Math.min(0.033, (now - t0) / 1000));
        t0 = now;

        fadeFrame();
        drawStars(now);

        const T = now * 0.001;
        dynamicScale = 1.0 + 0.12 * Math.sin(T*0.9) + 0.05 * Math.sin(T*0.37 + 1.4);

        const ax = 0.6*T + 0.35*Math.sin(T*0.9) + cxTilt*0.6;
        const ay = 0.8*T + 0.25*Math.cos(T*1.2) + cyTilt*0.5;
        const az = 0.5*T + 0.30*Math.sin(T*0.7);
        const aw = 0.7*T + 0.20*Math.cos(T*0.6 + 1.0);
        const au = 0.9*T + 0.15*Math.sin(T*0.5 + 0.7);
        const avv= 1.1*T + 0.10*Math.cos(T*0.4 + 1.4);

        ctx.save();
        ctx.lineWidth = Math.max(1, Math.min(3, baseScale / 220));

        for (let i=0; i<COUNT; i++) {
          const v = nodes[i];
          const rot = rot4(v, ax, ay, az, aw, au, avv);
          const [sx, sy, sc] = projectTo2D(rot);

          const p = prev[i];
          const px = p[0] || sx, py = p[1] || sy;

          const hue = hues[i];
          const depthAlpha = LINE_ALPHA * (0.6 + 0.4 * Math.min(1, sc / (baseScale*0.018)));
          ctx.strokeStyle = `hsla(${hue}, 100%, 60%, ${depthAlpha})`;

          ctx.lineWidth = Math.max(1.0, Math.min(7, sc * 0.016));
          ctx.beginPath();
          ctx.moveTo(px, py);
          ctx.lineTo(sx, sy);
          ctx.stroke();

          p[0] = sx; p[1] = sy;
          curr[i] = [sx, sy, sc];

          if (isComet[i]) {
            const trail = cometTrails[i];
            trail.push([sx, sy]);
            if (trail.length > 14) trail.shift();

            ctx.save();
            ctx.globalAlpha = 0.8;
            ctx.lineWidth = Math.max(1.2, Math.min(6, sc * 0.012));
            ctx.strokeStyle = `hsla(${hue}, 100%, 70%, ${Math.min(0.95, depthAlpha + 0.2)})`;
            ctx.beginPath();
            for (let k=0; k<trail.length; k++) {
              const [tx, ty] = trail[k];
              if (k === 0) ctx.moveTo(tx, ty); else ctx.lineTo(tx, ty);
            }
            ctx.stroke();
            ctx.restore();

            ctx.save();
            ctx.globalAlpha = 0.9;
            ctx.beginPath();
            ctx.arc(sx, sy, Math.max(1.5, Math.min(4.5, sc*0.009)), 0, Math.PI*2);
            ctx.fillStyle = `hsla(${hue}, 100%, 65%, 0.85)`;
            ctx.fill();
            ctx.restore();
          }
        }

        ctx.globalAlpha = 0.18;
        ctx.lineWidth = 1;
        for (let i=0; i<COUNT; i+=3) {
          const j = buddies[i];
          const a = curr[i];
          const b = curr[j];
          if (!a || !b) continue;
          const hueMix = ((hues[i] + hues[j]) * 0.5) % 360;
          ctx.strokeStyle = `hsla(${hueMix}, 100%, 70%, 0.18)`;
          ctx.beginPath();
          ctx.moveTo(a[0], a[1]);
          ctx.lineTo(b[0], b[1]);
          ctx.stroke();
        }

        ctx.restore();
        requestAnimationFrame(frame);
      }

      ctx.fillStyle = '#000';
      ctx.fillRect(0,0,canvas.width,canvas.height);
      requestAnimationFrame(frame);
    })();
  </script>
</body>
</html>
MikeHiggins.me.uk LinkedIn GitHub Bluesky Contra Upwork
© 2026 Mike Higgins. All rights reserved.