"use client"; import { motion } from "framer-motion"; import { useEffect, useState } from "react"; import { COLORS, VizFrame } from "./common"; import { useReducedMotion } from "@/lib/hooks/useReducedMotion"; function rng(seed = 12) { let s = seed; return () => { s = (s * 1664525 + 1013904223) % 4294967296; return s / 4294967296 - 0.5; }; } export function NeuronNetwork({ layers = [3, 5, 4, 2], width = 720, height = 460, pulse = true, }: { layers?: number[]; width?: number; height?: number; pulse?: boolean; }) { const padX = 70; const padY = 50; const colW = (width - padX * 2) / (layers.length - 1); const positions = layers.map((n, li) => Array.from({ length: n }, (_, ni) => { const x = padX + li * colW; const ySpan = height - padY * 2; const step = ySpan / (n + 1); const y = padY + step * (ni + 1); return { x, y }; }), ); // Random weights, stable per layout const r = rng(7); const weights: number[][][] = []; for (let li = 0; li < layers.length - 1; li++) { const W: number[][] = []; for (let i = 0; i < layers[li]; i++) { const row: number[] = []; for (let j = 0; j < layers[li + 1]; j++) row.push(r()); W.push(row); } weights.push(W); } const [activeLayer, setActiveLayer] = useState(0); const reduce = useReducedMotion(); useEffect(() => { if (!pulse || reduce) return; const id = setInterval( () => setActiveLayer((a) => (a + 1) % (layers.length - 1)), 900, ); return () => clearInterval(id); }, [layers.length, pulse, reduce]); return ( {/* Edges */} {weights.map((W, li) => W.map((row, i) => row.map((w, j) => { const a = positions[li][i]; const b = positions[li + 1][j]; const isActive = li === activeLayer; return ( = 0 ? COLORS.accent : COLORS.honey} strokeOpacity={isActive ? 0.85 : 0.18} strokeWidth={Math.abs(w) * 3 + 0.5} initial={false} animate={{ strokeOpacity: isActive ? 0.85 : 0.18 }} /> ); }), ), )} {/* Nodes */} {positions.map((col, li) => ( {col.map((p, ni) => ( ))} {li === 0 ? "input" : li === layers.length - 1 ? "output" : `hidden ${li}`} ))} ); }