From ffe96074f459352e978205411a9bb7d33b5c0a04 Mon Sep 17 00:00:00 2001 From: Till Tomczak Date: Tue, 29 Apr 2025 12:32:18 +0200 Subject: [PATCH] =?UTF-8?q?Verbessere=20die=20Funktionalit=C3=A4t=20des=20?= =?UTF-8?q?neuronalen=20Netzwerk-Hintergrunds=20in=20neural-network-backgr?= =?UTF-8?q?ound.js=20durch=20die=20Einf=C3=BChrung=20einer=20flexiblen=20K?= =?UTF-8?q?onfigurationsstruktur,=20die=20benutzerdefinierte=20Optionen=20?= =?UTF-8?q?unterst=C3=BCtzt.=20Optimiere=20die=20Cluster-Generierung=20und?= =?UTF-8?q?=20-Verteilung,=20verbessere=20die=20Verbindungslogik=20zwische?= =?UTF-8?q?n=20Knoten=20und=20implementiere=20erweiterte=20Fehlerbehandlun?= =?UTF-8?q?g=20in=20den=20API-Funktionen=20in=20mindmap.js.=20F=C3=BCge=20?= =?UTF-8?q?Fallback-Knoten=20hinzu,=20um=20die=20Benutzererfahrung=20zu=20?= =?UTF-8?q?verbessern,=20und=20implementiere=20visuelle=20R=C3=BCckmeldung?= =?UTF-8?q?en=20f=C3=BCr=20die=20Initialisierung=20der=20Mindmap.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- __pycache__/app.cpython-313.pyc | Bin 69218 -> 69190 bytes static/js/mindmap.js | 112 +++++- static/neural-network-background.js | 598 ++++++++++++++++++++-------- 3 files changed, 523 insertions(+), 187 deletions(-) diff --git a/__pycache__/app.cpython-313.pyc b/__pycache__/app.cpython-313.pyc index 6bee820f42876557d9f0c26913a25239adfb78df..b814309481afddfdbc5f633a8ffb0262aac4da30 100644 GIT binary patch delta 10831 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 cowoO0Wj(Uq#yw?^Gbb;}v3#84SW;;JKcmE fetch(endpoint).then(r => r.json()); - const post = (endpoint, body) => - fetch(endpoint, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(body) - }).then(r => r.json()); - const del = endpoint => - fetch(endpoint, { method: 'DELETE' }).then(r => r.json()); + const get = async endpoint => { + try { + const response = await fetch(endpoint); + if (!response.ok) { + console.error(`API-Fehler: ${endpoint} antwortet mit Status ${response.status}`); + return []; // Leeres Array zurückgeben bei Fehlern + } + return await response.json(); + } catch (error) { + console.error(`Fehler beim Abrufen von ${endpoint}:`, error); + return []; // Leeres Array zurückgeben bei Netzwerkfehlern + } + }; + + const post = async (endpoint, body) => { + try { + const response = await fetch(endpoint, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body) + }); + if (!response.ok) { + console.error(`API-Fehler: ${endpoint} antwortet mit Status ${response.status}`); + return {}; // Leeres Objekt zurückgeben bei Fehlern + } + return await response.json(); + } catch (error) { + console.error(`Fehler beim POST zu ${endpoint}:`, error); + return {}; // Leeres Objekt zurückgeben bei Netzwerkfehlern + } + }; + + const del = async endpoint => { + try { + const response = await fetch(endpoint, { method: 'DELETE' }); + if (!response.ok) { + console.error(`API-Fehler: ${endpoint} antwortet mit Status ${response.status}`); + return {}; // Leeres Objekt zurückgeben bei Fehlern + } + return await response.json(); + } catch (error) { + console.error(`Fehler beim DELETE zu ${endpoint}:`, error); + return {}; // Leeres Objekt zurückgeben bei Netzwerkfehlern + } + }; /* 3. Kategorien laden für Style-Informationen */ let categories = await get('/api/categories'); @@ -92,9 +128,12 @@ // Graph leeren (für Reload-Fälle) cy.elements().remove(); + // Überprüfen, ob nodes ein Array ist, wenn nicht, setze es auf ein leeres Array + const nodesArray = Array.isArray(nodes) ? nodes : []; + // Knoten zum Graph hinzufügen cy.add( - nodes.map(node => { + nodesArray.map(node => { // Kategorie-Informationen für Styling abrufen const category = categories.find(c => c.id === node.category_id) || {}; @@ -112,9 +151,12 @@ }) ); + // Überprüfen, ob relationships ein Array ist, wenn nicht, setze es auf ein leeres Array + const relationshipsArray = Array.isArray(relationships) ? relationships : []; + // Kanten zum Graph hinzufügen cy.add( - relationships.map(rel => ({ + relationshipsArray.map(rel => ({ data: { id: `${rel.parent_id}_${rel.child_id}`, source: rel.parent_id.toString(), @@ -123,6 +165,54 @@ })) ); + // Wenn keine Knoten geladen wurden, Fallback-Knoten erstellen + if (nodesArray.length === 0) { + // Mindestens einen Standardknoten hinzufügen + cy.add({ + data: { + id: 'fallback-1', + name: 'Mindmap', + description: 'Erstellen Sie hier Ihre eigene Mindmap', + color: '#3b82f6', + icon: 'help-circle' + }, + position: { x: 300, y: 200 } + }); + + // Erfolgsmeldung anzeigen + console.log('Mindmap erfolgreich initialisiert mit Fallback-Knoten'); + + // Info-Meldung für Benutzer anzeigen + const infoBox = document.createElement('div'); + infoBox.classList.add('info-message'); + infoBox.style.position = 'absolute'; + infoBox.style.top = '50%'; + infoBox.style.left = '50%'; + infoBox.style.transform = 'translate(-50%, -50%)'; + infoBox.style.padding = '15px 20px'; + infoBox.style.backgroundColor = 'rgba(59, 130, 246, 0.9)'; + infoBox.style.color = 'white'; + infoBox.style.borderRadius = '8px'; + infoBox.style.zIndex = '5'; + infoBox.style.maxWidth = '80%'; + infoBox.style.textAlign = 'center'; + infoBox.style.boxShadow = '0 4px 12px rgba(0,0,0,0.15)'; + infoBox.innerHTML = 'Mindmap erfolgreich initialisiert.
Verwenden Sie die Werkzeugleiste, um Knoten hinzuzufügen.'; + + document.getElementById('cy').appendChild(infoBox); + + // Meldung nach 5 Sekunden ausblenden + setTimeout(() => { + infoBox.style.opacity = '0'; + infoBox.style.transition = 'opacity 0.5s ease'; + setTimeout(() => { + if (infoBox.parentNode) { + infoBox.parentNode.removeChild(infoBox); + } + }, 500); + }, 5000); + } + // Layout anwenden wenn keine Positionsdaten vorhanden const nodesWithoutPosition = cy.nodes().filter(node => !node.position() || (node.position().x === 0 && node.position().y === 0) diff --git a/static/neural-network-background.js b/static/neural-network-background.js index 2e5edeb..00c702f 100644 --- a/static/neural-network-background.js +++ b/static/neural-network-background.js @@ -5,9 +5,52 @@ */ class NeuralNetworkBackground { - constructor() { + 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 = document.createElement('canvas'); this.canvas.id = 'neural-network-background'; this.canvas.style.position = 'fixed'; this.canvas.style.top = '0'; @@ -36,7 +79,6 @@ class NeuralNetworkBackground { 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; @@ -58,33 +100,13 @@ class NeuralNetworkBackground { flowColor: '#a0c7e0' // Sanfteres Blitz-Blau }; - // Farben für Light Mode dezenter und harmonischer gestalten + // Optimierte Farbpalette für Light Mode mit verbesserter Harmonie und Lesbarkeit this.lightModeColors = { - background: '#f5f7fa', // Hellerer Hintergrund für subtileren Kontrast - nodeColor: '#5570b0', // Gedämpfteres Blau - nodePulse: '#7aa8d0', // Sanfteres Türkis für Glow - connectionColor: '#8a8fc0', // Dezenteres Lila - flowColor: '#6d97d0' // Sanfteres Blau für Blitze - }; - - // Konfigurationsobjekt für subtilere, sanftere Neuronen - this.config = { - nodeCount: 35, // Reduziert für bessere Leistung und subtileres Aussehen - nodeSize: 3.5, // Größere Knoten für bessere Sichtbarkeit - nodeVariation: 0.5, // Weniger Varianz für gleichmäßigeres Erscheinungsbild - connectionDistance: 250, // Größere Verbindungsdistanz - connectionOpacity: 0.18, // Schwächere Verbindungen für subtileren Effekt - animationSpeed: 0.015, // Langsamere Animation für sanftere Bewegung - pulseSpeed: 0.0015, // Langsameres Pulsieren für subtilere Animation - flowSpeed: 0.45, // Langsamer für bessere Sichtbarkeit - flowDensity: 0.003, // Weniger Blitze gleichzeitig erzeugen - flowLength: 0.1, // Kürzere Blitze für dezentere Effekte - maxConnections: 3, // Weniger Verbindungen pro Neuron - clusteringFactor: 0.45, // Stärkeres Clustering - linesFadeDuration: 4000, // Längere Dauer für sanfteres Ein-/Ausblenden von Linien (ms) - linesWidth: 0.7, // Dünnere unterliegende Linien für subtileren Eindruck - linesOpacity: 0.3, // Geringere Opazität für Linien - maxFlowCount: 8 // Begrenzte Anzahl gleichzeitiger Flüsse + 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 @@ -228,60 +250,169 @@ class NeuralNetworkBackground { const width = this.canvas.width / (window.devicePixelRatio || 1); const height = this.canvas.height / (window.devicePixelRatio || 1); - // Erstelle Cluster-Zentren für neuronale Netzwerkmuster - const clusterCount = Math.floor(5 + Math.random() * 4); // 5-8 Cluster + // 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: Math.random() * width, - y: Math.random() * height, - radius: 100 + Math.random() * 150 + 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 }); } - // Create nodes with random positions and properties + // 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++) { - // Entscheide, ob dieser Knoten zu einem Cluster gehört oder nicht - const useCluster = Math.random() < this.config.clusteringFactor; - let x, y; + const inCluster = Math.random() < this.config.clusteringFactor; + let x, y, clusterType = -1; // -1 bedeutet "kein Cluster" + let assignedCluster = null; - if (useCluster && clusters.length > 0) { + if (inCluster && 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; + 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(0, Math.min(width, x)); - y = Math.max(0, Math.min(height, y)); + 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 - x = Math.random() * width; - y = Math.random() * height; + // 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 größer - const nodeImportance = useCluster ? 1.2 : 0.8; - const size = this.config.nodeSize * nodeImportance + Math.random() * this.config.nodeVariation; + // 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, - y: (Math.random() - 0.5) * this.config.animationSpeed + 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: 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 + 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); @@ -289,76 +420,113 @@ class NeuralNetworkBackground { } createConnections() { - this.connections = []; - this.flows = []; // Reset flows - - // Create connections between nearby nodes + // Connection probability matrix based on distance and cluster membership 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; - + 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); - if (distance < this.config.connectionDistance) { - potentialConnections.push({ - index: j, - distance: distance - }); + // 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; + } } - } - - // Sortiere nach Entfernung - potentialConnections.sort((a, b) => a.distance - b.distance); - - // Wähle die nächsten N Verbindungen, maximal maxConnections - const maxConn = Math.min( - this.config.maxConnections, - potentialConnections.length, - 1 + Math.floor(Math.random() * this.config.maxConnections) - ); - - for (let c = 0; c < maxConn; c++) { - const connection = potentialConnections[c]; - const j = connection.index; - const nodeB = this.nodes[j]; - const distance = connection.distance; - // Create weighted connection (closer = stronger) - const connectionStrength = Math.max(0, 1 - distance / this.config.connectionDistance); - const connOpacity = connectionStrength * this.config.connectionOpacity; + // Zufällige Variation der Verbindungsdistanz + const connectionDistance = Math.random() * 275 + 25; - // 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 + Math.random() * 1000, // Zufällige Dauer - visibleDuration: 10000 + Math.random() * 15000, // Wie lange die Linie sichtbar bleibt - 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); + // Ü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); } } } @@ -841,6 +1009,8 @@ class NeuralNetworkBackground { // 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, @@ -972,7 +1142,7 @@ class NeuralNetworkBackground { this.ctx.fill(); } - this.ctx.shadowBlur = 0; // Reset shadow for other elements + this.ctx.shadowBlur = 2; // Reset shadow for other elements } // Draw flows with fading effect @@ -1070,10 +1240,8 @@ class NeuralNetworkBackground { // 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)); } @@ -1092,58 +1260,60 @@ class NeuralNetworkBackground { y: startNode.y + (endNode.y - startNode.y) * endProgress }; - // Prüfe, ob der Fluss den aktuellen Verbindungsfortschritt überschritten hat if (endProgress > connProgress) continue; - // Farbe des Flusses basierend auf dem aktuellen Modus - const flowColor = this.isDarkMode ? this.darkModeColors.flowColor : this.lightModeColors.flowColor; - const rgbFlowColor = this.hexToRgb(flowColor); - - const baseAngle = Math.atan2(p2.y - p1.y, p2.x - p1.x); + // 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(); - // Subtilere Untergrundspur für den Blitz + // 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 = `rgba(${rgbFlowColor.r}, ${rgbFlowColor.g}, ${rgbFlowColor.b}, ${0.12 * fadeFactor})`; // Reduziert von 0.15 - this.ctx.lineWidth = 2.5; // Reduziert von 3 - this.ctx.shadowColor = `rgba(${rgbFlowColor.r}, ${rgbFlowColor.g}, ${rgbFlowColor.b}, ${0.08 * fadeFactor})`; // Reduziert von 0.1 - this.ctx.shadowBlur = 6; // Reduziert von 7 + 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(); - // Zickzack-Blitz mit geringerer Vibration generieren - const zigzag = this.generateZigZagPoints(p1, p2, 6, 7); + // Abgerundeter Zickzack-Blitz mit weicheren Kurven + const zigzag = this.generateZigZagPoints(p1, p2, 4, 6, true); - // Hauptblitz mit dezenterem Ein-/Ausblendeffekt - this.ctx.strokeStyle = `rgba(${rgbFlowColor.r}, ${rgbFlowColor.g}, ${rgbFlowColor.b}, ${0.4 * fadeFactor})`; // Reduziert von 0.5 - this.ctx.lineWidth = 1.0; // Reduziert von 1.2 - this.ctx.shadowColor = `rgba(${rgbFlowColor.r}, ${rgbFlowColor.g}, ${rgbFlowColor.b}, ${0.2 * fadeFactor})`; // Reduziert von 0.25 - this.ctx.shadowBlur = 5; // Reduziert von 6 + // 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++) { - this.ctx.lineTo(zigzag[i].x, zigzag[i].y); + 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(); - // Intensivere und mehr Funken - const sparks = this.generateSparkPoints(zigzag, 8 + Math.floor(Math.random() * 5)); + // Funken mit lila Glühen + const sparks = this.generateSparkPoints(zigzag, 10 + Math.floor(Math.random() * 6)); - // Intensiveres Funkenlicht mit dynamischem Ein-/Ausblendeffekt - const sparkBaseOpacity = this.isDarkMode ? 0.85 : 0.75; - const sparkBaseColor = this.isDarkMode - ? `rgba(230, 240, 250, ${sparkBaseOpacity * fadeFactor})` - : `rgba(190, 230, 250, ${sparkBaseOpacity * fadeFactor})`; + 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(); - // Dynamischere Stern/Funken-Form - const points = 4 + Math.floor(Math.random() * 4); // 4-7 Spitzen + // Weichere Sternform + const points = 4 + Math.floor(Math.random() * 3); const outerRadius = spark.size * 2.0; - const innerRadius = spark.size * 0.35; + const innerRadius = spark.size * 0.5; for (let i = 0; i < points * 2; i++) { const radius = i % 2 === 0 ? outerRadius : innerRadius; @@ -1160,21 +1330,60 @@ class NeuralNetworkBackground { this.ctx.closePath(); - // Intensiveres Glühen - this.ctx.shadowColor = this.isDarkMode - ? `rgba(200, 225, 255, ${0.6 * fadeFactor})` - : `rgba(160, 220, 255, ${0.5 * fadeFactor})`; - this.ctx.shadowBlur = 12; - this.ctx.fillStyle = sparkBaseColor; + // Intensives lila Glühen + this.ctx.shadowColor = 'rgba(255, 0, 255, 0.8)'; + this.ctx.shadowBlur = 25; + this.ctx.fillStyle = sparkGradient; this.ctx.fill(); - - // Zusätzlicher innerer Glüheffekt für ausgewählte Funken - if (spark.size > 4 && Math.random() > 0.5) { + // 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.6, 0, Math.PI * 2); + this.ctx.arc(spark.x, spark.y, spark.size * 0.8, 0, Math.PI * 2); this.ctx.fillStyle = this.isDarkMode - ? `rgba(240, 250, 255, ${0.7 * fadeFactor})` - : `rgba(220, 240, 255, ${0.6 * fadeFactor})`; + ? `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(); } } @@ -1222,21 +1431,46 @@ class NeuralNetworkBackground { // Cleanup method destroy() { - if (this.animationFrameId) { - cancelAnimationFrame(this.animationFrameId); - } - - window.removeEventListener('resize', this.resizeCanvas.bind(this)); - - if (this.canvas && this.canvas.parentNode) { - this.canvas.parentNode.removeChild(this.canvas); - } - - if (this.gl) { - // Clean up WebGL resources - this.gl.deleteBuffer(this.positionBuffer); - this.gl.deleteBuffer(this.sizeBuffer); - this.gl.deleteProgram(this.shaderProgram); + // 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); + } } } @@ -1349,7 +1583,19 @@ window.addEventListener('load', () => { // Event listener to clean up when the window is closed window.addEventListener('beforeunload', function() { - if (window.neuralNetworkBackground) { - window.neuralNetworkBackground.destroy(); - } + 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(); + } });