|
|
|
document.addEventListener('DOMContentLoaded', () => { |
|
|
|
window.backgroundAnimationInitialized = true; |
|
console.log('Background animation script initialized'); |
|
|
|
|
|
function initializeCanvas() { |
|
console.log('Initializing background animation canvas'); |
|
const canvas = document.getElementById('background-canvas'); |
|
if (!canvas) { |
|
console.error('Background animation canvas not found!'); |
|
return; |
|
} |
|
|
|
const ctx = canvas.getContext('2d'); |
|
if (!ctx) { |
|
console.error('Could not get 2D context for background animation canvas'); |
|
return; |
|
} |
|
|
|
|
|
const container = canvas.parentElement; |
|
if (container) { |
|
canvas.width = container.clientWidth || 800; |
|
canvas.height = container.clientHeight || 400; |
|
} else { |
|
canvas.width = 800; |
|
canvas.height = 400; |
|
} |
|
|
|
|
|
ctx.clearRect(0, 0, canvas.width, canvas.height); |
|
|
|
|
|
resetAnimation(); |
|
updateCanvas(); |
|
} |
|
|
|
|
|
if (typeof window !== 'undefined') { |
|
window.initBackgroundCanvas = initializeCanvas; |
|
} |
|
|
|
|
|
const canvas = document.getElementById('background-canvas'); |
|
const ctx = canvas.getContext('2d'); |
|
|
|
|
|
const startButton = document.getElementById('start-background-animation'); |
|
const pauseButton = document.getElementById('pause-background-animation'); |
|
const resetButton = document.getElementById('reset-background-animation'); |
|
|
|
|
|
const neuronCountSlider = document.getElementById('neuron-count'); |
|
const neuronCountValue = document.getElementById('neuron-count-value'); |
|
const connectionDistanceSlider = document.getElementById('connection-distance'); |
|
const connectionDistanceValue = document.getElementById('connection-distance-value'); |
|
const firingSpeedSlider = document.getElementById('firing-speed'); |
|
const firingSpeedValue = document.getElementById('firing-speed-value'); |
|
const firingColorSelect = document.getElementById('firing-color'); |
|
|
|
|
|
const activeNeuronsCount = document.getElementById('active-neurons-count'); |
|
const connectionsCount = document.getElementById('connections-count'); |
|
const firingRateElement = document.getElementById('firing-rate'); |
|
|
|
|
|
let animationState = { |
|
running: false, |
|
neurons: [], |
|
connections: [], |
|
config: { |
|
neuronCount: 150, |
|
connectionDistance: 100, |
|
firingSpeed: 5, |
|
firingColor: 'blue' |
|
}, |
|
stats: { |
|
activeNeurons: 0, |
|
connectionCount: 0, |
|
firingRate: 0, |
|
firingHistory: [] |
|
}, |
|
animationFrameId: null, |
|
lastTimestamp: 0 |
|
}; |
|
|
|
|
|
class Neuron { |
|
constructor(x, y) { |
|
this.x = x; |
|
this.y = y; |
|
this.radius = 3 + Math.random() * 2; |
|
this.connections = []; |
|
this.firing = false; |
|
this.fireProgress = 0; |
|
this.lastFireTime = 0; |
|
this.refractionPeriod = 500 + Math.random() * 1500; |
|
this.activated = false; |
|
this.activationLevel = 0; |
|
this.threshold = 0.4 + Math.random() * 0.3; |
|
this.speedMultiplier = 0.8 + Math.random() * 0.4; |
|
this.activationDecay = 0.02; |
|
} |
|
|
|
|
|
addConnection(neuron, strength) { |
|
this.connections.push({ |
|
target: neuron, |
|
strength: strength, |
|
active: false, |
|
progress: 0 |
|
}); |
|
} |
|
|
|
|
|
update(deltaTime, speed) { |
|
|
|
const timeIncrement = deltaTime * (speed / 5); |
|
|
|
|
|
if (this.activationLevel > 0 && !this.firing) { |
|
this.activationLevel = Math.max(0, this.activationLevel - this.activationDecay * (timeIncrement / 16)); |
|
} |
|
|
|
|
|
const currentTime = Date.now(); |
|
if (!this.firing && this.activationLevel >= this.threshold && |
|
(currentTime - this.lastFireTime > this.refractionPeriod)) { |
|
this.firing = true; |
|
this.fireProgress = 0; |
|
this.lastFireTime = currentTime; |
|
animationState.stats.firingHistory.push(currentTime); |
|
} |
|
|
|
|
|
if (this.firing) { |
|
this.fireProgress += 0.05 * this.speedMultiplier * (timeIncrement / 16); |
|
|
|
|
|
if (this.fireProgress >= 1) { |
|
this.firing = false; |
|
this.activationLevel = 0; |
|
|
|
|
|
this.connections.forEach(conn => { |
|
if (Math.random() < conn.strength) { |
|
conn.active = true; |
|
conn.progress = 0; |
|
} |
|
}); |
|
} |
|
} |
|
|
|
|
|
this.connections.forEach(conn => { |
|
if (conn.active) { |
|
conn.progress += 0.04 * this.speedMultiplier * (timeIncrement / 16); |
|
|
|
|
|
if (conn.progress >= 1) { |
|
conn.active = false; |
|
conn.target.activationLevel += conn.strength; |
|
} |
|
} |
|
}); |
|
} |
|
|
|
|
|
draw(ctx, colorScheme) { |
|
|
|
this.connections.forEach(conn => { |
|
const { target, active, progress, strength } = conn; |
|
|
|
|
|
if (active) { |
|
|
|
const lineWidth = 1 + strength * 1.5; |
|
|
|
|
|
const signalX = this.x + (target.x - this.x) * progress; |
|
const signalY = this.y + (target.y - this.y) * progress; |
|
|
|
|
|
ctx.beginPath(); |
|
ctx.moveTo(this.x, this.y); |
|
ctx.lineTo(target.x, target.y); |
|
ctx.strokeStyle = `rgba(200, 200, 200, ${strength * 0.3})`; |
|
ctx.lineWidth = 0.5; |
|
ctx.stroke(); |
|
|
|
|
|
ctx.beginPath(); |
|
ctx.arc(signalX, signalY, lineWidth + 1, 0, Math.PI * 2); |
|
|
|
|
|
if (colorScheme === 'rainbow') { |
|
const hue = (Date.now() / 20) % 360; |
|
ctx.fillStyle = `hsl(${hue}, 80%, 60%)`; |
|
} else { |
|
switch (colorScheme) { |
|
case 'blue': |
|
ctx.fillStyle = `rgba(52, 152, 219, ${0.7 + progress * 0.3})`; |
|
break; |
|
case 'purple': |
|
ctx.fillStyle = `rgba(155, 89, 182, ${0.7 + progress * 0.3})`; |
|
break; |
|
case 'green': |
|
ctx.fillStyle = `rgba(46, 204, 113, ${0.7 + progress * 0.3})`; |
|
break; |
|
default: |
|
ctx.fillStyle = `rgba(52, 152, 219, ${0.7 + progress * 0.3})`; |
|
} |
|
} |
|
ctx.fill(); |
|
|
|
} else if (this.firing || this.activationLevel > 0.2) { |
|
|
|
ctx.beginPath(); |
|
ctx.moveTo(this.x, this.y); |
|
ctx.lineTo(target.x, target.y); |
|
ctx.strokeStyle = `rgba(200, 200, 200, ${strength * 0.5})`; |
|
ctx.lineWidth = 0.5; |
|
ctx.stroke(); |
|
} else { |
|
|
|
ctx.beginPath(); |
|
ctx.moveTo(this.x, this.y); |
|
ctx.lineTo(target.x, target.y); |
|
ctx.strokeStyle = `rgba(200, 200, 200, ${strength * 0.2})`; |
|
ctx.lineWidth = 0.2; |
|
ctx.stroke(); |
|
} |
|
}); |
|
|
|
|
|
ctx.beginPath(); |
|
ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2); |
|
|
|
|
|
if (this.firing) { |
|
|
|
if (colorScheme === 'rainbow') { |
|
const hue = (Date.now() / 20) % 360; |
|
ctx.fillStyle = `hsl(${hue}, 80%, 60%)`; |
|
} else { |
|
switch (colorScheme) { |
|
case 'blue': |
|
ctx.fillStyle = `rgba(52, 152, 219, ${0.7 + this.fireProgress * 0.3})`; |
|
break; |
|
case 'purple': |
|
ctx.fillStyle = `rgba(155, 89, 182, ${0.7 + this.fireProgress * 0.3})`; |
|
break; |
|
case 'green': |
|
ctx.fillStyle = `rgba(46, 204, 113, ${0.7 + this.fireProgress * 0.3})`; |
|
break; |
|
default: |
|
ctx.fillStyle = `rgba(52, 152, 219, ${0.7 + this.fireProgress * 0.3})`; |
|
} |
|
} |
|
|
|
|
|
ctx.shadowColor = ctx.fillStyle; |
|
ctx.shadowBlur = 10; |
|
} else if (this.activationLevel > 0) { |
|
|
|
ctx.fillStyle = `rgba(127, 140, 141, ${0.3 + this.activationLevel * 0.7})`; |
|
ctx.shadowBlur = 0; |
|
} else { |
|
|
|
ctx.fillStyle = 'rgba(127, 140, 141, 0.2)'; |
|
ctx.shadowBlur = 0; |
|
} |
|
|
|
ctx.fill(); |
|
ctx.shadowBlur = 0; |
|
} |
|
} |
|
|
|
|
|
function initNetwork() { |
|
|
|
animationState.neurons = []; |
|
animationState.connections = []; |
|
animationState.stats.activeNeurons = 0; |
|
animationState.stats.connectionCount = 0; |
|
animationState.stats.firingHistory = []; |
|
|
|
const { neuronCount, connectionDistance } = animationState.config; |
|
|
|
|
|
for (let i = 0; i < neuronCount; i++) { |
|
const x = Math.random() * canvas.width; |
|
const y = Math.random() * canvas.height; |
|
animationState.neurons.push(new Neuron(x, y)); |
|
} |
|
|
|
|
|
let connectionCount = 0; |
|
|
|
animationState.neurons.forEach(neuron => { |
|
animationState.neurons.forEach(target => { |
|
if (neuron !== target) { |
|
|
|
const dx = neuron.x - target.x; |
|
const dy = neuron.y - target.y; |
|
const distance = Math.sqrt(dx * dx + dy * dy); |
|
|
|
|
|
if (distance < connectionDistance) { |
|
const probability = 1 - (distance / connectionDistance); |
|
if (Math.random() < probability * 0.3) { |
|
|
|
const strength = 0.2 + (1 - distance / connectionDistance) * 0.6; |
|
neuron.addConnection(target, strength); |
|
connectionCount++; |
|
} |
|
} |
|
} |
|
}); |
|
}); |
|
|
|
animationState.stats.connectionCount = connectionCount; |
|
|
|
|
|
for (let i = 0; i < 3; i++) { |
|
const randomIndex = Math.floor(Math.random() * animationState.neurons.length); |
|
animationState.neurons[randomIndex].firing = true; |
|
animationState.neurons[randomIndex].lastFireTime = Date.now(); |
|
} |
|
|
|
|
|
updateStatsDisplay(); |
|
} |
|
|
|
|
|
function resizeCanvas() { |
|
const container = canvas.parentElement; |
|
canvas.width = container.clientWidth; |
|
canvas.height = container.clientHeight; |
|
|
|
|
|
if (animationState.neurons.length > 0) { |
|
initNetwork(); |
|
} |
|
} |
|
|
|
|
|
function updateStatsDisplay() { |
|
if (!activeNeuronsCount || !connectionsCount || !firingRateElement) return; |
|
|
|
|
|
const activeCount = animationState.neurons.filter(n => n.firing || n.activationLevel > 0.2).length; |
|
animationState.stats.activeNeurons = activeCount; |
|
|
|
|
|
const now = Date.now(); |
|
const recentFirings = animationState.stats.firingHistory.filter(time => now - time < 1000).length; |
|
animationState.stats.firingRate = recentFirings; |
|
|
|
|
|
animationState.stats.firingHistory = animationState.stats.firingHistory.filter(time => now - time < 1000); |
|
|
|
|
|
activeNeuronsCount.textContent = activeCount; |
|
connectionsCount.textContent = animationState.stats.connectionCount; |
|
firingRateElement.textContent = `${recentFirings} Hz`; |
|
} |
|
|
|
|
|
function animate(timestamp) { |
|
if (!animationState.running) return; |
|
|
|
|
|
const deltaTime = timestamp - (animationState.lastTimestamp || timestamp); |
|
animationState.lastTimestamp = timestamp; |
|
|
|
|
|
ctx.clearRect(0, 0, canvas.width, canvas.height); |
|
|
|
|
|
animationState.neurons.forEach(neuron => { |
|
neuron.update(deltaTime, animationState.config.firingSpeed); |
|
neuron.draw(ctx, animationState.config.firingColor); |
|
}); |
|
|
|
|
|
if (Math.random() < 0.01 * (animationState.config.firingSpeed / 5)) { |
|
const randomIndex = Math.floor(Math.random() * animationState.neurons.length); |
|
const randomNeuron = animationState.neurons[randomIndex]; |
|
|
|
if (!randomNeuron.firing && Date.now() - randomNeuron.lastFireTime > randomNeuron.refractionPeriod) { |
|
randomNeuron.activationLevel = randomNeuron.threshold; |
|
} |
|
} |
|
|
|
|
|
if (timestamp % 500 < 20) { |
|
updateStatsDisplay(); |
|
} |
|
|
|
|
|
animationState.animationFrameId = requestAnimationFrame(animate); |
|
} |
|
|
|
|
|
function startAnimation() { |
|
if (!animationState.running) { |
|
animationState.running = true; |
|
animationState.lastTimestamp = 0; |
|
animationState.animationFrameId = requestAnimationFrame(animate); |
|
|
|
startButton.disabled = true; |
|
pauseButton.disabled = false; |
|
resetButton.disabled = false; |
|
} |
|
} |
|
|
|
|
|
function pauseAnimation() { |
|
if (animationState.running) { |
|
animationState.running = false; |
|
if (animationState.animationFrameId) { |
|
cancelAnimationFrame(animationState.animationFrameId); |
|
} |
|
|
|
startButton.disabled = false; |
|
pauseButton.disabled = true; |
|
resetButton.disabled = false; |
|
} |
|
} |
|
|
|
|
|
function resetAnimation() { |
|
pauseAnimation(); |
|
initNetwork(); |
|
|
|
startButton.disabled = false; |
|
pauseButton.disabled = true; |
|
resetButton.disabled = false; |
|
} |
|
|
|
|
|
function initVisualization() { |
|
if (!canvas) return; |
|
|
|
resizeCanvas(); |
|
window.addEventListener('resize', resizeCanvas); |
|
|
|
|
|
if (neuronCountSlider) { |
|
animationState.config.neuronCount = parseInt(neuronCountSlider.value, 10); |
|
neuronCountValue.textContent = animationState.config.neuronCount; |
|
} |
|
|
|
if (connectionDistanceSlider) { |
|
animationState.config.connectionDistance = parseInt(connectionDistanceSlider.value, 10); |
|
connectionDistanceValue.textContent = animationState.config.connectionDistance; |
|
} |
|
|
|
if (firingSpeedSlider) { |
|
animationState.config.firingSpeed = parseInt(firingSpeedSlider.value, 10); |
|
firingSpeedValue.textContent = animationState.config.firingSpeed; |
|
} |
|
|
|
if (firingColorSelect) { |
|
animationState.config.firingColor = firingColorSelect.value; |
|
} |
|
|
|
|
|
initNetwork(); |
|
|
|
|
|
startButton.disabled = false; |
|
pauseButton.disabled = true; |
|
resetButton.disabled = true; |
|
} |
|
|
|
|
|
function setupControlListeners() { |
|
if (neuronCountSlider) { |
|
neuronCountSlider.addEventListener('input', () => { |
|
animationState.config.neuronCount = parseInt(neuronCountSlider.value, 10); |
|
neuronCountValue.textContent = animationState.config.neuronCount; |
|
}); |
|
|
|
neuronCountSlider.addEventListener('change', () => { |
|
|
|
resetAnimation(); |
|
}); |
|
} |
|
|
|
if (connectionDistanceSlider) { |
|
connectionDistanceSlider.addEventListener('input', () => { |
|
animationState.config.connectionDistance = parseInt(connectionDistanceSlider.value, 10); |
|
connectionDistanceValue.textContent = animationState.config.connectionDistance; |
|
}); |
|
|
|
connectionDistanceSlider.addEventListener('change', () => { |
|
resetAnimation(); |
|
}); |
|
} |
|
|
|
if (firingSpeedSlider) { |
|
firingSpeedSlider.addEventListener('input', () => { |
|
animationState.config.firingSpeed = parseInt(firingSpeedSlider.value, 10); |
|
firingSpeedValue.textContent = animationState.config.firingSpeed; |
|
}); |
|
} |
|
|
|
if (firingColorSelect) { |
|
firingColorSelect.addEventListener('change', () => { |
|
animationState.config.firingColor = firingColorSelect.value; |
|
}); |
|
} |
|
|
|
|
|
if (startButton) { |
|
startButton.addEventListener('click', startAnimation); |
|
} |
|
|
|
if (pauseButton) { |
|
pauseButton.addEventListener('click', pauseAnimation); |
|
} |
|
|
|
if (resetButton) { |
|
resetButton.addEventListener('click', resetAnimation); |
|
} |
|
|
|
|
|
document.addEventListener('tabSwitch', (e) => { |
|
if (e.detail.tab === 'background-animation') { |
|
|
|
if (animationState.neurons.length === 0) { |
|
initNetwork(); |
|
} |
|
|
|
if (!animationState.running) { |
|
startAnimation(); |
|
} |
|
} else if (animationState.running) { |
|
|
|
pauseAnimation(); |
|
} |
|
}); |
|
} |
|
|
|
|
|
initVisualization(); |
|
setupControlListeners(); |
|
}); |