Overhaul website to modernize design, integrate SVG visualizations, and enhance KI functionality; update documentation for MindMapProjekt.

This commit is contained in:
2025-04-27 15:09:29 +02:00
parent 88f8e98df0
commit 968515ce2b
79 changed files with 110 additions and 623 deletions

View File

@@ -0,0 +1,813 @@
/**
* Neural Network Background Animation
* Modern, darker, mystical theme using WebGL
* Subtle flowing network aesthetic
*/
class NeuralNetworkBackground {
constructor() {
// Canvas setup
this.canvas = document.createElement('canvas');
this.canvas.id = 'neural-network-background';
this.canvas.style.position = 'fixed';
this.canvas.style.top = '0';
this.canvas.style.left = '0';
this.canvas.style.width = '100%';
this.canvas.style.height = '100%';
this.canvas.style.zIndex = '-10'; // Ensure it's behind content but visible
this.canvas.style.pointerEvents = 'none';
this.canvas.style.opacity = '1'; // Force visibility
// If canvas already exists, remove it first
const existingCanvas = document.getElementById('neural-network-background');
if (existingCanvas) {
existingCanvas.remove();
}
// Append to body as first child to ensure it's behind everything
if (document.body.firstChild) {
document.body.insertBefore(this.canvas, document.body.firstChild);
} else {
document.body.appendChild(this.canvas);
}
// WebGL context
this.gl = this.canvas.getContext('webgl') || this.canvas.getContext('experimental-webgl');
if (!this.gl) {
console.warn('WebGL not supported, falling back to canvas rendering');
this.gl = null;
this.ctx = this.canvas.getContext('2d');
this.useWebGL = false;
} else {
this.useWebGL = true;
}
// Animation properties
this.nodes = [];
this.connections = [];
this.flows = []; // Flow animations along connections
this.animationFrameId = null;
this.isDarkMode = true; // Always use dark mode for the background
// Colors - Updated to be more subtle
this.darkModeColors = {
background: '#050a14', // Darker blue-black background
nodeColor: '#4a5568', // Darker nodes for subtlety
nodePulse: '#718096', // Subtle blue pulse
connectionColor: '#2d3748', // Darker connections
flowColor: '#4a88ff80' // Semi-transparent flow highlight
};
this.lightModeColors = {
background: '#f9fafb',
nodeColor: '#7c3aed',
nodePulse: '#8b5cf6',
connectionColor: '#a78bfa',
flowColor: '#c4b5fd'
};
// Config - Updated to be more flowing and subtle
this.config = {
nodeCount: 100, // Slightly fewer nodes for cleaner look
nodeSize: 0.8, // Smaller nodes
nodeVariation: 0.5, // Less variation for uniformity
connectionDistance: 200, // Longer connections for better flow
connectionOpacity: 0.2, // More subtle connections
animationSpeed: 0.08, // Much slower movement
pulseSpeed: 0.004, // Slower pulse
flowSpeed: 0.6, // Speed of flow animations
flowDensity: 0.001, // How often new flows start (lower = less frequent)
flowLength: 0.2 // Length of the flow (percentage of the connection)
};
// Initialize
this.init();
// Event listeners
window.addEventListener('resize', this.resizeCanvas.bind(this));
document.addEventListener('darkModeToggled', (event) => {
this.isDarkMode = event.detail.isDark;
});
// Log that the background is initialized
console.log('Neural Network Background initialized');
}
init() {
this.resizeCanvas();
if (this.useWebGL) {
this.initWebGL();
}
this.createNodes();
this.createConnections();
this.startAnimation();
}
resizeCanvas() {
const pixelRatio = window.devicePixelRatio || 1;
const width = window.innerWidth;
const height = window.innerHeight;
this.canvas.style.width = width + 'px';
this.canvas.style.height = height + 'px';
this.canvas.width = width * pixelRatio;
this.canvas.height = height * pixelRatio;
if (this.useWebGL) {
this.gl.viewport(0, 0, this.canvas.width, this.canvas.height);
} else if (this.ctx) {
this.ctx.scale(pixelRatio, pixelRatio);
}
// Recalculate node positions after resize
if (this.nodes.length) {
this.createNodes();
this.createConnections();
}
}
initWebGL() {
// Vertex shader
const vsSource = `
attribute vec2 aVertexPosition;
attribute float aPointSize;
uniform vec2 uResolution;
void main() {
// Convert from pixel to clip space
vec2 position = (aVertexPosition / uResolution) * 2.0 - 1.0;
// Flip Y coordinate
position.y = -position.y;
gl_Position = vec4(position, 0, 1);
gl_PointSize = aPointSize;
}
`;
// Fragment shader - Softer glow effect
const fsSource = `
precision mediump float;
uniform vec4 uColor;
void main() {
float distance = length(gl_PointCoord - vec2(0.5, 0.5));
// Softer glow with smoother falloff
float alpha = 1.0 - smoothstep(0.1, 0.5, distance);
alpha = pow(alpha, 1.5); // Make the glow even softer
gl_FragColor = vec4(uColor.rgb, uColor.a * alpha);
}
`;
// Initialize shaders
const vertexShader = this.loadShader(this.gl.VERTEX_SHADER, vsSource);
const fragmentShader = this.loadShader(this.gl.FRAGMENT_SHADER, fsSource);
// Create shader program
this.shaderProgram = this.gl.createProgram();
this.gl.attachShader(this.shaderProgram, vertexShader);
this.gl.attachShader(this.shaderProgram, fragmentShader);
this.gl.linkProgram(this.shaderProgram);
if (!this.gl.getProgramParameter(this.shaderProgram, this.gl.LINK_STATUS)) {
console.error('Unable to initialize the shader program: ' +
this.gl.getProgramInfoLog(this.shaderProgram));
return;
}
// Get attribute and uniform locations
this.programInfo = {
program: this.shaderProgram,
attribLocations: {
vertexPosition: this.gl.getAttribLocation(this.shaderProgram, 'aVertexPosition'),
pointSize: this.gl.getAttribLocation(this.shaderProgram, 'aPointSize')
},
uniformLocations: {
resolution: this.gl.getUniformLocation(this.shaderProgram, 'uResolution'),
color: this.gl.getUniformLocation(this.shaderProgram, 'uColor')
}
};
// Create buffers
this.positionBuffer = this.gl.createBuffer();
this.sizeBuffer = this.gl.createBuffer();
// Set clear color for WebGL context
const bgColor = this.hexToRgb(this.darkModeColors.background);
this.gl.clearColor(bgColor.r/255, bgColor.g/255, bgColor.b/255, 1.0);
}
loadShader(type, source) {
const shader = this.gl.createShader(type);
this.gl.shaderSource(shader, source);
this.gl.compileShader(shader);
if (!this.gl.getShaderParameter(shader, this.gl.COMPILE_STATUS)) {
console.error('An error occurred compiling the shaders: ' +
this.gl.getShaderInfoLog(shader));
this.gl.deleteShader(shader);
return null;
}
return shader;
}
createNodes() {
this.nodes = [];
const width = this.canvas.width / (window.devicePixelRatio || 1);
const height = this.canvas.height / (window.devicePixelRatio || 1);
// Create nodes with random positions and properties
for (let i = 0; i < this.config.nodeCount; i++) {
const node = {
x: Math.random() * width,
y: Math.random() * height,
size: this.config.nodeSize + Math.random() * this.config.nodeVariation,
speed: {
x: (Math.random() - 0.5) * this.config.animationSpeed,
y: (Math.random() - 0.5) * this.config.animationSpeed
},
pulsePhase: Math.random() * Math.PI * 2, // Random starting phase
connections: []
};
this.nodes.push(node);
}
}
createConnections() {
this.connections = [];
this.flows = []; // Reset flows
// Create connections between nearby nodes
for (let i = 0; i < this.nodes.length; i++) {
const nodeA = this.nodes[i];
nodeA.connections = [];
for (let j = i + 1; j < this.nodes.length; j++) {
const nodeB = this.nodes[j];
const dx = nodeB.x - nodeA.x;
const dy = nodeB.y - nodeA.y;
const distance = Math.sqrt(dx * dx + dy * dy);
if (distance < this.config.connectionDistance) {
// Create connection
const connection = {
from: i,
to: j,
distance: distance,
opacity: Math.max(0, 1 - distance / this.config.connectionDistance) * this.config.connectionOpacity,
hasFlow: false // Each connection can have a flow
};
this.connections.push(connection);
nodeA.connections.push(j);
nodeB.connections.push(i);
}
}
}
}
startAnimation() {
this.animationFrameId = requestAnimationFrame(this.animate.bind(this));
}
animate() {
// Update nodes
const width = this.canvas.width / (window.devicePixelRatio || 1);
const height = this.canvas.height / (window.devicePixelRatio || 1);
// Update node positions - slower, more flowing movement
for (let i = 0; i < this.nodes.length; i++) {
const node = this.nodes[i];
// Move node with slight randomness for organic feel
node.speed.x += (Math.random() - 0.5) * 0.001;
node.speed.y += (Math.random() - 0.5) * 0.001;
// Dampen speeds for stability
node.speed.x *= 0.99;
node.speed.y *= 0.99;
// Apply speed limits
node.speed.x = Math.max(-this.config.animationSpeed, Math.min(this.config.animationSpeed, node.speed.x));
node.speed.y = Math.max(-this.config.animationSpeed, Math.min(this.config.animationSpeed, node.speed.y));
// Move node
node.x += node.speed.x;
node.y += node.speed.y;
// Boundary check with smooth bounce
if (node.x < 0 || node.x > width) {
node.speed.x *= -0.8; // Softer bounce
node.x = Math.max(0, Math.min(node.x, width));
}
if (node.y < 0 || node.y > height) {
node.speed.y *= -0.8; // Softer bounce
node.y = Math.max(0, Math.min(node.y, height));
}
// Update pulse phase
node.pulsePhase += this.config.pulseSpeed;
if (node.pulsePhase > Math.PI * 2) {
node.pulsePhase -= Math.PI * 2;
}
}
// Update flows
this.updateFlows();
// Occasionally create new flows along connections
if (Math.random() < this.config.flowDensity) {
this.createNewFlow();
}
// Recalculate connections occasionally for a living network
if (Math.random() < 0.01) { // Only recalculate 1% of the time for performance
this.createConnections();
}
// Render
if (this.useWebGL) {
this.renderWebGL();
} else {
this.renderCanvas();
}
// Continue animation
this.animationFrameId = requestAnimationFrame(this.animate.bind(this));
}
// New method to update flow animations
updateFlows() {
// Update existing flows
for (let i = this.flows.length - 1; i >= 0; i--) {
const flow = this.flows[i];
flow.progress += this.config.flowSpeed / flow.connection.distance;
// Remove completed flows
if (flow.progress > 1.0) {
this.flows.splice(i, 1);
}
}
}
// New method to create flow animations
createNewFlow() {
if (this.connections.length === 0) return;
// Select a random connection with preference for more connected nodes
let connectionIdx = Math.floor(Math.random() * this.connections.length);
let attempts = 0;
// Try to find a connection with more connected nodes
while (attempts < 5) {
const testIdx = Math.floor(Math.random() * this.connections.length);
const testConn = this.connections[testIdx];
const fromNode = this.nodes[testConn.from];
if (fromNode.connections.length > 2) {
connectionIdx = testIdx;
break;
}
attempts++;
}
const connection = this.connections[connectionIdx];
// Create a new flow along this connection
this.flows.push({
connection: connection,
progress: 0,
direction: Math.random() > 0.5, // Randomly decide direction
length: this.config.flowLength + Math.random() * 0.1 // Slightly vary lengths
});
}
renderWebGL() {
this.gl.clear(this.gl.COLOR_BUFFER_BIT);
const width = this.canvas.width / (window.devicePixelRatio || 1);
const height = this.canvas.height / (window.devicePixelRatio || 1);
// Select shader program
this.gl.useProgram(this.programInfo.program);
// Set resolution uniform
this.gl.uniform2f(this.programInfo.uniformLocations.resolution, width, height);
// Draw connections first (behind nodes)
this.renderConnectionsWebGL();
// Draw flows on top of connections
this.renderFlowsWebGL();
// Draw nodes
this.renderNodesWebGL();
}
renderNodesWebGL() {
// Prepare node positions for WebGL
const positions = new Float32Array(this.nodes.length * 2);
const sizes = new Float32Array(this.nodes.length);
for (let i = 0; i < this.nodes.length; i++) {
const node = this.nodes[i];
positions[i * 2] = node.x;
positions[i * 2 + 1] = node.y;
// Size with subtle pulse effect
const pulse = Math.sin(node.pulsePhase) * 0.2 + 1;
sizes[i] = node.size * pulse * (node.connections.length > 3 ? 1.3 : 1);
}
// Bind position buffer
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.positionBuffer);
this.gl.bufferData(this.gl.ARRAY_BUFFER, positions, this.gl.STATIC_DRAW);
this.gl.vertexAttribPointer(
this.programInfo.attribLocations.vertexPosition,
2, // components per vertex
this.gl.FLOAT, // data type
false, // normalize
0, // stride
0 // offset
);
this.gl.enableVertexAttribArray(this.programInfo.attribLocations.vertexPosition);
// Bind size buffer
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.sizeBuffer);
this.gl.bufferData(this.gl.ARRAY_BUFFER, sizes, this.gl.STATIC_DRAW);
this.gl.vertexAttribPointer(
this.programInfo.attribLocations.pointSize,
1, // components per vertex
this.gl.FLOAT, // data type
false, // normalize
0, // stride
0 // offset
);
this.gl.enableVertexAttribArray(this.programInfo.attribLocations.pointSize);
// Set node color - more subtle
const colorObj = this.isDarkMode ? this.darkModeColors : this.lightModeColors;
const nodeColor = this.hexToRgb(colorObj.nodeColor);
this.gl.uniform4f(
this.programInfo.uniformLocations.color,
nodeColor.r / 255,
nodeColor.g / 255,
nodeColor.b / 255,
0.7 // Lower opacity for subtlety
);
// Draw nodes
this.gl.enable(this.gl.BLEND);
this.gl.blendFunc(this.gl.SRC_ALPHA, this.gl.ONE); // Additive blending for glow
this.gl.drawArrays(this.gl.POINTS, 0, this.nodes.length);
}
renderConnectionsWebGL() {
// For each connection, draw a line
for (const connection of this.connections) {
const fromNode = this.nodes[connection.from];
const toNode = this.nodes[connection.to];
// Line positions
const positions = new Float32Array([
fromNode.x, fromNode.y,
toNode.x, toNode.y
]);
// Bind position buffer
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.positionBuffer);
this.gl.bufferData(this.gl.ARRAY_BUFFER, positions, this.gl.STATIC_DRAW);
this.gl.vertexAttribPointer(
this.programInfo.attribLocations.vertexPosition,
2, // components per vertex
this.gl.FLOAT, // data type
false, // normalize
0, // stride
0 // offset
);
this.gl.enableVertexAttribArray(this.programInfo.attribLocations.vertexPosition);
// Disable point size attribute for lines
this.gl.disableVertexAttribArray(this.programInfo.attribLocations.pointSize);
// Set line color with connection opacity - darker, more subtle
const colorObj = this.isDarkMode ? this.darkModeColors : this.lightModeColors;
const lineColor = this.hexToRgb(colorObj.connectionColor);
this.gl.uniform4f(
this.programInfo.uniformLocations.color,
lineColor.r / 255,
lineColor.g / 255,
lineColor.b / 255,
connection.opacity * 0.8 // Reduced for subtlety
);
// Draw the line
this.gl.enable(this.gl.BLEND);
this.gl.blendFunc(this.gl.SRC_ALPHA, this.gl.ONE);
this.gl.lineWidth(1);
this.gl.drawArrays(this.gl.LINES, 0, 2);
}
}
// New method to render the flowing animations
renderFlowsWebGL() {
// For each flow, draw a segment along its connection
for (const flow of this.flows) {
const connection = flow.connection;
const fromNode = this.nodes[connection.from];
const toNode = this.nodes[connection.to];
// Calculate flow position
const startProgress = flow.progress;
const endProgress = Math.min(1, startProgress + flow.length);
// If flow hasn't started yet or has finished
if (startProgress >= 1 || endProgress <= 0) continue;
// Calculate actual positions
const direction = flow.direction ? 1 : -1;
let p1, p2;
if (direction > 0) {
p1 = {
x: fromNode.x + (toNode.x - fromNode.x) * startProgress,
y: fromNode.y + (toNode.y - fromNode.y) * startProgress
};
p2 = {
x: fromNode.x + (toNode.x - fromNode.x) * endProgress,
y: fromNode.y + (toNode.y - fromNode.y) * endProgress
};
} else {
p1 = {
x: toNode.x + (fromNode.x - toNode.x) * startProgress,
y: toNode.y + (fromNode.y - toNode.y) * startProgress
};
p2 = {
x: toNode.x + (fromNode.x - toNode.x) * endProgress,
y: toNode.y + (fromNode.y - toNode.y) * endProgress
};
}
// Line positions for the flow
const positions = new Float32Array([
p1.x, p1.y,
p2.x, p2.y
]);
// Bind position buffer
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.positionBuffer);
this.gl.bufferData(this.gl.ARRAY_BUFFER, positions, this.gl.STATIC_DRAW);
this.gl.vertexAttribPointer(
this.programInfo.attribLocations.vertexPosition,
2, // components per vertex
this.gl.FLOAT, // data type
false, // normalize
0, // stride
0 // offset
);
this.gl.enableVertexAttribArray(this.programInfo.attribLocations.vertexPosition);
// Disable point size attribute for lines
this.gl.disableVertexAttribArray(this.programInfo.attribLocations.pointSize);
// Fade the flow at the beginning and end
const fadeEdge = 0.2;
const fadeOpacity = Math.min(
startProgress / fadeEdge,
(1 - endProgress) / fadeEdge,
1
);
// Flow color - subtle glow
const colorObj = this.isDarkMode ? this.darkModeColors : this.lightModeColors;
const flowColor = this.hexToRgb(colorObj.flowColor);
this.gl.uniform4f(
this.programInfo.uniformLocations.color,
flowColor.r / 255,
flowColor.g / 255,
flowColor.b / 255,
0.4 * fadeOpacity // Subtle flow opacity
);
// Draw the flow line
this.gl.enable(this.gl.BLEND);
this.gl.blendFunc(this.gl.SRC_ALPHA, this.gl.ONE);
this.gl.lineWidth(1.5); // Slightly thicker for visibility
this.gl.drawArrays(this.gl.LINES, 0, 2);
}
}
renderCanvas() {
// Clear canvas
const width = this.canvas.width / (window.devicePixelRatio || 1);
const height = this.canvas.height / (window.devicePixelRatio || 1);
this.ctx.clearRect(0, 0, width, height);
// Set background
const backgroundColor = this.isDarkMode
? this.darkModeColors.background
: this.lightModeColors.background;
this.ctx.fillStyle = backgroundColor;
this.ctx.fillRect(0, 0, width, height);
// Draw connections
const connectionColor = this.isDarkMode
? this.darkModeColors.connectionColor
: this.lightModeColors.connectionColor;
for (const connection of this.connections) {
const fromNode = this.nodes[connection.from];
const toNode = this.nodes[connection.to];
this.ctx.beginPath();
this.ctx.moveTo(fromNode.x, fromNode.y);
this.ctx.lineTo(toNode.x, toNode.y);
const rgbColor = this.hexToRgb(connectionColor);
this.ctx.strokeStyle = `rgba(${rgbColor.r}, ${rgbColor.g}, ${rgbColor.b}, ${connection.opacity})`;
this.ctx.stroke();
}
// Draw flows
this.renderFlowsCanvas();
// Draw nodes
const nodeColor = this.isDarkMode
? this.darkModeColors.nodeColor
: this.lightModeColors.nodeColor;
const nodePulse = this.isDarkMode
? this.darkModeColors.nodePulse
: this.lightModeColors.nodePulse;
for (const node of this.nodes) {
// Node with subtle glow effect
const pulse = Math.sin(node.pulsePhase) * 0.2 + 1;
const nodeSize = node.size * pulse * (node.connections.length > 3 ? 1.3 : 1);
// Glow effect
const glow = this.ctx.createRadialGradient(
node.x, node.y, 0,
node.x, node.y, nodeSize * 2
);
const rgbNodeColor = this.hexToRgb(nodeColor);
const rgbPulseColor = this.hexToRgb(nodePulse);
glow.addColorStop(0, `rgba(${rgbPulseColor.r}, ${rgbPulseColor.g}, ${rgbPulseColor.b}, 0.6)`);
glow.addColorStop(0.5, `rgba(${rgbNodeColor.r}, ${rgbNodeColor.g}, ${rgbNodeColor.b}, 0.2)`);
glow.addColorStop(1, `rgba(${rgbNodeColor.r}, ${rgbNodeColor.g}, ${rgbNodeColor.b}, 0)`);
this.ctx.beginPath();
this.ctx.arc(node.x, node.y, nodeSize * 2, 0, Math.PI * 2);
this.ctx.fillStyle = glow;
this.ctx.fill();
// Main node
this.ctx.beginPath();
this.ctx.arc(node.x, node.y, nodeSize, 0, Math.PI * 2);
this.ctx.fillStyle = nodeColor;
this.ctx.fill();
}
}
// New method to render flows in Canvas mode
renderFlowsCanvas() {
if (!this.ctx) return;
const flowColor = this.isDarkMode
? this.darkModeColors.flowColor
: this.lightModeColors.flowColor;
const rgbFlowColor = this.hexToRgb(flowColor);
for (const flow of this.flows) {
const connection = flow.connection;
const fromNode = this.nodes[connection.from];
const toNode = this.nodes[connection.to];
// Calculate flow position
const startProgress = flow.progress;
const endProgress = Math.min(1, startProgress + flow.length);
// If flow hasn't started yet or has finished
if (startProgress >= 1 || endProgress <= 0) continue;
// Calculate actual positions
const direction = flow.direction ? 1 : -1;
let p1, p2;
if (direction > 0) {
p1 = {
x: fromNode.x + (toNode.x - fromNode.x) * startProgress,
y: fromNode.y + (toNode.y - fromNode.y) * startProgress
};
p2 = {
x: fromNode.x + (toNode.x - fromNode.x) * endProgress,
y: fromNode.y + (toNode.y - fromNode.y) * endProgress
};
} else {
p1 = {
x: toNode.x + (fromNode.x - toNode.x) * startProgress,
y: toNode.y + (fromNode.y - toNode.y) * startProgress
};
p2 = {
x: toNode.x + (fromNode.x - toNode.x) * endProgress,
y: toNode.y + (fromNode.y - toNode.y) * endProgress
};
}
// Fade the flow at the beginning and end
const fadeEdge = 0.2;
const fadeOpacity = Math.min(
startProgress / fadeEdge,
(1 - endProgress) / fadeEdge,
1
);
// Draw flow
this.ctx.beginPath();
this.ctx.moveTo(p1.x, p1.y);
this.ctx.lineTo(p2.x, p2.y);
this.ctx.strokeStyle = `rgba(${rgbFlowColor.r}, ${rgbFlowColor.g}, ${rgbFlowColor.b}, ${0.4 * fadeOpacity})`;
this.ctx.lineWidth = 1.5;
this.ctx.stroke();
}
}
// Helper method to convert hex to RGB
hexToRgb(hex) {
// Remove # if present
hex = hex.replace(/^#/, '');
// Handle rgba hex format
let alpha = 1;
if (hex.length === 8) {
alpha = parseInt(hex.slice(6, 8), 16) / 255;
hex = hex.slice(0, 6);
}
// Parse hex values
const bigint = parseInt(hex, 16);
const r = (bigint >> 16) & 255;
const g = (bigint >> 8) & 255;
const b = bigint & 255;
return { r, g, b, a: alpha };
}
// Cleanup method
destroy() {
if (this.animationFrameId) {
cancelAnimationFrame(this.animationFrameId);
}
window.removeEventListener('resize', this.resizeCanvas.bind(this));
if (this.canvas && this.canvas.parentNode) {
this.canvas.parentNode.removeChild(this.canvas);
}
if (this.gl) {
// Clean up WebGL resources
this.gl.deleteBuffer(this.positionBuffer);
this.gl.deleteBuffer(this.sizeBuffer);
this.gl.deleteProgram(this.shaderProgram);
}
}
}
// Initialize when DOM is loaded
document.addEventListener('DOMContentLoaded', () => {
// Short delay to ensure DOM is fully loaded
setTimeout(() => {
if (!window.neuralNetworkBackground) {
console.log('Creating Neural Network Background');
window.neuralNetworkBackground = new NeuralNetworkBackground();
}
}, 100);
});
// Re-initialize when page is fully loaded (for safety)
window.addEventListener('load', () => {
if (!window.neuralNetworkBackground) {
console.log('Re-initializing Neural Network Background on full load');
window.neuralNetworkBackground = new NeuralNetworkBackground();
}
});
// Clean up when window is closed
window.addEventListener('beforeunload', () => {
if (window.neuralNetworkBackground) {
window.neuralNetworkBackground.destroy();
}
});