← AI

Neural Network Pulse

A layered network of glowing nodes joined by faint lines. Waves of signals fire from the input layer and race along the strongest connections, shifting colour as they go; each node lights up as signals reach it and fires on to the next layer. Tap or click a node to send a wave of your own.

How to use it

  1. Paste the snippet just before </head>.
  2. Add data-neural-network-pulse to an element with a height. The network is drawn on a canvas that fills it, behind any content.
  3. It glows best on a dark background.

Options

Add any of these attributes to the same element.

data-layersHow many nodes in each layer, comma-separated. Default 4,6,6,3.
data-colorColour of the signals leaving the input layer. Default #a78bfa.
data-color2Colour they shift to by the output layer. Default #22d3ee.
data-rateTime between waves. Default 1600 ms.
data-speedHow fast signals travel. Default 1.
data-click="false"Don’t fire a wave on tap or click.

The canvas stops drawing when it’s scrolled out of view. With reduced motion turned on it shows a still frame with one path lit.

1 · Snippet
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
  (function () {
    // Neural Network Pulse: a layered network of nodes joined by faint lines.
    // Waves of signals fire from the input layer and race along the strongest
    // connections; each node lights up as signals reach it and fires on to the
    // next layer. Tap or click to fire a wave. Drawn on a canvas that fills the
    // element, and paused when off screen.
    // Usage: <div data-neural-network-pulse style="height:300px;background:#0b0718"></div>
    // Optional: data-layers (nodes in each layer, comma-separated, default
    // "4,6,6,3"), data-color (signal colour, default #a78bfa), data-color2
    // (colour signals shift to by the output layer, default #22d3ee),
    // data-rate (ms between waves, default 1600), data-speed (default 1),
    // data-click="false" (don't fire a wave on tap).
    var DEFAULTS = { layers: '4,6,6,3', color: '#a78bfa', color2: '#22d3ee', rate: 1600, speed: 1 };

    var css =
      '[data-neural-network-pulse]{position:relative;overflow:hidden}' +
      '.nnp-canvas{position:absolute;inset:0;width:100%;height:100%;display:block}';
    var style = document.createElement('style');
    style.textContent = css;
    document.head.appendChild(style);

    function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }

    function rgb(hex) {
      var h = hex.replace('#', '');
      if (h.length === 3) h = h.replace(/./g, '$&$&');
      var n = parseInt(h, 16);
      return [n >> 16 & 255, n >> 8 & 255, n & 255];
    }
    function mix(a, b, t) { return [0, 1, 2].map(function (i) { return Math.round(a[i] + (b[i] - a[i]) * t); }); }
    function css3(c, a) { return 'rgba(' + c[0] + ',' + c[1] + ',' + c[2] + ',' + a + ')'; }

    function build(el) {
      var d = el.dataset;
      var canvas = document.createElement('canvas');
      canvas.className = 'nnp-canvas';
      canvas.setAttribute('aria-hidden', 'true');
      el.insertBefore(canvas, el.firstChild);
      var ctx = canvas.getContext('2d');
      var sizes = (d.layers || DEFAULTS.layers).split(',').map(function (n) { return Math.max(1, Math.min(16, parseInt(n, 10) || 1)); });
      var c1 = rgb(d.color || DEFAULTS.color), c2 = rgb(d.color2 || DEFAULTS.color2);
      var rate = +d.rate || DEFAULTS.rate;
      var speed = +d.speed || DEFAULTS.speed;
      var w = 0, h = 0, dpr = 1, r = 6;
      var layers = [], edges = [], pulses = [], wave = 0, nextWave = 0, running = false, raf = 0;

      // Fixed random weights, so the same paths are favoured wave after wave.
      sizes.forEach(function (n, li) {
        var layer = [];
        for (var i = 0; i < n; i++) layer.push({ li: li, x: 0, y: 0, a: 0, fired: -1, out: [] });
        layers.push(layer);
        if (li === 0) return;
        layers[li - 1].forEach(function (from) {
          layer.forEach(function (to) {
            var e = { from: from, to: to, w: Math.pow(Math.random(), 1.6), a: 0 };
            edges.push(e);
            from.out.push(e);
          });
        });
      });
      // Every node gets at least one strong connection onwards.
      layers.forEach(function (layer) {
        layer.forEach(function (n) {
          if (!n.out.length) return;
          var best = n.out[Math.floor(Math.random() * n.out.length)];
          best.w = Math.max(best.w, 0.85);
        });
      });

      function layout() {
        w = el.clientWidth; h = el.clientHeight;
        dpr = Math.min(2, window.devicePixelRatio || 1);
        canvas.width = Math.round(w * dpr); canvas.height = Math.round(h * dpr);
        ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
        var most = Math.max.apply(null, sizes);
        var padX = Math.max(18, w * 0.08), padY = Math.max(16, h * 0.1);
        var gapY = (h - padY * 2) / Math.max(1, most - 1);
        r = Math.max(3, Math.min(9, gapY * 0.2, (w - padX * 2) / sizes.length * 0.12));
        layers.forEach(function (layer, li) {
          var x = sizes.length === 1 ? w / 2 : padX + (w - padX * 2) * li / (sizes.length - 1);
          var span = gapY * (layer.length - 1);
          layer.forEach(function (n, i) { n.x = x; n.y = h / 2 - span / 2 + gapY * i; });
        });
        edges.forEach(function (e) { e.len = Math.hypot(e.to.x - e.from.x, e.to.y - e.from.y); });
        if (!running) draw();
      }

      function fire(n, t, id) {
        if (n.fired === id) return;
        n.fired = id;
        n.a = 1;
        n.out.forEach(function (e) {
          if (Math.random() > e.w * 1.1 + 0.05) return;
          pulses.push({ e: e, t0: t + Math.random() * 60, dur: (e.len / (0.32 * speed)) * (0.85 + Math.random() * 0.3), id: id, s: 0.5 + e.w * 0.5 });
        });
      }

      function startWave(t, from) {
        wave++;
        if (from) { fire(from, t, wave); return; }
        layers[0].forEach(function (n) { if (Math.random() < 0.7) fire(n, t + Math.random() * 120, wave); });
      }

      function draw(now) {
        var t = now || 0;
        ctx.clearRect(0, 0, w, h);
        var L = sizes.length - 1 || 1;
        // Lines, brightening briefly after a signal has used them.
        ctx.lineWidth = 1;
        edges.forEach(function (e) {
          e.a *= 0.94;
          ctx.strokeStyle = css3(mix(c1, c2, e.from.li / L), 0.07 + e.w * 0.08 + e.a * 0.4);
          ctx.beginPath(); ctx.moveTo(e.from.x, e.from.y); ctx.lineTo(e.to.x, e.to.y); ctx.stroke();
        });
        // Signals: a bright head with a fading tail.
        ctx.lineCap = 'round';
        for (var i = pulses.length - 1; i >= 0; i--) {
          var p = pulses[i], e = p.e;
          var k = (t - p.t0) / p.dur;
          if (k < 0) continue;
          if (k >= 1) {
            e.a = 1;
            e.to.a = Math.min(1, e.to.a + 0.55);
            fire(e.to, t, p.id);
            pulses.splice(i, 1);
            continue;
          }
          var ease = k < 0.5 ? 2 * k * k : 1 - Math.pow(-2 * k + 2, 2) / 2;
          var tail = Math.max(0, ease - 0.22);
          var dx = e.to.x - e.from.x, dy = e.to.y - e.from.y;
          var hx = e.from.x + dx * ease, hy = e.from.y + dy * ease;
          var tx = e.from.x + dx * tail, ty = e.from.y + dy * tail;
          var col = mix(c1, c2, (e.from.li + ease) / L);
          var g = ctx.createLinearGradient(tx, ty, hx, hy);
          g.addColorStop(0, css3(col, 0));
          g.addColorStop(1, css3(col, 0.9 * p.s));
          ctx.strokeStyle = g;
          ctx.lineWidth = Math.max(1.5, r * 0.4);
          ctx.beginPath(); ctx.moveTo(tx, ty); ctx.lineTo(hx, hy); ctx.stroke();
          var hg = ctx.createRadialGradient(hx, hy, 0, hx, hy, r * 1.3);
          hg.addColorStop(0, 'rgba(255,255,255,' + (0.95 * p.s) + ')');
          hg.addColorStop(0.35, css3(col, 0.8 * p.s));
          hg.addColorStop(1, css3(col, 0));
          ctx.fillStyle = hg;
          ctx.beginPath(); ctx.arc(hx, hy, r * 1.3, 0, Math.PI * 2); ctx.fill();
        }
        // Nodes: a dark core with a ring, glowing while active.
        layers.forEach(function (layer) {
          layer.forEach(function (n) {
            var col = mix(c1, c2, n.li / L);
            if (n.a > 0.02) {
              var gr = r * (2.2 + n.a * 1.6);
              var glow = ctx.createRadialGradient(n.x, n.y, r * 0.5, n.x, n.y, gr);
              glow.addColorStop(0, css3(col, 0.55 * n.a));
              glow.addColorStop(1, css3(col, 0));
              ctx.fillStyle = glow;
              ctx.beginPath(); ctx.arc(n.x, n.y, gr, 0, Math.PI * 2); ctx.fill();
            }
            ctx.fillStyle = css3(mix([14, 10, 30], col, 0.15 + n.a * 0.85), 1);
            ctx.beginPath(); ctx.arc(n.x, n.y, r, 0, Math.PI * 2); ctx.fill();
            ctx.strokeStyle = css3(col, 0.45 + n.a * 0.55);
            ctx.lineWidth = Math.max(1, r * 0.22);
            ctx.stroke();
            n.a *= 0.955;
          });
        });
      }

      function frame(now) {
        if (!running) return;
        if (now >= nextWave) { startWave(now); nextWave = now + rate * (0.8 + Math.random() * 0.4); }
        draw(now);
        raf = requestAnimationFrame(frame);
      }

      function start() {
        if (running || reduced()) return;
        running = true;
        nextWave = Math.max(nextWave, performance.now() + 200);
        raf = requestAnimationFrame(frame);
      }
      function stop() { running = false; cancelAnimationFrame(raf); }

      if (d.click !== 'false' && !el.closest('[aria-hidden="true"]')) {
        el.addEventListener('pointerdown', function (ev) {
          var b = el.getBoundingClientRect(), x = ev.clientX - b.left, y = ev.clientY - b.top;
          var best = null, bd = Infinity;
          layers.forEach(function (layer) {
            layer.forEach(function (n) { var dd = Math.hypot(n.x - x, n.y - y); if (dd < bd && n.out.length) { bd = dd; best = n; } });
          });
          var t = performance.now();
          if (bd < r * 4) startWave(t, best); else startWave(t);
          nextWave = t + rate;
        });
      }

      layout();
      if (reduced()) {
        // A still frame: one path lit through the network.
        var n = layers[0][Math.floor(layers[0].length / 2)];
        while (n) {
          n.a = 0.9;
          var e = n.out.slice().sort(function (p, q) { return q.w - p.w; })[0];
          if (e) e.a = 1;
          n = e && e.to;
        }
        draw(0);
      }
      new ResizeObserver(layout).observe(el);
      return { start: start, stop: stop };
    }

    function init() {
      var io = new IntersectionObserver(function (entries) {
        entries.forEach(function (e) {
          var api = e.target._nnp;
          if (e.isIntersecting) api.start(); else api.stop();
        });
      }, { threshold: 0.05 });
      document.querySelectorAll('[data-neural-network-pulse]').forEach(function (el) {
        if (el._nnp) return;
        el._nnp = build(el);
        io.observe(el);
      });
    }

    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
    else init();
  })();
</script>
2 · Your HTML
<div data-neural-network-pulse style="height: 300px; background: #0b0718"></div>