• 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

SILKY - 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>Being Silky</title>
  <style>
    *, *::before, *::after {
      margin: 0; padding: 0;
      box-sizing: border-box;
      user-select: none;
      -webkit-tap-highlight-color: transparent;
    }

    html, body {
      width: 100%; height: 100%;
      overflow: hidden;
      background: #000;
      touch-action: none;
    }

    #stage {
      position: fixed;
      inset: 0;
      display: flex;
      align-items: center;
      justify-content: center;
      pointer-events: none;
      z-index: 2;
    }

    #grid {
      position: relative;
      transform: scale(1.5);
    }

    .p {
      position: absolute;
      border-radius: 50%;
      will-change: transform, opacity;
      mix-blend-mode: plus-lighter;
    }

    @keyframes pulse-glow {
      from { filter: grayscale(1) sepia(1) hue-rotate(80deg) saturate(5) contrast(1.2) brightness(1)    drop-shadow(0 0 3px rgba(120,180,255,.3)); }
      to   { filter: grayscale(1) sepia(1) hue-rotate(80deg) saturate(5) contrast(1.2) brightness(1.15) drop-shadow(0 0 10px rgba(160,210,255,.7)); }
    }

    #stage { animation: pulse-glow 4s infinite alternate ease-in-out; }

    /* 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">
  <div id="grid"></div>
</div>
<script>
(function () {
  'use strict';

  var ROWS = 13, COLS = 13;
  var CELL = 14, GAP = 8, STEP = CELL + GAP;
  var GW = COLS * STEP, GH = ROWS * STEP;
  var COUNT = ROWS * COLS;

  var vw = window.innerWidth  * 0.5;
  var vh = window.innerHeight * 0.5;

  var grid = document.getElementById('grid');
  grid.style.width  = GW + 'px';
  grid.style.height = GH + 'px';

  var particles = [];
  var baseScale = [], baseOpacity = [];
  var cR = (ROWS - 1) / 2, cC = (COLS - 1) / 2;
  var maxD = Math.sqrt(cR * cR + cC * cC);

  for (var i = 0; i < COUNT; i++) {
    var row = Math.floor(i / COLS);
    var col = i % COLS;

    var dr = row - cR, dc = col - cC;
    var d  = Math.sqrt(dr * dr + dc * dc) / maxD;

    var sc  = 2 + (1 - d) * 3;
    var op  = 0.12 + (1 - d) * 0.88;
    var lum = Math.round(18 + (1 - d) * 77);
    var gr  = (0.8 + (1 - d) * 7.2).toFixed(1);

    baseScale[i]   = sc;
    baseOpacity[i] = op;

    var el = document.createElement('div');
    el.className = 'p';
    el.style.width      = CELL + 'px';
    el.style.height     = CELL + 'px';
    el.style.left       = (col * STEP) + 'px';
    el.style.top        = (row * STEP) + 'px';
    el.style.background = 'hsl(210,85%,' + lum + '%)';
    el.style.boxShadow  = '0 0 ' + gr + 'px rgba(100,170,255,.85)';
    el.style.zIndex     = Math.round(COUNT * (1 - d));
    el.style.opacity    = op;
    el.style.transform  = 'translate(-50%,-50%) translate(0px,0px) scale(' + sc + ')';
    /* offset left/top by half cell so transform-origin works from true centre */
    el.style.left       = (col * STEP + CELL * 0.5) + 'px';
    el.style.top        = (row * STEP + CELL * 0.5) + 'px';

    grid.appendChild(el);
    particles.push({ el: el, d: d, tx: 0, ty: 0, cx: 0, cy: 0 });
  }

  var curX = 0, curY = 0;

  function easeInOutExpo(t) {
    if (t === 0) return 0;
    if (t === 1) return 1;
    t *= 2;
    return t < 1
      ? 0.5 * Math.pow(2, 10 * (t - 1))
      : 0.5 * (2 - Math.pow(2, -10 * (t - 1)));
  }
  function easeInOutQuad(t) {
    return t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2;
  }
  function lerp(a, b, t) { return a + (b - a) * t; }

  var FOLLOW_MIN = 0.04;   /* centre particles — fastest  */
  var FOLLOW_MAX = 0.012;  /* edge particles  — slowest   */

  function followSpeed(d) {
    return FOLLOW_MIN + (FOLLOW_MAX - FOLLOW_MIN) * d;
  }

  function pulse() {
    particles.forEach(function (p, i) {
      var delay = Math.round(p.d * 400);
      var sc    = baseScale[i];
      var op    = baseOpacity[i];
      var el    = p.el;

      setTimeout(function () {
        /* burst outward */
        el.style.transition = 'opacity 0.15s ease-out, filter 0.15s ease-out';
        el.style.opacity    = 1;
        p.pulseScale        = sc * 2.5;

        setTimeout(function () {
          /* settle back */
          el.style.transition = 'opacity 0.6s ease-in-out, filter 0.6s ease-in-out';
          el.style.opacity    = op;
          p.pulseScale        = null;
        }, 160);
      }, delay);
    });
  }

  var lastPulseCycle = -1;
  var autoStart = performance.now();
  var autoActive = true;

  function autoX(e) {
    var dur = 6000;
    var t   = (e % dur) / dur;
    var dir = (Math.floor(e / dur) % 2 === 0) ? 1 : -1;
    var pos = (dir === 1) ? easeInOutExpo(t) : 1 - easeInOutExpo(t);
    return ((pos * 2 - 1) * vw * 0.4 + Math.sin(e * 0.0007) * vw * 0.2) / 1.5;
  }
  function autoY(e) {
    var dur = 2000;
    var t   = (e % dur) / dur;
    var dir = (Math.floor(e / dur) % 2 === 0) ? 1 : -1;
    var pos = (dir === 1) ? easeInOutQuad(t) : 1 - easeInOutQuad(t);
    return ((pos * 2 - 1) * vh * 0.4 + Math.cos(e * 0.00012) * vh * 0.2) / 1.5;
  }

  function tick(now) {
    if (autoActive) {
      var elapsed = now - autoStart;
      curX = autoX(elapsed);
      curY = autoY(elapsed);

      var cycle = Math.floor(elapsed / 6000);
      if (cycle > lastPulseCycle) { lastPulseCycle = cycle; pulse(); }
    }

    for (var i = 0; i < COUNT; i++) {
      var p  = particles[i];
      var sp = followSpeed(p.d);
      p.cx   = lerp(p.cx, curX, sp);
      p.cy   = lerp(p.cy, curY, sp);

      var sc = (p.pulseScale !== undefined && p.pulseScale !== null)
               ? p.pulseScale : baseScale[i];

      p.el.style.transform =
        'translate(-50%,-50%) translate(' + p.cx.toFixed(2) + 'px,' +
        p.cy.toFixed(2) + 'px) scale(' + sc + ')';
    }

    requestAnimationFrame(tick);
  }

  requestAnimationFrame(tick);
  setTimeout(pulse, 200);

  var idleTimer = null;

  function onMove(e) {
    var ev = (e.touches && e.touches.length) ? e.touches[0] : e;
    if (!ev) return;
    autoActive = false;
    clearTimeout(idleTimer);
    curX = (ev.pageX - vw) / 1.5;
    curY = (ev.pageY - vh) / 1.5;
    idleTimer = setTimeout(function () {
      autoActive = true;
      autoStart  = performance.now() - 200; /* slight offset to avoid jump */
      lastPulseCycle = -1;
    }, 1500);
  }

  document.addEventListener('mousemove',  onMove);
  document.addEventListener('touchmove',  onMove, { passive: true });
  document.addEventListener('touchstart', onMove, { passive: true });

  window.addEventListener('resize', function () {
    vw = window.innerWidth  * 0.5;
    vh = window.innerHeight * 0.5;
  });

}());
</script>
</body>
</html>
MikeHiggins.me.uk LinkedIn GitHub Bluesky Contra Upwork
© 2026 Mike Higgins. All rights reserved.