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
- Paste the snippet just before
</head>. - Add
data-neural-network-pulseto an element with a height. The network is drawn on a canvas that fills it, behind any content. - It glows best on a dark background.
Options
Add any of these attributes to the same element.
data-layers | How many nodes in each layer, comma-separated. Default 4,6,6,3. |
data-color | Colour of the signals leaving the input layer. Default #a78bfa. |
data-color2 | Colour they shift to by the output layer. Default #22d3ee. |
data-rate | Time between waves. Default 1600 ms. |
data-speed | How 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.
<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><div data-neural-network-pulse style="height: 300px; background: #0b0718"></div>