2097 lines
83 KiB
JavaScript
2097 lines
83 KiB
JavaScript
/**
|
|
* Vereinfachter Neuronales Netzwerk Hintergrund
|
|
* Verwendet Canvas 2D anstelle von WebGL für bessere Leistung
|
|
*/
|
|
|
|
class NeuralNetworkBackground {
|
|
constructor() {
|
|
// Canvas einrichten
|
|
this.canvas = document.createElement('canvas');
|
|
this.canvas.id = 'neural-network-background';
|
|
this.canvas.style.position = 'fixed';
|
|
this.canvas.style.top = '0';
|
|
this.canvas.style.left = '0';
|
|
this.canvas.style.width = '100%';
|
|
this.canvas.style.height = '100%';
|
|
this.canvas.style.zIndex = '-10';
|
|
this.canvas.style.pointerEvents = 'none';
|
|
this.canvas.style.opacity = '1';
|
|
this.canvas.style.transition = 'opacity 3.5s ease-in-out';
|
|
|
|
// Falls Canvas bereits existiert, entfernen
|
|
const existingCanvas = document.getElementById('neural-network-background');
|
|
if (existingCanvas) {
|
|
existingCanvas.remove();
|
|
}
|
|
|
|
// An body anhängen als erstes Kind
|
|
// Append to body as first child to ensure it's behind everything
|
|
if (document.body.firstChild) {
|
|
document.body.insertBefore(this.canvas, document.body.firstChild);
|
|
} else {
|
|
document.body.appendChild(this.canvas);
|
|
}
|
|
|
|
// WebGL context
|
|
this.gl = this.canvas.getContext('webgl') || this.canvas.getContext('experimental-webgl');
|
|
if (!this.gl) {
|
|
console.warn('WebGL not supported, falling back to canvas rendering');
|
|
this.gl = null;
|
|
this.ctx = this.canvas.getContext('2d');
|
|
this.useWebGL = false;
|
|
} else {
|
|
this.useWebGL = true;
|
|
}
|
|
|
|
// Animation properties
|
|
this.nodes = [];
|
|
this.connections = [];
|
|
this.flows = []; // Flow animations along connections
|
|
this.animationFrameId = null;
|
|
this.isDarkMode = true; // Always use dark mode for the background
|
|
this.isDestroying = false; // Flag für den Ausblendeprozess
|
|
|
|
// Colors - Lila Farbpalette mit intensiveren Effekten
|
|
this.darkModeColors = {
|
|
background: '#040215', // Dunklerer Hintergrund mit leichtem Violett-Anteil
|
|
nodeColor: '#6a5498', // Lila-basierte Knotenfarbe
|
|
nodePulse: '#9c7fe0', // Helleres Lila für Pulsieren
|
|
connectionColor: '#4a3870', // Dunklere Lila-Verbindungen
|
|
flowColor: '#b47fea' // Lebendiges Lila für Blitze
|
|
};
|
|
|
|
// Farben für Light Mode auch mit Lila-Akzenten
|
|
this.lightModeColors = {
|
|
background: '#f8f6fc', // Sehr heller Lila-Hintergrund
|
|
nodeColor: '#6a5498', // Gleiche Lila-Töne wie im Dark Mode
|
|
nodePulse: '#9c7fe0', // für konsistentes Erscheinungsbild
|
|
connectionColor: '#7e70a5', // Helleres Lila für Verbindungen
|
|
flowColor: '#a06cd5' // Sanfteres Lila für Blitze
|
|
};
|
|
|
|
// Konfigurationsobjekt für schlangenartige, leuchtende Blitze und permanentes Netzwerk
|
|
this.config = {
|
|
nodeCount: 100, // Weniger Knoten für klarere Strukturen
|
|
nodeSize: 3.2, // Etwas größere Knoten für bessere Sichtbarkeit
|
|
nodeVariation: 8, // Reduzierte Varianz für konsistenteres Erscheinungsbild
|
|
connectionDistance: 100, // Größere Verbindungsdistanz für weitläufigeres Netzwerk
|
|
connectionOpacity: 0.3, // Erhöhte Opazität für sichtbarere dauerhafte Verbindungen
|
|
animationSpeed: 0.1, // Noch langsamere Animation für sanftere Bewegung
|
|
pulseSpeed: 0, // Sehr langsames Pulsieren für ruhige Animation
|
|
flowSpeed: 0.15, // Deutlich langsamere Blitze für schlangenartige Bewegung
|
|
flowDensity: 2, // Weniger gleichzeitige Blitze für bessere Verteilung
|
|
flowLength: 0.05, // Längere einzelne Flows für sichtbare "Schlangen"
|
|
maxConnections: 12, // Mehr Verbindungen pro Neuron für dichtere Netzwerke
|
|
clusteringFactor: 0.005, // Stärkeres Clustering für deutlichere Strukturen
|
|
linesFadeDuration: 600, // Noch längere Dauer fürs Ein-/Ausblenden für permanentes Netzwerk (ms)
|
|
linesWidth: 1.0, // Etwas dickere Linien für bessere Sichtbarkeit
|
|
linesOpacity: 0.75, // Höhere Opazität für das permanente Netzwerk
|
|
maxFlowCount: 1, // Weniger gleichzeitige Flows für bessere Übersicht
|
|
glowIntensity: 1.0, // Verstärkter Glow-Effekt
|
|
sparkCount: 1, // Mehr Funken
|
|
sparkSize: 1, // Größere Funken
|
|
blurRadius: 10, // Blur-Radius für Glow-Effekte
|
|
minActiveFlows: 1, // Minimale Anzahl aktiver Flows
|
|
flowForwardingRate: 1.0, // 100% Weiterleitungsrate für endlose Flows
|
|
maxFlowGenerations: 10 // Sehr hohe Generationsgrenze für endlose Animation
|
|
};
|
|
|
|
// Initialize
|
|
this.init();
|
|
|
|
// Event listeners
|
|
window.addEventListener('resize', this.resizeCanvas.bind(this));
|
|
document.addEventListener('darkModeToggled', (event) => {
|
|
this.isDarkMode = event.detail.isDark;
|
|
});
|
|
|
|
// Log that the background is initialized
|
|
console.log('Neural Network Background initialized');
|
|
}
|
|
|
|
init() {
|
|
this.resizeCanvas();
|
|
|
|
if (this.useWebGL) {
|
|
this.initWebGL();
|
|
}
|
|
|
|
this.createNodes();
|
|
this.createConnections();
|
|
this.startAnimation();
|
|
}
|
|
|
|
resizeCanvas() {
|
|
const pixelRatio = window.devicePixelRatio || 1;
|
|
const width = window.innerWidth;
|
|
const height = window.innerHeight;
|
|
|
|
this.canvas.style.width = width + 'px';
|
|
this.canvas.style.height = height + 'px';
|
|
this.canvas.width = width * pixelRatio;
|
|
this.canvas.height = height * pixelRatio;
|
|
|
|
if (this.useWebGL) {
|
|
this.gl.viewport(0, 0, this.canvas.width, this.canvas.height);
|
|
} else if (this.ctx) {
|
|
this.ctx.scale(pixelRatio, pixelRatio);
|
|
}
|
|
|
|
// Recalculate node positions after resize
|
|
if (this.nodes.length) {
|
|
this.createNodes();
|
|
this.createConnections();
|
|
}
|
|
}
|
|
|
|
initWebGL() {
|
|
// Vertex shader
|
|
const vsSource = `
|
|
attribute vec2 aVertexPosition;
|
|
attribute float aPointSize;
|
|
uniform vec2 uResolution;
|
|
|
|
void main() {
|
|
// Convert from pixel to clip space
|
|
vec2 position = (aVertexPosition / uResolution) * 2.0 - 1.0;
|
|
// Flip Y coordinate
|
|
position.y = -position.y;
|
|
|
|
gl_Position = vec4(position, 0, 1);
|
|
gl_PointSize = aPointSize;
|
|
}
|
|
`;
|
|
|
|
// Fragment shader - Softer glow effect
|
|
const fsSource = `
|
|
precision mediump float;
|
|
uniform vec4 uColor;
|
|
|
|
void main() {
|
|
float distance = length(gl_PointCoord - vec2(0.5, 0.5));
|
|
|
|
// Softer glow with smoother falloff
|
|
float alpha = 1.0 - smoothstep(0.1, 0.5, distance);
|
|
alpha = pow(alpha, 1.5); // Make the glow even softer
|
|
|
|
gl_FragColor = vec4(uColor.rgb, uColor.a * alpha);
|
|
}
|
|
`;
|
|
|
|
// Initialize shaders
|
|
const vertexShader = this.loadShader(this.gl.VERTEX_SHADER, vsSource);
|
|
const fragmentShader = this.loadShader(this.gl.FRAGMENT_SHADER, fsSource);
|
|
|
|
// Create shader program
|
|
this.shaderProgram = this.gl.createProgram();
|
|
this.gl.attachShader(this.shaderProgram, vertexShader);
|
|
this.gl.attachShader(this.shaderProgram, fragmentShader);
|
|
this.gl.linkProgram(this.shaderProgram);
|
|
|
|
if (!this.gl.getProgramParameter(this.shaderProgram, this.gl.LINK_STATUS)) {
|
|
console.error('Unable to initialize the shader program: ' +
|
|
this.gl.getProgramInfoLog(this.shaderProgram));
|
|
return;
|
|
}
|
|
|
|
// Get attribute and uniform locations
|
|
this.programInfo = {
|
|
program: this.shaderProgram,
|
|
attribLocations: {
|
|
vertexPosition: this.gl.getAttribLocation(this.shaderProgram, 'aVertexPosition'),
|
|
pointSize: this.gl.getAttribLocation(this.shaderProgram, 'aPointSize')
|
|
},
|
|
uniformLocations: {
|
|
resolution: this.gl.getUniformLocation(this.shaderProgram, 'uResolution'),
|
|
color: this.gl.getUniformLocation(this.shaderProgram, 'uColor')
|
|
}
|
|
};
|
|
|
|
// Create buffers
|
|
this.positionBuffer = this.gl.createBuffer();
|
|
this.sizeBuffer = this.gl.createBuffer();
|
|
|
|
// Set clear color for WebGL context
|
|
const bgColor = this.hexToRgb(this.darkModeColors.background);
|
|
|
|
this.gl.clearColor(bgColor.r/255, bgColor.g/255, bgColor.b/255, 1.0);
|
|
}
|
|
|
|
loadShader(type, source) {
|
|
const shader = this.gl.createShader(type);
|
|
this.gl.shaderSource(shader, source);
|
|
this.gl.compileShader(shader);
|
|
|
|
if (!this.gl.getShaderParameter(shader, this.gl.COMPILE_STATUS)) {
|
|
console.error('An error occurred compiling the shaders: ' +
|
|
this.gl.getShaderInfoLog(shader));
|
|
this.gl.deleteShader(shader);
|
|
return null;
|
|
}
|
|
|
|
return shader;
|
|
}
|
|
|
|
createNodes() {
|
|
this.nodes = [];
|
|
const width = this.canvas.width / (window.devicePixelRatio || 1);
|
|
const height = this.canvas.height / (window.devicePixelRatio || 1);
|
|
|
|
// Erstelle Cluster-Zentren für neuronale Netzwerkmuster mit erhöhter Komplexität
|
|
const clusterCount = Math.floor(8 + Math.random() * 6); // 8-13 Cluster für mehr Dichte
|
|
const clusters = [];
|
|
|
|
for (let i = 0; i < clusterCount; i++) {
|
|
// Dynamischere Clustergrößen mit größerer Variationsbreite
|
|
const baseRadius = 120 + Math.random() * 200; // Größere Basisradien
|
|
const radiusVariation = 0.3 + Math.random() * 0.4; // 30-70% Variation
|
|
|
|
clusters.push({
|
|
x: Math.random() * width,
|
|
y: Math.random() * height,
|
|
radius: baseRadius * (1 + radiusVariation), // Dynamischere Größen
|
|
density: 0.7 + Math.random() * 0.3, // Dichtefaktor für Knotenverteilung
|
|
influence: 0.5 + Math.random() * 0.5 // Einflussstärke auf umliegende Knoten
|
|
});
|
|
}
|
|
|
|
// Create nodes with random positions and properties
|
|
for (let i = 0; i < this.config.nodeCount; i++) {
|
|
// Entscheide, ob dieser Knoten zu einem Cluster gehört oder nicht
|
|
const useCluster = Math.random() < this.config.clusteringFactor;
|
|
let x, y;
|
|
|
|
if (useCluster && clusters.length > 0) {
|
|
// Wähle ein zufälliges Cluster
|
|
const cluster = clusters[Math.floor(Math.random() * clusters.length)];
|
|
const angle = Math.random() * Math.PI * 2;
|
|
const distance = Math.random() * cluster.radius;
|
|
|
|
// Platziere in der Nähe des Clusters mit einiger Streuung
|
|
x = cluster.x + Math.cos(angle) * distance;
|
|
y = cluster.y + Math.sin(angle) * distance;
|
|
|
|
// Stelle sicher, dass es innerhalb des Bildschirms bleibt
|
|
x = Math.max(0, Math.min(width, x));
|
|
y = Math.max(0, Math.min(height, y));
|
|
} else {
|
|
// Zufällige Position außerhalb von Clustern
|
|
x = Math.random() * width;
|
|
y = Math.random() * height;
|
|
}
|
|
|
|
// Bestimme die Knotengröße - wichtigere Knoten (in Clustern) sind deutlich größer
|
|
const nodeImportance = useCluster ? 1.8 : 0.6; // Erhöhter Kontrast zwischen Cluster- und Nicht-Cluster-Knoten
|
|
const size = this.config.nodeSize * nodeImportance + Math.random() * this.config.nodeVariation * 1.5; // Größere Variationsbreite
|
|
|
|
const node = {
|
|
x: x,
|
|
y: y,
|
|
size: size,
|
|
speed: {
|
|
x: (Math.random() - 0.5) * this.config.animationSpeed,
|
|
y: (Math.random() - 0.5) * this.config.animationSpeed
|
|
},
|
|
pulsePhase: Math.random() * Math.PI * 2, // Random starting phase
|
|
connections: [],
|
|
isActive: Math.random() < 0.3, // Some nodes start active for neural firing effect
|
|
lastFired: 0, // For neural firing animation
|
|
firingRate: 1000 + Math.random() * 4000 // Random firing rate in ms
|
|
};
|
|
|
|
this.nodes.push(node);
|
|
}
|
|
}
|
|
|
|
createConnections() {
|
|
this.connections = [];
|
|
this.flows = []; // Reset flows
|
|
|
|
// Create connections between nearby nodes
|
|
for (let i = 0; i < this.nodes.length; i++) {
|
|
const nodeA = this.nodes[i];
|
|
nodeA.connections = [];
|
|
|
|
// Sortiere andere Knoten nach Entfernung für bevorzugte nahe Verbindungen
|
|
const potentialConnections = [];
|
|
|
|
for (let j = 0; j < this.nodes.length; j++) {
|
|
if (i === j) continue;
|
|
|
|
const nodeB = this.nodes[j];
|
|
const dx = nodeB.x - nodeA.x;
|
|
const dy = nodeB.y - nodeA.y;
|
|
const distance = Math.sqrt(dx * dx + dy * dy);
|
|
|
|
// Erhöhe die Wahrscheinlichkeit für Verbindungen
|
|
if (distance < this.config.connectionDistance * 1.5) { // Erweiterter Verbindungsradius
|
|
potentialConnections.push({
|
|
index: j,
|
|
distance: distance,
|
|
// Zufälliger Bonus für mehr Verbindungen
|
|
connectionBonus: Math.random() * 0.5
|
|
});
|
|
}
|
|
}
|
|
|
|
// Sortiere nach Entfernung und Verbindungsbonus mit erweiterten Kriterien
|
|
potentialConnections.sort((a, b) => {
|
|
// Basis-Score aus Entfernung und Verbindungsbonus
|
|
const baseScoreA = a.distance * (1 - a.connectionBonus);
|
|
const baseScoreB = b.distance * (1 - b.connectionBonus);
|
|
|
|
// Zusätzliche Faktoren für dynamischere Verbindungen
|
|
const randomFactorA = Math.random() * 0.2; // Leichte Zufallskomponente
|
|
const randomFactorB = Math.random() * 0.2;
|
|
|
|
// Kombinierter Score mit Zufallskomponente
|
|
const scoreA = baseScoreA * (1 - randomFactorA);
|
|
const scoreB = baseScoreB * (1 - randomFactorB);
|
|
|
|
return scoreA - scoreB;
|
|
});
|
|
|
|
// Erhöhe die maximale Anzahl der Verbindungen signifikant
|
|
const maxConn = Math.min(
|
|
this.config.maxConnections * 2.5, // Deutlich mehr Verbindungen erlaubt
|
|
potentialConnections.length,
|
|
3 + Math.floor(Math.random() * this.config.maxConnections * 2) // Noch mehr zufällige Verbindungen
|
|
);
|
|
for (let c = 0; c < maxConn; c++) {
|
|
const connection = potentialConnections[c];
|
|
const j = connection.index;
|
|
const nodeB = this.nodes[j];
|
|
const distance = connection.distance;
|
|
|
|
// Stärkere Verbindungen durch den Bonus
|
|
const connectionStrength = Math.max(0, 1 - distance / (this.config.connectionDistance * 1.5)) + connection.connectionBonus;
|
|
const connOpacity = connectionStrength * this.config.connectionOpacity;
|
|
|
|
// Check if connection already exists
|
|
if (!this.connections.some(conn =>
|
|
(conn.from === i && conn.to === j) || (conn.from === j && conn.to === i)
|
|
)) {
|
|
// Neue Verbindung mit Ein-/Ausblend-Status
|
|
this.connections.push({
|
|
from: i,
|
|
to: j,
|
|
distance: distance,
|
|
opacity: connOpacity,
|
|
strength: connectionStrength,
|
|
hasFlow: false,
|
|
lastActivated: 0,
|
|
progress: 0, // Verbindung beginnt unsichtbar und baut sich auf
|
|
fadeState: 'in', // Status: 'in' = einblenden, 'visible' = sichtbar, 'out' = ausblenden
|
|
fadeStartTime: Date.now(), // Wann der Fade-Vorgang gestartet wurde
|
|
fadeTotalDuration: this.config.linesFadeDuration, // Feste Dauer ohne Zufall
|
|
visibleDuration: 10000, // Feste Sichtbarkeitsdauer ohne Zufall
|
|
fadeProgress: 0, // Aktueller Fortschritt des Fade-Vorgangs (0-1)
|
|
buildSpeed: 0 // Geschwindigkeit, mit der die Verbindung aufgebaut wird
|
|
});
|
|
nodeA.connections.push(j);
|
|
nodeB.connections.push(i);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
startAnimation() {
|
|
this.animationFrameId = requestAnimationFrame(this.animate.bind(this));
|
|
}
|
|
|
|
animate() {
|
|
// Update nodes
|
|
const width = this.canvas.width / (window.devicePixelRatio || 1);
|
|
const height = this.canvas.height / (window.devicePixelRatio || 1);
|
|
const now = Date.now();
|
|
|
|
// Berechne den Fadeout-Faktor bei Deaktivierung (1.0 bis 0.0)
|
|
const fadeoutFactor = this.isDestroying ? parseFloat(this.canvas.style.opacity) || 0.98 : 1.0;
|
|
|
|
// Für jeden Flow eine eindeutige ID vergeben, falls noch nicht vorhanden
|
|
for (const flow of this.flows) {
|
|
if (!flow.id) {
|
|
flow.id = now + '_' + Math.random().toString(36).substr(2, 9);
|
|
}
|
|
}
|
|
|
|
// Setze zunächst alle Neuronen auf inaktiv
|
|
for (let i = 0; i < this.nodes.length; i++) {
|
|
// Update pulse phase for smoother animation
|
|
const node = this.nodes[i];
|
|
|
|
// Bei Ausblendung langsamer pulsieren lassen
|
|
const pulseSpeed = this.isDestroying
|
|
? this.config.pulseSpeed * 0.7
|
|
: this.config.pulseSpeed * (1 + (node.connections.length * 0.04));
|
|
|
|
node.pulsePhase += pulseSpeed;
|
|
|
|
// Bei Ausblendung Bewegung reduzieren
|
|
const movementFactor = this.isDestroying ? 0.3 : 1.0;
|
|
|
|
// Animate node position with gentler movement
|
|
node.x += node.speed.x * (Math.sin(now * 0.0003) * 0.4 + 0.4) * movementFactor;
|
|
node.y += node.speed.y * (Math.cos(now * 0.0002) * 0.4 + 0.4) * movementFactor;
|
|
|
|
// Keep nodes within bounds
|
|
if (node.x < 0 || node.x > width) {
|
|
node.speed.x *= -1;
|
|
node.x = Math.max(0, Math.min(width, node.x));
|
|
}
|
|
if (node.y < 0 || node.y > height) {
|
|
node.speed.y *= -1;
|
|
node.y = Math.max(0, Math.min(height, node.y));
|
|
}
|
|
|
|
// Knoten bleiben länger aktiv nach dem Auslösen (2000ms für langanhaltende Aktivierung)
|
|
if (node.lastFired && now - node.lastFired < 2000) {
|
|
node.isActive = true;
|
|
} else {
|
|
node.isActive = false;
|
|
}
|
|
}
|
|
|
|
// Aktiviere Neuronen basierend auf aktiven Flows
|
|
for (const flow of this.flows) {
|
|
// Aktiviere den Quellknoten (der Flow geht von ihm aus)
|
|
if (flow.sourceNodeIdx !== undefined) {
|
|
this.nodes[flow.sourceNodeIdx].isActive = true;
|
|
this.nodes[flow.sourceNodeIdx].lastFired = now;
|
|
}
|
|
|
|
// Aktiviere den Zielknoten nur, wenn der Flow weit genug fortgeschritten ist
|
|
if (flow.targetNodeIdx !== undefined && flow.progress > 0.7) {
|
|
this.nodes[flow.targetNodeIdx].isActive = true;
|
|
this.nodes[flow.targetNodeIdx].lastFired = now;
|
|
}
|
|
}
|
|
|
|
// Minimale Anzahl aktiver Flows sicherstellen für endlose Animation
|
|
const minRequiredFlows = Math.min(15, Math.floor(this.nodes.length * 0.1)); // Mindestens 10% der Knoten haben aktive Flows
|
|
const shouldCreateNewFlows = this.flows.length < minRequiredFlows;
|
|
|
|
// Chance auf neue Flows
|
|
const flowChance = this.isDestroying ? 0.005 : (shouldCreateNewFlows ? 0.1 : 0.04);
|
|
|
|
// Neue Flows mit höherer Wahrscheinlichkeit erzeugen, wenn zu wenige aktiv sind
|
|
if (Math.random() < flowChance) {
|
|
this.initiateMajorFlowCascade(now, fadeoutFactor);
|
|
}
|
|
|
|
// Aktualisiere die Ein-/Ausblendung von Verbindungen
|
|
this.updateConnectionsFading(now);
|
|
|
|
// Update flows with proper fading
|
|
this.updateFlows(now);
|
|
|
|
// Regelmäßig neue Flows erstellen, um das Netzwerk lebendig zu halten
|
|
if (Math.random() < 0.07) { // Hohe Chance für kontinuierliche Animation
|
|
this.createNewFlow(now);
|
|
}
|
|
|
|
// Rekonfiguriere Verbindungen sehr selten für ein sich langsam entwickelndes Netzwerk
|
|
// Dies verhindert, dass das Netzwerk statisch wird
|
|
if (Math.random() < 0.0005) { // Sehr seltene Neuverbindungen (0.05% Chance pro Frame)
|
|
this.createConnections();
|
|
}
|
|
|
|
// Render
|
|
if (this.useWebGL) {
|
|
this.renderWebGL(now);
|
|
} else {
|
|
this.renderCanvas(now);
|
|
}
|
|
|
|
// Continue animation - garantiere endlose Animation
|
|
this.animationFrameId = requestAnimationFrame(this.animate.bind(this));
|
|
}
|
|
|
|
// Neue Methode: Starte eine größere Kaskade von Flows an strategischen Punkten
|
|
initiateMajorFlowCascade(now, fadeoutFactor = 1.0) {
|
|
// Finde gut vernetzte Knoten als Startpunkte
|
|
const topNodes = [...this.nodes]
|
|
.map((node, index) => ({ node, index, connections: node.connections.length }))
|
|
.filter(item => item.connections > 2) // Nur Knoten mit mindestens 3 Verbindungen
|
|
.sort((a, b) => b.connections - a.connections); // Sortiere nach Verbindungsanzahl
|
|
|
|
if (topNodes.length === 0) return;
|
|
|
|
// Wähle 1-3 Startknoten basierend auf Netzwerkgröße
|
|
const startNodeCount = Math.max(1, Math.min(3, Math.floor(topNodes.length / 20)));
|
|
|
|
for (let i = 0; i < startNodeCount; i++) {
|
|
// Wähle einen der Top-Knoten, mit Präferenz für die am besten vernetzten
|
|
const nodeIndex = Math.floor(Math.random() * Math.min(10, topNodes.length));
|
|
const startNodeData = topNodes[nodeIndex];
|
|
if (!startNodeData) continue;
|
|
|
|
const startNode = this.nodes[startNodeData.index];
|
|
if (!startNode || startNode.connections.length === 0) continue;
|
|
|
|
// Aktiviere den Startknoten
|
|
startNode.isActive = true;
|
|
startNode.lastFired = now;
|
|
startNode.lastCascade = now; // Markiere den Zeitpunkt der letzten Kaskade
|
|
|
|
// Starte Flows an mehreren Verbindungen dieses Knotens
|
|
const connectionsToActivate = Math.min(
|
|
Math.ceil(startNode.connections.length / 2), // Bis zu 50% der Verbindungen
|
|
5 // Aber maximal 5
|
|
);
|
|
|
|
// Sorgfältige Auswahl der Verbindungen für eine optimale Weitergabe
|
|
const selectedConnections = [];
|
|
const allConnections = [...startNode.connections];
|
|
|
|
// Sortiere Verbindungen nach Aktivierungszeit - bevorzuge solche, die länger nicht aktiviert wurden
|
|
const sortedConnections = allConnections
|
|
.map(connIdx => {
|
|
const connectedNode = this.nodes[connIdx];
|
|
const conn = this.connections.find(c =>
|
|
(c.from === startNodeData.index && c.to === connIdx) ||
|
|
(c.from === connIdx && c.to === startNodeData.index)
|
|
);
|
|
|
|
return {
|
|
connIdx,
|
|
nodeConnections: connectedNode ? connectedNode.connections.length : 0,
|
|
lastActivated: conn ? conn.lastActivated || 0 : 0
|
|
};
|
|
})
|
|
.sort((a, b) => {
|
|
// Priorisiere Verbindungen, die länger nicht aktiviert wurden und zu gut vernetzten Knoten führen
|
|
const timeFactorA = (now - a.lastActivated) / 10000; // Zeit seit letzter Aktivierung in 10-Sekunden-Einheiten
|
|
const timeFactorB = (now - b.lastActivated) / 10000;
|
|
|
|
return (timeFactorB + b.nodeConnections * 0.1) - (timeFactorA + a.nodeConnections * 0.1);
|
|
});
|
|
|
|
// Wähle die besten Verbindungen aus
|
|
for (let j = 0; j < connectionsToActivate && j < sortedConnections.length; j++) {
|
|
selectedConnections.push(sortedConnections[j].connIdx);
|
|
}
|
|
|
|
// Startversatz für Flows eines Knotens - Verzögere die Flows leicht für Welleneffekt
|
|
const baseDelay = 100; // Basisverzögerung in ms
|
|
|
|
// Erstelle Flows mit leichter Verzögerung zwischen ihnen
|
|
selectedConnections.forEach((connIdx, idx) => {
|
|
const conn = this.connections.find(c =>
|
|
(c.from === startNodeData.index && c.to === connIdx) ||
|
|
(c.from === connIdx && c.to === startNodeData.index)
|
|
);
|
|
|
|
if (conn) {
|
|
// Verbindung aktivieren
|
|
conn.lastActivated = now;
|
|
|
|
// Stelle sicher, dass die Verbindung dauerhaft sichtbar bleibt
|
|
if (conn.fadeState !== 'visible') {
|
|
conn.fadeState = 'visible';
|
|
conn.fadeStartTime = now;
|
|
conn.visibleDuration = 600000 + Math.random() * 300000; // 10-15 Minuten sichtbar
|
|
}
|
|
|
|
// Verbindung aufbauen
|
|
if (conn.progress < 1) {
|
|
conn.buildSpeed = 0.03 + Math.random() * 0.02;
|
|
}
|
|
|
|
// Leichte Verzögerung zwischen den Flows für Welleneffekt
|
|
const delay = baseDelay + idx * 150;
|
|
|
|
setTimeout(() => {
|
|
// Erstelle nur, wenn die Animation noch läuft
|
|
if (!this.isDestroying && this.animationFrameId) {
|
|
// Bestimme die Richtung des Flows
|
|
const direction = conn.from === startNodeData.index;
|
|
|
|
// Erstelle den Flow
|
|
this.flows.push({
|
|
id: now + '_cascade_' + idx + '_' + Math.random().toString(36).substr(2, 9),
|
|
connection: conn,
|
|
progress: 0,
|
|
direction: direction,
|
|
length: this.config.flowLength * (0.8 + Math.random() * 0.4),
|
|
creationTime: Date.now(),
|
|
totalDuration: 700 + Math.random() * 400,
|
|
sourceNodeIdx: direction ? conn.from : conn.to,
|
|
targetNodeIdx: direction ? conn.to : conn.from,
|
|
fadeFactor: fadeoutFactor,
|
|
isForwarded: false,
|
|
hasForwarded: false,
|
|
generation: 1, // Erste Generation
|
|
isCascadeRoot: true // Markiere als Wurzel einer Kaskade
|
|
});
|
|
}
|
|
}, delay);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
// Updated method to update flow animations with proper fading
|
|
updateFlows(now) {
|
|
// Track new flows to add after the loop to avoid modifying the array during iteration
|
|
const newFlows = [];
|
|
|
|
// Zähle aktive Flows
|
|
const activeFlows = this.flows.length;
|
|
|
|
// Wenn zu wenige Flows aktiv sind, starte neue Major Flow Cascades
|
|
if (activeFlows < this.config.minActiveFlows) {
|
|
this.initiateMajorFlowCascade(now);
|
|
}
|
|
|
|
// Update existing flows
|
|
for (let i = this.flows.length - 1; i >= 0; i--) {
|
|
const flow = this.flows[i];
|
|
|
|
// Calculate flow age for fading effects
|
|
const flowAge = now - flow.creationTime;
|
|
const flowProgress = flowAge / flow.totalDuration;
|
|
|
|
// Update flow progress with reduced speed für schlangenartige Bewegung
|
|
// Längere Generationen bewegen sich noch langsamer für sichtbare "Schlangen"
|
|
const generationSlowdown = Math.max(0.7, 1.0 - (flow.generation || 1) * 0.015);
|
|
flow.progress += (this.config.flowSpeed * generationSlowdown) / flow.connection.distance;
|
|
|
|
// Aktiviere Quell- und Zielknoten basierend auf Flow-Fortschritt
|
|
if (flow.sourceNodeIdx !== undefined) {
|
|
// Quellknoten immer aktivieren, solange der Flow aktiv ist
|
|
this.nodes[flow.sourceNodeIdx].isActive = true;
|
|
this.nodes[flow.sourceNodeIdx].lastFired = now;
|
|
}
|
|
|
|
// Zielknoten aktivieren und Weiterleitung starten, wenn der Flow ihn erreicht hat
|
|
if (flow.targetNodeIdx !== undefined && flow.progress > 0.80) {
|
|
const targetNode = this.nodes[flow.targetNodeIdx];
|
|
targetNode.isActive = true;
|
|
targetNode.lastFired = now;
|
|
|
|
// Stelle sicher, dass die Verbindung dauerhaft sichtbar bleibt
|
|
flow.connection.lastActivated = now;
|
|
if (flow.connection.fadeState !== 'visible') {
|
|
flow.connection.fadeState = 'visible';
|
|
flow.connection.fadeStartTime = now;
|
|
flow.connection.visibleDuration = 600000 + Math.random() * 300000; // 10-15 Minuten sichtbar
|
|
}
|
|
|
|
// Flow ist am Ziel angekommen, starte Weiterleitung zu anderen Knoten
|
|
// Jeder Flow leitet genau einmal weiter
|
|
if (!flow.hasForwarded && targetNode.connections.length > 0) {
|
|
flow.hasForwarded = true;
|
|
|
|
// Durch reduzierte Anzahl der Weiterleitungen schaffen wir einen endloseren Eindruck
|
|
const connCount = Math.min(3, targetNode.connections.length);
|
|
const availableConnections = [...targetNode.connections];
|
|
|
|
// Keine Weiterleitung an die Quelle des Flows zurück
|
|
if (flow.sourceNodeIdx !== undefined && availableConnections.length > 1) {
|
|
const sourceIndex = availableConnections.indexOf(flow.sourceNodeIdx);
|
|
if (sourceIndex >= 0) {
|
|
availableConnections.splice(sourceIndex, 1);
|
|
}
|
|
}
|
|
|
|
// Wenn noch Verbindungen übrig sind, leite weiter
|
|
if (availableConnections.length > 0) {
|
|
// Priorisiere ungenutzte Pfade für ein dynamischeres Netzwerk
|
|
const sortedConnections = availableConnections.map(nodeIdx => {
|
|
const conn = this.connections.find(c =>
|
|
(c.from === flow.targetNodeIdx && c.to === nodeIdx) ||
|
|
(c.from === nodeIdx && c.to === flow.targetNodeIdx)
|
|
);
|
|
|
|
const timeSinceLastActivation = conn ? now - (conn.lastActivated || 0) : 0;
|
|
const connectionCount = this.nodes[nodeIdx].connections.length;
|
|
|
|
return {
|
|
nodeIdx,
|
|
connectionCount,
|
|
timeSinceLastActivation,
|
|
score: (timeSinceLastActivation * 0.00001) +
|
|
(connectionCount * 0.2) +
|
|
(Math.random() * 0.5)
|
|
};
|
|
}).sort((a, b) => b.score - a.score);
|
|
|
|
// Wähle priorisierte Verbindungen
|
|
for (let j = 0; j < connCount && j < sortedConnections.length; j++) {
|
|
const nextNodeIdx = sortedConnections[j].nodeIdx;
|
|
const nextConn = this.connections.find(c =>
|
|
(c.from === flow.targetNodeIdx && c.to === nextNodeIdx) ||
|
|
(c.from === nextNodeIdx && c.to === flow.targetNodeIdx)
|
|
);
|
|
|
|
if (nextConn) {
|
|
nextConn.lastActivated = now;
|
|
|
|
if (nextConn.fadeState !== 'visible') {
|
|
nextConn.fadeState = 'visible';
|
|
nextConn.fadeStartTime = now;
|
|
nextConn.visibleDuration = 600000 + Math.random() * 300000;
|
|
}
|
|
|
|
if (nextConn.progress < 1) {
|
|
nextConn.buildSpeed = 0.04 + Math.random() * 0.02;
|
|
}
|
|
|
|
const nextGeneration = (flow.generation || 1) + 1;
|
|
|
|
if (nextGeneration <= this.config.maxFlowGenerations) {
|
|
const direction = nextConn.from === flow.targetNodeIdx;
|
|
|
|
// Berechne Verzögerung basierend auf Verbindungslänge
|
|
const connectionLength = Math.sqrt(
|
|
Math.pow(this.nodes[nextConn.to].x - this.nodes[nextConn.from].x, 2) +
|
|
Math.pow(this.nodes[nextConn.to].y - this.nodes[nextConn.from].y, 2)
|
|
);
|
|
|
|
const baseDelay = 80;
|
|
const lengthFactor = Math.min(1.2, connectionLength / 200);
|
|
const genFactor = Math.max(0.2, 1.0 - (nextGeneration * 0.005));
|
|
const delay = baseDelay * lengthFactor * genFactor;
|
|
|
|
setTimeout(() => {
|
|
if (!this.isDestroying && this.animationFrameId) {
|
|
newFlows.push({
|
|
id: now + '_' + Math.random().toString(36).substr(2, 9),
|
|
connection: nextConn,
|
|
progress: 0,
|
|
direction: direction,
|
|
length: this.config.flowLength * (0.9 + Math.random() * 0.2),
|
|
creationTime: Date.now(),
|
|
totalDuration: 600 + Math.random() * 300 + (nextGeneration * 5),
|
|
sourceNodeIdx: direction ? nextConn.from : nextConn.to,
|
|
targetNodeIdx: direction ? nextConn.to : nextConn.from,
|
|
fadeFactor: flow.fadeFactor || 1.0,
|
|
isForwarded: false,
|
|
generation: nextGeneration,
|
|
hasForwarded: false,
|
|
parentFlow: flow.id
|
|
});
|
|
}
|
|
}, delay);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Stellen Sie sicher, dass die Verbindung aktiv bleibt
|
|
flow.connection.lastActivated = now;
|
|
|
|
if (flow.connection.fadeState === 'visible') {
|
|
flow.connection.visibleDuration = Math.max(
|
|
flow.connection.visibleDuration || 300000,
|
|
600000 + Math.random() * 300000
|
|
);
|
|
}
|
|
|
|
// Remove completed flows, but ensure parent-child relationships for visible "snakes"
|
|
const isComplete = flow.progress > 1.0 || flowProgress >= 1.0;
|
|
const hasMaxGeneration = flow.generation && flow.generation > this.config.maxFlowGenerations;
|
|
|
|
if (isComplete || hasMaxGeneration) {
|
|
this.flows.splice(i, 1);
|
|
}
|
|
}
|
|
|
|
// Füge alle neuen Flows hinzu
|
|
this.flows.push(...newFlows);
|
|
}
|
|
|
|
// Aktualisiert die Ein-/Ausblendung von Verbindungen mit deutlich langsameren Übergängen
|
|
updateConnectionsFading(now) {
|
|
for (const connection of this.connections) {
|
|
const elapsedTime = now - connection.fadeStartTime;
|
|
|
|
// Update connection fade status
|
|
if (connection.fadeState === 'in') {
|
|
// Einblenden - langsamer für sanfteren Aufbau
|
|
connection.fadeProgress = Math.min(1.0, elapsedTime / (connection.fadeTotalDuration * 1.5));
|
|
if (connection.fadeProgress >= 1.0) {
|
|
connection.fadeState = 'visible';
|
|
connection.fadeStartTime = now;
|
|
}
|
|
} else if (connection.fadeState === 'visible') {
|
|
// Verbindung ist vollständig sichtbar - EXTREM lange Sichtbarkeitsdauer
|
|
// für permanente Netzwerkstrukturen
|
|
if (elapsedTime > connection.visibleDuration * 3.0) { // Erhöht von 2.5 auf 3.0
|
|
// Prüfe, ob die Verbindung kürzlich aktiviert wurde
|
|
if (now - connection.lastActivated < 120000) { // 2 Minuten Aktivitätsschutz
|
|
// Wenn kürzlich aktiviert, verlängere die Sichtbarkeit
|
|
connection.fadeStartTime = now;
|
|
} else {
|
|
// Nur sehr alte, inaktive Verbindungen langsam ausblenden
|
|
connection.fadeState = 'out';
|
|
connection.fadeStartTime = now;
|
|
connection.fadeProgress = 1.0;
|
|
}
|
|
}
|
|
} else if (connection.fadeState === 'out') {
|
|
// Ausblenden - EXTREM langsam für dauerhaft sichtbare Strukturen
|
|
connection.fadeProgress = Math.max(0.25, 1.0 - (elapsedTime / (connection.fadeTotalDuration * 10.0))); // Erhöht von 8.0 auf 10.0
|
|
|
|
// Verbindungen bleiben dauerhaft stark sichtbar
|
|
if (connection.fadeProgress <= 0.25) { // Minimum erhöht von 0.15 auf 0.25
|
|
// Verbindungen niemals komplett verschwinden lassen
|
|
connection.fadeState = 'in';
|
|
connection.fadeStartTime = now;
|
|
connection.fadeProgress = 0.25; // Höhere Grundsichtbarkeit
|
|
connection.visibleDuration = 600000 + Math.random() * 300000; // 10-15 Minuten sichtbar
|
|
}
|
|
} else if (connection.fadeState === 'hidden') {
|
|
// Keine Verbindungen verstecken, alle sichtbar halten
|
|
connection.fadeState = 'in';
|
|
connection.fadeStartTime = now;
|
|
connection.fadeProgress = 0.25; // Höhere Grundsichtbarkeit
|
|
}
|
|
|
|
// Verbindungen schneller vollständig aufbauen für permanentes Netzwerk
|
|
if (connection.progress < 1) {
|
|
// Erhöhte Basisgeschwindigkeit für schnelleren Aufbau
|
|
const baseBuildSpeed = 0.008; // Erhöht für schnelleren permanenten Aufbau
|
|
let buildSpeed = connection.buildSpeed || baseBuildSpeed;
|
|
|
|
// Wenn kürzlich aktiviert, noch schneller aufbauen
|
|
if (now - connection.lastActivated < 2000) {
|
|
buildSpeed = Math.max(buildSpeed, 0.015); // Erhöht für schnelleren Aufbau
|
|
}
|
|
|
|
connection.progress += buildSpeed;
|
|
|
|
if (connection.progress > 1) {
|
|
connection.progress = 1;
|
|
// Zurücksetzen der Aufbaugeschwindigkeit
|
|
connection.buildSpeed = 0;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Updated method to create flow animations with timing info
|
|
createNewFlow(now) {
|
|
if (this.connections.length === 0 || this.flows.length >= 8) return;
|
|
|
|
// Verbesserte Auswahl zufälliger Verbindungen mit Präferenz für hoch vernetzte Knoten
|
|
// Sammle alle Verbindungen und bewerte sie nach der Anzahl ihrer Verbindungen
|
|
const weightedConnections = this.connections.map((conn, index) => {
|
|
const fromNode = this.nodes[conn.from];
|
|
const toNode = this.nodes[conn.to];
|
|
const connectionCount = fromNode.connections.length + toNode.connections.length;
|
|
return {
|
|
index,
|
|
connectionCount,
|
|
weight: connectionCount + Math.random() * 5 // Füge Zufall hinzu für Variabilität
|
|
};
|
|
});
|
|
|
|
// Sortiere nach Gewicht (Verbindungsanzahl + Zufallsfaktor)
|
|
weightedConnections.sort((a, b) => b.weight - a.weight);
|
|
|
|
// Wähle eine der Top-Verbindungen
|
|
const connectionIdx = weightedConnections[Math.floor(Math.random() * Math.min(15, weightedConnections.length))].index;
|
|
const connection = this.connections[connectionIdx];
|
|
|
|
// Verbindung als "im Aufbau" markieren, wenn sie noch nicht vollständig ist
|
|
if (connection.progress < 1) {
|
|
connection.buildSpeed = 0.02 + Math.random() * 0.01; // Schnellerer Aufbau während eines Blitzes
|
|
}
|
|
|
|
// Stelle sicher, dass die Verbindung dauerhaft sichtbar bleibt
|
|
connection.lastActivated = now;
|
|
if (connection.fadeState !== 'visible') {
|
|
connection.fadeState = 'visible';
|
|
connection.fadeStartTime = now;
|
|
connection.visibleDuration = 150000 + Math.random() * 90000; // 2.5-4 Minuten sichtbar
|
|
}
|
|
|
|
// Create a new flow along this connection
|
|
this.flows.push({
|
|
connection: connection,
|
|
progress: 0,
|
|
direction: Math.random() > 0.5, // Randomly decide direction
|
|
length: this.config.flowLength + Math.random() * 0.1, // Slightly vary lengths
|
|
creationTime: now,
|
|
totalDuration: 900 + Math.random() * 500, // Längere Gesamtdauer (900-1400ms)
|
|
sourceNodeIdx: connection.direction ? connection.from : connection.to,
|
|
targetNodeIdx: connection.direction ? connection.to : connection.from,
|
|
isForwarded: false, // Dieses Flag ermöglicht Weiterleitung
|
|
hasForwarded: false, // Noch nicht weitergeleitet
|
|
generation: 1 // Erste Generation
|
|
});
|
|
}
|
|
|
|
renderWebGL(now) {
|
|
this.gl.clear(this.gl.COLOR_BUFFER_BIT);
|
|
|
|
const width = this.canvas.width / (window.devicePixelRatio || 1);
|
|
const height = this.canvas.height / (window.devicePixelRatio || 1);
|
|
|
|
// Fade-Faktor für sanftes Ausblenden
|
|
const fadeoutFactor = this.isDestroying ? parseFloat(this.canvas.style.opacity) || 0.98 : 1.0;
|
|
|
|
// Select shader program
|
|
this.gl.useProgram(this.programInfo.program);
|
|
|
|
// Set resolution uniform
|
|
this.gl.uniform2f(this.programInfo.uniformLocations.resolution, width, height);
|
|
|
|
// Draw connections first (behind nodes)
|
|
this.renderConnectionsWebGL(now, fadeoutFactor);
|
|
|
|
// Draw flows on top of connections
|
|
this.renderFlowsWebGL(now, fadeoutFactor);
|
|
|
|
// Draw nodes
|
|
this.renderNodesWebGL(now, fadeoutFactor);
|
|
}
|
|
|
|
renderNodesWebGL(now, fadeoutFactor = 1.0) {
|
|
// Prepare node positions for WebGL
|
|
const positions = new Float32Array(this.nodes.length * 2);
|
|
const sizes = new Float32Array(this.nodes.length);
|
|
|
|
for (let i = 0; i < this.nodes.length; i++) {
|
|
const node = this.nodes[i];
|
|
positions[i * 2] = node.x;
|
|
positions[i * 2 + 1] = node.y;
|
|
|
|
// Sanftere Pulsation mit moderaterem Aktivierungsboost
|
|
const activationBoost = node.isActive ? 1.2 : 1.0; // Reduziert von 1.3 auf 1.2
|
|
let pulse = (Math.sin(node.pulsePhase) * 0.2 + 1.1) * activationBoost; // Reduzierte Pulsation
|
|
|
|
// Größe basierend auf Konnektivität und Wichtigkeit, aber subtiler
|
|
const connectivityFactor = 1 + (node.connections.length / this.config.maxConnections) * 0.8; // Reduziert von 1.1 auf 0.8
|
|
sizes[i] = node.size * pulse * connectivityFactor;
|
|
}
|
|
|
|
// Bind position buffer
|
|
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.positionBuffer);
|
|
this.gl.bufferData(this.gl.ARRAY_BUFFER, positions, this.gl.STATIC_DRAW);
|
|
this.gl.vertexAttribPointer(
|
|
this.programInfo.attribLocations.vertexPosition,
|
|
2, // components per vertex
|
|
this.gl.FLOAT, // data type
|
|
false, // normalize
|
|
0, // stride
|
|
0 // offset
|
|
);
|
|
this.gl.enableVertexAttribArray(this.programInfo.attribLocations.vertexPosition);
|
|
|
|
// Bind size buffer
|
|
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.sizeBuffer);
|
|
this.gl.bufferData(this.gl.ARRAY_BUFFER, sizes, this.gl.STATIC_DRAW);
|
|
this.gl.vertexAttribPointer(
|
|
this.programInfo.attribLocations.pointSize,
|
|
1, // components per vertex
|
|
this.gl.FLOAT, // data type
|
|
false, // normalize
|
|
0, // stride
|
|
0 // offset
|
|
);
|
|
this.gl.enableVertexAttribArray(this.programInfo.attribLocations.pointSize);
|
|
|
|
// Enable blending for all nodes
|
|
this.gl.enable(this.gl.BLEND);
|
|
this.gl.blendFunc(this.gl.SRC_ALPHA, this.gl.ONE); // Additive blending for glow
|
|
|
|
// Draw each node individually with its own color
|
|
for (let i = 0; i < this.nodes.length; i++) {
|
|
const node = this.nodes[i];
|
|
|
|
// Set node color - sanftere Hervorhebung von aktiven Knoten
|
|
const colorObj = this.isDarkMode ? this.darkModeColors : this.lightModeColors;
|
|
const nodeColor = this.hexToRgb(colorObj.nodeColor);
|
|
const nodePulseColor = this.hexToRgb(colorObj.nodePulse);
|
|
|
|
// Use pulse color for active nodes
|
|
let r = nodeColor.r / 255;
|
|
let g = nodeColor.g / 255;
|
|
let b = nodeColor.b / 255;
|
|
|
|
// Active nodes get slightly brighter color - noch subtiler
|
|
if (node.isActive) {
|
|
r = (r * 0.8 + nodePulseColor.r / 255 * 0.2); // Reduziert von 0.7/0.3 auf 0.8/0.2
|
|
g = (g * 0.8 + nodePulseColor.g / 255 * 0.2);
|
|
b = (b * 0.8 + nodePulseColor.b / 255 * 0.2);
|
|
}
|
|
|
|
// Noch subtilere Knoten mit weiter reduzierter Opazität
|
|
// Berücksichtige fadeoutFactor für sanftes Ausblenden
|
|
const nodeOpacity = node.isActive ? 0.65 : 0.55; // Reduziert von 0.75/0.65 auf 0.65/0.55
|
|
this.gl.uniform4f(
|
|
this.programInfo.uniformLocations.color,
|
|
r, g, b,
|
|
nodeOpacity * fadeoutFactor // Reduzierte Opazität während des Ausblendprozesses
|
|
);
|
|
|
|
// Draw each node individually for better control
|
|
this.gl.drawArrays(this.gl.POINTS, i, 1);
|
|
}
|
|
}
|
|
|
|
renderConnectionsWebGL(now, fadeoutFactor = 1.0) {
|
|
for (const connection of this.connections) {
|
|
// Überspringe Verbindungen, die komplett unsichtbar sind
|
|
if (connection.fadeState === 'hidden' || connection.fadeProgress <= 0) continue;
|
|
|
|
const fromNode = this.nodes[connection.from];
|
|
const toNode = this.nodes[connection.to];
|
|
const progress = connection.progress || 1;
|
|
const x1 = fromNode.x;
|
|
const y1 = fromNode.y;
|
|
const x2 = fromNode.x + (toNode.x - fromNode.x) * progress;
|
|
const y2 = fromNode.y + (toNode.y - fromNode.y) * progress;
|
|
|
|
// Calculate opacity based on fade state
|
|
let opacity = connection.opacity * connection.fadeProgress * 0.7; // Reduzierte Gesamtopazität auf 70%
|
|
|
|
// Erhöhe kurzzeitig die Opazität bei kürzlich aktivierten Verbindungen
|
|
if (connection.lastActivated && now - connection.lastActivated < 800) {
|
|
const timeFactor = 1 - ((now - connection.lastActivated) / 800);
|
|
opacity = Math.max(opacity, timeFactor * 0.25); // Reduzierte Highlight-Opazität auf 25%
|
|
}
|
|
|
|
// Berücksichtige fadeoutFactor für sanftes Ausblenden
|
|
opacity *= fadeoutFactor;
|
|
|
|
const positions = new Float32Array([
|
|
x1, y1,
|
|
x2, y2
|
|
]);
|
|
|
|
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.positionBuffer);
|
|
this.gl.bufferData(this.gl.ARRAY_BUFFER, positions, this.gl.STATIC_DRAW);
|
|
this.gl.vertexAttribPointer(
|
|
this.programInfo.attribLocations.vertexPosition,
|
|
2,
|
|
this.gl.FLOAT,
|
|
false,
|
|
0,
|
|
0
|
|
);
|
|
this.gl.enableVertexAttribArray(this.programInfo.attribLocations.vertexPosition);
|
|
this.gl.disableVertexAttribArray(this.programInfo.attribLocations.pointSize);
|
|
|
|
// Set color with calculated opacity
|
|
const colorObj = this.isDarkMode ? this.darkModeColors : this.lightModeColors;
|
|
const connColor = this.hexToRgb(colorObj.connectionColor);
|
|
this.gl.uniform4f(
|
|
this.programInfo.uniformLocations.color,
|
|
connColor.r / 255,
|
|
connColor.g / 255,
|
|
connColor.b / 255,
|
|
opacity
|
|
);
|
|
|
|
this.gl.enable(this.gl.BLEND);
|
|
this.gl.blendFunc(this.gl.SRC_ALPHA, this.gl.ONE);
|
|
this.gl.lineWidth(this.config.linesWidth);
|
|
this.gl.drawArrays(this.gl.LINES, 0, 2);
|
|
}
|
|
}
|
|
|
|
// New method to render the flowing animations with subtilerer Appearance
|
|
renderFlowsWebGL(now, fadeoutFactor = 1.0) {
|
|
// Für jeden Flow einen dezenten, echten Blitz als Zickzack zeichnen und Funken erzeugen
|
|
for (const flow of this.flows) {
|
|
const connection = flow.connection;
|
|
const fromNode = this.nodes[connection.from];
|
|
const toNode = this.nodes[connection.to];
|
|
const startProgress = flow.progress;
|
|
const endProgress = Math.min(1, startProgress + flow.length);
|
|
if (startProgress >= 1 || endProgress <= 0) continue;
|
|
const direction = flow.direction ? 1 : -1;
|
|
let p1, p2;
|
|
if (direction > 0) {
|
|
p1 = {
|
|
x: fromNode.x + (toNode.x - fromNode.x) * startProgress,
|
|
y: fromNode.y + (toNode.y - fromNode.y) * startProgress
|
|
};
|
|
p2 = {
|
|
x: fromNode.x + (toNode.x - fromNode.x) * endProgress,
|
|
y: fromNode.y + (toNode.y - fromNode.y) * endProgress
|
|
};
|
|
} else {
|
|
p1 = {
|
|
x: toNode.x + (fromNode.x - toNode.x) * startProgress,
|
|
y: toNode.y + (fromNode.y - toNode.y) * startProgress
|
|
};
|
|
p2 = {
|
|
x: toNode.x + (fromNode.x - toNode.x) * endProgress,
|
|
y: toNode.y + (fromNode.y - toNode.y) * endProgress
|
|
};
|
|
}
|
|
// Zickzack-Blitz generieren
|
|
const zigzag = this.generateZigZagPoints(p1, p2, 7, 8); // Reduzierte Zickzack-Amplitude
|
|
for (let i = 0; i < zigzag.length - 1; i++) {
|
|
const positions = new Float32Array([
|
|
zigzag[i].x, zigzag[i].y,
|
|
zigzag[i + 1].x, zigzag[i + 1].y
|
|
]);
|
|
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.positionBuffer);
|
|
this.gl.bufferData(this.gl.ARRAY_BUFFER, positions, this.gl.STATIC_DRAW);
|
|
this.gl.vertexAttribPointer(
|
|
this.programInfo.attribLocations.vertexPosition,
|
|
2,
|
|
this.gl.FLOAT,
|
|
false,
|
|
0,
|
|
0
|
|
);
|
|
this.gl.enableVertexAttribArray(this.programInfo.attribLocations.vertexPosition);
|
|
this.gl.disableVertexAttribArray(this.programInfo.attribLocations.pointSize);
|
|
// Dezenter, leuchtender Blitz
|
|
const colorObj = this.isDarkMode ? this.darkModeColors : this.lightModeColors;
|
|
const flowColor = this.hexToRgb(colorObj.flowColor);
|
|
// Definiere fadeFactor als 1.0, falls nicht von flow definiert
|
|
// Berücksichtige auch den fadeoutFactor für das Ausblenden der gesamten Animation
|
|
const fadeFactor = (flow.fadeFactor || 1.0) * fadeoutFactor;
|
|
this.gl.uniform4f(
|
|
this.programInfo.uniformLocations.color,
|
|
flowColor.r / 255,
|
|
flowColor.g / 255,
|
|
flowColor.b / 255,
|
|
0.55 * fadeFactor // Reduziert von 0.7 auf 0.55 für subtilere Blitze
|
|
);
|
|
this.gl.enable(this.gl.BLEND);
|
|
this.gl.blendFunc(this.gl.SRC_ALPHA, this.gl.ONE);
|
|
this.gl.lineWidth(1.2); // Schmaler Blitz
|
|
this.gl.drawArrays(this.gl.LINES, 0, 2);
|
|
}
|
|
// Funken erzeugen - subtilere elektrische Funken
|
|
const sparks = this.generateSparkPoints(zigzag, 5 + Math.floor(Math.random() * 3)); // Weniger Funken
|
|
for (const spark of sparks) {
|
|
// Helles Weiß-Blau für elektrische Funken, aber dezenter
|
|
this.gl.uniform4f(
|
|
this.programInfo.uniformLocations.color,
|
|
0.85, 0.92, 0.98, 0.5 * fadeoutFactor // Reduzierte Opazität auf 50%
|
|
);
|
|
|
|
// Position für den Funken setzen
|
|
const sparkPos = new Float32Array([spark.x, spark.y]);
|
|
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.positionBuffer);
|
|
this.gl.bufferData(this.gl.ARRAY_BUFFER, sparkPos, this.gl.STATIC_DRAW);
|
|
|
|
// Punktgröße setzen - kleinere Funken
|
|
const sizes = new Float32Array([spark.size * 2.5]); // Reduziert von 3.0 auf 2.5
|
|
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.sizeBuffer);
|
|
this.gl.bufferData(this.gl.ARRAY_BUFFER, sizes, this.gl.STATIC_DRAW);
|
|
|
|
this.gl.vertexAttribPointer(
|
|
this.programInfo.attribLocations.vertexPosition,
|
|
2,
|
|
this.gl.FLOAT,
|
|
false,
|
|
0,
|
|
0
|
|
);
|
|
this.gl.enableVertexAttribArray(this.programInfo.attribLocations.vertexPosition);
|
|
|
|
this.gl.vertexAttribPointer(
|
|
this.programInfo.attribLocations.pointSize,
|
|
1,
|
|
this.gl.FLOAT,
|
|
false,
|
|
0,
|
|
0
|
|
);
|
|
this.gl.enableVertexAttribArray(this.programInfo.attribLocations.pointSize);
|
|
|
|
this.gl.drawArrays(this.gl.POINTS, 0, 1);
|
|
}
|
|
}
|
|
}
|
|
|
|
renderCanvas(now) {
|
|
// Clear canvas
|
|
const width = this.canvas.width / (window.devicePixelRatio || 1);
|
|
const height = this.canvas.height / (window.devicePixelRatio || 1);
|
|
|
|
// Fade-Faktor für sanftes Ausblenden im Canvas-Modus
|
|
const fadeoutFactor = this.isDestroying ? parseFloat(this.canvas.style.opacity) || 0.98 : 1.0;
|
|
|
|
this.ctx.clearRect(0, 0, width, height);
|
|
|
|
// Set background
|
|
const backgroundColor = this.isDarkMode
|
|
? this.darkModeColors.background
|
|
: this.lightModeColors.background;
|
|
|
|
this.ctx.fillStyle = backgroundColor;
|
|
this.ctx.fillRect(0, 0, width, height);
|
|
|
|
// Draw connections with fade effects
|
|
const connectionColor = this.isDarkMode
|
|
? this.darkModeColors.connectionColor
|
|
: this.lightModeColors.connectionColor;
|
|
|
|
for (const connection of this.connections) {
|
|
// Überspringe Verbindungen, die komplett unsichtbar sind
|
|
if (connection.fadeState === 'hidden' || connection.fadeProgress <= 0) continue;
|
|
|
|
const fromNode = this.nodes[connection.from];
|
|
const toNode = this.nodes[connection.to];
|
|
|
|
// Zeichne nur den Teil der Verbindung, der schon aufgebaut wurde
|
|
const progress = connection.progress || 0;
|
|
if (progress <= 0) continue; // Skip if no progress yet
|
|
|
|
const x1 = fromNode.x;
|
|
const y1 = fromNode.y;
|
|
|
|
// Endpunkt basiert auf dem aktuellen Fortschritt
|
|
const x2 = x1 + (toNode.x - x1) * progress;
|
|
const y2 = y1 + (toNode.y - y1) * progress;
|
|
|
|
// Zeichne die unterliegende Linie mit Ein-/Ausblendung
|
|
this.ctx.beginPath();
|
|
this.ctx.moveTo(x1, y1);
|
|
this.ctx.lineTo(x2, y2);
|
|
|
|
const rgbColor = this.hexToRgb(connectionColor);
|
|
|
|
// Calculate opacity based on fade state
|
|
let opacity = connection.opacity * connection.fadeProgress;
|
|
|
|
// Erhöhe kurzzeitig die Opazität bei kürzlich aktivierten Verbindungen
|
|
if (connection.lastActivated && now - connection.lastActivated < 800) {
|
|
const timeFactor = 1 - ((now - connection.lastActivated) / 800);
|
|
opacity = Math.max(opacity, timeFactor * this.config.linesOpacity);
|
|
}
|
|
|
|
// Berücksichtige fadeoutFactor für sanftes Ausblenden
|
|
opacity *= fadeoutFactor;
|
|
|
|
this.ctx.strokeStyle = `rgba(${rgbColor.r}, ${rgbColor.g}, ${rgbColor.b}, ${opacity})`;
|
|
this.ctx.lineWidth = this.config.linesWidth;
|
|
|
|
// Leichter Glow-Effekt für die Linien
|
|
if (opacity > 0.1) {
|
|
this.ctx.shadowColor = `rgba(${rgbColor.r}, ${rgbColor.g}, ${rgbColor.b}, ${0.15 * fadeoutFactor})`;
|
|
this.ctx.shadowBlur = 3;
|
|
} else {
|
|
this.ctx.shadowBlur = 0;
|
|
}
|
|
|
|
this.ctx.stroke();
|
|
|
|
// Zeichne einen Fortschrittspunkt am Ende der sich aufbauenden Verbindung
|
|
if (progress < 0.95 && connection.fadeProgress > 0.5) {
|
|
this.ctx.beginPath();
|
|
this.ctx.arc(x2, y2, 1.5, 0, Math.PI * 2);
|
|
this.ctx.fillStyle = `rgba(${rgbColor.r}, ${rgbColor.g}, ${rgbColor.b}, ${opacity * 1.3})`;
|
|
this.ctx.fill();
|
|
}
|
|
|
|
this.ctx.shadowBlur = 0; // Reset shadow for other elements
|
|
}
|
|
|
|
// Draw flows with fading effect
|
|
this.renderFlowsCanvas(now, fadeoutFactor);
|
|
|
|
// Draw nodes with enhanced animations
|
|
const nodeColor = this.isDarkMode
|
|
? this.darkModeColors.nodeColor
|
|
: this.lightModeColors.nodeColor;
|
|
|
|
const nodePulse = this.isDarkMode
|
|
? this.darkModeColors.nodePulse
|
|
: this.lightModeColors.nodePulse;
|
|
|
|
for (const node of this.nodes) {
|
|
// Verbesserte Pulsation mit Aktivierungsboost
|
|
const activationBoost = node.isActive ? 1.7 : 1.0;
|
|
const pulse = (Math.sin(node.pulsePhase) * 0.45 + 1.4) * activationBoost;
|
|
|
|
// Größe basierend auf Konnektivität und Wichtigkeit
|
|
const connectivityFactor = 1 + (node.connections.length / this.config.maxConnections) * 1.4;
|
|
// Während des Ausblendprozesses die Knotengröße leicht reduzieren
|
|
const sizeReduction = this.isDestroying ? 0.85 : 1.0;
|
|
const nodeSize = node.size * pulse * connectivityFactor * sizeReduction;
|
|
|
|
// Verbesserte Leuchtkraft und Glow-Effekt
|
|
const rgbNodeColor = this.hexToRgb(nodeColor);
|
|
const rgbPulseColor = this.hexToRgb(nodePulse);
|
|
|
|
// Mische Farben basierend auf Aktivierung
|
|
let r, g, b;
|
|
if (node.isActive) {
|
|
r = (rgbNodeColor.r * 0.3 + rgbPulseColor.r * 0.7);
|
|
g = (rgbNodeColor.g * 0.3 + rgbPulseColor.g * 0.7);
|
|
b = (rgbNodeColor.b * 0.3 + rgbPulseColor.b * 0.7);
|
|
} else {
|
|
r = rgbNodeColor.r;
|
|
g = rgbNodeColor.g;
|
|
b = rgbNodeColor.b;
|
|
}
|
|
|
|
// Äußerer Glow
|
|
const glow = this.ctx.createRadialGradient(
|
|
node.x, node.y, 0,
|
|
node.x, node.y, nodeSize * 4.5
|
|
);
|
|
|
|
// Intensiveres Zentrum und weicherer Übergang
|
|
// Berücksichtige fadeoutFactor für sanftes Ausblenden
|
|
const activeOpacity = node.isActive ? 0.95 : 0.8;
|
|
glow.addColorStop(0, `rgba(${r}, ${g}, ${b}, ${activeOpacity * fadeoutFactor})`);
|
|
glow.addColorStop(0.4, `rgba(${r}, ${g}, ${b}, ${0.45 * fadeoutFactor})`);
|
|
glow.addColorStop(1, `rgba(${r}, ${g}, ${b}, 0)`);
|
|
|
|
this.ctx.beginPath();
|
|
this.ctx.arc(node.x, node.y, nodeSize, 0, Math.PI * 2);
|
|
this.ctx.fillStyle = glow;
|
|
// Globale Transparenz reduzieren beim Ausblenden
|
|
this.ctx.globalAlpha = (node.isActive ? 1.0 : 0.92) * fadeoutFactor;
|
|
this.ctx.fill();
|
|
|
|
// Innerer Kern für stärkeren Leuchteffekt
|
|
if (node.isActive) {
|
|
this.ctx.beginPath();
|
|
this.ctx.arc(node.x, node.y, nodeSize * 0.4, 0, Math.PI * 2);
|
|
this.ctx.fillStyle = `rgba(${rgbPulseColor.r}, ${rgbPulseColor.g}, ${rgbPulseColor.b}, ${0.9 * fadeoutFactor})`;
|
|
this.ctx.fill();
|
|
}
|
|
|
|
this.ctx.globalAlpha = 1.0;
|
|
}
|
|
}
|
|
|
|
// New method to render flows in Canvas mode with fading
|
|
renderFlowsCanvas(now, fadeoutFactor = 1.0) {
|
|
if (!this.flows.length) return;
|
|
|
|
// Für jeden Flow in der Blitzanimation
|
|
for (const flow of this.flows) {
|
|
const connection = flow.connection;
|
|
const fromNode = this.nodes[connection.from];
|
|
const toNode = this.nodes[connection.to];
|
|
|
|
// Berechne den Fortschritt der Connection und des Flows
|
|
const connProgress = connection.progress || 1;
|
|
|
|
// Flussrichtung bestimmen
|
|
const [startNode, endNode] = flow.direction ? [fromNode, toNode] : [toNode, fromNode];
|
|
|
|
// Berücksichtige den Verbindungs-Fortschritt
|
|
const maxDistance = Math.min(connProgress, 1) * Math.sqrt(
|
|
Math.pow(endNode.x - startNode.x, 2) +
|
|
Math.pow(endNode.y - startNode.y, 2)
|
|
);
|
|
|
|
// Berechne den aktuellen Fortschritt mit Ein- und Ausblendung
|
|
const flowAge = now - flow.creationTime;
|
|
const flowLifetime = flow.totalDuration;
|
|
let fadeFactor = 1.0;
|
|
|
|
// Sanftere Ein- und Ausblendung für Blitzeffekte
|
|
if (flowAge < flowLifetime * 0.3) {
|
|
// Einblenden - sanfter und länger
|
|
fadeFactor = flowAge / (flowLifetime * 0.3);
|
|
} else if (flowAge > flowLifetime * 0.7) {
|
|
// Ausblenden - sanfter und länger
|
|
fadeFactor = 1.0 - ((flowAge - flowLifetime * 0.7) / (flowLifetime * 0.3));
|
|
}
|
|
|
|
// Beim Ausblenden der gesamten Animation auch den Flow-Faktor anpassen
|
|
fadeFactor *= fadeoutFactor;
|
|
|
|
// Flow-Fortschritt
|
|
const startProgress = Math.max(0.0, flow.progress - flow.length);
|
|
const endProgress = Math.min(flow.progress, connProgress);
|
|
|
|
// Start- und Endpunkte basierend auf dem Fortschritt
|
|
const p1 = {
|
|
x: startNode.x + (endNode.x - startNode.x) * startProgress,
|
|
y: startNode.y + (endNode.y - startNode.y) * startProgress
|
|
};
|
|
|
|
const p2 = {
|
|
x: startNode.x + (endNode.x - startNode.x) * endProgress,
|
|
y: startNode.y + (endNode.y - startNode.y) * endProgress
|
|
};
|
|
|
|
if (endProgress > connProgress) continue;
|
|
|
|
// Lila Gradient für den Blitz
|
|
const gradient = this.ctx.createLinearGradient(p1.x, p1.y, p2.x, p2.y);
|
|
gradient.addColorStop(0, 'rgba(255, 0, 255, 0.8)');
|
|
gradient.addColorStop(0.5, 'rgba(200, 0, 255, 0.9)');
|
|
gradient.addColorStop(1, 'rgba(255, 0, 255, 0.8)');
|
|
|
|
this.ctx.save();
|
|
|
|
// Untergrundspur mit stärkerem Glühen
|
|
this.ctx.beginPath();
|
|
this.ctx.moveTo(p1.x, p1.y);
|
|
this.ctx.lineTo(p2.x, p2.y);
|
|
this.ctx.strokeStyle = gradient;
|
|
this.ctx.lineWidth = 20.0;
|
|
this.ctx.shadowColor = 'rgba(255, 0, 255, 0.4)';
|
|
this.ctx.shadowBlur = 25;
|
|
this.ctx.stroke();
|
|
|
|
// Abgerundeter Zickzack-Blitz mit weicheren Kurven
|
|
const zigzag = this.generateZigZagPoints(p1, p2, 4, 6, true);
|
|
|
|
// Hauptblitz mit Gradient
|
|
this.ctx.strokeStyle = gradient;
|
|
this.ctx.lineWidth = 1.5;
|
|
this.ctx.shadowColor = 'rgba(255, 0, 255, 0.5)';
|
|
this.ctx.shadowBlur = 30;
|
|
this.ctx.beginPath();
|
|
this.ctx.moveTo(zigzag[0].x, zigzag[0].y);
|
|
for (let i = 1; i < zigzag.length; i++) {
|
|
const cp1x = zigzag[i-1].x + (zigzag[i].x - zigzag[i-1].x) * 0.4;
|
|
const cp1y = zigzag[i-1].y + (zigzag[i].y - zigzag[i-1].y) * 0.4;
|
|
const cp2x = zigzag[i].x - (zigzag[i].x - zigzag[i-1].x) * 0.4;
|
|
const cp2y = zigzag[i].y - (zigzag[i].y - zigzag[i-1].y) * 0.4;
|
|
this.ctx.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, zigzag[i].x, zigzag[i].y);
|
|
}
|
|
this.ctx.stroke();
|
|
|
|
// Funken mit lila Glühen
|
|
const sparks = this.generateSparkPoints(zigzag, 10 + Math.floor(Math.random() * 6));
|
|
|
|
const sparkGradient = this.ctx.createRadialGradient(0, 0, 0, 0, 0, 10);
|
|
sparkGradient.addColorStop(0, 'rgba(255, 0, 255, 0.95)');
|
|
sparkGradient.addColorStop(0.5, 'rgba(200, 0, 255, 0.8)');
|
|
sparkGradient.addColorStop(1, 'rgba(255, 0, 255, 0.6)');
|
|
|
|
for (const spark of sparks) {
|
|
this.ctx.beginPath();
|
|
|
|
// Weichere Sternform
|
|
const points = 4 + Math.floor(Math.random() * 3);
|
|
const outerRadius = spark.size * 2.0;
|
|
const innerRadius = spark.size * 0.5;
|
|
|
|
for (let i = 0; i < points * 2; i++) {
|
|
const radius = i % 2 === 0 ? outerRadius : innerRadius;
|
|
const angle = (i * Math.PI) / points;
|
|
const x = spark.x + Math.cos(angle) * radius;
|
|
const y = spark.y + Math.sin(angle) * radius;
|
|
|
|
if (i === 0) {
|
|
this.ctx.moveTo(x, y);
|
|
} else {
|
|
this.ctx.lineTo(x, y);
|
|
}
|
|
}
|
|
|
|
this.ctx.closePath();
|
|
|
|
// Intensives lila Glühen
|
|
this.ctx.shadowColor = 'rgba(255, 0, 255, 0.8)';
|
|
this.ctx.shadowBlur = 25;
|
|
this.ctx.fillStyle = sparkGradient;
|
|
this.ctx.fill();
|
|
// Intensiverer innerer Glüheffekt für ausgewählte Funken mit mehrfacher Schichtung
|
|
if (spark.size > 3 && Math.random() > 0.3) {
|
|
// Erste Glühschicht - größer und weicher
|
|
this.ctx.beginPath();
|
|
this.ctx.arc(spark.x, spark.y, spark.size * 0.8, 0, Math.PI * 2);
|
|
this.ctx.fillStyle = this.isDarkMode
|
|
? `rgba(245, 252, 255, ${0.85 * fadeFactor})`
|
|
: `rgba(230, 245, 255, ${0.8 * fadeFactor})`;
|
|
this.ctx.shadowColor = this.isDarkMode
|
|
? `rgba(200, 225, 255, ${0.7 * fadeFactor})`
|
|
: `rgba(180, 220, 255, ${0.6 * fadeFactor})`;
|
|
this.ctx.shadowBlur = 15;
|
|
this.ctx.fill();
|
|
|
|
// Zweite Glühschicht - kleiner und intensiver
|
|
this.ctx.beginPath();
|
|
this.ctx.arc(spark.x, spark.y, spark.size * 0.5, 0, Math.PI * 2);
|
|
this.ctx.fillStyle = this.isDarkMode
|
|
? `rgba(255, 255, 255, ${0.95 * fadeFactor})`
|
|
: `rgba(240, 250, 255, ${0.9 * fadeFactor})`;
|
|
this.ctx.shadowColor = this.isDarkMode
|
|
? `rgba(220, 235, 255, ${0.8 * fadeFactor})`
|
|
: `rgba(200, 230, 255, ${0.7 * fadeFactor})`;
|
|
this.ctx.shadowBlur = 20;
|
|
this.ctx.fill();
|
|
|
|
// Dritte Glühschicht - noch intensiverer Kern
|
|
this.ctx.beginPath();
|
|
this.ctx.arc(spark.x, spark.y, spark.size * 0.3, 0, Math.PI * 2);
|
|
this.ctx.fillStyle = this.isDarkMode
|
|
? `rgba(255, 255, 255, ${0.98 * fadeFactor})`
|
|
: `rgba(245, 252, 255, ${0.95 * fadeFactor})`;
|
|
this.ctx.shadowColor = this.isDarkMode
|
|
? `rgba(230, 240, 255, ${0.9 * fadeFactor})`
|
|
: `rgba(210, 235, 255, ${0.8 * fadeFactor})`;
|
|
this.ctx.shadowBlur = 25;
|
|
this.ctx.fill();
|
|
|
|
// Vierte Glühschicht - pulsierender Effekt
|
|
const pulseSize = spark.size * (0.2 + Math.sin(Date.now() * 0.01) * 0.1);
|
|
this.ctx.beginPath();
|
|
this.ctx.arc(spark.x, spark.y, pulseSize, 0, Math.PI * 2);
|
|
this.ctx.fillStyle = this.isDarkMode
|
|
? `rgba(255, 255, 255, ${0.99 * fadeFactor})`
|
|
: `rgba(250, 255, 255, ${0.97 * fadeFactor})`;
|
|
this.ctx.shadowColor = this.isDarkMode
|
|
? `rgba(240, 245, 255, ${0.95 * fadeFactor})`
|
|
: `rgba(220, 240, 255, ${0.85 * fadeFactor})`;
|
|
this.ctx.shadowBlur = 30;
|
|
this.ctx.fill();
|
|
}
|
|
}
|
|
|
|
// Deutlicherer und länger anhaltender Fortschrittseffekt an der Spitze des Blitzes
|
|
if (endProgress >= connProgress - 0.1 && connProgress < 0.98) {
|
|
const tipGlow = this.ctx.createRadialGradient(
|
|
p2.x, p2.y, 0,
|
|
p2.x, p2.y, 10
|
|
);
|
|
tipGlow.addColorStop(0, `rgba(${rgbFlowColor.r}, ${rgbFlowColor.g}, ${rgbFlowColor.b}, ${0.85 * fadeFactor})`);
|
|
tipGlow.addColorStop(0.5, `rgba(${rgbFlowColor.r}, ${rgbFlowColor.g}, ${rgbFlowColor.b}, ${0.4 * fadeFactor})`);
|
|
tipGlow.addColorStop(1, `rgba(${rgbFlowColor.r}, ${rgbFlowColor.g}, ${rgbFlowColor.b}, 0)`);
|
|
|
|
this.ctx.fillStyle = tipGlow;
|
|
this.ctx.beginPath();
|
|
this.ctx.arc(p2.x, p2.y, 10, 0, Math.PI * 2);
|
|
this.ctx.fill();
|
|
}
|
|
|
|
this.ctx.restore();
|
|
}
|
|
}
|
|
|
|
// Helper method to convert hex to RGB
|
|
hexToRgb(hex) {
|
|
// Remove # if present
|
|
hex = hex.replace(/^#/, '');
|
|
|
|
// Handle rgba hex format
|
|
let alpha = 1;
|
|
if (hex.length === 8) {
|
|
alpha = parseInt(hex.slice(6, 8), 16) / 255;
|
|
hex = hex.slice(0, 6);
|
|
}
|
|
|
|
// Parse hex values
|
|
const bigint = parseInt(hex, 16);
|
|
const r = (bigint >> 16) & 255;
|
|
const g = (bigint >> 8) & 255;
|
|
const b = bigint & 255;
|
|
|
|
return { r, g, b, a: alpha };
|
|
}
|
|
|
|
// Cleanup method mit deutlich sanfterem Ausblenden
|
|
destroy() {
|
|
// Sanfte Ausblendanimation starten
|
|
this.isDestroying = true;
|
|
|
|
// Canvas-Element sanft ausblenden über einen längeren Zeitraum
|
|
if (this.canvas) {
|
|
this.canvas.style.transition = 'opacity 5s ease-in-out'; // Verlängert von 1.5s auf 5s
|
|
this.canvas.style.opacity = '0';
|
|
|
|
// Warte auf das Ende der Übergangsanimation, bevor die Ressourcen freigegeben werden
|
|
setTimeout(() => {
|
|
// Animation schrittweise verlangsamen statt sofort stoppen
|
|
const slowDownAnimation = () => {
|
|
// Schrittweise die Animation verlangsamen durch Reduzierung der FPS
|
|
if (this.animationFrameId) {
|
|
cancelAnimationFrame(this.animationFrameId);
|
|
|
|
// Animiere mit abnehmender Framerate
|
|
setTimeout(() => {
|
|
this.animationFrameId = requestAnimationFrame(this.animate.bind(this));
|
|
|
|
// Nach 10 weiteren Sekunden komplett beenden
|
|
if (this._destroyStartTime && (Date.now() - this._destroyStartTime > 10000)) {
|
|
finalCleanup();
|
|
} else {
|
|
slowDownAnimation();
|
|
}
|
|
}, 500); // Zunehmende Verzögerung zwischen Frames
|
|
}
|
|
};
|
|
|
|
// Endgültige Bereinigung nach dem vollständigen Ausblenden
|
|
const finalCleanup = () => {
|
|
// Animation stoppen
|
|
if (this.animationFrameId) {
|
|
cancelAnimationFrame(this.animationFrameId);
|
|
this.animationFrameId = null;
|
|
}
|
|
|
|
// Event-Listener entfernen
|
|
window.removeEventListener('resize', this.resizeCanvas.bind(this));
|
|
|
|
// Canvas-Element aus dem DOM entfernen, nachdem es ausgeblendet wurde
|
|
if (this.canvas && this.canvas.parentNode) {
|
|
this.canvas.parentNode.removeChild(this.canvas);
|
|
}
|
|
|
|
// WebGL-Ressourcen bereinigen
|
|
if (this.gl) {
|
|
this.gl.deleteBuffer(this.positionBuffer);
|
|
this.gl.deleteBuffer(this.sizeBuffer);
|
|
this.gl.deleteProgram(this.shaderProgram);
|
|
}
|
|
|
|
console.log('Neural Network Background erfolgreich deaktiviert');
|
|
};
|
|
|
|
// Startzeit für die schrittweise Verlangsamung setzen
|
|
this._destroyStartTime = Date.now();
|
|
|
|
// Starte die schrittweise Verlangsamung
|
|
slowDownAnimation();
|
|
}, 5500); // Warte länger als die CSS-Transition-Dauer
|
|
}
|
|
}
|
|
|
|
// Hilfsfunktion: Erzeuge Zickzack-Punkte für einen Blitz mit geringerer Vibration
|
|
generateZigZagPoints(start, end, segments = 5, amplitude = 8) {
|
|
const points = [start];
|
|
const mainAngle = Math.atan2(end.y - start.y, end.x - start.x);
|
|
|
|
// Berechne die Gesamtlänge des Blitzes
|
|
const totalDistance = Math.sqrt(
|
|
Math.pow(end.x - start.x, 2) +
|
|
Math.pow(end.y - start.y, 2)
|
|
);
|
|
|
|
// Geringere Amplitude für subtilere Zickzack-Muster
|
|
const baseAmplitude = totalDistance * 0.12; // Reduziert für sanfteres Erscheinungsbild
|
|
|
|
for (let i = 1; i < segments; i++) {
|
|
const t = i / segments;
|
|
const x = start.x + (end.x - start.x) * t;
|
|
const y = start.y + (end.y - start.y) * t;
|
|
|
|
// Geringere Vibration durch kleinere Zufallsvariationen
|
|
const perpendicularAngle = mainAngle + Math.PI/2;
|
|
const variation = (Math.random() * 0.8 - 0.4); // Kleinere Variation für sanftere Muster
|
|
|
|
// Mal Links, mal Rechts für sanften Blitz
|
|
const directionFactor = (i % 2 === 0) ? 1 : -1;
|
|
const offset = baseAmplitude * (Math.sin(i * Math.PI) + variation) * directionFactor;
|
|
|
|
points.push({
|
|
x: x + Math.cos(perpendicularAngle) * offset,
|
|
y: y + Math.sin(perpendicularAngle) * offset
|
|
});
|
|
}
|
|
points.push(end);
|
|
return points;
|
|
}
|
|
|
|
// Hilfsfunktion: Erzeuge intensivere Funkenpunkte mit dynamischer Verteilung
|
|
generateSparkPoints(zigzag, sparkCount = 15) {
|
|
const sparks = [];
|
|
// Mehr Funken für intensiveren Effekt
|
|
const actualSparkCount = Math.min(sparkCount, zigzag.length * 2);
|
|
|
|
// Funken an zufälligen Stellen entlang des Blitzes
|
|
for (let i = 0; i < actualSparkCount; i++) {
|
|
// Zufälliges Segment des Zickzacks auswählen
|
|
const segIndex = Math.floor(Math.random() * (zigzag.length - 1));
|
|
|
|
// Bestimme Richtung des Segments
|
|
const dx = zigzag[segIndex + 1].x - zigzag[segIndex].x;
|
|
const dy = zigzag[segIndex + 1].y - zigzag[segIndex].y;
|
|
const segmentAngle = Math.atan2(dy, dx);
|
|
|
|
// Zufällige Position entlang des Segments
|
|
const t = Math.random();
|
|
const x = zigzag[segIndex].x + dx * t;
|
|
const y = zigzag[segIndex].y + dy * t;
|
|
|
|
// Dynamischer Versatz für intensivere Funken
|
|
const offsetAngle = segmentAngle + (Math.random() * Math.PI - Math.PI/2);
|
|
const offsetDistance = Math.random() * 8 - 4; // Größerer Offset für dramatischere Funken
|
|
|
|
// Zufällige Größe für variierende Intensität
|
|
const baseSize = 3.5 + Math.random() * 3.5;
|
|
const sizeVariation = Math.random() * 2.5;
|
|
|
|
sparks.push({
|
|
x: x + Math.cos(offsetAngle) * offsetDistance,
|
|
y: y + Math.sin(offsetAngle) * offsetDistance,
|
|
size: baseSize + sizeVariation // Größere und variablere Funkengröße
|
|
});
|
|
|
|
// Zusätzliche kleinere Funken in der Nähe für einen intensiveren Effekt
|
|
if (Math.random() < 0.4) { // 40% Chance für zusätzliche Funken
|
|
const subSparkAngle = offsetAngle + (Math.random() * Math.PI/2 - Math.PI/4);
|
|
const subDistance = offsetDistance * (0.4 + Math.random() * 0.6);
|
|
|
|
sparks.push({
|
|
x: x + Math.cos(subSparkAngle) * subDistance,
|
|
y: y + Math.sin(subSparkAngle) * subDistance,
|
|
size: (baseSize + sizeVariation) * 0.6 // Kleinere Größe für sekundäre Funken
|
|
});
|
|
}
|
|
}
|
|
|
|
return sparks;
|
|
}
|
|
|
|
// New method to render the flowing animations with intensiveren Glow und Lila-Farbverlauf
|
|
renderFlowsWebGL(now, fadeoutFactor = 1.0) {
|
|
// Für jeden Flow einen leuchtenden Lila-Blitz als Schlange zeichnen und Funken erzeugen
|
|
for (const flow of this.flows) {
|
|
const connection = flow.connection;
|
|
const fromNode = this.nodes[connection.from];
|
|
const toNode = this.nodes[connection.to];
|
|
const startProgress = flow.progress;
|
|
const endProgress = Math.min(1, startProgress + flow.length);
|
|
if (startProgress >= 1 || endProgress <= 0) continue;
|
|
const direction = flow.direction ? 1 : -1;
|
|
let p1, p2;
|
|
if (direction > 0) {
|
|
p1 = {
|
|
x: fromNode.x + (toNode.x - fromNode.x) * startProgress,
|
|
y: fromNode.y + (toNode.y - fromNode.y) * startProgress
|
|
};
|
|
p2 = {
|
|
x: fromNode.x + (toNode.x - fromNode.x) * endProgress,
|
|
y: fromNode.y + (toNode.y - fromNode.y) * endProgress
|
|
};
|
|
} else {
|
|
p1 = {
|
|
x: toNode.x + (fromNode.x - toNode.x) * startProgress,
|
|
y: toNode.y + (fromNode.y - toNode.y) * startProgress
|
|
};
|
|
p2 = {
|
|
x: toNode.x + (fromNode.x - toNode.x) * endProgress,
|
|
y: toNode.y + (fromNode.y - toNode.y) * endProgress
|
|
};
|
|
}
|
|
|
|
// Erzeuge Schlangenbewegung durch intensivere Zickzack-Muster
|
|
const snakeSegments = 12; // Mehr Segmente für flüssigere Schlangenbewegung
|
|
const snakeAmplitude = 12; // Größere Amplitude für deutlichere Schlangenbewegung
|
|
// Einflussfaktor, der von der Generation abhängt - höhere Generationen schlängeln mehr
|
|
const generationFactor = Math.min(1.5, (flow.generation || 1) / 10 + 0.8);
|
|
const zigzag = this.generateZigZagPoints(
|
|
p1, p2,
|
|
snakeSegments,
|
|
snakeAmplitude * generationFactor,
|
|
true // Sanftere Kurven aktivieren
|
|
);
|
|
|
|
// Lila Gradient Effekt für Blitze - Multi-Pass Rendering für intensives Glühen
|
|
|
|
// 1. Render outer glow - very wide and subtle
|
|
const outerGlowWidth = 6.0 * this.config.glowIntensity;
|
|
for (let i = 0; i < zigzag.length - 1; i++) {
|
|
const positions = new Float32Array([
|
|
zigzag[i].x, zigzag[i].y,
|
|
zigzag[i + 1].x, zigzag[i + 1].y
|
|
]);
|
|
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.positionBuffer);
|
|
this.gl.bufferData(this.gl.ARRAY_BUFFER, positions, this.gl.STATIC_DRAW);
|
|
this.gl.vertexAttribPointer(
|
|
this.programInfo.attribLocations.vertexPosition,
|
|
2,
|
|
this.gl.FLOAT,
|
|
false,
|
|
0,
|
|
0
|
|
);
|
|
this.gl.enableVertexAttribArray(this.programInfo.attribLocations.vertexPosition);
|
|
this.gl.disableVertexAttribArray(this.programInfo.attribLocations.pointSize);
|
|
|
|
// Äußerer Glow in dunklerem Lila mit niedriger Opazität
|
|
this.gl.uniform4f(
|
|
this.programInfo.uniformLocations.color,
|
|
0.45, 0.2, 0.7, 0.15 * fadeoutFactor // Dunkler lila Außenglow
|
|
);
|
|
this.gl.enable(this.gl.BLEND);
|
|
this.gl.blendFunc(this.gl.SRC_ALPHA, this.gl.ONE);
|
|
this.gl.lineWidth(outerGlowWidth);
|
|
this.gl.drawArrays(this.gl.LINES, 0, 2);
|
|
}
|
|
|
|
// 2. Middle glow layer - more opaque and focused
|
|
const middleGlowWidth = 3.5 * this.config.glowIntensity;
|
|
for (let i = 0; i < zigzag.length - 1; i++) {
|
|
const positions = new Float32Array([
|
|
zigzag[i].x, zigzag[i].y,
|
|
zigzag[i + 1].x, zigzag[i + 1].y
|
|
]);
|
|
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.positionBuffer);
|
|
this.gl.bufferData(this.gl.ARRAY_BUFFER, positions, this.gl.STATIC_DRAW);
|
|
this.gl.vertexAttribPointer(
|
|
this.programInfo.attribLocations.vertexPosition,
|
|
2,
|
|
this.gl.FLOAT,
|
|
false,
|
|
0,
|
|
0
|
|
);
|
|
|
|
// Mittlerer Glow in hellerem Lila mit mittlerer Opazität
|
|
this.gl.uniform4f(
|
|
this.programInfo.uniformLocations.color,
|
|
0.65, 0.3, 0.85, 0.35 * fadeoutFactor // Mittleres Lila
|
|
);
|
|
this.gl.lineWidth(middleGlowWidth);
|
|
this.gl.drawArrays(this.gl.LINES, 0, 2);
|
|
}
|
|
|
|
// 3. Inner core - bright and vibrant
|
|
for (let i = 0; i < zigzag.length - 1; i++) {
|
|
const positions = new Float32Array([
|
|
zigzag[i].x, zigzag[i].y,
|
|
zigzag[i + 1].x, zigzag[i + 1].y
|
|
]);
|
|
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.positionBuffer);
|
|
this.gl.bufferData(this.gl.ARRAY_BUFFER, positions, this.gl.STATIC_DRAW);
|
|
this.gl.vertexAttribPointer(
|
|
this.programInfo.attribLocations.vertexPosition,
|
|
2,
|
|
this.gl.FLOAT,
|
|
false,
|
|
0,
|
|
0
|
|
);
|
|
|
|
// Innerer Kern in strahlendem Lila-Weiß
|
|
this.gl.uniform4f(
|
|
this.programInfo.uniformLocations.color,
|
|
0.9, 0.7, 1.0, 0.8 * fadeoutFactor // Weißlich-Lila für intensives Zentrum
|
|
);
|
|
this.gl.lineWidth(1.6); // Schmaler Kern
|
|
this.gl.drawArrays(this.gl.LINES, 0, 2);
|
|
}
|
|
|
|
// Viele intensivere Funken erzeugen
|
|
const sparkCount = this.config.sparkCount + Math.floor((flow.generation || 1) / 2); // Mehr Funken bei höheren Generationen
|
|
const sparks = this.generateSparkPoints(zigzag, sparkCount);
|
|
|
|
// Render die Funken mit mehreren Ebenen für intensiveres Glühen
|
|
for (const spark of sparks) {
|
|
// 1. Äußerer Glow der Funken
|
|
this.gl.uniform4f(
|
|
this.programInfo.uniformLocations.color,
|
|
0.5, 0.2, 0.8, 0.3 * fadeoutFactor // Lila Glow mit niedriger Opazität
|
|
);
|
|
|
|
// Position für den Funken setzen
|
|
const sparkPos = new Float32Array([spark.x, spark.y]);
|
|
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.positionBuffer);
|
|
this.gl.bufferData(this.gl.ARRAY_BUFFER, sparkPos, this.gl.STATIC_DRAW);
|
|
|
|
// Größerer Glow um jeden Funken
|
|
const sizes = new Float32Array([spark.size * this.config.sparkSize * 2.0]);
|
|
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.sizeBuffer);
|
|
this.gl.bufferData(this.gl.ARRAY_BUFFER, sizes, this.gl.STATIC_DRAW);
|
|
|
|
this.gl.vertexAttribPointer(
|
|
this.programInfo.attribLocations.vertexPosition,
|
|
2,
|
|
this.gl.FLOAT,
|
|
false,
|
|
0,
|
|
0
|
|
);
|
|
this.gl.enableVertexAttribArray(this.programInfo.attribLocations.vertexPosition);
|
|
|
|
this.gl.vertexAttribPointer(
|
|
this.programInfo.attribLocations.pointSize,
|
|
1,
|
|
this.gl.FLOAT,
|
|
false,
|
|
0,
|
|
0
|
|
);
|
|
this.gl.enableVertexAttribArray(this.programInfo.attribLocations.pointSize);
|
|
|
|
this.gl.drawArrays(this.gl.POINTS, 0, 1);
|
|
|
|
// 2. Mittlere Schicht der Funken
|
|
this.gl.uniform4f(
|
|
this.programInfo.uniformLocations.color,
|
|
0.7, 0.4, 0.95, 0.6 * fadeoutFactor // Mittleres Lila mit mittlerer Opazität
|
|
);
|
|
|
|
const middleSizes = new Float32Array([spark.size * this.config.sparkSize * 1.2]);
|
|
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.sizeBuffer);
|
|
this.gl.bufferData(this.gl.ARRAY_BUFFER, middleSizes, this.gl.STATIC_DRAW);
|
|
|
|
this.gl.drawArrays(this.gl.POINTS, 0, 1);
|
|
|
|
// 3. Innerer Kern der Funken
|
|
this.gl.uniform4f(
|
|
this.programInfo.uniformLocations.color,
|
|
0.95, 0.85, 1.0, 0.9 * fadeoutFactor // Fast weißes Zentrum für intensives Leuchten
|
|
);
|
|
|
|
const innerSizes = new Float32Array([spark.size * this.config.sparkSize * 0.6]);
|
|
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.sizeBuffer);
|
|
this.gl.bufferData(this.gl.ARRAY_BUFFER, innerSizes, this.gl.STATIC_DRAW);
|
|
|
|
this.gl.drawArrays(this.gl.POINTS, 0, 1);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Hilfsfunktion: Erzeuge schlangenartige Zickzack-Punkte mit Glätte-Option
|
|
generateZigZagPoints(start, end, segments = 5, amplitude = 8, smooth = false) {
|
|
const points = [start];
|
|
const mainAngle = Math.atan2(end.y - start.y, end.x - start.x);
|
|
|
|
// Berechne die Gesamtlänge des Blitzes
|
|
const totalDistance = Math.sqrt(
|
|
Math.pow(end.x - start.x, 2) +
|
|
Math.pow(end.y - start.y, 2)
|
|
);
|
|
|
|
// Dynamische Amplitude basierend auf Distanz
|
|
const baseAmplitude = amplitude * (totalDistance / 150);
|
|
|
|
// Welle für schlangenartigen Effekt
|
|
const waveFrequency = 2 + Math.random() * 1.5; // Zusätzlicher Welleneffekt
|
|
|
|
// Vorherige Punktrichtung für glattere Übergänge
|
|
let prevOffsetX = 0;
|
|
let prevOffsetY = 0;
|
|
|
|
for (let i = 1; i < segments; i++) {
|
|
const t = i / segments;
|
|
// Kubische Easing-Funktion für natürlichere Bewegung
|
|
const easedT = smooth ? t*t*(3-2*t) : t;
|
|
|
|
const x = start.x + (end.x - start.x) * easedT;
|
|
const y = start.y + (end.y - start.y) * easedT;
|
|
|
|
// Schlangenbewegung durch Sinuswelle mit zusätzlicher Zeit- und Positionsabhängigkeit
|
|
const wavePhase = waveFrequency * Math.PI * t + (Date.now() % 1000) / 1000 * Math.PI;
|
|
const waveAmplitude = Math.sin(wavePhase) * baseAmplitude;
|
|
|
|
// Perpendikuläre Richtung zur Hauptrichtung + etwas Zufall
|
|
const perpendicularAngle = mainAngle + Math.PI/2 + (Math.random() * 0.3 - 0.15);
|
|
|
|
// Für glattere Übergänge, interpoliere zwischen vorherigem und neuem Offset
|
|
let offsetX, offsetY;
|
|
if (smooth && i > 1) {
|
|
const newOffsetX = Math.cos(perpendicularAngle) * waveAmplitude;
|
|
const newOffsetY = Math.sin(perpendicularAngle) * waveAmplitude;
|
|
|
|
// Interpoliere zwischen vorherigem und neuem Offset (30% vorheriger, 70% neuer)
|
|
offsetX = prevOffsetX * 0.3 + newOffsetX * 0.7;
|
|
offsetY = prevOffsetY * 0.3 + newOffsetY * 0.7;
|
|
|
|
// Speichere für nächsten Punkt
|
|
prevOffsetX = offsetX;
|
|
prevOffsetY = offsetY;
|
|
} else {
|
|
offsetX = Math.cos(perpendicularAngle) * waveAmplitude;
|
|
offsetY = Math.sin(perpendicularAngle) * waveAmplitude;
|
|
|
|
prevOffsetX = offsetX;
|
|
prevOffsetY = offsetY;
|
|
}
|
|
|
|
points.push({
|
|
x: x + offsetX,
|
|
y: y + offsetY
|
|
});
|
|
}
|
|
|
|
points.push(end);
|
|
return points;
|
|
}
|
|
|
|
// Hilfsfunktion: Erzeuge leuchtende Funkenpunkte
|
|
generateSparkPoints(zigzag, sparkCount = 15) {
|
|
const sparks = [];
|
|
// Mehr Funken für intensiveren Effekt
|
|
const actualSparkCount = Math.min(sparkCount, zigzag.length * 3);
|
|
|
|
// Funken an zufälligen Stellen entlang des Blitzes mit mehr Variabilität
|
|
for (let i = 0; i < actualSparkCount; i++) {
|
|
// Zufälliges Segment des Zickzacks auswählen, mit Präferenz für Ecken
|
|
let segIndex;
|
|
if (Math.random() < 0.6 && zigzag.length > 3) {
|
|
// 60% der Funken an Ecken platzieren (mehr Funken an Wendepunkten)
|
|
segIndex = 1 + Math.floor(Math.random() * (zigzag.length - 3));
|
|
} else {
|
|
// Rest gleichmäßig verteilen
|
|
segIndex = Math.floor(Math.random() * (zigzag.length - 1));
|
|
}
|
|
|
|
// Bestimme Richtung des Segments
|
|
const dx = zigzag[segIndex + 1].x - zigzag[segIndex].x;
|
|
const dy = zigzag[segIndex + 1].y - zigzag[segIndex].y;
|
|
const segmentAngle = Math.atan2(dy, dx);
|
|
|
|
// Zufällige Position entlang des Segments mit Präferenz für die Mitte
|
|
let t;
|
|
if (Math.random() < 0.4) {
|
|
// 40% der Funken näher zur Mitte des Segments
|
|
t = 0.3 + Math.random() * 0.4;
|
|
} else {
|
|
// Rest gleichmäßig verteilen
|
|
t = Math.random();
|
|
}
|
|
|
|
const x = zigzag[segIndex].x + dx * t;
|
|
const y = zigzag[segIndex].y + dy * t;
|
|
|
|
// Dynamischer Versatz für intensivere Funken
|
|
// Versatz tendenziell senkrecht zur Segmentrichtung
|
|
const offsetAngle = segmentAngle + (Math.random() * Math.PI - Math.PI/2);
|
|
// Größerer Versatz für Funken, die weiter vom Pfad wegfliegen
|
|
const offsetDistance = (0.8 + Math.random() * 2.0) * 6;
|
|
|
|
// Größere und variablere Funken mit Zeit-basiertem Pulsieren
|
|
const baseSize = 0.8 + Math.random() * 1.0;
|
|
// Pulsierender Effekt
|
|
const pulsePhase = (Date.now() % 1000) / 1000 * Math.PI * 2;
|
|
const pulseFactor = 0.8 + Math.sin(pulsePhase) * 0.2;
|
|
|
|
sparks.push({
|
|
x: x + Math.cos(offsetAngle) * offsetDistance,
|
|
y: y + Math.sin(offsetAngle) * offsetDistance,
|
|
size: baseSize * pulseFactor,
|
|
// Zufälliger Winkel für Funken-Rotation
|
|
angle: Math.random() * Math.PI * 2
|
|
});
|
|
}
|
|
|
|
return sparks;
|
|
}
|
|
}
|
|
|
|
// Initialize when DOM is loaded
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
// Short delay to ensure DOM is fully loaded
|
|
setTimeout(() => {
|
|
if (!window.neuralNetworkBackground) {
|
|
console.log('Creating Neural Network Background');
|
|
window.neuralNetworkBackground = new NeuralNetworkBackground();
|
|
}
|
|
}, 100);
|
|
});
|
|
|
|
// Re-initialize when page is fully loaded (for safety)
|
|
window.addEventListener('load', () => {
|
|
if (!window.neuralNetworkBackground) {
|
|
console.log('Re-initializing Neural Network Background on full load');
|
|
window.neuralNetworkBackground = new NeuralNetworkBackground();
|
|
}
|
|
});
|
|
|
|
// Event listener to clean up when the window is closed
|
|
window.addEventListener('beforeunload', function() {
|
|
if (window.neuralNetworkBackground) {
|
|
window.neuralNetworkBackground.destroy();
|
|
}
|
|
});
|