/** * 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(); } });