neural-network ai machine-learning animated pulses nodes graph
Neural Network
neural-network.js
/**
* Neural Network — Animated neural network with signal pulses and neuron activations.
* Category: interactive | Animate: true | Interactive: false | Complexity: medium
*/
export default function neuralNetwork(ctx, _state) {
// Layer configuration: [input, hidden1, hidden2, output]
const LAYERS = [4, 5, 5, 3];
const PULSE_SPEED = 0.4;
const PULSE_RADIUS = 5;
const NEURON_RADIUS = 10;
const ACTIVATION_INTERVAL = 1.8;
function isDark() {
return document.documentElement.classList.contains('dark');
}
// Build neuron positions from layer config
function buildNeurons(width, height) {
const neurons = [];
const layerCount = LAYERS.length;
for (let l = 0; l < layerCount; l++) {
const count = LAYERS[l];
const x = (width / (layerCount + 1)) * (l + 1);
for (let n = 0; n < count; n++) {
const y = (height / (count + 1)) * (n + 1);
neurons.push({ l, n, x, y, glow: 0, nextActivation: Math.random() * ACTIVATION_INTERVAL });
}
}
return neurons;
}
// Build connections between adjacent layers
function buildConnections(neurons) {
const connections = [];
const layerCount = LAYERS.length;
for (let l = 0; l < layerCount - 1; l++) {
const fromNeurons = neurons.filter((n) => n.l === l);
const toNeurons = neurons.filter((n) => n.l === l + 1);
for (const from of fromNeurons) {
for (const to of toNeurons) {
connections.push({ from, to, pulses: [] });
}
}
}
return connections;
}
let neurons = [];
let connections = [];
let initialized = false;
return {
frame({ width, height, delta, reducedMotion }) {
if (!initialized || neurons.length === 0) {
neurons = buildNeurons(width, height);
connections = buildConnections(neurons);
initialized = true;
}
const dark = isDark();
const lineColor = dark ? 'rgba(200, 180, 160, 0.4)' : 'rgba(87, 71, 55, 0.6)';
const pulseColor = dark ? '#c8a87a' : '#a07850';
const neuronFill = dark ? 'rgba(200, 180, 160, 0.15)' : 'rgba(87, 71, 55, 0.12)';
const neuronStroke = dark ? 'rgba(200, 180, 160, 0.6)' : 'rgba(87, 71, 55, 0.7)';
const glowColor = dark ? 'rgba(200, 168, 122, 0.8)' : 'rgba(160, 120, 80, 0.8)';
// Draw connections
ctx.lineWidth = 1;
ctx.strokeStyle = lineColor;
for (const conn of connections) {
ctx.beginPath();
ctx.moveTo(conn.from.x, conn.from.y);
ctx.lineTo(conn.to.x, conn.to.y);
ctx.stroke();
}
if (!reducedMotion) {
const dt = Math.min(delta, 0.05);
// Update neuron activations
for (const neuron of neurons) {
neuron.nextActivation -= dt;
if (neuron.nextActivation <= 0) {
neuron.glow = 1;
neuron.nextActivation = ACTIVATION_INTERVAL * (0.5 + Math.random());
// Spawn pulse on a random outgoing connection
const outgoing = connections.filter((c) => c.from === neuron);
if (outgoing.length > 0) {
const conn = outgoing[Math.floor(Math.random() * outgoing.length)];
conn.pulses.push({ t: 0 });
}
}
neuron.glow = Math.max(0, neuron.glow - dt * 1.5);
}
// Update and draw pulses
for (const conn of connections) {
const dx = conn.to.x - conn.from.x;
const dy = conn.to.y - conn.from.y;
for (let i = conn.pulses.length - 1; i >= 0; i--) {
conn.pulses[i].t += dt * PULSE_SPEED;
if (conn.pulses[i].t >= 1) {
conn.pulses.splice(i, 1);
continue;
}
const t = conn.pulses[i].t;
const px = conn.from.x + dx * t;
const py = conn.from.y + dy * t;
const alpha = 1 - Math.abs(t - 0.5) * 2; // fade in/out
// Trail
ctx.beginPath();
const trailT = Math.max(0, t - 0.08);
ctx.moveTo(conn.from.x + dx * trailT, conn.from.y + dy * trailT);
ctx.lineTo(px, py);
ctx.strokeStyle = `rgba(${dark ? '200, 168, 122' : '160, 120, 80'}, ${alpha * 0.3})`;
ctx.lineWidth = 2;
ctx.stroke();
// Pulse dot
ctx.beginPath();
ctx.arc(px, py, PULSE_RADIUS * alpha, 0, Math.PI * 2);
ctx.fillStyle = pulseColor;
ctx.globalAlpha = alpha;
ctx.fill();
ctx.globalAlpha = 1;
}
}
}
// Draw neurons
for (const neuron of neurons) {
const r = NEURON_RADIUS;
if (!reducedMotion && neuron.glow > 0) {
const grd = ctx.createRadialGradient(neuron.x, neuron.y, 0, neuron.x, neuron.y, r * 2.5);
grd.addColorStop(
0,
`rgba(${dark ? '200, 168, 122' : '160, 120, 80'}, ${neuron.glow * 0.4})`
);
grd.addColorStop(1, 'rgba(0,0,0,0)');
ctx.beginPath();
ctx.arc(neuron.x, neuron.y, r * 2.5, 0, Math.PI * 2);
ctx.fillStyle = grd;
ctx.fill();
}
ctx.beginPath();
ctx.arc(neuron.x, neuron.y, r, 0, Math.PI * 2);
ctx.fillStyle = neuronFill;
ctx.fill();
ctx.strokeStyle = !reducedMotion && neuron.glow > 0 ? glowColor : neuronStroke;
ctx.lineWidth = neuron.glow > 0 ? 2 : 1;
ctx.stroke();
}
},
resize({ width, height }) {
neurons = buildNeurons(width, height);
connections = buildConnections(neurons);
},
destroy() {
neurons = [];
connections = [];
initialized = false;
},
};
}
embed.html
<canvas id="canvas-effect" style="width:100%;height:400px;display:block" aria-hidden="true"></canvas>
<script>
/**
* mountCanvas — Minimal runtime for Canvas effects.
*
* Handles HiDPI scaling, ResizeObserver, RAF loop, pointer tracking,
* reduced-motion, and cleanup. The effect only implements draw logic.
*
* @param {HTMLCanvasElement} canvas
* @param {(ctx: CanvasRenderingContext2D, state: object) => object} createEffect
* @param {object} [options]
* @returns {() => void} destroy function
*/
function mountCanvas(canvas, createEffect, options = {}) {
const ctx = canvas.getContext('2d', { alpha: true });
if (!ctx) throw new Error('2D context not available');
const opts = {
animate: true,
clear: true,
maxDpr: 2,
pauseWhenHidden: true,
respectReducedMotion: true,
...options,
};
const reducedMotion =
opts.respectReducedMotion && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
const state = {
canvas,
ctx,
width: 0,
height: 0,
pixelWidth: 0,
pixelHeight: 0,
dpr: 1,
time: 0,
delta: 0,
frame: 0,
playing: false,
reducedMotion,
pointer: { x: 0, y: 0, inside: false, down: false },
};
let rafId = 0;
let lastTime = 0;
let disposed = false;
function measure() {
const rect = canvas.getBoundingClientRect();
const w = Math.max(1, rect.width);
const h = Math.max(1, rect.height);
const dpr = Math.min(window.devicePixelRatio || 1, opts.maxDpr);
const pw = Math.round(w * dpr);
const ph = Math.round(h * dpr);
if (canvas.width !== pw || canvas.height !== ph) {
canvas.width = pw;
canvas.height = ph;
}
state.width = w;
state.height = h;
state.pixelWidth = pw;
state.pixelHeight = ph;
state.dpr = dpr;
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
}
function clear() {
ctx.clearRect(0, 0, state.width, state.height);
}
const effect = createEffect(ctx, state) || {};
function onResize() {
measure();
effect.resize?.(state);
if (!opts.animate || reducedMotion) {
if (opts.clear) clear();
effect.frame?.(state);
}
}
function loop(now) {
if (disposed) return;
if (opts.pauseWhenHidden && document.hidden) {
rafId = requestAnimationFrame(loop);
return;
}
state.delta = lastTime ? (now - lastTime) / 1000 : 0;
state.time = now / 1000;
state.frame += 1;
lastTime = now;
if (opts.clear) clear();
effect.frame?.(state);
rafId = requestAnimationFrame(loop);
}
function onPointerMove(e) {
const rect = canvas.getBoundingClientRect();
state.pointer.x = e.clientX - rect.left;
state.pointer.y = e.clientY - rect.top;
if (!opts.animate || reducedMotion) {
if (opts.clear) clear();
effect.frame?.(state);
}
}
function onPointerEnter() {
state.pointer.inside = true;
}
function onPointerLeave() {
state.pointer.inside = false;
state.pointer.down = false;
}
function onPointerDown() {
state.pointer.down = true;
}
function onPointerUp() {
state.pointer.down = false;
}
const ro = new ResizeObserver(onResize);
ro.observe(canvas);
canvas.addEventListener('pointermove', onPointerMove);
canvas.addEventListener('pointerenter', onPointerEnter);
canvas.addEventListener('pointerleave', onPointerLeave);
canvas.addEventListener('pointerdown', onPointerDown);
window.addEventListener('pointerup', onPointerUp);
measure();
if (opts.animate && !reducedMotion) {
state.playing = true;
rafId = requestAnimationFrame(loop);
} else {
effect.frame?.(state);
}
return function destroy() {
disposed = true;
state.playing = false;
cancelAnimationFrame(rafId);
ro.disconnect();
canvas.removeEventListener('pointermove', onPointerMove);
canvas.removeEventListener('pointerenter', onPointerEnter);
canvas.removeEventListener('pointerleave', onPointerLeave);
canvas.removeEventListener('pointerdown', onPointerDown);
window.removeEventListener('pointerup', onPointerUp);
effect.destroy?.(state);
};
}
/**
* Neural Network — Animated neural network with signal pulses and neuron activations.
* Category: interactive | Animate: true | Interactive: false | Complexity: medium
*/
const __effect = function neuralNetwork(ctx, _state) {
// Layer configuration: [input, hidden1, hidden2, output]
const LAYERS = [4, 5, 5, 3];
const PULSE_SPEED = 0.4;
const PULSE_RADIUS = 5;
const NEURON_RADIUS = 10;
const ACTIVATION_INTERVAL = 1.8;
function isDark() {
return document.documentElement.classList.contains('dark');
}
// Build neuron positions from layer config
function buildNeurons(width, height) {
const neurons = [];
const layerCount = LAYERS.length;
for (let l = 0; l < layerCount; l++) {
const count = LAYERS[l];
const x = (width / (layerCount + 1)) * (l + 1);
for (let n = 0; n < count; n++) {
const y = (height / (count + 1)) * (n + 1);
neurons.push({ l, n, x, y, glow: 0, nextActivation: Math.random() * ACTIVATION_INTERVAL });
}
}
return neurons;
}
// Build connections between adjacent layers
function buildConnections(neurons) {
const connections = [];
const layerCount = LAYERS.length;
for (let l = 0; l < layerCount - 1; l++) {
const fromNeurons = neurons.filter((n) => n.l === l);
const toNeurons = neurons.filter((n) => n.l === l + 1);
for (const from of fromNeurons) {
for (const to of toNeurons) {
connections.push({ from, to, pulses: [] });
}
}
}
return connections;
}
let neurons = [];
let connections = [];
let initialized = false;
return {
frame({ width, height, delta, reducedMotion }) {
if (!initialized || neurons.length === 0) {
neurons = buildNeurons(width, height);
connections = buildConnections(neurons);
initialized = true;
}
const dark = isDark();
const lineColor = dark ? 'rgba(200, 180, 160, 0.4)' : 'rgba(87, 71, 55, 0.6)';
const pulseColor = dark ? '#c8a87a' : '#a07850';
const neuronFill = dark ? 'rgba(200, 180, 160, 0.15)' : 'rgba(87, 71, 55, 0.12)';
const neuronStroke = dark ? 'rgba(200, 180, 160, 0.6)' : 'rgba(87, 71, 55, 0.7)';
const glowColor = dark ? 'rgba(200, 168, 122, 0.8)' : 'rgba(160, 120, 80, 0.8)';
// Draw connections
ctx.lineWidth = 1;
ctx.strokeStyle = lineColor;
for (const conn of connections) {
ctx.beginPath();
ctx.moveTo(conn.from.x, conn.from.y);
ctx.lineTo(conn.to.x, conn.to.y);
ctx.stroke();
}
if (!reducedMotion) {
const dt = Math.min(delta, 0.05);
// Update neuron activations
for (const neuron of neurons) {
neuron.nextActivation -= dt;
if (neuron.nextActivation <= 0) {
neuron.glow = 1;
neuron.nextActivation = ACTIVATION_INTERVAL * (0.5 + Math.random());
// Spawn pulse on a random outgoing connection
const outgoing = connections.filter((c) => c.from === neuron);
if (outgoing.length > 0) {
const conn = outgoing[Math.floor(Math.random() * outgoing.length)];
conn.pulses.push({ t: 0 });
}
}
neuron.glow = Math.max(0, neuron.glow - dt * 1.5);
}
// Update and draw pulses
for (const conn of connections) {
const dx = conn.to.x - conn.from.x;
const dy = conn.to.y - conn.from.y;
for (let i = conn.pulses.length - 1; i >= 0; i--) {
conn.pulses[i].t += dt * PULSE_SPEED;
if (conn.pulses[i].t >= 1) {
conn.pulses.splice(i, 1);
continue;
}
const t = conn.pulses[i].t;
const px = conn.from.x + dx * t;
const py = conn.from.y + dy * t;
const alpha = 1 - Math.abs(t - 0.5) * 2; // fade in/out
// Trail
ctx.beginPath();
const trailT = Math.max(0, t - 0.08);
ctx.moveTo(conn.from.x + dx * trailT, conn.from.y + dy * trailT);
ctx.lineTo(px, py);
ctx.strokeStyle = `rgba(${dark ? '200, 168, 122' : '160, 120, 80'}, ${alpha * 0.3})`;
ctx.lineWidth = 2;
ctx.stroke();
// Pulse dot
ctx.beginPath();
ctx.arc(px, py, PULSE_RADIUS * alpha, 0, Math.PI * 2);
ctx.fillStyle = pulseColor;
ctx.globalAlpha = alpha;
ctx.fill();
ctx.globalAlpha = 1;
}
}
}
// Draw neurons
for (const neuron of neurons) {
const r = NEURON_RADIUS;
if (!reducedMotion && neuron.glow > 0) {
const grd = ctx.createRadialGradient(neuron.x, neuron.y, 0, neuron.x, neuron.y, r * 2.5);
grd.addColorStop(
0,
`rgba(${dark ? '200, 168, 122' : '160, 120, 80'}, ${neuron.glow * 0.4})`
);
grd.addColorStop(1, 'rgba(0,0,0,0)');
ctx.beginPath();
ctx.arc(neuron.x, neuron.y, r * 2.5, 0, Math.PI * 2);
ctx.fillStyle = grd;
ctx.fill();
}
ctx.beginPath();
ctx.arc(neuron.x, neuron.y, r, 0, Math.PI * 2);
ctx.fillStyle = neuronFill;
ctx.fill();
ctx.strokeStyle = !reducedMotion && neuron.glow > 0 ? glowColor : neuronStroke;
ctx.lineWidth = neuron.glow > 0 ? 2 : 1;
ctx.stroke();
}
},
resize({ width, height }) {
neurons = buildNeurons(width, height);
connections = buildConnections(neurons);
},
destroy() {
neurons = [];
connections = [];
initialized = false;
},
};
}
const canvas = document.getElementById('canvas-effect');
mountCanvas(canvas, __effect, { animate: true, clear: true });
</script> Details
Animated Reduced Motion aria-hidden
neural-networkaimachine-learninganimatedpulsesnodesgraph
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
| LAYERS | array | [4, 5, 5, 3] | Neuron count per layer — [input, hidden1, hidden2, output] |
| PULSE_SPEED | number | 0.4 | Signal pulse travel speed (fraction of connection length per second) |
| ACTIVATION_INTERVAL | number | 1.8 | Base seconds between random neuron activations |
A four-layer feedforward network rendered entirely on canvas. Neuron positions are computed from the layer configuration so the layout stays balanced at any canvas size. Signal pulses spawn on random outgoing connections whenever a neuron activates, traveling along the connection line with a short trail and a fade-in/out alpha curve. Dark mode detection reads document.documentElement.classList.contains('dark') so the color palette switches automatically. When reducedMotion is true, the network renders as a static diagram with no pulses or activations.