Homepage / main.js
CompactAI's picture
Upload 7 files
91a0086 verified
raw
history blame
30.2 kB
/**
* 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();
});