Files
website/static/neural-network-background.js

1602 lines
62 KiB
JavaScript

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