Spaces:
Running
Running
| /** | |
| * FMN-GPT Website Interactive Visualizations | |
| * ========================================== | |
| * Canvas-based animations and interactive demos | |
| */ | |
| // ======================================== | |
| // Configuration | |
| // ======================================== | |
| const CONFIG = { | |
| colors: { | |
| accent: '#e85d3b', | |
| accentLight: '#ff8a6b', | |
| secondary: '#d4a853', | |
| secondaryLight: '#e8c87a', | |
| bg: '#faf8f5', | |
| bgAlt: '#f5f0e8', | |
| bgDark: '#1a1815', | |
| text: '#2d2a26', | |
| textLight: '#6b6560', | |
| textMuted: '#9a948d', | |
| border: '#e5e0d8', | |
| success: '#50c878', | |
| info: '#4a9eff' | |
| }, | |
| neuron: { | |
| count: 112, | |
| maxLoops: 30, | |
| layers: 6 | |
| } | |
| }; | |
| // ======================================== | |
| // Utility Functions | |
| // ======================================== | |
| function lerp(a, b, t) { | |
| return a + (b - a) * t; | |
| } | |
| function clamp(val, min, max) { | |
| return Math.max(min, Math.min(max, val)); | |
| } | |
| function randomRange(min, max) { | |
| return Math.random() * (max - min) + min; | |
| } | |
| function hexToRgba(hex, alpha) { | |
| const r = parseInt(hex.slice(1, 3), 16); | |
| const g = parseInt(hex.slice(3, 5), 16); | |
| const b = parseInt(hex.slice(5, 7), 16); | |
| return `rgba(${r}, ${g}, ${b}, ${alpha})`; | |
| } | |
| // ======================================== | |
| // Hero Neuron Canvas Animation | |
| // ======================================== | |
| class NeuronNetwork { | |
| constructor(canvas) { | |
| this.canvas = canvas; | |
| this.ctx = canvas.getContext('2d'); | |
| this.neurons = []; | |
| this.connections = []; | |
| this.particles = []; | |
| this.mouse = { x: 0, y: 0 }; | |
| this.animationId = null; | |
| this.resize(); | |
| this.init(); | |
| this.bindEvents(); | |
| this.animate(); | |
| } | |
| resize() { | |
| const rect = this.canvas.parentElement.getBoundingClientRect(); | |
| this.canvas.width = rect.width * window.devicePixelRatio; | |
| this.canvas.height = rect.height * window.devicePixelRatio; | |
| this.ctx.scale(window.devicePixelRatio, window.devicePixelRatio); | |
| this.width = rect.width; | |
| this.height = rect.height; | |
| } | |
| init() { | |
| // Create neurons | |
| const count = Math.min(50, Math.floor(this.width * this.height / 15000)); | |
| this.neurons = []; | |
| for (let i = 0; i < count; i++) { | |
| this.neurons.push({ | |
| x: randomRange(50, this.width - 50), | |
| y: randomRange(50, this.height - 50), | |
| vx: randomRange(-0.3, 0.3), | |
| vy: randomRange(-0.3, 0.3), | |
| radius: randomRange(3, 6), | |
| pulse: randomRange(0, Math.PI * 2), | |
| pulseSpeed: randomRange(0.02, 0.05) | |
| }); | |
| } | |
| // Create initial connections | |
| this.updateConnections(); | |
| } | |
| updateConnections() { | |
| this.connections = []; | |
| const maxDist = 150; | |
| for (let i = 0; i < this.neurons.length; i++) { | |
| for (let j = i + 1; j < this.neurons.length; j++) { | |
| const dx = this.neurons[i].x - this.neurons[j].x; | |
| const dy = this.neurons[i].y - this.neurons[j].y; | |
| const dist = Math.sqrt(dx * dx + dy * dy); | |
| if (dist < maxDist) { | |
| this.connections.push({ | |
| from: i, | |
| to: j, | |
| strength: 1 - dist / maxDist | |
| }); | |
| } | |
| } | |
| } | |
| } | |
| bindEvents() { | |
| window.addEventListener('resize', () => { | |
| this.resize(); | |
| this.init(); | |
| }); | |
| this.canvas.addEventListener('mousemove', (e) => { | |
| const rect = this.canvas.getBoundingClientRect(); | |
| this.mouse.x = e.clientX - rect.left; | |
| this.mouse.y = e.clientY - rect.top; | |
| }); | |
| } | |
| animate() { | |
| this.ctx.clearRect(0, 0, this.width, this.height); | |
| // Update neurons | |
| for (const neuron of this.neurons) { | |
| neuron.x += neuron.vx; | |
| neuron.y += neuron.vy; | |
| neuron.pulse += neuron.pulseSpeed; | |
| // Bounce off walls | |
| if (neuron.x < 20 || neuron.x > this.width - 20) neuron.vx *= -1; | |
| if (neuron.y < 20 || neuron.y > this.height - 20) neuron.vy *= -1; | |
| // Mouse interaction | |
| const dx = this.mouse.x - neuron.x; | |
| const dy = this.mouse.y - neuron.y; | |
| const dist = Math.sqrt(dx * dx + dy * dy); | |
| if (dist < 100 && dist > 0) { | |
| neuron.x -= dx * 0.01; | |
| neuron.y -= dy * 0.01; | |
| } | |
| } | |
| // Update connections | |
| this.updateConnections(); | |
| // Draw connections | |
| for (const conn of this.connections) { | |
| const from = this.neurons[conn.from]; | |
| const to = this.neurons[conn.to]; | |
| this.ctx.beginPath(); | |
| this.ctx.moveTo(from.x, from.y); | |
| this.ctx.lineTo(to.x, to.y); | |
| this.ctx.strokeStyle = hexToRgba(CONFIG.colors.accent, conn.strength * 0.3); | |
| this.ctx.lineWidth = conn.strength * 2; | |
| this.ctx.stroke(); | |
| } | |
| // Draw neurons | |
| for (const neuron of this.neurons) { | |
| const pulseRadius = neuron.radius + Math.sin(neuron.pulse) * 2; | |
| // Glow | |
| const gradient = this.ctx.createRadialGradient( | |
| neuron.x, neuron.y, 0, | |
| neuron.x, neuron.y, pulseRadius * 3 | |
| ); | |
| gradient.addColorStop(0, hexToRgba(CONFIG.colors.accent, 0.3)); | |
| gradient.addColorStop(1, hexToRgba(CONFIG.colors.accent, 0)); | |
| this.ctx.beginPath(); | |
| this.ctx.arc(neuron.x, neuron.y, pulseRadius * 3, 0, Math.PI * 2); | |
| this.ctx.fillStyle = gradient; | |
| this.ctx.fill(); | |
| // Core | |
| this.ctx.beginPath(); | |
| this.ctx.arc(neuron.x, neuron.y, pulseRadius, 0, Math.PI * 2); | |
| this.ctx.fillStyle = CONFIG.colors.accent; | |
| this.ctx.fill(); | |
| } | |
| this.animationId = requestAnimationFrame(() => this.animate()); | |
| } | |
| destroy() { | |
| if (this.animationId) { | |
| cancelAnimationFrame(this.animationId); | |
| } | |
| } | |
| } | |
| // ======================================== | |
| // FMN Visualization | |
| // ======================================== | |
| class FMNVisualization { | |
| constructor(canvas) { | |
| this.canvas = canvas; | |
| this.ctx = canvas.getContext('2d'); | |
| this.time = 0; | |
| this.animationId = null; | |
| this.dataFlow = []; | |
| this.resize(); | |
| this.initDataFlow(); | |
| this.animate(); | |
| } | |
| resize() { | |
| const rect = this.canvas.parentElement.getBoundingClientRect(); | |
| const dpr = window.devicePixelRatio || 1; | |
| this.canvas.width = 400 * dpr; | |
| this.canvas.height = 300 * dpr; | |
| this.ctx.scale(dpr, dpr); | |
| this.width = 400; | |
| this.height = 300; | |
| } | |
| initDataFlow() { | |
| this.dataFlow = []; | |
| for (let i = 0; i < 8; i++) { | |
| this.dataFlow.push({ | |
| progress: Math.random(), | |
| speed: 0.003 + Math.random() * 0.002, | |
| path: Math.floor(Math.random() * 3) | |
| }); | |
| } | |
| } | |
| animate() { | |
| this.ctx.clearRect(0, 0, this.width, this.height); | |
| this.time += 0.016; | |
| const centerX = this.width / 2; | |
| const centerY = this.height / 2; | |
| // Background | |
| this.ctx.fillStyle = '#faf8f5'; | |
| this.ctx.fillRect(0, 0, this.width, this.height); | |
| const inputY = centerY - 90; | |
| const w1Y = centerY - 25; | |
| const w2Y = centerY + 25; | |
| const outputY = centerY + 90; | |
| // Draw static connection lines first (faded) | |
| this.ctx.strokeStyle = 'rgba(107, 101, 96, 0.15)'; | |
| this.ctx.lineWidth = 1; | |
| // Input to W1 connections | |
| for (let i = 0; i < 5; i++) { | |
| for (let j = 0; j < 3; j++) { | |
| const x1 = centerX - 80 + i * 40; | |
| const x2 = centerX - 40 + j * 40; | |
| this.ctx.beginPath(); | |
| this.ctx.moveTo(x1, inputY + 14); | |
| this.ctx.lineTo(x2, w1Y - 14); | |
| this.ctx.stroke(); | |
| } | |
| } | |
| // W2 to output connections | |
| for (let i = 0; i < 3; i++) { | |
| for (let j = 0; j < 5; j++) { | |
| const x1 = centerX - 40 + i * 40; | |
| const x2 = centerX - 80 + j * 40; | |
| this.ctx.beginPath(); | |
| this.ctx.moveTo(x1, w2Y + 14); | |
| this.ctx.lineTo(x2, outputY - 14); | |
| this.ctx.stroke(); | |
| } | |
| } | |
| // Input layer neurons | |
| for (let i = 0; i < 5; i++) { | |
| const x = centerX - 80 + i * 40; | |
| this.drawNeuron(x, inputY, 14, '#4a9eff'); | |
| } | |
| // Projection layer 1 | |
| for (let i = 0; i < 3; i++) { | |
| const x = centerX - 40 + i * 40; | |
| this.drawNeuron(x, w1Y, 16, CONFIG.colors.accent); | |
| } | |
| // Projection layer 2 | |
| for (let i = 0; i < 3; i++) { | |
| const x = centerX - 40 + i * 40; | |
| this.drawNeuron(x, w2Y, 16, CONFIG.colors.secondary); | |
| } | |
| // Output layer neurons | |
| for (let i = 0; i < 5; i++) { | |
| const x = centerX - 80 + i * 40; | |
| this.drawNeuron(x, outputY, 14, '#50c878'); | |
| } | |
| // Operation symbol | |
| this.ctx.font = 'bold 20px Inter, sans-serif'; | |
| this.ctx.fillStyle = CONFIG.colors.textMuted; | |
| this.ctx.textAlign = 'center'; | |
| this.ctx.textBaseline = 'middle'; | |
| this.ctx.fillText('?', centerX, centerY); | |
| // Animated data flow particles | |
| for (const particle of this.dataFlow) { | |
| particle.progress += particle.speed; | |
| if (particle.progress > 1) { | |
| particle.progress = 0; | |
| particle.path = Math.floor(Math.random() * 3); | |
| } | |
| const alpha = Math.sin(particle.progress * Math.PI) * 0.8; | |
| const inputIdx = particle.path; | |
| const wIdx = particle.path % 3; | |
| const outIdx = particle.path; | |
| const inputX = centerX - 80 + inputIdx * 40; | |
| const w1X = centerX - 40 + wIdx * 40; | |
| const w2X = centerX - 40 + wIdx * 40; | |
| const outX = centerX - 80 + outIdx * 40; | |
| let px, py; | |
| if (particle.progress < 0.33) { | |
| const t = particle.progress / 0.33; | |
| px = lerp(inputX, w1X, t); | |
| py = lerp(inputY, w1Y, t); | |
| } else if (particle.progress < 0.66) { | |
| const t = (particle.progress - 0.33) / 0.33; | |
| px = lerp(w1X, w2X, t); | |
| py = lerp(w1Y, w2Y, t); | |
| } else { | |
| const t = (particle.progress - 0.66) / 0.34; | |
| px = lerp(w2X, outX, t); | |
| py = lerp(w2Y, outputY, t); | |
| } | |
| this.ctx.beginPath(); | |
| this.ctx.arc(px, py, 4, 0, Math.PI * 2); | |
| this.ctx.fillStyle = `rgba(232, 93, 59, ${alpha})`; | |
| this.ctx.fill(); | |
| } | |
| // Labels | |
| this.ctx.font = '11px Inter, sans-serif'; | |
| this.ctx.fillStyle = CONFIG.colors.textMuted; | |
| this.ctx.textAlign = 'center'; | |
| this.ctx.textBaseline = 'top'; | |
| this.ctx.fillText('Input', centerX, inputY - 30); | |
| this.ctx.fillText('[REDACTED]', centerX - 70, w1Y - 8); | |
| this.ctx.fillText('[REDACTED]', centerX + 70, w2Y - 8); | |
| this.ctx.fillText('Output', centerX, outputY + 25); | |
| this.animationId = requestAnimationFrame(() => this.animate()); | |
| } | |
| drawNeuron(x, y, radius, color) { | |
| // Subtle shadow | |
| this.ctx.beginPath(); | |
| this.ctx.arc(x, y + 2, radius, 0, Math.PI * 2); | |
| this.ctx.fillStyle = 'rgba(0, 0, 0, 0.1)'; | |
| this.ctx.fill(); | |
| // Main circle | |
| this.ctx.beginPath(); | |
| this.ctx.arc(x, y, radius, 0, Math.PI * 2); | |
| this.ctx.fillStyle = color; | |
| this.ctx.fill(); | |
| // Highlight | |
| this.ctx.beginPath(); | |
| this.ctx.arc(x - radius * 0.3, y - radius * 0.3, radius * 0.4, 0, Math.PI * 2); | |
| this.ctx.fillStyle = 'rgba(255, 255, 255, 0.3)'; | |
| this.ctx.fill(); | |
| } | |
| destroy() { | |
| if (this.animationId) { | |
| cancelAnimationFrame(this.animationId); | |
| } | |
| } | |
| } | |
| // ======================================== | |
| // Routing Visualization | |
| // ======================================== | |
| class RoutingVisualization { | |
| constructor(canvas) { | |
| this.canvas = canvas; | |
| this.ctx = canvas.getContext('2d'); | |
| this.time = 0; | |
| this.particles = []; | |
| this.animationId = null; | |
| this.resize(); | |
| this.initParticles(); | |
| this.animate(); | |
| } | |
| resize() { | |
| const dpr = window.devicePixelRatio || 1; | |
| this.canvas.width = 400 * dpr; | |
| this.canvas.height = 300 * dpr; | |
| this.ctx.scale(dpr, dpr); | |
| this.width = 400; | |
| this.height = 300; | |
| } | |
| initParticles() { | |
| this.particles = []; | |
| const layers = 6; | |
| const layerSpacing = this.width / (layers + 1); | |
| for (let i = 0; i < 12; i++) { | |
| const startLayer = Math.floor(Math.random() * layers); | |
| const startY = randomRange(60, this.height - 60); | |
| this.particles.push({ | |
| x: (startLayer + 1) * layerSpacing, | |
| y: startY, | |
| targetX: 0, | |
| targetY: 0, | |
| speed: 0.8 + Math.random() * 0.4, | |
| color: Math.random() > 0.5 ? CONFIG.colors.accent : CONFIG.colors.secondary, | |
| trail: [] | |
| }); | |
| this.setNewTarget(this.particles[i]); | |
| } | |
| } | |
| setNewTarget(particle) { | |
| const layers = 6; | |
| const layerSpacing = this.width / (layers + 1); | |
| const targetLayer = Math.floor(Math.random() * layers); | |
| particle.targetX = (targetLayer + 1) * layerSpacing; | |
| particle.targetY = randomRange(60, this.height - 60); | |
| } | |
| animate() { | |
| this.ctx.clearRect(0, 0, this.width, this.height); | |
| this.time += 0.016; | |
| // Background | |
| this.ctx.fillStyle = '#faf8f5'; | |
| this.ctx.fillRect(0, 0, this.width, this.height); | |
| const layers = 6; | |
| const layerSpacing = this.width / (layers + 1); | |
| // Draw layer columns | |
| for (let i = 1; i <= layers; i++) { | |
| const x = i * layerSpacing; | |
| // Layer column background | |
| this.ctx.fillStyle = 'rgba(229, 224, 216, 0.3)'; | |
| this.ctx.fillRect(x - 15, 40, 30, this.height - 70); | |
| // Layer label | |
| this.ctx.font = '10px Inter, sans-serif'; | |
| this.ctx.fillStyle = CONFIG.colors.textMuted; | |
| this.ctx.textAlign = 'center'; | |
| this.ctx.fillText(`L${i - 1}`, x, this.height - 15); | |
| } | |
| // Title | |
| this.ctx.font = 'bold 13px Inter, sans-serif'; | |
| this.ctx.fillStyle = CONFIG.colors.text; | |
| this.ctx.textAlign = 'center'; | |
| this.ctx.fillText('Cross-Layer Backward Routing', this.width / 2, 22); | |
| // Update and draw particles with trails | |
| for (const p of this.particles) { | |
| const dx = p.targetX - p.x; | |
| const dy = p.targetY - p.y; | |
| const dist = Math.sqrt(dx * dx + dy * dy); | |
| if (dist < 3) { | |
| this.setNewTarget(p); | |
| } else { | |
| const moveX = (dx / dist) * p.speed; | |
| const moveY = (dy / dist) * p.speed; | |
| p.x += moveX; | |
| p.y += moveY; | |
| // Add to trail | |
| p.trail.push({ x: p.x, y: p.y }); | |
| if (p.trail.length > 15) { | |
| p.trail.shift(); | |
| } | |
| } | |
| // Draw trail | |
| for (let i = 0; i < p.trail.length - 1; i++) { | |
| const alpha = (i / p.trail.length) * 0.4; | |
| this.ctx.beginPath(); | |
| this.ctx.moveTo(p.trail[i].x, p.trail[i].y); | |
| this.ctx.lineTo(p.trail[i + 1].x, p.trail[i + 1].y); | |
| this.ctx.strokeStyle = `rgba(232, 93, 59, ${alpha})`; | |
| this.ctx.lineWidth = 2; | |
| this.ctx.stroke(); | |
| } | |
| // Draw particle | |
| this.ctx.beginPath(); | |
| this.ctx.arc(p.x, p.y, 5, 0, Math.PI * 2); | |
| this.ctx.fillStyle = p.color; | |
| this.ctx.fill(); | |
| } | |
| this.animationId = requestAnimationFrame(() => this.animate()); | |
| } | |
| destroy() { | |
| if (this.animationId) { | |
| cancelAnimationFrame(this.animationId); | |
| } | |
| } | |
| } | |
| // ======================================== | |
| // Recurrent Mixer Visualization | |
| // ======================================== | |
| class RecurrentVisualization { | |
| constructor(canvas) { | |
| this.canvas = canvas; | |
| this.ctx = canvas.getContext('2d'); | |
| this.time = 0; | |
| this.states = []; | |
| this.animationId = null; | |
| this.resize(); | |
| this.initStates(); | |
| this.animate(); | |
| } | |
| resize() { | |
| const dpr = window.devicePixelRatio || 1; | |
| this.canvas.width = 400 * dpr; | |
| this.canvas.height = 300 * dpr; | |
| this.ctx.scale(dpr, dpr); | |
| this.width = 400; | |
| this.height = 300; | |
| } | |
| initStates() { | |
| this.states = []; | |
| for (let i = 0; i < 8; i++) { | |
| this.states.push({ | |
| value: randomRange(0.3, 0.7), | |
| target: randomRange(0.3, 0.7), | |
| gate: randomRange(0.2, 0.8), | |
| history: [] | |
| }); | |
| } | |
| } | |
| animate() { | |
| this.ctx.clearRect(0, 0, this.width, this.height); | |
| this.time += 0.016; | |
| // Background | |
| this.ctx.fillStyle = '#faf8f5'; | |
| this.ctx.fillRect(0, 0, this.width, this.height); | |
| const barWidth = 32; | |
| const barSpacing = 46; | |
| const startX = (this.width - (this.states.length * barSpacing)) / 2 + 5; | |
| const baseY = this.height - 50; | |
| const maxHeight = 140; | |
| // Title | |
| this.ctx.font = 'bold 13px Inter, sans-serif'; | |
| this.ctx.fillStyle = CONFIG.colors.text; | |
| this.ctx.textAlign = 'center'; | |
| this.ctx.fillText('Per-Channel Gated State', this.width / 2, 22); | |
| for (let i = 0; i < this.states.length; i++) { | |
| const state = this.states[i]; | |
| const x = startX + i * barSpacing; | |
| // Update state smoothly | |
| state.gate = 0.4 + Math.sin(this.time * 0.8 + i * 0.7) * 0.35; | |
| state.value = lerp(state.value, state.target, state.gate * 0.08); | |
| // Randomly change target | |
| if (Math.random() < 0.008) { | |
| state.target = randomRange(0.2, 0.95); | |
| } | |
| // Store history | |
| state.history.push(state.value); | |
| if (state.history.length > 50) state.history.shift(); | |
| // Draw bar background | |
| this.ctx.fillStyle = 'rgba(229, 224, 216, 0.4)'; | |
| this.ctx.beginPath(); | |
| this.ctx.roundRect(x - barWidth / 2, baseY - maxHeight, barWidth, maxHeight, 4); | |
| this.ctx.fill(); | |
| // Draw current state bar | |
| const currentHeight = maxHeight * state.value; | |
| const gradient = this.ctx.createLinearGradient(x, baseY, x, baseY - currentHeight); | |
| gradient.addColorStop(0, CONFIG.colors.accent); | |
| gradient.addColorStop(1, CONFIG.colors.accentLight); | |
| this.ctx.fillStyle = gradient; | |
| this.ctx.beginPath(); | |
| this.ctx.roundRect(x - barWidth / 2, baseY - currentHeight, barWidth, currentHeight, 4); | |
| this.ctx.fill(); | |
| // Draw gate indicator | |
| const gateHeight = 8; | |
| const gateY = baseY - maxHeight - 18; | |
| const gateFilled = state.gate * barWidth; | |
| this.ctx.fillStyle = 'rgba(229, 224, 216, 0.6)'; | |
| this.ctx.fillRect(x - barWidth / 2, gateY, barWidth, gateHeight); | |
| this.ctx.fillStyle = CONFIG.colors.success; | |
| this.ctx.fillRect(x - barWidth / 2, gateY, gateFilled, gateHeight); | |
| // Channel label | |
| this.ctx.font = '10px Inter, sans-serif'; | |
| this.ctx.fillStyle = CONFIG.colors.textMuted; | |
| this.ctx.textAlign = 'center'; | |
| this.ctx.fillText(`c${i}`, x, baseY + 15); | |
| } | |
| // Legend | |
| this.ctx.font = '10px Inter, sans-serif'; | |
| this.ctx.fillStyle = CONFIG.colors.textMuted; | |
| this.ctx.textAlign = 'left'; | |
| this.ctx.fillText('Gate:', 15, 22); | |
| this.ctx.fillStyle = CONFIG.colors.success; | |
| this.ctx.fillRect(45, 14, 25, 8); | |
| this.animationId = requestAnimationFrame(() => this.animate()); | |
| } | |
| destroy() { | |
| if (this.animationId) { | |
| cancelAnimationFrame(this.animationId); | |
| } | |
| } | |
| } | |
| // ======================================== | |
| // Loop Counter Visualization | |
| // ======================================== | |
| class LoopVisualization { | |
| constructor(canvas, slider, indicator, valueDisplay) { | |
| this.canvas = canvas; | |
| this.ctx = canvas.getContext('2d'); | |
| this.slider = slider; | |
| this.indicator = indicator; | |
| this.valueDisplay = valueDisplay; | |
| this.maxLoops = 30; | |
| this.loopCounts = []; | |
| this.animationId = null; | |
| this.resize(); | |
| this.initLoops(); | |
| this.bindEvents(); | |
| this.animate(); | |
| } | |
| resize() { | |
| const dpr = window.devicePixelRatio || 1; | |
| this.canvas.width = 400 * dpr; | |
| this.canvas.height = 300 * dpr; | |
| this.ctx.scale(dpr, dpr); | |
| this.width = 400; | |
| this.height = 300; | |
| } | |
| initLoops() { | |
| this.loopCounts = []; | |
| for (let i = 0; i < 20; i++) { | |
| this.loopCounts.push({ | |
| count: Math.floor(randomRange(0, this.maxLoops * 0.6)), | |
| targetCount: Math.floor(randomRange(5, this.maxLoops)), | |
| incrementing: Math.random() > 0.2 | |
| }); | |
| } | |
| this.updateIndicator(); | |
| } | |
| bindEvents() { | |
| if (this.slider) { | |
| this.slider.addEventListener('input', (e) => { | |
| this.maxLoops = parseInt(e.target.value); | |
| if (this.valueDisplay) { | |
| this.valueDisplay.textContent = this.maxLoops; | |
| } | |
| this.updateIndicator(); | |
| }); | |
| } | |
| } | |
| updateIndicator() { | |
| if (!this.indicator) return; | |
| this.indicator.innerHTML = ''; | |
| for (let i = 0; i < this.maxLoops; i++) { | |
| const dot = document.createElement('div'); | |
| dot.className = 'loop-dot'; | |
| if (i < this.maxLoops * 0.7) { | |
| dot.classList.add('active'); | |
| } else if (i < this.maxLoops) { | |
| dot.classList.add('exhausted'); | |
| } | |
| this.indicator.appendChild(dot); | |
| } | |
| } | |
| animate() { | |
| this.ctx.clearRect(0, 0, this.width, this.height); | |
| // Background | |
| this.ctx.fillStyle = '#faf8f5'; | |
| this.ctx.fillRect(0, 0, this.width, this.height); | |
| const cols = 5; | |
| const rows = 4; | |
| const cellWidth = this.width / cols; | |
| const cellHeight = (this.height - 50) / rows; | |
| // Title | |
| this.ctx.font = 'bold 13px Inter, sans-serif'; | |
| this.ctx.fillStyle = CONFIG.colors.text; | |
| this.ctx.textAlign = 'center'; | |
| this.ctx.fillText('Neuron Loop Budget', this.width / 2, 22); | |
| for (let i = 0; i < this.loopCounts.length; i++) { | |
| const state = this.loopCounts[i]; | |
| const col = i % cols; | |
| const row = Math.floor(i / cols); | |
| const x = col * cellWidth + cellWidth / 2; | |
| const y = row * cellHeight + cellHeight / 2 + 35; | |
| // Smoothly increment towards target | |
| if (state.incrementing) { | |
| if (state.count < state.targetCount) { | |
| state.count += 0.05; | |
| } else if (Math.random() < 0.02) { | |
| state.targetCount = Math.floor(randomRange(state.count, this.maxLoops)); | |
| } | |
| if (state.count >= this.maxLoops) { | |
| state.count = this.maxLoops; | |
| state.incrementing = false; | |
| } | |
| } | |
| const radius = 26; | |
| const progress = Math.min(state.count / this.maxLoops, 1); | |
| const exhausted = state.count >= this.maxLoops; | |
| // Background ring | |
| this.ctx.beginPath(); | |
| this.ctx.arc(x, y, radius, 0, Math.PI * 2); | |
| this.ctx.strokeStyle = 'rgba(229, 224, 216, 0.6)'; | |
| this.ctx.lineWidth = 5; | |
| this.ctx.stroke(); | |
| // Progress ring | |
| if (progress > 0) { | |
| this.ctx.beginPath(); | |
| this.ctx.arc(x, y, radius, -Math.PI / 2, -Math.PI / 2 + progress * Math.PI * 2); | |
| this.ctx.strokeStyle = exhausted ? CONFIG.colors.textMuted : CONFIG.colors.accent; | |
| this.ctx.lineWidth = 5; | |
| this.ctx.lineCap = 'round'; | |
| this.ctx.stroke(); | |
| } | |
| // Center fill | |
| this.ctx.beginPath(); | |
| this.ctx.arc(x, y, radius - 10, 0, Math.PI * 2); | |
| this.ctx.fillStyle = exhausted ? 'rgba(154, 148, 141, 0.1)' : 'rgba(232, 93, 59, 0.1)'; | |
| this.ctx.fill(); | |
| // Center count | |
| this.ctx.font = 'bold 13px Inter, sans-serif'; | |
| this.ctx.fillStyle = exhausted ? CONFIG.colors.textMuted : CONFIG.colors.text; | |
| this.ctx.textAlign = 'center'; | |
| this.ctx.textBaseline = 'middle'; | |
| this.ctx.fillText(Math.floor(state.count).toString(), x, y); | |
| } | |
| this.animationId = requestAnimationFrame(() => this.animate()); | |
| } | |
| destroy() { | |
| if (this.animationId) { | |
| cancelAnimationFrame(this.animationId); | |
| } | |
| } | |
| } | |
| // ======================================== | |
| // Tab Switching | |
| // ======================================== | |
| function initTabs() { | |
| const tabBtns = document.querySelectorAll('.tab-btn'); | |
| const tabPanes = document.querySelectorAll('.tab-pane'); | |
| tabBtns.forEach(btn => { | |
| btn.addEventListener('click', () => { | |
| const tabId = btn.dataset.tab; | |
| // Update buttons | |
| tabBtns.forEach(b => b.classList.remove('active')); | |
| btn.classList.add('active'); | |
| // Update panes | |
| tabPanes.forEach(pane => { | |
| pane.classList.remove('active'); | |
| if (pane.id === `${tabId}-pane`) { | |
| pane.classList.add('active'); | |
| } | |
| }); | |
| }); | |
| }); | |
| } | |
| // ======================================== | |
| // Scroll Animations | |
| // ======================================== | |
| function initScrollAnimations() { | |
| const observer = new IntersectionObserver((entries) => { | |
| entries.forEach(entry => { | |
| if (entry.isIntersecting) { | |
| entry.target.classList.add('visible'); | |
| } | |
| }); | |
| }, { | |
| threshold: 0.1, | |
| rootMargin: '0px 0px -50px 0px' | |
| }); | |
| document.querySelectorAll('.fade-in-up').forEach(el => { | |
| observer.observe(el); | |
| }); | |
| } | |
| // ======================================== | |
| // Initialize Everything | |
| // ======================================== | |
| let heroNetwork = null; | |
| let fmnViz = null; | |
| let routingViz = null; | |
| let recurrentViz = null; | |
| let loopViz = null; | |
| document.addEventListener('DOMContentLoaded', () => { | |
| // Hero animation | |
| const heroCanvas = document.getElementById('neuron-canvas'); | |
| if (heroCanvas) { | |
| heroNetwork = new NeuronNetwork(heroCanvas); | |
| } | |
| // Tab visualizations | |
| const fmnCanvas = document.getElementById('fmn-canvas'); | |
| if (fmnCanvas) { | |
| fmnViz = new FMNVisualization(fmnCanvas); | |
| } | |
| const routingCanvas = document.getElementById('routing-canvas'); | |
| if (routingCanvas) { | |
| routingViz = new RoutingVisualization(routingCanvas); | |
| } | |
| const recurrentCanvas = document.getElementById('recurrent-canvas'); | |
| if (recurrentCanvas) { | |
| recurrentViz = new RecurrentVisualization(recurrentCanvas); | |
| } | |
| const loopsCanvas = document.getElementById('loops-canvas'); | |
| const loopSlider = document.getElementById('loop-slider'); | |
| const loopIndicator = document.getElementById('loop-indicator'); | |
| const loopValue = document.getElementById('loop-value'); | |
| if (loopsCanvas) { | |
| loopViz = new LoopVisualization(loopsCanvas, loopSlider, loopIndicator, loopValue); | |
| } | |
| // Initialize tabs | |
| initTabs(); | |
| // Initialize scroll animations | |
| initScrollAnimations(); | |
| }); | |
| // Cleanup on page unload | |
| window.addEventListener('beforeunload', () => { | |
| if (heroNetwork) heroNetwork.destroy(); | |
| if (fmnViz) fmnViz.destroy(); | |
| if (routingViz) routingViz.destroy(); | |
| if (recurrentViz) recurrentViz.destroy(); | |
| if (loopViz) loopViz.destroy(); | |
| }); |