Canvas 2D · 4.1 KB gzip
Neural Nodes
A network graph with pulses of light traveling its edges: pulse rate rises with activity; glows green on done, flickers red on error.
neuralgraphnodesnetworkreasoning
Interactive preview
Usage
Packages are @vikast908/core and
@vikast908/loaders. Prefer 0.1.1+
(0.1.0 loaders is broken on npm).
npm install @vikast908/loaders@0.1.1 @vikast908/core@0.1.1
import '@vikast908/loaders/neural-nodes'; <orbux-neural-nodes state="thinking"></orbux-neural-nodes>
Browser-only alternative: Download HTML (self-contained) or load the hosted ESM bundle:
<script type="module" src="http://localhost:4321/orbux/neural-nodes.js"></script> <orbux-neural-nodes state="thinking"></orbux-neural-nodes>
Component source neural-nodes.ts
import { type AgentState, Canvas2DElement, lerp, noise1D, type OrbuxFrameInfo } from '@vikast908/core';
const TAU = Math.PI * 2;
interface Node {
x: number;
y: number;
glow: number;
}
interface Edge {
a: number;
b: number;
}
interface Pulse {
edge: number;
t: number;
dir: 1 | -1;
}
interface Tune {
rate: number; // pulses per second
speed: number; // pulse travel speed
base: number; // ambient edge/node brightness
}
const STATE_TUNE: Record<AgentState, Tune> = {
idle: { rate: 0.6, speed: 0.4, base: 0.18 },
thinking: { rate: 2.5, speed: 0.9, base: 0.28 },
streaming: { rate: 6, speed: 1.7, base: 0.42 },
'tool-calling': { rate: 4, speed: 1.3, base: 0.36 },
waiting: { rate: 0.8, speed: 0.5, base: 0.22 },
done: { rate: 0.4, speed: 0.6, base: 0.7 },
error: { rate: 3, speed: 1.1, base: 0.3 },
};
/**
* Neural Nodes: a small network graph with pulses of light traveling along its
* edges. Pulse rate and speed rise with activity; the graph glows green on
* `done` and flickers red on `error`.
*/
export class OrbuxNeuralNodes extends Canvas2DElement {
#nodes: Node[] = [];
#edges: Edge[] = [];
#pulses: Pulse[] = [];
#spawnAcc = 0;
#cur: Tune = { ...STATE_TUNE.thinking };
protected override setup(): void {
// 1 center node + 6 ring nodes
this.#nodes = [{ x: 0.5, y: 0.5, glow: 0 }];
for (let i = 0; i < 6; i++) {
const a = (i / 6) * TAU - Math.PI / 2;
this.#nodes.push({
x: 0.5 + Math.cos(a) * 0.34,
y: 0.5 + Math.sin(a) * 0.34,
glow: 0,
});
}
this.#edges = [];
for (let i = 1; i <= 6; i++) {
this.#edges.push({ a: 0, b: i }); // spoke
this.#edges.push({ a: i, b: (i % 6) + 1 }); // rim
}
}
protected draw(info: OrbuxFrameInfo): void {
const ctx = this.ctx;
if (!ctx || this.#nodes.length === 0) return;
const t = STATE_TUNE[info.state];
const rate = info.state === 'done' || info.state === 'error' ? 18 : 3;
const k = info.dt === 0 ? 1 : 1 - Math.exp(-rate * info.dt);
this.#cur.rate = lerp(this.#cur.rate, t.rate, k);
this.#cur.speed = lerp(this.#cur.speed, t.speed, k);
this.#cur.base = lerp(this.#cur.base, t.base, k);
const primary = this.cssColor('--orbux-color', '#6366f1');
const secondary = this.cssColor('--orbux-color-2', '#a855f7');
const terminal =
info.state === 'done'
? this.cssColor('--orbux-color-success', '#22c55e')
: info.state === 'error'
? this.cssColor('--orbux-color-error', '#ef4444')
: null;
const edgeColor = terminal ?? primary;
const pulseColor = terminal ?? secondary;
const px = (n: Node) => n.x * this.width;
const py = (n: Node) => n.y * this.height;
const r = Math.min(this.width, this.height);
// spawn pulses
if (info.state === 'done') {
this.#spawnAcc = 0;
} else {
this.#spawnAcc += info.dt * this.#cur.rate * info.speed * 1.5;
while (this.#spawnAcc >= 1) {
this.#spawnAcc -= 1;
const edge = Math.floor(Math.random() * this.#edges.length);
this.#pulses.push({ edge, t: 0, dir: Math.random() < 0.5 ? 1 : -1 });
}
}
// update + advance pulses
for (const p of this.#pulses) {
p.t += info.dt * this.#cur.speed * info.speed * 1.5;
if (p.t >= 1) {
const e = this.#edges[p.edge];
if (e) {
const target = this.#nodes[p.dir === 1 ? e.b : e.a];
if (target) target.glow = 1;
}
}
}
this.#pulses = this.#pulses.filter((p) => p.t < 1);
// decay glows
for (const n of this.#nodes) {
n.glow = Math.max(info.state === 'done' ? 0.85 : 0, n.glow - info.dt * 2.2);
}
this.clear();
// edges
ctx.lineWidth = Math.max(1, r * 0.006);
for (let i = 0; i < this.#edges.length; i++) {
const e = this.#edges[i];
if (!e) continue;
const a = this.#nodes[e.a];
const b = this.#nodes[e.b];
if (!a || !b) continue;
const flicker = info.state === 'error' && noise1D(i + 1, info.elapsed * 9) > 0.72 ? 0 : 1;
ctx.globalAlpha = this.#cur.base * flicker;
ctx.strokeStyle = edgeColor;
if (info.state === 'error') ctx.setLineDash([r * 0.03, r * 0.03]);
else ctx.setLineDash([]);
ctx.beginPath();
ctx.moveTo(px(a), py(a));
ctx.lineTo(px(b), py(b));
ctx.stroke();
}
ctx.setLineDash([]);
ctx.globalAlpha = 1;
// pulses (additive)
ctx.globalCompositeOperation = 'lighter';
for (const p of this.#pulses) {
const e = this.#edges[p.edge];
if (!e) continue;
const a = this.#nodes[e.a];
const b = this.#nodes[e.b];
if (!a || !b) continue;
const tt = p.dir === 1 ? p.t : 1 - p.t;
const x = lerp(px(a), px(b), tt);
const y = lerp(py(a), py(b), tt);
const s = r * 0.02;
ctx.beginPath();
ctx.arc(x, y, s, 0, TAU);
ctx.fillStyle = pulseColor;
ctx.shadowBlur = s * 3;
ctx.shadowColor = pulseColor;
ctx.fill();
}
// nodes
for (const n of this.#nodes) {
const s = r * (0.028 + 0.02 * n.glow);
ctx.beginPath();
ctx.arc(px(n), py(n), s, 0, TAU);
ctx.fillStyle = n.glow > 0.05 ? pulseColor : edgeColor;
ctx.shadowBlur = s * (1 + 3 * n.glow);
ctx.shadowColor = n.glow > 0.05 ? pulseColor : edgeColor;
ctx.globalAlpha = 0.5 + 0.5 * Math.max(this.#cur.base, n.glow);
ctx.fill();
}
ctx.globalCompositeOperation = 'source-over';
ctx.globalAlpha = 1;
ctx.shadowBlur = 0;
}
}
if (typeof customElements !== 'undefined' && !customElements.get('orbux-neural-nodes')) {
customElements.define('orbux-neural-nodes', OrbuxNeuralNodes);
}
declare global {
interface HTMLElementTagNameMap {
'orbux-neural-nodes': OrbuxNeuralNodes;
}
}
Depends on @vikast908/core. Paste the source, add the base
element, and set state from your agent code.
Attributes & properties
| Name | Type | Description |
|---|---|---|
state | AgentState | idle · thinking · streaming · tool-calling · waiting · done · error |
size | CSS length | Overall square size. |
--orbux-width | CSS length | Optional inline-size override. |
--orbux-height | CSS length | Optional block-size override. |
speed | number | Animation speed multiplier (1 = default). |
label | string | Accessible label (defaults per state). |
paused | boolean | Freeze the animation. |
Respects prefers-reduced-motion, exposes
status/progressbar semantics with a live label, and auto-pauses (sets
data-paused) when offscreen or the tab is hidden.