Verbessere die Funktionalität des neuronalen Netzwerk-Hintergrunds in neural-network-background.js durch die Einführung einer flexiblen Konfigurationsstruktur, die benutzerdefinierte Optionen unterstützt. Optimiere die Cluster-Generierung und -Verteilung, verbessere die Verbindungslogik zwischen Knoten und implementiere erweiterte Fehlerbehandlung in den API-Funktionen in mindmap.js. Füge Fallback-Knoten hinzu, um die Benutzererfahrung zu verbessern, und implementiere visuelle Rückmeldungen für die Initialisierung der Mindmap.

This commit is contained in:
2025-04-29 12:32:18 +02:00
parent 49ccf3908a
commit ffe96074f4
3 changed files with 523 additions and 187 deletions

Binary file not shown.

View File

@@ -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.<br>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)

View File

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