/** * Neural Network Background Animation * Modern, darker, mystical theme using WebGL * Subtle flowing network aesthetic */ class NeuralNetworkBackground { constructor(canvasId, options = {}) { this.canvas = document.getElementById(canvasId); if (!this.canvas) { console.error('Canvas-Element mit der ID', canvasId, 'nicht gefunden'); return; } this.ctx = this.canvas.getContext('2d'); // Zusammengeführte Konfiguration mit Standardwerten und benutzerdefinierten Optionen this.config = { nodeCount: options.nodeCount || 150, // Anzahl der Knoten im Netzwerk nodeSize: options.nodeSize || 4, // Basisgröße der Knoten nodeColor: options.nodeColor || '#3498db', // Hauptfarbe der Knoten nodeSecondaryColor: options.nodeSecondaryColor || '#2ecc71', // Zweite Farbe für bestimmte Knoten nodeVariation: options.nodeVariation || 0.5, // Variation der Knotengröße (0-1) connectionOpacity: options.connectionOpacity || 0.15, // Basisdeckkraft der Verbindungen connectionWidth: options.connectionWidth || 1.5, // Basisbreite der Verbindungen connectionVariation: options.connectionVariation || 0.5, // Variation der Verbindungsbreite (0-1) connectionDistance: options.connectionDistance || Math.floor(25 + Math.random() * 275), // Maximale Distanz für Verbindungen connectionColor: options.connectionColor || '#ffffff', // Farbe der Verbindungen backgroundColor: options.backgroundColor || 'rgba(20, 20, 40, 1)', // Hintergrundfarbe animationSpeed: options.animationSpeed || 0.5, // Geschwindigkeit der Animation (0-2) responsiveness: options.responsiveness !== undefined ? options.responsiveness : 0.8, // Reaktion auf Mausbewegungen (0-1) clusteringFactor: options.clusteringFactor || 0.98, // Extrem hoher Clustering-Faktor für noch deutlichere Cluster clusterCount: options.clusterCount || [4, 7], // Bereich für die Anzahl der Cluster (min, max) - reduzierte Anzahl für klarere Strukturen clusterSpread: options.clusterSpread || 0.5, // Wie weit sich Cluster verteilen dürfen (0-1) - reduziert für kompaktere Cluster clusterDensity: options.clusterDensity || 0.9, // Dichte innerhalb der Cluster (0-1) - höherer Wert für deutlichere Cluster clusterSeparation: options.clusterSeparation || 0.7, // Minimale Trennung zwischen Clustern (0-1) - höherer Wert für bessere Abgrenzung interClusterConnectionFactor: options.interClusterConnectionFactor || 0.2, // Faktor für Verbindungen zwischen Clustern - reduziert für klarere Abgrenzung intraClusterConnectionFactor: options.intraClusterConnectionFactor || 0.9, // Faktor für Verbindungen innerhalb von Clustern - erhöht für stärkere Verbindungen nonClusterNodeFactor: options.nonClusterNodeFactor || 0.3, // Faktor für Knoten außerhalb von Clustern - reduziert für Betonung der Cluster pulseEffect: options.pulseEffect !== undefined ? options.pulseEffect : true, // Aktiviere/deaktiviere Pulseffekt pulseSpeed: options.pulseSpeed || 0.02, // Geschwindigkeit des Pulsierens adaptiveDensity: options.adaptiveDensity !== undefined ? options.adaptiveDensity : true, // Passt Dichte an die Bildschirmgröße an highlightImportantNodes: options.highlightImportantNodes !== undefined ? options.highlightImportantNodes : true, // Betont wichtige Knoten smoothness: options.smoothness || 0.85, // Allgemeine Animationsglättung (0-1) darkMode: options.darkMode !== undefined ? options.darkMode : true, // Dunkles Farbschema complexConnections: options.complexConnections !== undefined ? options.complexConnections : true, // Intelligentere Verbindungsberechnung useAlternateLayout: options.useAlternateLayout !== undefined ? options.useAlternateLayout : false, // Alternative Layout-Algorithmen enableParticleEffects: options.enableParticleEffects !== undefined ? options.enableParticleEffects : true, // Partikeleffekte für bestimmte Interaktionen targetFPS: options.targetFPS || 30, // Ziel-FPS für Leistungsoptimierung optimizationLevel: options.optimizationLevel || 'high', // Grad der Leistungsoptimierung ('low', 'medium', 'high') }; // Canvas setup 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.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 - Subtilere Farben mit weniger Intensität this.darkModeColors = { background: '#030610', // Dunkler Hintergrund beibehalten nodeColor: '#5a75b0', // Gedämpftere Knotenfarbe nodePulse: '#94a7d0', // Weniger intensives Pulsieren connectionColor: '#485880', // Subtilere Verbindungen flowColor: '#a0c7e0' // Sanfteres Blitz-Blau }; // Optimierte Farbpalette für Light Mode mit verbesserter Harmonie und Lesbarkeit this.lightModeColors = { background: '#f8fafc', // Weicherer, neutraler Hintergrund nodeColor: '#4a6baf', // Tiefes, sattes Blau für bessere Kontrastwirkung nodePulse: '#6c9ad0', // Frisches, lebendiges Türkis für dynamische Effekte connectionColor: '#7a8fbf', // Harmonisches Violett-Blau für subtile Verbindungen flowColor: '#5d8ac0' // Klares, kräftiges Blau für präzise Blitzeffekte }; // 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); // Bestimme die Anzahl der Cluster basierend auf dem konfigurierten Bereich const minClusters = this.config.clusterCount[0]; const maxClusters = this.config.clusterCount[1]; const clusterCount = Math.floor(minClusters + Math.random() * (maxClusters - minClusters + 1)); const clusters = []; // Intelligentere Verteilung der Cluster im Raum mit verbesserter Separation const gridSize = Math.ceil(Math.sqrt(clusterCount)); const cellWidth = width / gridSize; const cellHeight = height / gridSize; // Erstelle ein Array von möglichen Positionen const positions = []; for (let y = 0; y < gridSize; y++) { for (let x = 0; x < gridSize; x++) { positions.push({ x: (x + 0.2 + Math.random() * 0.6) * cellWidth, y: (y + 0.2 + Math.random() * 0.6) * cellHeight }); } } // Mische die Positionen for (let i = positions.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [positions[i], positions[j]] = [positions[j], positions[i]]; } // Erstelle die Cluster mit optimierten Parametern for (let i = 0; i < clusterCount; i++) { const pos = positions[i % positions.length]; // Größere Cluster-Radien für bessere Sichtbarkeit und Trennung const baseRadius = 130 + Math.random() * 170; clusters.push({ x: pos.x, y: pos.y, radius: baseRadius, density: this.config.clusterDensity * (0.9 + Math.random() * 0.2), // Hohe Dichte mit leichter Variation separation: this.config.clusterSeparation, // Verwende den Separationsparameter type: Math.floor(Math.random() * 3) // 0: Standard, 1: Dicht, 2: Sternförmig }); } // Stelle sicher, dass Cluster ausreichend voneinander getrennt sind if (this.config.clusterSeparation > 0.5) { this.ensureClusterSeparation(clusters, width, height); } // Erstelle Knoten mit Berücksichtigung der Cluster und verbesserten Parametern for (let i = 0; i < this.config.nodeCount; i++) { const inCluster = Math.random() < this.config.clusteringFactor; let x, y, clusterType = -1; // -1 bedeutet "kein Cluster" let assignedCluster = null; if (inCluster && clusters.length > 0) { // Wähle ein zufälliges Cluster assignedCluster = clusters[Math.floor(Math.random() * clusters.length)]; clusterType = assignedCluster.type; // Verschiedene Verteilungsmuster je nach Cluster-Typ let angle, distance; switch (assignedCluster.type) { case 0: // Standard-Cluster mit gleichmäßiger Verteilung angle = Math.random() * Math.PI * 2; // Quadratische Verteilung für mehr Knoten in der Mitte distance = assignedCluster.radius * Math.sqrt(Math.random()) * assignedCluster.density; break; case 1: // Dichtes Cluster mit Konzentration in der Mitte angle = Math.random() * Math.PI * 2; // Kubische Verteilung für noch mehr Konzentration in der Mitte distance = assignedCluster.radius * Math.pow(Math.random(), 2.0) * assignedCluster.density; break; case 2: // Sternförmiges Cluster mit Strahlen // Bevorzuge bestimmte Winkel für Strahleneffekt const rayCount = 6 + Math.floor(Math.random() * 4); // 6-9 Strahlen für deutlichere Sterne const baseAngle = Math.random() * Math.PI * 2; // Zufällige Basisrotation const rayIndex = Math.floor(Math.random() * rayCount); const rayAngleSpread = 0.2; // Streuung innerhalb des Strahls angle = baseAngle + (rayIndex / rayCount) * Math.PI * 2 + (Math.random() - 0.5) * rayAngleSpread; distance = (0.3 + Math.random() * 0.7) * cluster.radius * cluster.density; // Längere Strahlen break; default: angle = Math.random() * Math.PI * 2; distance = Math.random() * cluster.radius; } // Platziere in der Nähe des Clusters mit einiger Streuung x = cluster.x + Math.cos(angle) * distance; y = cluster.y + Math.sin(angle) * distance; // Stelle sicher, dass es innerhalb des Bildschirms bleibt x = Math.max(20, Math.min(width - 20, x)); y = Math.max(20, Math.min(height - 20, y)); } else { // Zufällige Position außerhalb von Clustern, mit reduzierter Dichte in Clusternähe let validPosition = false; let attempts = 0; while (!validPosition && attempts < 10) { x = Math.random() * width; y = Math.random() * height; // Prüfe Abstand zu allen Clustern let minDistanceRatio = 1.0; for (const cluster of clusters) { const dx = x - cluster.x; const dy = y - cluster.y; const distance = Math.sqrt(dx * dx + dy * dy); const distanceRatio = distance / (cluster.radius * 1.5); // Größerer Ausschlussbereich minDistanceRatio = Math.min(minDistanceRatio, distanceRatio); } // Akzeptiere Position, wenn sie weit genug von allen Clustern entfernt ist // oder nach mehreren Versuchen if (minDistanceRatio > 1.0 || attempts > 5) { validPosition = true; } attempts++; } } // Bestimme die Knotengröße - wichtigere Knoten (in Clustern) sind deutlich größer let nodeImportance; if (clusterType === -1) { // Nicht-Cluster-Knoten sind kleiner nodeImportance = 0.5; } else { // Cluster-Knoten sind größer, mit Variation je nach Typ switch (clusterType) { case 0: nodeImportance = 1.5; break; // Standard case 1: nodeImportance = 1.8; break; // Dichteres Cluster, größere Knoten case 2: nodeImportance = 1.3 + Math.random() * 0.7; break; // Variable Größe für Strahleneffekt default: nodeImportance = 1.5; } } const size = this.config.nodeSize * nodeImportance + Math.random() * this.config.nodeVariation * 1.2; const node = { x: x, y: y, size: size, clusterType: clusterType, // Speichere den Cluster-Typ für spätere Verwendung speed: { x: (Math.random() - 0.5) * this.config.animationSpeed * (clusterType === -1 ? 1.5 : 0.7), // Nicht-Cluster-Knoten bewegen sich mehr y: (Math.random() - 0.5) * this.config.animationSpeed * (clusterType === -1 ? 1.5 : 0.7) }, pulsePhase: Math.random() * Math.PI * 2, // Random starting phase connections: [], isActive: clusterType !== -1 && Math.random() < 0.4, // Cluster-Knoten häufiger aktiv lastFired: 0, firingRate: clusterType === -1 ? 2000 + Math.random() * 5000 : 800 + Math.random() * 2000 // Schnellere Feuerrate für Cluster }; this.nodes.push(node); } } createConnections() { // Connection probability matrix based on distance and cluster membership for (let i = 0; i < this.nodes.length; i++) { const nodeA = this.nodes[i]; for (let j = i + 1; j < this.nodes.length; j++) { const nodeB = this.nodes[j]; // Berechne Distanz zwischen den Knoten const dx = nodeB.x - nodeA.x; const dy = nodeB.y - nodeA.y; const distance = Math.sqrt(dx * dx + dy * dy); // Basiswahrscheinlichkeit basierend auf Distanz let connectionProbability = 0; // Verschiedene Verbindungsregeln basierend auf Cluster-Zugehörigkeit const bothInCluster = nodeA.clusterType !== -1 && nodeB.clusterType !== -1; const sameClusterType = nodeA.clusterType === nodeB.clusterType; // Maximale Verbindungsdistanz - dynamisch basierend auf Clusterzugehörigkeit let maxDistance; if (bothInCluster && sameClusterType) { // Innerhalb des gleichen Cluster-Typs: höhere Wahrscheinlichkeit für Verbindungen maxDistance = 230; // Großzügige Verbindungsdistanz innerhalb von Clustern if (distance < maxDistance) { // Höhere Wahrscheinlichkeit für nahe Knoten im selben Cluster connectionProbability = Math.pow(1 - distance / maxDistance, 1.5) * 0.95; // Zusätzliche Regeln für spezifische Cluster-Typen if (nodeA.clusterType === 1) { // Dichte Cluster: noch stärkere Verbindungen im Zentrum connectionProbability *= 1.2; } else if (nodeA.clusterType === 2) { // Sternförmige Cluster: bevorzuge Verbindungen entlang ähnlicher Winkel // Berechne die Winkel der Knoten vom Clusterzentrum const centerX = nodeA.x - dx / 2; // Grobe Schätzung des Zentrums const centerY = nodeA.y - dy / 2; const angleA = Math.atan2(nodeA.y - centerY, nodeA.x - centerX); const angleB = Math.atan2(nodeB.y - centerY, nodeB.x - centerX); // Berechne den Winkelunterschied und normalisiere ihn let angleDiff = Math.abs(angleA - angleB); if (angleDiff > Math.PI) angleDiff = 2 * Math.PI - angleDiff; // Bevorzuge Verbindungen mit ähnlichem Winkel (entlang der Strahlen) if (angleDiff < 0.3) { connectionProbability *= 1.3; } else { connectionProbability *= 0.7; } } } } else if (bothInCluster && !sameClusterType) { // Verschiedene Cluster-Typen: reduzierte Wahrscheinlichkeit, aber einige Cross-Cluster-Verbindungen maxDistance = 180; if (distance < maxDistance) { connectionProbability = Math.pow(1 - distance / maxDistance, 2) * 0.3; } } else if ((nodeA.clusterType !== -1) !== (nodeB.clusterType !== -1)) { // Ein Knoten im Cluster, einer außerhalb: sehr geringe Wahrscheinlichkeit maxDistance = 150; if (distance < maxDistance) { connectionProbability = Math.pow(1 - distance / maxDistance, 2.5) * 0.15; } } else { // Beide außerhalb von Clustern: mittlere Wahrscheinlichkeit für große Distanzen maxDistance = 250; if (distance < maxDistance) { connectionProbability = Math.pow(1 - distance / maxDistance, 1.2) * 0.4; } } // Zufällige Variation der Verbindungsdistanz const connectionDistance = Math.random() * 275 + 25; // Überprüfe, ob wir eine Verbindung erstellen if (Math.random() < connectionProbability) { const connection = { nodeA: i, nodeB: j, strength: 0.1 + Math.random() * 0.9, // Zufällige Verbindungsstärke active: false, signalPosition: 0, signalSpeed: 0.02 + Math.random() * 0.08, pulsePhase: Math.random() * Math.PI * 2 }; // Verbindungen innerhalb des gleichen Clusters sind tendenziell aktiver if (bothInCluster && sameClusterType) { connection.active = Math.random() < 0.5; // 50% Chance für aktive Verbindungen connection.strength *= 1.3; // Stärkere Verbindungen } else if (bothInCluster && !sameClusterType) { connection.active = Math.random() < 0.3; // 30% Chance für Cross-Cluster } else { connection.active = Math.random() < 0.15; // 15% Chance für andere } // Speichere die Verbindung in beiden Knoten nodeA.connections.push({index: j, connectionIndex: this.connections.length}); nodeB.connections.push({index: i, connectionIndex: this.connections.length}); this.connections.push(connection); } } } } 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); const now = Date.now(); // Setze zunächst alle Neuronen auf inaktiv for (let i = 0; i < this.nodes.length; i++) { // Update pulse phase for smoother animation const node = this.nodes[i]; node.pulsePhase += this.config.pulseSpeed * (1 + (node.connections.length * 0.04)); // Animate node position with gentler movement node.x += node.speed.x * (Math.sin(now * 0.0003) * 0.4 + 0.4); node.y += node.speed.y * (Math.cos(now * 0.0002) * 0.4 + 0.4); // Keep nodes within bounds if (node.x < 0 || node.x > width) { node.speed.x *= -1; node.x = Math.max(0, Math.min(width, node.x)); } if (node.y < 0 || node.y > height) { node.speed.y *= -1; node.y = Math.max(0, Math.min(height, node.y)); } // Setze alle Knoten standardmäßig auf inaktiv node.isActive = false; } // Aktiviere Neuronen basierend auf aktiven Flows for (const flow of this.flows) { // Aktiviere den Quellknoten (der Flow geht von ihm aus) if (flow.sourceNodeIdx !== undefined) { this.nodes[flow.sourceNodeIdx].isActive = true; this.nodes[flow.sourceNodeIdx].lastFired = now; } // Aktiviere den Zielknoten nur, wenn der Flow weit genug fortgeschritten ist if (flow.targetNodeIdx !== undefined && flow.progress > 0.9) { this.nodes[flow.targetNodeIdx].isActive = true; this.nodes[flow.targetNodeIdx].lastFired = now; } } // Zufällig neue Flows zwischen Knoten initiieren if (Math.random() < 0.015) { // Reduzierte Chance in jedem Frame (1.5% statt 2%) const randomNodeIdx = Math.floor(Math.random() * this.nodes.length); const node = this.nodes[randomNodeIdx]; // Nur aktivieren, wenn Knoten Verbindungen hat if (node.connections.length > 0) { node.isActive = true; node.lastFired = now; // Wähle eine zufällige Verbindung dieses Knotens const randomConnIdx = Math.floor(Math.random() * node.connections.length); const connectedNodeIdx = node.connections[randomConnIdx]; // Finde die entsprechende Verbindung const conn = this.connections.find(c => (c.from === randomNodeIdx && c.to === connectedNodeIdx) || (c.from === connectedNodeIdx && c.to === randomNodeIdx) ); if (conn) { // Markiere die Verbindung als kürzlich aktiviert conn.lastActivated = now; // Stelle sicher, dass die Verbindung sichtbar bleibt if (conn.fadeState === 'out') { conn.fadeState = 'visible'; conn.fadeStartTime = now; } // Verbindung soll schneller aufgebaut werden if (conn.progress < 1) { conn.buildSpeed = 0.015 + Math.random() * 0.01; } // Erstelle einen neuen Flow, wenn nicht zu viele existieren if (this.flows.length < this.config.maxFlowCount) { // Bestimme die Richtung (vom aktivierten Knoten weg) const direction = conn.from === randomNodeIdx; this.flows.push({ connection: conn, progress: 0, direction: direction, length: this.config.flowLength + Math.random() * 0.05, creationTime: now, totalDuration: 1000 + Math.random() * 600, sourceNodeIdx: direction ? conn.from : conn.to, targetNodeIdx: direction ? conn.to : conn.from }); } } } } // Aktualisiere die Ein-/Ausblendung von Verbindungen for (const connection of this.connections) { const elapsedTime = now - connection.fadeStartTime; // Update connection fade status if (connection.fadeState === 'in') { // Einblenden connection.fadeProgress = Math.min(1.0, elapsedTime / connection.fadeTotalDuration); if (connection.fadeProgress >= 1.0) { connection.fadeState = 'visible'; connection.fadeStartTime = now; } } else if (connection.fadeState === 'visible') { // Verbindung ist vollständig sichtbar if (elapsedTime > connection.visibleDuration) { connection.fadeState = 'out'; connection.fadeStartTime = now; connection.fadeProgress = 1.0; } } else if (connection.fadeState === 'out') { // Ausblenden, aber nie komplett verschwinden connection.fadeProgress = Math.max(0.1, 1.0 - (elapsedTime / connection.fadeTotalDuration)); // Verbindungen bleiben immer minimal sichtbar (nie komplett unsichtbar) if (connection.fadeProgress <= 0.1) { // Statt Verbindung komplett zu verstecken, setzen wir sie zurück auf "in" connection.fadeState = 'in'; connection.fadeStartTime = now; connection.fadeProgress = 0.1; // Minimal sichtbar bleiben connection.visibleDuration = 15000 + Math.random() * 20000; // Längere Sichtbarkeit } } else if (connection.fadeState === 'hidden') { // Keine Verbindungen mehr verstecken, stattdessen immer wieder einblenden connection.fadeState = 'in'; connection.fadeStartTime = now; connection.fadeProgress = 0.1; } // Verbindungen immer vollständig aufbauen und nicht zurücksetzen if (connection.progress < 1) { // Konstante Aufbaugeschwindigkeit, unabhängig vom Status const baseBuildSpeed = 0.003; let buildSpeed = connection.buildSpeed || baseBuildSpeed; // Wenn kürzlich aktiviert, schneller aufbauen if (now - connection.lastActivated < 2000) { buildSpeed = Math.max(buildSpeed, 0.006); } connection.progress += buildSpeed; if (connection.progress > 1) { connection.progress = 1; // Zurücksetzen der Aufbaugeschwindigkeit connection.buildSpeed = 0; } } } // Update flows with proper fading this.updateFlows(now); // Seltener neue Flows erstellen if (Math.random() < this.config.flowDensity && this.flows.length < this.config.maxFlowCount) { this.createNewFlow(now); } // Recalculate connections occasionally for a living network if (Math.random() < 0.005) { // Only recalculate 0.5% of the time for performance this.createConnections(); } // Render if (this.useWebGL) { this.renderWebGL(now); } else { this.renderCanvas(now); } // Continue animation this.animationFrameId = requestAnimationFrame(this.animate.bind(this)); } // Updated method to update flow animations with proper fading updateFlows(now) { // Update existing flows for (let i = this.flows.length - 1; i >= 0; i--) { const flow = this.flows[i]; // Calculate flow age for fading effects const flowAge = now - flow.creationTime; const flowProgress = flowAge / flow.totalDuration; // Update flow progress flow.progress += this.config.flowSpeed / flow.connection.distance; // Aktiviere Quell- und Zielknoten basierend auf Flow-Fortschritt if (flow.sourceNodeIdx !== undefined) { // Quellknoten immer aktivieren, solange der Flow aktiv ist this.nodes[flow.sourceNodeIdx].isActive = true; this.nodes[flow.sourceNodeIdx].lastFired = now; } // Zielknoten erst aktivieren, wenn der Flow ihn erreicht hat if (flow.targetNodeIdx !== undefined && flow.progress > 0.9) { this.nodes[flow.targetNodeIdx].isActive = true; this.nodes[flow.targetNodeIdx].lastFired = now; } // Stellen Sie sicher, dass die Verbindung aktiv bleibt flow.connection.lastActivated = now; // Remove completed or expired flows if (flow.progress > 1.0 || flowProgress >= 1.0) { this.flows.splice(i, 1); } } } // Updated method to create flow animations with timing info createNewFlow(now) { if (this.connections.length === 0 || this.flows.length >= 6) 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]; // Verbindung als "im Aufbau" markieren, wenn sie noch nicht vollständig ist if (connection.progress < 1) { connection.buildSpeed = 0.015 + Math.random() * 0.01; // Schnellerer Aufbau während eines Blitzes } // 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 creationTime: now, totalDuration: 800 + Math.random() * 500 // Zufällige Gesamtdauer (800-1300ms) }); } renderWebGL(now) { 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(now); // Draw flows on top of connections this.renderFlowsWebGL(now); // Draw nodes this.renderNodesWebGL(now); } renderNodesWebGL(now) { // 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; // Sanftere Pulsation mit moderaterem Aktivierungsboost const activationBoost = node.isActive ? 1.3 : 1.0; let pulse = (Math.sin(node.pulsePhase) * 0.25 + 1.2) * activationBoost; // Größe basierend auf Konnektivität und Wichtigkeit, aber subtiler const connectivityFactor = 1 + (node.connections.length / this.config.maxConnections) * 1.1; sizes[i] = node.size * pulse * connectivityFactor; } // 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); // Enable blending for all nodes this.gl.enable(this.gl.BLEND); this.gl.blendFunc(this.gl.SRC_ALPHA, this.gl.ONE); // Additive blending for glow // Draw each node individually with its own color for (let i = 0; i < this.nodes.length; i++) { const node = this.nodes[i]; // Set node color - sanftere Hervorhebung von aktiven Knoten const colorObj = this.isDarkMode ? this.darkModeColors : this.lightModeColors; const nodeColor = this.hexToRgb(colorObj.nodeColor); const nodePulseColor = this.hexToRgb(colorObj.nodePulse); // Use pulse color for active nodes let r = nodeColor.r / 255; let g = nodeColor.g / 255; let b = nodeColor.b / 255; // Active nodes get slightly brighter color - subtiler if (node.isActive) { r = (r * 0.7 + nodePulseColor.r / 255 * 0.3); g = (g * 0.7 + nodePulseColor.g / 255 * 0.3); b = (b * 0.7 + nodePulseColor.b / 255 * 0.3); } // Subtilere Knoten mit reduzierter Opazität this.gl.uniform4f( this.programInfo.uniformLocations.color, r, g, b, node.isActive ? 0.75 : 0.65 // Geringere Sichtbarkeit für subtileres Erscheinungsbild ); // Draw each node individually for better control this.gl.drawArrays(this.gl.POINTS, i, 1); } } renderConnectionsWebGL(now) { for (const connection of this.connections) { // Überspringe Verbindungen, die komplett unsichtbar sind if (connection.fadeState === 'hidden' || connection.fadeProgress <= 0) continue; const fromNode = this.nodes[connection.from]; const toNode = this.nodes[connection.to]; const progress = connection.progress || 1; const x1 = fromNode.x; const y1 = fromNode.y; const x2 = fromNode.x + (toNode.x - fromNode.x) * progress; const y2 = fromNode.y + (toNode.y - fromNode.y) * progress; // Calculate opacity based on fade state let opacity = connection.opacity * connection.fadeProgress * 0.85; // Reduzierte Gesamtopazität // Erhöhe kurzzeitig die Opazität bei kürzlich aktivierten Verbindungen if (connection.lastActivated && now - connection.lastActivated < 800) { const timeFactor = 1 - ((now - connection.lastActivated) / 800); opacity = Math.max(opacity, timeFactor * 0.3); } const positions = new Float32Array([ x1, y1, x2, y2 ]); 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, this.gl.FLOAT, false, 0, 0 ); this.gl.enableVertexAttribArray(this.programInfo.attribLocations.vertexPosition); this.gl.disableVertexAttribArray(this.programInfo.attribLocations.pointSize); // Set color with calculated opacity const colorObj = this.isDarkMode ? this.darkModeColors : this.lightModeColors; const connColor = this.hexToRgb(colorObj.connectionColor); this.gl.uniform4f( this.programInfo.uniformLocations.color, connColor.r / 255, connColor.g / 255, connColor.b / 255, opacity ); this.gl.enable(this.gl.BLEND); this.gl.blendFunc(this.gl.SRC_ALPHA, this.gl.ONE); this.gl.lineWidth(this.config.linesWidth); this.gl.drawArrays(this.gl.LINES, 0, 2); } } // New method to render the flowing animations renderFlowsWebGL(now) { // Für jeden Flow einen dezenten, echten Blitz als Zickzack zeichnen und Funken erzeugen for (const flow of this.flows) { const connection = flow.connection; const fromNode = this.nodes[connection.from]; const toNode = this.nodes[connection.to]; const startProgress = flow.progress; const endProgress = Math.min(1, startProgress + flow.length); if (startProgress >= 1 || endProgress <= 0) continue; 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 }; } // Zickzack-Blitz generieren const zigzag = this.generateZigZagPoints(p1, p2, 8, 10); for (let i = 0; i < zigzag.length - 1; i++) { const positions = new Float32Array([ zigzag[i].x, zigzag[i].y, zigzag[i + 1].x, zigzag[i + 1].y ]); 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, this.gl.FLOAT, false, 0, 0 ); this.gl.enableVertexAttribArray(this.programInfo.attribLocations.vertexPosition); this.gl.disableVertexAttribArray(this.programInfo.attribLocations.pointSize); // Dezenter, leuchtender Blitz const colorObj = this.isDarkMode ? this.darkModeColors : this.lightModeColors; const flowColor = this.hexToRgb(colorObj.flowColor); // Definiere fadeFactor als 1.0, falls nicht von flow definiert const fadeFactor = flow.fadeFactor || 1.0; this.gl.uniform4f( this.programInfo.uniformLocations.color, flowColor.r / 255, flowColor.g / 255, flowColor.b / 255, 0.7 * fadeFactor // Reduced from higher values ); this.gl.enable(this.gl.BLEND); this.gl.blendFunc(this.gl.SRC_ALPHA, this.gl.ONE); this.gl.lineWidth(1.3); // Schmaler Blitz this.gl.drawArrays(this.gl.LINES, 0, 2); } // Funken erzeugen - echte elektrische Funken const sparks = this.generateSparkPoints(zigzag, 7 + Math.floor(Math.random() * 3)); for (const spark of sparks) { // Helles Weiß-Blau für elektrische Funken this.gl.uniform4f( this.programInfo.uniformLocations.color, 0.88, 0.95, 1.0, 0.65 // Elektrisches Blau-Weiß ); // Position für den Funken setzen const sparkPos = new Float32Array([spark.x, spark.y]); this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.positionBuffer); this.gl.bufferData(this.gl.ARRAY_BUFFER, sparkPos, this.gl.STATIC_DRAW); // Punktgröße setzen const sizes = new Float32Array([spark.size * 3.0]); 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.vertexPosition, 2, this.gl.FLOAT, false, 0, 0 ); this.gl.enableVertexAttribArray(this.programInfo.attribLocations.vertexPosition); this.gl.vertexAttribPointer( this.programInfo.attribLocations.pointSize, 1, this.gl.FLOAT, false, 0, 0 ); this.gl.enableVertexAttribArray(this.programInfo.attribLocations.pointSize); this.gl.drawArrays(this.gl.POINTS, 0, 1); } } } renderCanvas(now) { // 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 with fade effects const connectionColor = this.isDarkMode ? this.darkModeColors.connectionColor : this.lightModeColors.connectionColor; for (const connection of this.connections) { // Überspringe Verbindungen, die komplett unsichtbar sind if (connection.fadeState === 'hidden' || connection.fadeProgress <= 0) continue; const fromNode = this.nodes[connection.from]; const toNode = this.nodes[connection.to]; // Zeichne nur den Teil der Verbindung, der schon aufgebaut wurde const progress = connection.progress || 0; if (progress <= 0) continue; // Skip if no progress yet const x1 = fromNode.x; const y1 = fromNode.y; // Endpunkt basiert auf dem aktuellen Fortschritt const x2 = x1 + (toNode.x - x1) * progress; const y2 = y1 + (toNode.y - y1) * progress; // Zeichne die unterliegende Linie mit Ein-/Ausblendung this.ctx.beginPath(); this.ctx.moveTo(x1, y1); this.ctx.lineTo(x2, y2); const rgbColor = this.hexToRgb(connectionColor); // Calculate opacity based on fade state let opacity = connection.opacity * connection.fadeProgress; // Erhöhe kurzzeitig die Opazität bei kürzlich aktivierten Verbindungen if (connection.lastActivated && now - connection.lastActivated < 800) { const timeFactor = 1 - ((now - connection.lastActivated) / 800); opacity = Math.max(opacity, timeFactor * this.config.linesOpacity); } this.ctx.strokeStyle = `rgba(${rgbColor.r}, ${rgbColor.g}, ${rgbColor.b}, ${opacity})`; this.ctx.lineWidth = this.config.linesWidth; // Leichter Glow-Effekt für die Linien if (opacity > 0.1) { this.ctx.shadowColor = `rgba(${rgbColor.r}, ${rgbColor.g}, ${rgbColor.b}, 0.15)`; this.ctx.shadowBlur = 3; } else { this.ctx.shadowBlur = 0; } this.ctx.stroke(); // Zeichne einen Fortschrittspunkt am Ende der sich aufbauenden Verbindung if (progress < 0.95 && connection.fadeProgress > 0.5) { this.ctx.beginPath(); this.ctx.arc(x2, y2, 1.5, 0, Math.PI * 2); this.ctx.fillStyle = `rgba(${rgbColor.r}, ${rgbColor.g}, ${rgbColor.b}, ${opacity * 1.3})`; this.ctx.fill(); } this.ctx.shadowBlur = 2; // Reset shadow for other elements } // Draw flows with fading effect this.renderFlowsCanvas(now); // Draw nodes with enhanced animations 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) { // Verbesserte Pulsation mit Aktivierungsboost const activationBoost = node.isActive ? 1.7 : 1.0; const pulse = (Math.sin(node.pulsePhase) * 0.45 + 1.4) * activationBoost; // Größe basierend auf Konnektivität und Wichtigkeit const connectivityFactor = 1 + (node.connections.length / this.config.maxConnections) * 1.4; const nodeSize = node.size * pulse * connectivityFactor; // Verbesserte Leuchtkraft und Glow-Effekt const rgbNodeColor = this.hexToRgb(nodeColor); const rgbPulseColor = this.hexToRgb(nodePulse); // Mische Farben basierend auf Aktivierung let r, g, b; if (node.isActive) { r = (rgbNodeColor.r * 0.3 + rgbPulseColor.r * 0.7); g = (rgbNodeColor.g * 0.3 + rgbPulseColor.g * 0.7); b = (rgbNodeColor.b * 0.3 + rgbPulseColor.b * 0.7); } else { r = rgbNodeColor.r; g = rgbNodeColor.g; b = rgbNodeColor.b; } // Äußerer Glow const glow = this.ctx.createRadialGradient( node.x, node.y, 0, node.x, node.y, nodeSize * 4.5 ); // Intensiveres Zentrum und weicherer Übergang glow.addColorStop(0, `rgba(${r}, ${g}, ${b}, ${node.isActive ? 0.95 : 0.8})`); glow.addColorStop(0.4, `rgba(${r}, ${g}, ${b}, 0.45)`); glow.addColorStop(1, `rgba(${r}, ${g}, ${b}, 0)`); this.ctx.beginPath(); this.ctx.arc(node.x, node.y, nodeSize, 0, Math.PI * 2); this.ctx.fillStyle = glow; this.ctx.globalAlpha = node.isActive ? 1.0 : 0.92; this.ctx.fill(); // Innerer Kern für stärkeren Leuchteffekt if (node.isActive) { this.ctx.beginPath(); this.ctx.arc(node.x, node.y, nodeSize * 0.4, 0, Math.PI * 2); this.ctx.fillStyle = `rgba(${rgbPulseColor.r}, ${rgbPulseColor.g}, ${rgbPulseColor.b}, 0.9)`; this.ctx.fill(); } this.ctx.globalAlpha = 1.0; } } // New method to render flows in Canvas mode with fading renderFlowsCanvas(now) { if (!this.flows.length) return; // Für jeden Flow in der Blitzanimation for (const flow of this.flows) { const connection = flow.connection; const fromNode = this.nodes[connection.from]; const toNode = this.nodes[connection.to]; // Berechne den Fortschritt der Connection und des Flows const connProgress = connection.progress || 1; // Flussrichtung bestimmen const [startNode, endNode] = flow.direction ? [fromNode, toNode] : [toNode, fromNode]; // Berücksichtige den Verbindungs-Fortschritt const maxDistance = Math.min(connProgress, 1) * Math.sqrt( Math.pow(endNode.x - startNode.x, 2) + Math.pow(endNode.y - startNode.y, 2) ); // Berechne den aktuellen Fortschritt mit Ein- und Ausblendung const flowAge = now - flow.creationTime; const flowLifetime = flow.totalDuration; let fadeFactor = 1.0; // Sanftere Ein- und Ausblendung für Blitzeffekte if (flowAge < flowLifetime * 0.3) { fadeFactor = flowAge / (flowLifetime * 0.3); } else if (flowAge > flowLifetime * 0.7) { fadeFactor = 1.0 - ((flowAge - flowLifetime * 0.7) / (flowLifetime * 0.3)); } // Flow-Fortschritt const startProgress = Math.max(0.0, flow.progress - flow.length); const endProgress = Math.min(flow.progress, connProgress); // Start- und Endpunkte basierend auf dem Fortschritt const p1 = { x: startNode.x + (endNode.x - startNode.x) * startProgress, y: startNode.y + (endNode.y - startNode.y) * startProgress }; const p2 = { x: startNode.x + (endNode.x - startNode.x) * endProgress, y: startNode.y + (endNode.y - startNode.y) * endProgress }; if (endProgress > connProgress) continue; // Lila Gradient für den Blitz const gradient = this.ctx.createLinearGradient(p1.x, p1.y, p2.x, p2.y); gradient.addColorStop(0, 'rgba(255, 0, 255, 0.8)'); gradient.addColorStop(0.5, 'rgba(200, 0, 255, 0.9)'); gradient.addColorStop(1, 'rgba(255, 0, 255, 0.8)'); this.ctx.save(); // Untergrundspur mit stärkerem Glühen this.ctx.beginPath(); this.ctx.moveTo(p1.x, p1.y); this.ctx.lineTo(p2.x, p2.y); this.ctx.strokeStyle = gradient; this.ctx.lineWidth = 20.0; this.ctx.shadowColor = 'rgba(255, 0, 255, 0.4)'; this.ctx.shadowBlur = 25; this.ctx.stroke(); // Abgerundeter Zickzack-Blitz mit weicheren Kurven const zigzag = this.generateZigZagPoints(p1, p2, 4, 6, true); // Hauptblitz mit Gradient this.ctx.strokeStyle = gradient; this.ctx.lineWidth = 1.5; this.ctx.shadowColor = 'rgba(255, 0, 255, 0.5)'; this.ctx.shadowBlur = 30; this.ctx.beginPath(); this.ctx.moveTo(zigzag[0].x, zigzag[0].y); for (let i = 1; i < zigzag.length; i++) { const cp1x = zigzag[i-1].x + (zigzag[i].x - zigzag[i-1].x) * 0.4; const cp1y = zigzag[i-1].y + (zigzag[i].y - zigzag[i-1].y) * 0.4; const cp2x = zigzag[i].x - (zigzag[i].x - zigzag[i-1].x) * 0.4; const cp2y = zigzag[i].y - (zigzag[i].y - zigzag[i-1].y) * 0.4; this.ctx.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, zigzag[i].x, zigzag[i].y); } this.ctx.stroke(); // Funken mit lila Glühen const sparks = this.generateSparkPoints(zigzag, 10 + Math.floor(Math.random() * 6)); const sparkGradient = this.ctx.createRadialGradient(0, 0, 0, 0, 0, 10); sparkGradient.addColorStop(0, 'rgba(255, 0, 255, 0.95)'); sparkGradient.addColorStop(0.5, 'rgba(200, 0, 255, 0.8)'); sparkGradient.addColorStop(1, 'rgba(255, 0, 255, 0.6)'); for (const spark of sparks) { this.ctx.beginPath(); // Weichere Sternform const points = 4 + Math.floor(Math.random() * 3); const outerRadius = spark.size * 2.0; const innerRadius = spark.size * 0.5; for (let i = 0; i < points * 2; i++) { const radius = i % 2 === 0 ? outerRadius : innerRadius; const angle = (i * Math.PI) / points; const x = spark.x + Math.cos(angle) * radius; const y = spark.y + Math.sin(angle) * radius; if (i === 0) { this.ctx.moveTo(x, y); } else { this.ctx.lineTo(x, y); } } this.ctx.closePath(); // Intensives lila Glühen this.ctx.shadowColor = 'rgba(255, 0, 255, 0.8)'; this.ctx.shadowBlur = 25; this.ctx.fillStyle = sparkGradient; this.ctx.fill(); // Intensiverer innerer Glüheffekt für ausgewählte Funken mit mehrfacher Schichtung if (spark.size > 3 && Math.random() > 0.3) { // Erste Glühschicht - größer und weicher this.ctx.beginPath(); this.ctx.arc(spark.x, spark.y, spark.size * 0.8, 0, Math.PI * 2); this.ctx.fillStyle = this.isDarkMode ? `rgba(245, 252, 255, ${0.85 * fadeFactor})` : `rgba(230, 245, 255, ${0.8 * fadeFactor})`; this.ctx.shadowColor = this.isDarkMode ? `rgba(200, 225, 255, ${0.7 * fadeFactor})` : `rgba(180, 220, 255, ${0.6 * fadeFactor})`; this.ctx.shadowBlur = 15; this.ctx.fill(); // Zweite Glühschicht - kleiner und intensiver this.ctx.beginPath(); this.ctx.arc(spark.x, spark.y, spark.size * 0.5, 0, Math.PI * 2); this.ctx.fillStyle = this.isDarkMode ? `rgba(255, 255, 255, ${0.95 * fadeFactor})` : `rgba(240, 250, 255, ${0.9 * fadeFactor})`; this.ctx.shadowColor = this.isDarkMode ? `rgba(220, 235, 255, ${0.8 * fadeFactor})` : `rgba(200, 230, 255, ${0.7 * fadeFactor})`; this.ctx.shadowBlur = 20; this.ctx.fill(); // Dritte Glühschicht - noch intensiverer Kern this.ctx.beginPath(); this.ctx.arc(spark.x, spark.y, spark.size * 0.3, 0, Math.PI * 2); this.ctx.fillStyle = this.isDarkMode ? `rgba(255, 255, 255, ${0.98 * fadeFactor})` : `rgba(245, 252, 255, ${0.95 * fadeFactor})`; this.ctx.shadowColor = this.isDarkMode ? `rgba(230, 240, 255, ${0.9 * fadeFactor})` : `rgba(210, 235, 255, ${0.8 * fadeFactor})`; this.ctx.shadowBlur = 25; this.ctx.fill(); // Vierte Glühschicht - pulsierender Effekt const pulseSize = spark.size * (0.2 + Math.sin(Date.now() * 0.01) * 0.1); this.ctx.beginPath(); this.ctx.arc(spark.x, spark.y, pulseSize, 0, Math.PI * 2); this.ctx.fillStyle = this.isDarkMode ? `rgba(255, 255, 255, ${0.99 * fadeFactor})` : `rgba(250, 255, 255, ${0.97 * fadeFactor})`; this.ctx.shadowColor = this.isDarkMode ? `rgba(240, 245, 255, ${0.95 * fadeFactor})` : `rgba(220, 240, 255, ${0.85 * fadeFactor})`; this.ctx.shadowBlur = 30; this.ctx.fill(); } } // Deutlicherer und länger anhaltender Fortschrittseffekt an der Spitze des Blitzes if (endProgress >= connProgress - 0.1 && connProgress < 0.98) { const tipGlow = this.ctx.createRadialGradient( p2.x, p2.y, 0, p2.x, p2.y, 10 ); tipGlow.addColorStop(0, `rgba(${rgbFlowColor.r}, ${rgbFlowColor.g}, ${rgbFlowColor.b}, ${0.85 * fadeFactor})`); tipGlow.addColorStop(0.5, `rgba(${rgbFlowColor.r}, ${rgbFlowColor.g}, ${rgbFlowColor.b}, ${0.4 * fadeFactor})`); tipGlow.addColorStop(1, `rgba(${rgbFlowColor.r}, ${rgbFlowColor.g}, ${rgbFlowColor.b}, 0)`); this.ctx.fillStyle = tipGlow; this.ctx.beginPath(); this.ctx.arc(p2.x, p2.y, 10, 0, Math.PI * 2); this.ctx.fill(); } this.ctx.restore(); } } // 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() { // Sanftes Ausblenden der Animation vor dem Entfernen if (this.canvas) { // Aktuelle Opazität abrufen und Animation starten const currentOpacity = parseFloat(this.canvas.style.opacity) || 1; this.canvas.style.transition = 'opacity 1500ms ease-out'; // Animation starten setTimeout(() => { this.canvas.style.opacity = '0'; }, 10); // Erst nach dem vollständigen Ausblenden Ressourcen freigeben setTimeout(() => { // Animation beenden if (this.animationFrameId) { cancelAnimationFrame(this.animationFrameId); } // Event-Listener entfernen window.removeEventListener('resize', this.resizeCanvas.bind(this)); // Canvas aus dem DOM entfernen if (this.canvas && this.canvas.parentNode) { this.canvas.parentNode.removeChild(this.canvas); } // WebGL-Ressourcen bereinigen if (this.gl) { this.gl.deleteBuffer(this.positionBuffer); this.gl.deleteBuffer(this.sizeBuffer); this.gl.deleteProgram(this.shaderProgram); } console.log('Neural Network Background sanft ausgeblendet und bereinigt'); }, 1500); // Entspricht der Transitions-Dauer } else { // Fallback für den Fall, dass kein Canvas existiert if (this.animationFrameId) { cancelAnimationFrame(this.animationFrameId); } } } // Hilfsfunktion: Erzeuge Zickzack-Punkte für einen Blitz mit geringerer Vibration generateZigZagPoints(start, end, segments = 5, amplitude = 8) { const points = [start]; const mainAngle = Math.atan2(end.y - start.y, end.x - start.x); // Berechne die Gesamtlänge des Blitzes const totalDistance = Math.sqrt( Math.pow(end.x - start.x, 2) + Math.pow(end.y - start.y, 2) ); // Geringere Amplitude für subtilere Zickzack-Muster const baseAmplitude = totalDistance * 0.12; // Reduziert für sanfteres Erscheinungsbild for (let i = 1; i < segments; i++) { const t = i / segments; const x = start.x + (end.x - start.x) * t; const y = start.y + (end.y - start.y) * t; // Geringere Vibration durch kleinere Zufallsvariationen const perpendicularAngle = mainAngle + Math.PI/2; const variation = (Math.random() * 0.8 - 0.4); // Kleinere Variation für sanftere Muster // Mal Links, mal Rechts für sanften Blitz const directionFactor = (i % 2 === 0) ? 1 : -1; const offset = baseAmplitude * (Math.sin(i * Math.PI) + variation) * directionFactor; points.push({ x: x + Math.cos(perpendicularAngle) * offset, y: y + Math.sin(perpendicularAngle) * offset }); } points.push(end); return points; } // Hilfsfunktion: Erzeuge intensivere Funkenpunkte mit dynamischer Verteilung generateSparkPoints(zigzag, sparkCount = 15) { const sparks = []; // Mehr Funken für intensiveren Effekt const actualSparkCount = Math.min(sparkCount, zigzag.length * 2); // Funken an zufälligen Stellen entlang des Blitzes for (let i = 0; i < actualSparkCount; i++) { // Zufälliges Segment des Zickzacks auswählen const segIndex = Math.floor(Math.random() * (zigzag.length - 1)); // Bestimme Richtung des Segments const dx = zigzag[segIndex + 1].x - zigzag[segIndex].x; const dy = zigzag[segIndex + 1].y - zigzag[segIndex].y; const segmentAngle = Math.atan2(dy, dx); // Zufällige Position entlang des Segments const t = Math.random(); const x = zigzag[segIndex].x + dx * t; const y = zigzag[segIndex].y + dy * t; // Dynamischer Versatz für intensivere Funken const offsetAngle = segmentAngle + (Math.random() * Math.PI - Math.PI/2); const offsetDistance = Math.random() * 8 - 4; // Größerer Offset für dramatischere Funken // Zufällige Größe für variierende Intensität const baseSize = 3.5 + Math.random() * 3.5; const sizeVariation = Math.random() * 2.5; sparks.push({ x: x + Math.cos(offsetAngle) * offsetDistance, y: y + Math.sin(offsetAngle) * offsetDistance, size: baseSize + sizeVariation // Größere und variablere Funkengröße }); // Zusätzliche kleinere Funken in der Nähe für einen intensiveren Effekt if (Math.random() < 0.4) { // 40% Chance für zusätzliche Funken const subSparkAngle = offsetAngle + (Math.random() * Math.PI/2 - Math.PI/4); const subDistance = offsetDistance * (0.4 + Math.random() * 0.6); sparks.push({ x: x + Math.cos(subSparkAngle) * subDistance, y: y + Math.sin(subSparkAngle) * subDistance, size: (baseSize + sizeVariation) * 0.6 // Kleinere Größe für sekundäre Funken }); } } return sparks; } } // 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(); } }); // Event listener to clean up when the window is closed window.addEventListener('beforeunload', function() { if (window.neuralNetworkBackground) { // Sanftes Ausblenden vor dem Schließen der Seite initiieren window.neuralNetworkBackground.destroy(); } }); // Füge Handler für Navigationsänderungen hinzu (für SPA-Anwendungen) document.addEventListener('visibilitychange', function() { if (document.visibilityState === 'hidden' && window.neuralNetworkBackground) { // Sanftes Ausblenden wenn der Tab in den Hintergrund wechselt window.neuralNetworkBackground.destroy(); } else if (document.visibilityState === 'visible' && !window.neuralNetworkBackground) { // Neu initialisieren, wenn der Tab wieder sichtbar wird window.neuralNetworkBackground = new NeuralNetworkBackground(); } });