/** * 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 this.canvas.style.transition = 'opacity 3.5s ease-in-out'; // Längere Übergangsanimation (von 1.5s auf 3.5s) // 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 this.isDestroying = false; // Flag für den Ausblendeprozess // Colors - Lila Farbpalette mit intensiveren Effekten this.darkModeColors = { background: '#040215', // Dunklerer Hintergrund mit leichtem Violett-Anteil nodeColor: '#6a5498', // Lila-basierte Knotenfarbe nodePulse: '#9c7fe0', // Helleres Lila für Pulsieren connectionColor: '#4a3870', // Dunklere Lila-Verbindungen flowColor: '#b47fea' // Lebendiges Lila für Blitze }; // Farben für Light Mode auch mit Lila-Akzenten this.lightModeColors = { background: '#f8f6fc', // Sehr heller Lila-Hintergrund nodeColor: '#6a5498', // Gleiche Lila-Töne wie im Dark Mode nodePulse: '#9c7fe0', // für konsistentes Erscheinungsbild connectionColor: '#7e70a5', // Helleres Lila für Verbindungen flowColor: '#a06cd5' // Sanfteres Lila für Blitze }; // Konfigurationsobjekt für schlangenartige, leuchtende Blitze und permanentes Netzwerk this.config = { nodeCount: 100, // Weniger Knoten für klarere Strukturen nodeSize: 3.2, // Etwas größere Knoten für bessere Sichtbarkeit nodeVariation: 8, // Reduzierte Varianz für konsistenteres Erscheinungsbild connectionDistance: 100, // Größere Verbindungsdistanz für weitläufigeres Netzwerk connectionOpacity: 0.3, // Erhöhte Opazität für sichtbarere dauerhafte Verbindungen animationSpeed: 0.1, // Noch langsamere Animation für sanftere Bewegung pulseSpeed: 0, // Sehr langsames Pulsieren für ruhige Animation flowSpeed: 0.15, // Deutlich langsamere Blitze für schlangenartige Bewegung flowDensity: 2, // Weniger gleichzeitige Blitze für bessere Verteilung flowLength: 0.05, // Längere einzelne Flows für sichtbare "Schlangen" maxConnections: 12, // Mehr Verbindungen pro Neuron für dichtere Netzwerke clusteringFactor: 0.005, // Stärkeres Clustering für deutlichere Strukturen linesFadeDuration: 600, // Noch längere Dauer fürs Ein-/Ausblenden für permanentes Netzwerk (ms) linesWidth: 1.0, // Etwas dickere Linien für bessere Sichtbarkeit linesOpacity: 0.75, // Höhere Opazität für das permanente Netzwerk maxFlowCount: 1, // Weniger gleichzeitige Flows für bessere Übersicht glowIntensity: 1.0, // Verstärkter Glow-Effekt sparkCount: 1, // Mehr Funken sparkSize: 1, // Größere Funken blurRadius: 10, // Blur-Radius für Glow-Effekte minActiveFlows: 1, // Minimale Anzahl aktiver Flows flowForwardingRate: 1.0, // 100% Weiterleitungsrate für endlose Flows maxFlowGenerations: 10 // Sehr hohe Generationsgrenze für endlose Animation }; // 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); // Erstelle Cluster-Zentren für neuronale Netzwerkmuster mit erhöhter Komplexität const clusterCount = Math.floor(8 + Math.random() * 6); // 8-13 Cluster für mehr Dichte const clusters = []; for (let i = 0; i < clusterCount; i++) { // Dynamischere Clustergrößen mit größerer Variationsbreite const baseRadius = 120 + Math.random() * 200; // Größere Basisradien const radiusVariation = 0.3 + Math.random() * 0.4; // 30-70% Variation clusters.push({ x: Math.random() * width, y: Math.random() * height, radius: baseRadius * (1 + radiusVariation), // Dynamischere Größen density: 0.7 + Math.random() * 0.3, // Dichtefaktor für Knotenverteilung influence: 0.5 + Math.random() * 0.5 // Einflussstärke auf umliegende Knoten }); } // Create nodes with random positions and properties for (let i = 0; i < this.config.nodeCount; i++) { // Entscheide, ob dieser Knoten zu einem Cluster gehört oder nicht const useCluster = Math.random() < this.config.clusteringFactor; let x, y; if (useCluster && clusters.length > 0) { // Wähle ein zufälliges Cluster const cluster = clusters[Math.floor(Math.random() * clusters.length)]; const angle = Math.random() * Math.PI * 2; const 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(0, Math.min(width, x)); y = Math.max(0, Math.min(height, y)); } else { // Zufällige Position außerhalb von Clustern x = Math.random() * width; y = Math.random() * height; } // Bestimme die Knotengröße - wichtigere Knoten (in Clustern) sind deutlich größer const nodeImportance = useCluster ? 1.8 : 0.6; // Erhöhter Kontrast zwischen Cluster- und Nicht-Cluster-Knoten const size = this.config.nodeSize * nodeImportance + Math.random() * this.config.nodeVariation * 1.5; // Größere Variationsbreite const node = { x: x, y: y, size: size, 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: [], isActive: Math.random() < 0.3, // Some nodes start active for neural firing effect lastFired: 0, // For neural firing animation firingRate: 1000 + Math.random() * 4000 // Random firing rate in ms }; 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 = []; // Sortiere andere Knoten nach Entfernung für bevorzugte nahe Verbindungen const potentialConnections = []; for (let j = 0; j < this.nodes.length; j++) { if (i === j) continue; 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); // Erhöhe die Wahrscheinlichkeit für Verbindungen if (distance < this.config.connectionDistance * 1.5) { // Erweiterter Verbindungsradius potentialConnections.push({ index: j, distance: distance, // Zufälliger Bonus für mehr Verbindungen connectionBonus: Math.random() * 0.5 }); } } // Sortiere nach Entfernung und Verbindungsbonus mit erweiterten Kriterien potentialConnections.sort((a, b) => { // Basis-Score aus Entfernung und Verbindungsbonus const baseScoreA = a.distance * (1 - a.connectionBonus); const baseScoreB = b.distance * (1 - b.connectionBonus); // Zusätzliche Faktoren für dynamischere Verbindungen const randomFactorA = Math.random() * 0.2; // Leichte Zufallskomponente const randomFactorB = Math.random() * 0.2; // Kombinierter Score mit Zufallskomponente const scoreA = baseScoreA * (1 - randomFactorA); const scoreB = baseScoreB * (1 - randomFactorB); return scoreA - scoreB; }); // Erhöhe die maximale Anzahl der Verbindungen signifikant const maxConn = Math.min( this.config.maxConnections * 2.5, // Deutlich mehr Verbindungen erlaubt potentialConnections.length, 3 + Math.floor(Math.random() * this.config.maxConnections * 2) // Noch mehr zufällige Verbindungen ); for (let c = 0; c < maxConn; c++) { const connection = potentialConnections[c]; const j = connection.index; const nodeB = this.nodes[j]; const distance = connection.distance; // Stärkere Verbindungen durch den Bonus const connectionStrength = Math.max(0, 1 - distance / (this.config.connectionDistance * 1.5)) + connection.connectionBonus; const connOpacity = connectionStrength * this.config.connectionOpacity; // Check if connection already exists if (!this.connections.some(conn => (conn.from === i && conn.to === j) || (conn.from === j && conn.to === i) )) { // Neue Verbindung mit Ein-/Ausblend-Status this.connections.push({ from: i, to: j, distance: distance, opacity: connOpacity, strength: connectionStrength, hasFlow: false, lastActivated: 0, progress: 0, // Verbindung beginnt unsichtbar und baut sich auf fadeState: 'in', // Status: 'in' = einblenden, 'visible' = sichtbar, 'out' = ausblenden fadeStartTime: Date.now(), // Wann der Fade-Vorgang gestartet wurde fadeTotalDuration: this.config.linesFadeDuration, // Feste Dauer ohne Zufall visibleDuration: 10000, // Feste Sichtbarkeitsdauer ohne Zufall fadeProgress: 0, // Aktueller Fortschritt des Fade-Vorgangs (0-1) buildSpeed: 0 // Geschwindigkeit, mit der die Verbindung aufgebaut wird }); 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); const now = Date.now(); // Berechne den Fadeout-Faktor bei Deaktivierung (1.0 bis 0.0) const fadeoutFactor = this.isDestroying ? parseFloat(this.canvas.style.opacity) || 0.98 : 1.0; // Für jeden Flow eine eindeutige ID vergeben, falls noch nicht vorhanden for (const flow of this.flows) { if (!flow.id) { flow.id = now + '_' + Math.random().toString(36).substr(2, 9); } } // 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]; // Bei Ausblendung langsamer pulsieren lassen const pulseSpeed = this.isDestroying ? this.config.pulseSpeed * 0.7 : this.config.pulseSpeed * (1 + (node.connections.length * 0.04)); node.pulsePhase += pulseSpeed; // Bei Ausblendung Bewegung reduzieren const movementFactor = this.isDestroying ? 0.3 : 1.0; // Animate node position with gentler movement node.x += node.speed.x * (Math.sin(now * 0.0003) * 0.4 + 0.4) * movementFactor; node.y += node.speed.y * (Math.cos(now * 0.0002) * 0.4 + 0.4) * movementFactor; // 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)); } // Knoten bleiben länger aktiv nach dem Auslösen (2000ms für langanhaltende Aktivierung) if (node.lastFired && now - node.lastFired < 2000) { node.isActive = true; } else { 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.7) { this.nodes[flow.targetNodeIdx].isActive = true; this.nodes[flow.targetNodeIdx].lastFired = now; } } // Minimale Anzahl aktiver Flows sicherstellen für endlose Animation const minRequiredFlows = Math.min(15, Math.floor(this.nodes.length * 0.1)); // Mindestens 10% der Knoten haben aktive Flows const shouldCreateNewFlows = this.flows.length < minRequiredFlows; // Chance auf neue Flows const flowChance = this.isDestroying ? 0.005 : (shouldCreateNewFlows ? 0.1 : 0.04); // Neue Flows mit höherer Wahrscheinlichkeit erzeugen, wenn zu wenige aktiv sind if (Math.random() < flowChance) { this.initiateMajorFlowCascade(now, fadeoutFactor); } // Aktualisiere die Ein-/Ausblendung von Verbindungen this.updateConnectionsFading(now); // Update flows with proper fading this.updateFlows(now); // Regelmäßig neue Flows erstellen, um das Netzwerk lebendig zu halten if (Math.random() < 0.07) { // Hohe Chance für kontinuierliche Animation this.createNewFlow(now); } // Rekonfiguriere Verbindungen sehr selten für ein sich langsam entwickelndes Netzwerk // Dies verhindert, dass das Netzwerk statisch wird if (Math.random() < 0.0005) { // Sehr seltene Neuverbindungen (0.05% Chance pro Frame) this.createConnections(); } // Render if (this.useWebGL) { this.renderWebGL(now); } else { this.renderCanvas(now); } // Continue animation - garantiere endlose Animation this.animationFrameId = requestAnimationFrame(this.animate.bind(this)); } // Neue Methode: Starte eine größere Kaskade von Flows an strategischen Punkten initiateMajorFlowCascade(now, fadeoutFactor = 1.0) { // Finde gut vernetzte Knoten als Startpunkte const topNodes = [...this.nodes] .map((node, index) => ({ node, index, connections: node.connections.length })) .filter(item => item.connections > 2) // Nur Knoten mit mindestens 3 Verbindungen .sort((a, b) => b.connections - a.connections); // Sortiere nach Verbindungsanzahl if (topNodes.length === 0) return; // Wähle 1-3 Startknoten basierend auf Netzwerkgröße const startNodeCount = Math.max(1, Math.min(3, Math.floor(topNodes.length / 20))); for (let i = 0; i < startNodeCount; i++) { // Wähle einen der Top-Knoten, mit Präferenz für die am besten vernetzten const nodeIndex = Math.floor(Math.random() * Math.min(10, topNodes.length)); const startNodeData = topNodes[nodeIndex]; if (!startNodeData) continue; const startNode = this.nodes[startNodeData.index]; if (!startNode || startNode.connections.length === 0) continue; // Aktiviere den Startknoten startNode.isActive = true; startNode.lastFired = now; startNode.lastCascade = now; // Markiere den Zeitpunkt der letzten Kaskade // Starte Flows an mehreren Verbindungen dieses Knotens const connectionsToActivate = Math.min( Math.ceil(startNode.connections.length / 2), // Bis zu 50% der Verbindungen 5 // Aber maximal 5 ); // Sorgfältige Auswahl der Verbindungen für eine optimale Weitergabe const selectedConnections = []; const allConnections = [...startNode.connections]; // Sortiere Verbindungen nach Aktivierungszeit - bevorzuge solche, die länger nicht aktiviert wurden const sortedConnections = allConnections .map(connIdx => { const connectedNode = this.nodes[connIdx]; const conn = this.connections.find(c => (c.from === startNodeData.index && c.to === connIdx) || (c.from === connIdx && c.to === startNodeData.index) ); return { connIdx, nodeConnections: connectedNode ? connectedNode.connections.length : 0, lastActivated: conn ? conn.lastActivated || 0 : 0 }; }) .sort((a, b) => { // Priorisiere Verbindungen, die länger nicht aktiviert wurden und zu gut vernetzten Knoten führen const timeFactorA = (now - a.lastActivated) / 10000; // Zeit seit letzter Aktivierung in 10-Sekunden-Einheiten const timeFactorB = (now - b.lastActivated) / 10000; return (timeFactorB + b.nodeConnections * 0.1) - (timeFactorA + a.nodeConnections * 0.1); }); // Wähle die besten Verbindungen aus for (let j = 0; j < connectionsToActivate && j < sortedConnections.length; j++) { selectedConnections.push(sortedConnections[j].connIdx); } // Startversatz für Flows eines Knotens - Verzögere die Flows leicht für Welleneffekt const baseDelay = 100; // Basisverzögerung in ms // Erstelle Flows mit leichter Verzögerung zwischen ihnen selectedConnections.forEach((connIdx, idx) => { const conn = this.connections.find(c => (c.from === startNodeData.index && c.to === connIdx) || (c.from === connIdx && c.to === startNodeData.index) ); if (conn) { // Verbindung aktivieren conn.lastActivated = now; // Stelle sicher, dass die Verbindung dauerhaft sichtbar bleibt if (conn.fadeState !== 'visible') { conn.fadeState = 'visible'; conn.fadeStartTime = now; conn.visibleDuration = 600000 + Math.random() * 300000; // 10-15 Minuten sichtbar } // Verbindung aufbauen if (conn.progress < 1) { conn.buildSpeed = 0.03 + Math.random() * 0.02; } // Leichte Verzögerung zwischen den Flows für Welleneffekt const delay = baseDelay + idx * 150; setTimeout(() => { // Erstelle nur, wenn die Animation noch läuft if (!this.isDestroying && this.animationFrameId) { // Bestimme die Richtung des Flows const direction = conn.from === startNodeData.index; // Erstelle den Flow this.flows.push({ id: now + '_cascade_' + idx + '_' + Math.random().toString(36).substr(2, 9), connection: conn, progress: 0, direction: direction, length: this.config.flowLength * (0.8 + Math.random() * 0.4), creationTime: Date.now(), totalDuration: 700 + Math.random() * 400, sourceNodeIdx: direction ? conn.from : conn.to, targetNodeIdx: direction ? conn.to : conn.from, fadeFactor: fadeoutFactor, isForwarded: false, hasForwarded: false, generation: 1, // Erste Generation isCascadeRoot: true // Markiere als Wurzel einer Kaskade }); } }, delay); } }); } } // Updated method to update flow animations with proper fading updateFlows(now) { // Track new flows to add after the loop to avoid modifying the array during iteration const newFlows = []; // Zähle aktive Flows const activeFlows = this.flows.length; // Wenn zu wenige Flows aktiv sind, starte neue Major Flow Cascades if (activeFlows < this.config.minActiveFlows) { this.initiateMajorFlowCascade(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 with reduced speed für schlangenartige Bewegung // Längere Generationen bewegen sich noch langsamer für sichtbare "Schlangen" const generationSlowdown = Math.max(0.7, 1.0 - (flow.generation || 1) * 0.015); flow.progress += (this.config.flowSpeed * generationSlowdown) / 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 aktivieren und Weiterleitung starten, wenn der Flow ihn erreicht hat if (flow.targetNodeIdx !== undefined && flow.progress > 0.80) { const targetNode = this.nodes[flow.targetNodeIdx]; targetNode.isActive = true; targetNode.lastFired = now; // Stelle sicher, dass die Verbindung dauerhaft sichtbar bleibt flow.connection.lastActivated = now; if (flow.connection.fadeState !== 'visible') { flow.connection.fadeState = 'visible'; flow.connection.fadeStartTime = now; flow.connection.visibleDuration = 600000 + Math.random() * 300000; // 10-15 Minuten sichtbar } // Flow ist am Ziel angekommen, starte Weiterleitung zu anderen Knoten // Jeder Flow leitet genau einmal weiter if (!flow.hasForwarded && targetNode.connections.length > 0) { flow.hasForwarded = true; // Durch reduzierte Anzahl der Weiterleitungen schaffen wir einen endloseren Eindruck const connCount = Math.min(3, targetNode.connections.length); const availableConnections = [...targetNode.connections]; // Keine Weiterleitung an die Quelle des Flows zurück if (flow.sourceNodeIdx !== undefined && availableConnections.length > 1) { const sourceIndex = availableConnections.indexOf(flow.sourceNodeIdx); if (sourceIndex >= 0) { availableConnections.splice(sourceIndex, 1); } } // Wenn noch Verbindungen übrig sind, leite weiter if (availableConnections.length > 0) { // Priorisiere ungenutzte Pfade für ein dynamischeres Netzwerk const sortedConnections = availableConnections.map(nodeIdx => { const conn = this.connections.find(c => (c.from === flow.targetNodeIdx && c.to === nodeIdx) || (c.from === nodeIdx && c.to === flow.targetNodeIdx) ); const timeSinceLastActivation = conn ? now - (conn.lastActivated || 0) : 0; const connectionCount = this.nodes[nodeIdx].connections.length; return { nodeIdx, connectionCount, timeSinceLastActivation, score: (timeSinceLastActivation * 0.00001) + (connectionCount * 0.2) + (Math.random() * 0.5) }; }).sort((a, b) => b.score - a.score); // Wähle priorisierte Verbindungen for (let j = 0; j < connCount && j < sortedConnections.length; j++) { const nextNodeIdx = sortedConnections[j].nodeIdx; const nextConn = this.connections.find(c => (c.from === flow.targetNodeIdx && c.to === nextNodeIdx) || (c.from === nextNodeIdx && c.to === flow.targetNodeIdx) ); if (nextConn) { nextConn.lastActivated = now; if (nextConn.fadeState !== 'visible') { nextConn.fadeState = 'visible'; nextConn.fadeStartTime = now; nextConn.visibleDuration = 600000 + Math.random() * 300000; } if (nextConn.progress < 1) { nextConn.buildSpeed = 0.04 + Math.random() * 0.02; } const nextGeneration = (flow.generation || 1) + 1; if (nextGeneration <= this.config.maxFlowGenerations) { const direction = nextConn.from === flow.targetNodeIdx; // Berechne Verzögerung basierend auf Verbindungslänge const connectionLength = Math.sqrt( Math.pow(this.nodes[nextConn.to].x - this.nodes[nextConn.from].x, 2) + Math.pow(this.nodes[nextConn.to].y - this.nodes[nextConn.from].y, 2) ); const baseDelay = 80; const lengthFactor = Math.min(1.2, connectionLength / 200); const genFactor = Math.max(0.2, 1.0 - (nextGeneration * 0.005)); const delay = baseDelay * lengthFactor * genFactor; setTimeout(() => { if (!this.isDestroying && this.animationFrameId) { newFlows.push({ id: now + '_' + Math.random().toString(36).substr(2, 9), connection: nextConn, progress: 0, direction: direction, length: this.config.flowLength * (0.9 + Math.random() * 0.2), creationTime: Date.now(), totalDuration: 600 + Math.random() * 300 + (nextGeneration * 5), sourceNodeIdx: direction ? nextConn.from : nextConn.to, targetNodeIdx: direction ? nextConn.to : nextConn.from, fadeFactor: flow.fadeFactor || 1.0, isForwarded: false, generation: nextGeneration, hasForwarded: false, parentFlow: flow.id }); } }, delay); } } } } } } // Stellen Sie sicher, dass die Verbindung aktiv bleibt flow.connection.lastActivated = now; if (flow.connection.fadeState === 'visible') { flow.connection.visibleDuration = Math.max( flow.connection.visibleDuration || 300000, 600000 + Math.random() * 300000 ); } // Remove completed flows, but ensure parent-child relationships for visible "snakes" const isComplete = flow.progress > 1.0 || flowProgress >= 1.0; const hasMaxGeneration = flow.generation && flow.generation > this.config.maxFlowGenerations; if (isComplete || hasMaxGeneration) { this.flows.splice(i, 1); } } // Füge alle neuen Flows hinzu this.flows.push(...newFlows); } // Aktualisiert die Ein-/Ausblendung von Verbindungen mit deutlich langsameren Übergängen updateConnectionsFading(now) { for (const connection of this.connections) { const elapsedTime = now - connection.fadeStartTime; // Update connection fade status if (connection.fadeState === 'in') { // Einblenden - langsamer für sanfteren Aufbau connection.fadeProgress = Math.min(1.0, elapsedTime / (connection.fadeTotalDuration * 1.5)); if (connection.fadeProgress >= 1.0) { connection.fadeState = 'visible'; connection.fadeStartTime = now; } } else if (connection.fadeState === 'visible') { // Verbindung ist vollständig sichtbar - EXTREM lange Sichtbarkeitsdauer // für permanente Netzwerkstrukturen if (elapsedTime > connection.visibleDuration * 3.0) { // Erhöht von 2.5 auf 3.0 // Prüfe, ob die Verbindung kürzlich aktiviert wurde if (now - connection.lastActivated < 120000) { // 2 Minuten Aktivitätsschutz // Wenn kürzlich aktiviert, verlängere die Sichtbarkeit connection.fadeStartTime = now; } else { // Nur sehr alte, inaktive Verbindungen langsam ausblenden connection.fadeState = 'out'; connection.fadeStartTime = now; connection.fadeProgress = 1.0; } } } else if (connection.fadeState === 'out') { // Ausblenden - EXTREM langsam für dauerhaft sichtbare Strukturen connection.fadeProgress = Math.max(0.25, 1.0 - (elapsedTime / (connection.fadeTotalDuration * 10.0))); // Erhöht von 8.0 auf 10.0 // Verbindungen bleiben dauerhaft stark sichtbar if (connection.fadeProgress <= 0.25) { // Minimum erhöht von 0.15 auf 0.25 // Verbindungen niemals komplett verschwinden lassen connection.fadeState = 'in'; connection.fadeStartTime = now; connection.fadeProgress = 0.25; // Höhere Grundsichtbarkeit connection.visibleDuration = 600000 + Math.random() * 300000; // 10-15 Minuten sichtbar } } else if (connection.fadeState === 'hidden') { // Keine Verbindungen verstecken, alle sichtbar halten connection.fadeState = 'in'; connection.fadeStartTime = now; connection.fadeProgress = 0.25; // Höhere Grundsichtbarkeit } // Verbindungen schneller vollständig aufbauen für permanentes Netzwerk if (connection.progress < 1) { // Erhöhte Basisgeschwindigkeit für schnelleren Aufbau const baseBuildSpeed = 0.008; // Erhöht für schnelleren permanenten Aufbau let buildSpeed = connection.buildSpeed || baseBuildSpeed; // Wenn kürzlich aktiviert, noch schneller aufbauen if (now - connection.lastActivated < 2000) { buildSpeed = Math.max(buildSpeed, 0.015); // Erhöht für schnelleren Aufbau } connection.progress += buildSpeed; if (connection.progress > 1) { connection.progress = 1; // Zurücksetzen der Aufbaugeschwindigkeit connection.buildSpeed = 0; } } } } // Updated method to create flow animations with timing info createNewFlow(now) { if (this.connections.length === 0 || this.flows.length >= 8) return; // Verbesserte Auswahl zufälliger Verbindungen mit Präferenz für hoch vernetzte Knoten // Sammle alle Verbindungen und bewerte sie nach der Anzahl ihrer Verbindungen const weightedConnections = this.connections.map((conn, index) => { const fromNode = this.nodes[conn.from]; const toNode = this.nodes[conn.to]; const connectionCount = fromNode.connections.length + toNode.connections.length; return { index, connectionCount, weight: connectionCount + Math.random() * 5 // Füge Zufall hinzu für Variabilität }; }); // Sortiere nach Gewicht (Verbindungsanzahl + Zufallsfaktor) weightedConnections.sort((a, b) => b.weight - a.weight); // Wähle eine der Top-Verbindungen const connectionIdx = weightedConnections[Math.floor(Math.random() * Math.min(15, weightedConnections.length))].index; const connection = this.connections[connectionIdx]; // Verbindung als "im Aufbau" markieren, wenn sie noch nicht vollständig ist if (connection.progress < 1) { connection.buildSpeed = 0.02 + Math.random() * 0.01; // Schnellerer Aufbau während eines Blitzes } // Stelle sicher, dass die Verbindung dauerhaft sichtbar bleibt connection.lastActivated = now; if (connection.fadeState !== 'visible') { connection.fadeState = 'visible'; connection.fadeStartTime = now; connection.visibleDuration = 150000 + Math.random() * 90000; // 2.5-4 Minuten sichtbar } // 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: 900 + Math.random() * 500, // Längere Gesamtdauer (900-1400ms) sourceNodeIdx: connection.direction ? connection.from : connection.to, targetNodeIdx: connection.direction ? connection.to : connection.from, isForwarded: false, // Dieses Flag ermöglicht Weiterleitung hasForwarded: false, // Noch nicht weitergeleitet generation: 1 // Erste Generation }); } 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); // Fade-Faktor für sanftes Ausblenden const fadeoutFactor = this.isDestroying ? parseFloat(this.canvas.style.opacity) || 0.98 : 1.0; // 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, fadeoutFactor); // Draw flows on top of connections this.renderFlowsWebGL(now, fadeoutFactor); // Draw nodes this.renderNodesWebGL(now, fadeoutFactor); } renderNodesWebGL(now, fadeoutFactor = 1.0) { // 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.2 : 1.0; // Reduziert von 1.3 auf 1.2 let pulse = (Math.sin(node.pulsePhase) * 0.2 + 1.1) * activationBoost; // Reduzierte Pulsation // Größe basierend auf Konnektivität und Wichtigkeit, aber subtiler const connectivityFactor = 1 + (node.connections.length / this.config.maxConnections) * 0.8; // Reduziert von 1.1 auf 0.8 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 - noch subtiler if (node.isActive) { r = (r * 0.8 + nodePulseColor.r / 255 * 0.2); // Reduziert von 0.7/0.3 auf 0.8/0.2 g = (g * 0.8 + nodePulseColor.g / 255 * 0.2); b = (b * 0.8 + nodePulseColor.b / 255 * 0.2); } // Noch subtilere Knoten mit weiter reduzierter Opazität // Berücksichtige fadeoutFactor für sanftes Ausblenden const nodeOpacity = node.isActive ? 0.65 : 0.55; // Reduziert von 0.75/0.65 auf 0.65/0.55 this.gl.uniform4f( this.programInfo.uniformLocations.color, r, g, b, nodeOpacity * fadeoutFactor // Reduzierte Opazität während des Ausblendprozesses ); // Draw each node individually for better control this.gl.drawArrays(this.gl.POINTS, i, 1); } } renderConnectionsWebGL(now, fadeoutFactor = 1.0) { 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.7; // Reduzierte Gesamtopazität auf 70% // 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.25); // Reduzierte Highlight-Opazität auf 25% } // Berücksichtige fadeoutFactor für sanftes Ausblenden opacity *= fadeoutFactor; 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 with subtilerer Appearance renderFlowsWebGL(now, fadeoutFactor = 1.0) { // 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, 7, 8); // Reduzierte Zickzack-Amplitude 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 // Berücksichtige auch den fadeoutFactor für das Ausblenden der gesamten Animation const fadeFactor = (flow.fadeFactor || 1.0) * fadeoutFactor; this.gl.uniform4f( this.programInfo.uniformLocations.color, flowColor.r / 255, flowColor.g / 255, flowColor.b / 255, 0.55 * fadeFactor // Reduziert von 0.7 auf 0.55 für subtilere Blitze ); this.gl.enable(this.gl.BLEND); this.gl.blendFunc(this.gl.SRC_ALPHA, this.gl.ONE); this.gl.lineWidth(1.2); // Schmaler Blitz this.gl.drawArrays(this.gl.LINES, 0, 2); } // Funken erzeugen - subtilere elektrische Funken const sparks = this.generateSparkPoints(zigzag, 5 + Math.floor(Math.random() * 3)); // Weniger Funken for (const spark of sparks) { // Helles Weiß-Blau für elektrische Funken, aber dezenter this.gl.uniform4f( this.programInfo.uniformLocations.color, 0.85, 0.92, 0.98, 0.5 * fadeoutFactor // Reduzierte Opazität auf 50% ); // 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 - kleinere Funken const sizes = new Float32Array([spark.size * 2.5]); // Reduziert von 3.0 auf 2.5 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); // Fade-Faktor für sanftes Ausblenden im Canvas-Modus const fadeoutFactor = this.isDestroying ? parseFloat(this.canvas.style.opacity) || 0.98 : 1.0; 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); } // Berücksichtige fadeoutFactor für sanftes Ausblenden opacity *= fadeoutFactor; 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 * fadeoutFactor})`; 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 = 0; // Reset shadow for other elements } // Draw flows with fading effect this.renderFlowsCanvas(now, fadeoutFactor); // 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; // Während des Ausblendprozesses die Knotengröße leicht reduzieren const sizeReduction = this.isDestroying ? 0.85 : 1.0; const nodeSize = node.size * pulse * connectivityFactor * sizeReduction; // 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 // Berücksichtige fadeoutFactor für sanftes Ausblenden const activeOpacity = node.isActive ? 0.95 : 0.8; glow.addColorStop(0, `rgba(${r}, ${g}, ${b}, ${activeOpacity * fadeoutFactor})`); glow.addColorStop(0.4, `rgba(${r}, ${g}, ${b}, ${0.45 * fadeoutFactor})`); 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; // Globale Transparenz reduzieren beim Ausblenden this.ctx.globalAlpha = (node.isActive ? 1.0 : 0.92) * fadeoutFactor; 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 * fadeoutFactor})`; this.ctx.fill(); } this.ctx.globalAlpha = 1.0; } } // New method to render flows in Canvas mode with fading renderFlowsCanvas(now, fadeoutFactor = 1.0) { 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) { // Einblenden - sanfter und länger fadeFactor = flowAge / (flowLifetime * 0.3); } else if (flowAge > flowLifetime * 0.7) { // Ausblenden - sanfter und länger fadeFactor = 1.0 - ((flowAge - flowLifetime * 0.7) / (flowLifetime * 0.3)); } // Beim Ausblenden der gesamten Animation auch den Flow-Faktor anpassen fadeFactor *= fadeoutFactor; // 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 mit deutlich sanfterem Ausblenden destroy() { // Sanfte Ausblendanimation starten this.isDestroying = true; // Canvas-Element sanft ausblenden über einen längeren Zeitraum if (this.canvas) { this.canvas.style.transition = 'opacity 5s ease-in-out'; // Verlängert von 1.5s auf 5s this.canvas.style.opacity = '0'; // Warte auf das Ende der Übergangsanimation, bevor die Ressourcen freigegeben werden setTimeout(() => { // Animation schrittweise verlangsamen statt sofort stoppen const slowDownAnimation = () => { // Schrittweise die Animation verlangsamen durch Reduzierung der FPS if (this.animationFrameId) { cancelAnimationFrame(this.animationFrameId); // Animiere mit abnehmender Framerate setTimeout(() => { this.animationFrameId = requestAnimationFrame(this.animate.bind(this)); // Nach 10 weiteren Sekunden komplett beenden if (this._destroyStartTime && (Date.now() - this._destroyStartTime > 10000)) { finalCleanup(); } else { slowDownAnimation(); } }, 500); // Zunehmende Verzögerung zwischen Frames } }; // Endgültige Bereinigung nach dem vollständigen Ausblenden const finalCleanup = () => { // Animation stoppen if (this.animationFrameId) { cancelAnimationFrame(this.animationFrameId); this.animationFrameId = null; } // Event-Listener entfernen window.removeEventListener('resize', this.resizeCanvas.bind(this)); // Canvas-Element aus dem DOM entfernen, nachdem es ausgeblendet wurde 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 erfolgreich deaktiviert'); }; // Startzeit für die schrittweise Verlangsamung setzen this._destroyStartTime = Date.now(); // Starte die schrittweise Verlangsamung slowDownAnimation(); }, 5500); // Warte länger als die CSS-Transition-Dauer } } // 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; } // New method to render the flowing animations with intensiveren Glow und Lila-Farbverlauf renderFlowsWebGL(now, fadeoutFactor = 1.0) { // Für jeden Flow einen leuchtenden Lila-Blitz als Schlange 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 }; } // Erzeuge Schlangenbewegung durch intensivere Zickzack-Muster const snakeSegments = 12; // Mehr Segmente für flüssigere Schlangenbewegung const snakeAmplitude = 12; // Größere Amplitude für deutlichere Schlangenbewegung // Einflussfaktor, der von der Generation abhängt - höhere Generationen schlängeln mehr const generationFactor = Math.min(1.5, (flow.generation || 1) / 10 + 0.8); const zigzag = this.generateZigZagPoints( p1, p2, snakeSegments, snakeAmplitude * generationFactor, true // Sanftere Kurven aktivieren ); // Lila Gradient Effekt für Blitze - Multi-Pass Rendering für intensives Glühen // 1. Render outer glow - very wide and subtle const outerGlowWidth = 6.0 * this.config.glowIntensity; 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); // Äußerer Glow in dunklerem Lila mit niedriger Opazität this.gl.uniform4f( this.programInfo.uniformLocations.color, 0.45, 0.2, 0.7, 0.15 * fadeoutFactor // Dunkler lila Außenglow ); this.gl.enable(this.gl.BLEND); this.gl.blendFunc(this.gl.SRC_ALPHA, this.gl.ONE); this.gl.lineWidth(outerGlowWidth); this.gl.drawArrays(this.gl.LINES, 0, 2); } // 2. Middle glow layer - more opaque and focused const middleGlowWidth = 3.5 * this.config.glowIntensity; 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 ); // Mittlerer Glow in hellerem Lila mit mittlerer Opazität this.gl.uniform4f( this.programInfo.uniformLocations.color, 0.65, 0.3, 0.85, 0.35 * fadeoutFactor // Mittleres Lila ); this.gl.lineWidth(middleGlowWidth); this.gl.drawArrays(this.gl.LINES, 0, 2); } // 3. Inner core - bright and vibrant 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 ); // Innerer Kern in strahlendem Lila-Weiß this.gl.uniform4f( this.programInfo.uniformLocations.color, 0.9, 0.7, 1.0, 0.8 * fadeoutFactor // Weißlich-Lila für intensives Zentrum ); this.gl.lineWidth(1.6); // Schmaler Kern this.gl.drawArrays(this.gl.LINES, 0, 2); } // Viele intensivere Funken erzeugen const sparkCount = this.config.sparkCount + Math.floor((flow.generation || 1) / 2); // Mehr Funken bei höheren Generationen const sparks = this.generateSparkPoints(zigzag, sparkCount); // Render die Funken mit mehreren Ebenen für intensiveres Glühen for (const spark of sparks) { // 1. Äußerer Glow der Funken this.gl.uniform4f( this.programInfo.uniformLocations.color, 0.5, 0.2, 0.8, 0.3 * fadeoutFactor // Lila Glow mit niedriger Opazität ); // 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); // Größerer Glow um jeden Funken const sizes = new Float32Array([spark.size * this.config.sparkSize * 2.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); // 2. Mittlere Schicht der Funken this.gl.uniform4f( this.programInfo.uniformLocations.color, 0.7, 0.4, 0.95, 0.6 * fadeoutFactor // Mittleres Lila mit mittlerer Opazität ); const middleSizes = new Float32Array([spark.size * this.config.sparkSize * 1.2]); this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.sizeBuffer); this.gl.bufferData(this.gl.ARRAY_BUFFER, middleSizes, this.gl.STATIC_DRAW); this.gl.drawArrays(this.gl.POINTS, 0, 1); // 3. Innerer Kern der Funken this.gl.uniform4f( this.programInfo.uniformLocations.color, 0.95, 0.85, 1.0, 0.9 * fadeoutFactor // Fast weißes Zentrum für intensives Leuchten ); const innerSizes = new Float32Array([spark.size * this.config.sparkSize * 0.6]); this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.sizeBuffer); this.gl.bufferData(this.gl.ARRAY_BUFFER, innerSizes, this.gl.STATIC_DRAW); this.gl.drawArrays(this.gl.POINTS, 0, 1); } } } // Hilfsfunktion: Erzeuge schlangenartige Zickzack-Punkte mit Glätte-Option generateZigZagPoints(start, end, segments = 5, amplitude = 8, smooth = false) { 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) ); // Dynamische Amplitude basierend auf Distanz const baseAmplitude = amplitude * (totalDistance / 150); // Welle für schlangenartigen Effekt const waveFrequency = 2 + Math.random() * 1.5; // Zusätzlicher Welleneffekt // Vorherige Punktrichtung für glattere Übergänge let prevOffsetX = 0; let prevOffsetY = 0; for (let i = 1; i < segments; i++) { const t = i / segments; // Kubische Easing-Funktion für natürlichere Bewegung const easedT = smooth ? t*t*(3-2*t) : t; const x = start.x + (end.x - start.x) * easedT; const y = start.y + (end.y - start.y) * easedT; // Schlangenbewegung durch Sinuswelle mit zusätzlicher Zeit- und Positionsabhängigkeit const wavePhase = waveFrequency * Math.PI * t + (Date.now() % 1000) / 1000 * Math.PI; const waveAmplitude = Math.sin(wavePhase) * baseAmplitude; // Perpendikuläre Richtung zur Hauptrichtung + etwas Zufall const perpendicularAngle = mainAngle + Math.PI/2 + (Math.random() * 0.3 - 0.15); // Für glattere Übergänge, interpoliere zwischen vorherigem und neuem Offset let offsetX, offsetY; if (smooth && i > 1) { const newOffsetX = Math.cos(perpendicularAngle) * waveAmplitude; const newOffsetY = Math.sin(perpendicularAngle) * waveAmplitude; // Interpoliere zwischen vorherigem und neuem Offset (30% vorheriger, 70% neuer) offsetX = prevOffsetX * 0.3 + newOffsetX * 0.7; offsetY = prevOffsetY * 0.3 + newOffsetY * 0.7; // Speichere für nächsten Punkt prevOffsetX = offsetX; prevOffsetY = offsetY; } else { offsetX = Math.cos(perpendicularAngle) * waveAmplitude; offsetY = Math.sin(perpendicularAngle) * waveAmplitude; prevOffsetX = offsetX; prevOffsetY = offsetY; } points.push({ x: x + offsetX, y: y + offsetY }); } points.push(end); return points; } // Hilfsfunktion: Erzeuge leuchtende Funkenpunkte generateSparkPoints(zigzag, sparkCount = 15) { const sparks = []; // Mehr Funken für intensiveren Effekt const actualSparkCount = Math.min(sparkCount, zigzag.length * 3); // Funken an zufälligen Stellen entlang des Blitzes mit mehr Variabilität for (let i = 0; i < actualSparkCount; i++) { // Zufälliges Segment des Zickzacks auswählen, mit Präferenz für Ecken let segIndex; if (Math.random() < 0.6 && zigzag.length > 3) { // 60% der Funken an Ecken platzieren (mehr Funken an Wendepunkten) segIndex = 1 + Math.floor(Math.random() * (zigzag.length - 3)); } else { // Rest gleichmäßig verteilen 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 mit Präferenz für die Mitte let t; if (Math.random() < 0.4) { // 40% der Funken näher zur Mitte des Segments t = 0.3 + Math.random() * 0.4; } else { // Rest gleichmäßig verteilen t = Math.random(); } const x = zigzag[segIndex].x + dx * t; const y = zigzag[segIndex].y + dy * t; // Dynamischer Versatz für intensivere Funken // Versatz tendenziell senkrecht zur Segmentrichtung const offsetAngle = segmentAngle + (Math.random() * Math.PI - Math.PI/2); // Größerer Versatz für Funken, die weiter vom Pfad wegfliegen const offsetDistance = (0.8 + Math.random() * 2.0) * 6; // Größere und variablere Funken mit Zeit-basiertem Pulsieren const baseSize = 0.8 + Math.random() * 1.0; // Pulsierender Effekt const pulsePhase = (Date.now() % 1000) / 1000 * Math.PI * 2; const pulseFactor = 0.8 + Math.sin(pulsePhase) * 0.2; sparks.push({ x: x + Math.cos(offsetAngle) * offsetDistance, y: y + Math.sin(offsetAngle) * offsetDistance, size: baseSize * pulseFactor, // Zufälliger Winkel für Funken-Rotation angle: Math.random() * Math.PI * 2 }); } 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) { window.neuralNetworkBackground.destroy(); } });