| <div class="neural-flow" |
| style="width:100%;margin:10px 0;aspect-ratio:3/1;min-height:260px;position:relative;overflow:hidden;"></div> |
| <script> |
| (() => { |
| const ensureAnime = (cb) => { |
| if (window.anime && typeof window.anime === 'function') return cb(); |
| let s = document.getElementById('anime-cdn-script'); |
| if (!s) { |
| s = document.createElement('script'); |
| s.id = 'anime-cdn-script'; |
| s.src = 'https://cdn.jsdelivr.net/npm/animejs@3.2.1/lib/anime.min.js'; |
| document.head.appendChild(s); |
| } |
| const onReady = () => { if (window.anime && typeof window.anime === 'function') cb(); }; |
| s.addEventListener('load', onReady, { once: true }); |
| if (window.anime) onReady(); |
| }; |
| |
| const bootstrap = () => { |
| const mount = document.currentScript ? document.currentScript.previousElementSibling : null; |
| const container = (mount && mount.querySelector && mount.querySelector('.neural-flow')) || document.querySelector('.neural-flow'); |
| if (!container) return; |
| if (container.dataset) { |
| if (container.dataset.mounted === 'true') return; |
| container.dataset.mounted = 'true'; |
| } |
| |
| |
| const canvas = document.createElement('canvas'); |
| canvas.style.display = 'block'; |
| canvas.style.width = '100%'; |
| canvas.style.height = '100%'; |
| container.appendChild(canvas); |
| const ctx = canvas.getContext('2d'); |
| |
| |
| const getColors = () => { |
| const isDark = document.documentElement.getAttribute('data-theme') === 'dark'; |
| return { |
| node: isDark ? 'rgba(206, 192, 250, 0.85)' : 'rgba(138, 100, 220, 0.8)', |
| nodeActive: isDark ? 'rgba(232, 137, 171, 1)' : 'rgba(220, 80, 130, 1)', |
| nodeGlow: isDark ? 'rgba(206, 192, 250, 0.4)' : 'rgba(138, 100, 220, 0.3)', |
| connection: isDark ? 'rgba(78, 165, 183, 0.08)' : 'rgba(78, 165, 183, 0.15)', |
| connectionActive: isDark ? 'rgba(232, 137, 171, 0.6)' : 'rgba(220, 80, 130, 0.5)', |
| accent: isDark ? 'rgba(78, 165, 183, 0.9)' : 'rgba(50, 130, 160, 0.85)', |
| particle: isDark ? 'rgba(232, 137, 171, 1)' : 'rgba(220, 80, 130, 1)', |
| }; |
| }; |
| |
| let colors = getColors(); |
| |
| |
| const observer = new MutationObserver(() => { |
| colors = getColors(); |
| }); |
| observer.observe(document.documentElement, { |
| attributes: true, |
| attributeFilter: ['data-theme'] |
| }); |
| |
| |
| const layers = [ |
| { nodes: 6, name: 'input' }, |
| { nodes: 10, name: 'hidden1' }, |
| { nodes: 8, name: 'hidden2' }, |
| { nodes: 4, name: 'output' } |
| ]; |
| |
| let nodes = []; |
| let connections = []; |
| let particles = []; |
| let width, height; |
| |
| const resize = () => { |
| width = container.clientWidth || 800; |
| height = Math.max(260, Math.round(width / 3)); |
| canvas.width = width; |
| canvas.height = height; |
| initNetwork(); |
| }; |
| |
| const initNetwork = () => { |
| nodes = []; |
| connections = []; |
| particles = []; |
| |
| const layerSpacing = width / (layers.length + 1); |
| const margin = height * 0.15; |
| |
| |
| let nodeIndex = 0; |
| const layerStartIndices = []; |
| |
| layers.forEach((layer, layerIdx) => { |
| layerStartIndices.push(nodeIndex); |
| const x = layerSpacing * (layerIdx + 1); |
| const availableHeight = height - 2 * margin; |
| const nodeSpacing = availableHeight / (layer.nodes + 1); |
| |
| for (let i = 0; i < layer.nodes; i++) { |
| const y = margin + nodeSpacing * (i + 1); |
| const node = { |
| x, |
| y, |
| layer: layerIdx, |
| index: i, |
| radius: 0, |
| targetRadius: 3.5 + Math.random() * 1.5, |
| pulse: Math.random() * Math.PI * 2, |
| activation: 0, |
| baseActivity: Math.random() * 0.1 |
| }; |
| nodes.push(node); |
| nodeIndex++; |
| } |
| }); |
| |
| |
| layers.forEach((layer, layerIdx) => { |
| if (layerIdx < layers.length - 1) { |
| const currentLayerStart = layerStartIndices[layerIdx]; |
| const nextLayerStart = layerStartIndices[layerIdx + 1]; |
| const nextLayerNodes = layers[layerIdx + 1].nodes; |
| |
| for (let i = 0; i < layer.nodes; i++) { |
| for (let j = 0; j < nextLayerNodes; j++) { |
| connections.push({ |
| from: currentLayerStart + i, |
| to: nextLayerStart + j, |
| weight: Math.random(), |
| opacity: 0, |
| activation: 0 |
| }); |
| } |
| } |
| } |
| }); |
| |
| |
| nodes.forEach((node, i) => { |
| anime({ |
| targets: node, |
| radius: node.targetRadius, |
| duration: 800, |
| delay: i * 8, |
| easing: 'easeOutElastic(1, .6)' |
| }); |
| }); |
| |
| |
| connections.forEach((conn, i) => { |
| anime({ |
| targets: conn, |
| opacity: 1, |
| duration: 400, |
| delay: 300 + i * 1, |
| easing: 'easeOutQuad' |
| }); |
| }); |
| |
| |
| setTimeout(() => { |
| startForwardPass(); |
| setInterval(startForwardPass, 2500 + Math.random() * 1000); |
| }, 1000); |
| }; |
| |
| |
| const activationPatterns = [ |
| |
| (inputNodes) => inputNodes, |
| |
| |
| (inputNodes) => [inputNodes[Math.floor(Math.random() * inputNodes.length)]], |
| |
| |
| (inputNodes) => inputNodes.slice(0, Math.ceil(inputNodes.length / 2)), |
| |
| |
| (inputNodes) => inputNodes.slice(Math.floor(inputNodes.length / 2)), |
| |
| |
| (inputNodes) => inputNodes.filter((_, i) => i % 2 === 0), |
| |
| |
| (inputNodes) => { |
| const num = 2 + Math.floor(Math.random() * 2); |
| return [...inputNodes].sort(() => Math.random() - 0.5).slice(0, num); |
| }, |
| |
| |
| (inputNodes) => inputNodes.slice(0, 3 + Math.floor(Math.random() * 3)) |
| ]; |
| |
| const startForwardPass = () => { |
| const inputNodes = nodes.filter(n => n.layer === 0); |
| |
| |
| const pattern = activationPatterns[Math.floor(Math.random() * activationPatterns.length)]; |
| const activeInputs = pattern(inputNodes); |
| |
| |
| activeInputs.forEach((node, idx) => { |
| anime({ |
| targets: node, |
| activation: 0.8 + Math.random() * 0.2, |
| duration: 200, |
| delay: idx * 60, |
| easing: 'easeOutQuad', |
| complete: () => { |
| anime({ |
| targets: node, |
| activation: node.baseActivity, |
| duration: 250, |
| delay: 400, |
| easing: 'easeInQuad' |
| }); |
| } |
| }); |
| }); |
| |
| |
| for (let layerIdx = 0; layerIdx < layers.length - 1; layerIdx++) { |
| setTimeout(() => { |
| propagateLayer(layerIdx); |
| }, 250 + layerIdx * 350); |
| } |
| }; |
| |
| const propagateLayer = (fromLayerIdx) => { |
| const fromNodes = nodes.filter(n => n.layer === fromLayerIdx); |
| const toNodes = nodes.filter(n => n.layer === fromLayerIdx + 1); |
| |
| const layerConnections = connections.filter(c => { |
| const fromNode = nodes[c.from]; |
| const toNode = nodes[c.to]; |
| return fromNode.layer === fromLayerIdx && toNode.layer === fromLayerIdx + 1; |
| }); |
| |
| |
| layerConnections.forEach((conn, idx) => { |
| const fromNode = nodes[conn.from]; |
| const activationStrength = fromNode.activation * conn.weight; |
| |
| if (activationStrength > 0.2) { |
| anime({ |
| targets: conn, |
| activation: activationStrength, |
| duration: 300, |
| delay: idx * 1, |
| easing: 'easeOutQuad', |
| complete: () => { |
| anime({ |
| targets: conn, |
| activation: 0, |
| duration: 250, |
| easing: 'easeInQuad' |
| }); |
| } |
| }); |
| |
| |
| if (Math.random() < 0.3) { |
| createParticle(conn, activationStrength); |
| } |
| } |
| }); |
| |
| |
| setTimeout(() => { |
| toNodes.forEach(toNode => { |
| const toNodeIdx = nodes.indexOf(toNode); |
| const incomingConns = layerConnections.filter(c => c.to === toNodeIdx); |
| |
| let sum = 0; |
| incomingConns.forEach(conn => { |
| const fromNode = nodes[conn.from]; |
| sum += fromNode.activation * conn.weight; |
| }); |
| |
| const activation = Math.min(1, sum / incomingConns.length * 1.5); |
| |
| if (activation > 0.25) { |
| anime({ |
| targets: toNode, |
| activation: activation, |
| duration: 200, |
| easing: 'easeOutQuad', |
| complete: () => { |
| anime({ |
| targets: toNode, |
| activation: toNode.baseActivity, |
| duration: 400, |
| delay: 300, |
| easing: 'easeInQuad' |
| }); |
| } |
| }); |
| } |
| }); |
| }, 150); |
| }; |
| |
| const createParticle = (connection, strength) => { |
| const fromNode = nodes[connection.from]; |
| const toNode = nodes[connection.to]; |
| if (!fromNode || !toNode) return; |
| |
| const particle = { |
| fromX: fromNode.x, |
| fromY: fromNode.y, |
| toX: toNode.x, |
| toY: toNode.y, |
| progress: 0, |
| strength: strength, |
| size: 1.5 + strength * 1.5, |
| trail: [] |
| }; |
| |
| particles.push(particle); |
| |
| anime({ |
| targets: particle, |
| progress: 1, |
| duration: 350, |
| easing: 'easeInOutQuad', |
| complete: () => { |
| |
| const idx = particles.indexOf(particle); |
| if (idx > -1) particles.splice(idx, 1); |
| } |
| }); |
| }; |
| |
| const draw = () => { |
| |
| ctx.clearRect(0, 0, width, height); |
| |
| |
| connections.forEach(conn => { |
| if (conn.opacity < 0.01) return; |
| |
| const fromNode = nodes[conn.from]; |
| const toNode = nodes[conn.to]; |
| if (!fromNode || !toNode) return; |
| |
| const baseOpacity = conn.opacity * conn.weight * 0.5; |
| const activeOpacity = conn.activation; |
| const totalOpacity = Math.max(baseOpacity, activeOpacity); |
| |
| if (totalOpacity < 0.01) return; |
| |
| const isActive = conn.activation > 0.1; |
| const connectionColor = isActive ? colors.connectionActive : colors.connection; |
| |
| ctx.beginPath(); |
| ctx.moveTo(fromNode.x, fromNode.y); |
| ctx.lineTo(toNode.x, toNode.y); |
| |
| const rgb = connectionColor.match(/[\d.]+/g); |
| ctx.strokeStyle = `rgba(${rgb[0]}, ${rgb[1]}, ${rgb[2]}, ${totalOpacity})`; |
| ctx.lineWidth = isActive ? 1.5 : 0.8; |
| ctx.stroke(); |
| }); |
| |
| |
| particles.forEach(particle => { |
| const x = particle.fromX + (particle.toX - particle.fromX) * particle.progress; |
| const y = particle.fromY + (particle.toY - particle.fromY) * particle.progress; |
| |
| |
| particle.trail.push({ x, y }); |
| if (particle.trail.length > 5) particle.trail.shift(); |
| |
| particle.trail.forEach((point, i) => { |
| const alpha = (i / particle.trail.length) * particle.strength; |
| const size = particle.size * alpha * 0.6; |
| |
| ctx.beginPath(); |
| ctx.arc(point.x, point.y, size, 0, Math.PI * 2); |
| const rgb = colors.particle.match(/[\d.]+/g); |
| ctx.fillStyle = `rgba(${rgb[0]}, ${rgb[1]}, ${rgb[2]}, ${alpha * 0.5})`; |
| ctx.fill(); |
| }); |
| |
| |
| ctx.beginPath(); |
| ctx.arc(x, y, particle.size, 0, Math.PI * 2); |
| ctx.fillStyle = colors.particle; |
| ctx.shadowBlur = 8; |
| ctx.shadowColor = colors.particle; |
| ctx.fill(); |
| ctx.shadowBlur = 0; |
| }); |
| |
| |
| nodes.forEach((node, i) => { |
| if (node.radius < 0.1) return; |
| |
| node.pulse += 0.015; |
| const pulseSize = 1 + Math.sin(node.pulse) * 0.08; |
| const activationBoost = node.activation * 1.8; |
| const finalRadius = node.radius * pulseSize + activationBoost; |
| |
| |
| if (node.activation > 0.15) { |
| const glowRadius = finalRadius * 4; |
| const gradient = ctx.createRadialGradient(node.x, node.y, 0, node.x, node.y, glowRadius); |
| const glowAlpha = node.activation * 0.5; |
| gradient.addColorStop(0, colors.nodeGlow.replace(/[\d.]+\)$/, `${glowAlpha})`)); |
| gradient.addColorStop(1, colors.nodeGlow.replace(/[\d.]+\)$/, '0)')); |
| ctx.beginPath(); |
| ctx.arc(node.x, node.y, glowRadius, 0, Math.PI * 2); |
| ctx.fillStyle = gradient; |
| ctx.fill(); |
| } |
| |
| |
| const t = Math.min(1, node.activation / 0.8); |
| |
| const baseRgb = colors.node.match(/[\d.]+/g); |
| const activeRgb = colors.nodeActive.match(/[\d.]+/g); |
| const r = parseFloat(baseRgb[0]) + (parseFloat(activeRgb[0]) - parseFloat(baseRgb[0])) * t; |
| const g = parseFloat(baseRgb[1]) + (parseFloat(activeRgb[1]) - parseFloat(baseRgb[1])) * t; |
| const b = parseFloat(baseRgb[2]) + (parseFloat(activeRgb[2]) - parseFloat(baseRgb[2])) * t; |
| const a = parseFloat(baseRgb[3]) + (parseFloat(activeRgb[3]) - parseFloat(baseRgb[3])) * t; |
| |
| |
| ctx.beginPath(); |
| ctx.arc(node.x, node.y, finalRadius, 0, Math.PI * 2); |
| ctx.fillStyle = `rgba(${r}, ${g}, ${b}, ${a})`; |
| ctx.fill(); |
| |
| |
| if (node.activation > 0.4) { |
| ctx.beginPath(); |
| ctx.arc(node.x, node.y, finalRadius * 0.4, 0, Math.PI * 2); |
| ctx.fillStyle = colors.accent.replace(/[\d.]+\)$/, `${node.activation})`); |
| ctx.fill(); |
| } |
| }); |
| |
| requestAnimationFrame(draw); |
| }; |
| |
| |
| if (window.ResizeObserver) { |
| const ro = new ResizeObserver(resize); |
| ro.observe(container); |
| } else { |
| window.addEventListener('resize', resize); |
| } |
| |
| resize(); |
| draw(); |
| }; |
| |
| if (document.readyState === 'loading') { |
| document.addEventListener('DOMContentLoaded', () => ensureAnime(bootstrap), { once: true }); |
| } else { |
| ensureAnime(bootstrap); |
| } |
| })(); |
| </script> |