diff --git a/__pycache__/app.cpython-313.pyc b/__pycache__/app.cpython-313.pyc index 6bee820..b814309 100644 Binary files a/__pycache__/app.cpython-313.pyc and b/__pycache__/app.cpython-313.pyc differ diff --git a/static/js/mindmap.js b/static/js/mindmap.js index eeab545..110ac4b 100644 --- a/static/js/mindmap.js +++ b/static/js/mindmap.js @@ -67,15 +67,51 @@ }); /* 2. Hilfs-Funktionen für API-Zugriffe */ - const get = endpoint => 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(); + } });