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

1109 lines
39 KiB
JavaScript

/**
* Neural Network Background Animation
* Modern, darker, mystical theme using WebGL
* Subtle flowing network aesthetic
*/
class NeuralNetworkBackground {
constructor(options = {}) {
this.canvas = document.createElement('canvas');
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 = '-1';
this.canvas.style.pointerEvents = 'none';
this.canvas.style.opacity = '0.6'; // Noch weiter reduzierte Opazität für subtileren Hintergrund
// Standardkonfiguration mit subtileren Werten
this.config = {
nodeCount: 50, // Weniger Knoten
nodeSize: 1.2, // Kleinere Knoten
connectionDistance: 150, // Reduzierte Verbindungsdistanz
connectionOpacity: 0.3, // Sanftere Verbindungslinien
clusterCount: 2, // Weniger Cluster
clusterRadius: 380, // Größerer Cluster-Radius für mehr Verteilung
animationSpeed: 0.25, // Langsamere Animation
flowDensity: 0.05, // Deutlich weniger Flussanimationen
flowSpeed: 0.04, // Langsamerer Fluss
flowLength: 0.04, // Kürzere Flussstreifen
pulseSpeed: 0.0004, // Sehr sanftes Pulsieren
activationFrequency: 0.4, // Seltenere Aktivierungen
minConnectionsPerNode: 1,
maxConnectionsPerNode: 3, // Weniger maximale Verbindungen
lineUnderFlow: true, // Strichlinie unter Fluss anzeigen
lineUnderFlowOpacity: 0.2, // Opazität der Strichlinie
lineUnderFlowWidth: 1.0, // Breite der Strichlinie
sequentialFlows: true, // Flüsse nacheinander anzeigen
maxSimultaneousFlows: 3 // Maximale Anzahl gleichzeitiger Flüsse
};
// Benutzerkonfiguration übernehmen (falls vorhanden)
this.config = { ...this.config, ...options.config };
// Dunkler Modus mit subtileren Farben
this.darkModeColors = {
background: '#050a12', // Dunklerer Hintergrund
nodeColor: '#5a79b7', // Sanfteres Blau
connectionColor: '#2a3c68', // Gedämpfteres Blau
flowColor: '#4d6ea8', // Sanfteres Flussblau
lineUnderFlowColor: '#223a5c', // Dunkleres Blau für Strichlinie
nodePulse: { r: 120, g: 150, b: 200 } // Sanftere Pulse-Farbe
};
// Heller Modus mit subtileren Farben
this.lightModeColors = {
background: '#f9fbff', // Hellerer Hintergrund
nodeColor: '#b9d0f8', // Helleres, sanfteres Blau
connectionColor: '#e0f0ff', // Sehr helles Blau für subtilere Verbindungen
flowColor: '#92b2e6', // Sanftes mittleres Blau
lineUnderFlowColor: '#c7d8f2', // Helles Blau für Strichlinie
nodePulse: { r: 110, g: 140, b: 200 } // Sanftere Pulse-Farbe
};
// Erweiterte Konfiguration
this.nodes = [];
this.connections = [];
this.flows = [];
this.isDarkMode = options.isDarkMode !== undefined ? options.isDarkMode : true;
this.useWebGL = options.useWebGL !== undefined ? options.useWebGL : true;
this.frameRate = options.frameRate || 30; // Reduzierte Framerate für bessere Performance
this.lastFrameTime = 0;
this.frameInterval = 1000 / this.frameRate;
this.lastFlowTime = 0; // Zeit des letzten Fluss-Starts
this.flowInterval = 2000; // Mindestzeit zwischen neuen Flüssen (ms)
// Ereignisbehandlung für Fenstergröße und Theme-Wechsel
this.resizeTimeout = null;
window.addEventListener('resize', this.onResize.bind(this));
document.addEventListener('theme-changed', (e) => {
this.setDarkMode(e.detail.isDarkMode);
});
// Initialisierung
this.webglSetupComplete = false;
if (this.useWebGL && this.setupWebGL()) {
this.webglSetupComplete = true;
} else {
this.ctx = this.canvas.getContext('2d');
}
// 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);
}
// Animation properties
this.animationFrameId = null;
// Initialize
this.init();
// Event listeners
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
const clusterCount = Math.floor(5 + Math.random() * 4); // 5-8 Cluster
const clusters = [];
for (let i = 0; i < clusterCount; i++) {
clusters.push({
x: Math.random() * width,
y: Math.random() * height,
radius: 100 + Math.random() * 150
});
}
// 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 größer
const nodeImportance = useCluster ? 1.2 : 0.8;
const size = this.config.nodeSize * nodeImportance + Math.random() * this.config.nodeVariation;
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);
if (distance < this.config.connectionDistance) {
potentialConnections.push({
index: j,
distance: distance
});
}
}
// Sortiere nach Entfernung
potentialConnections.sort((a, b) => a.distance - b.distance);
// Wähle die nächsten N Verbindungen, maximal maxConnections
const maxConn = Math.min(
this.config.maxConnections,
potentialConnections.length,
1 + Math.floor(Math.random() * this.config.maxConnections)
);
for (let c = 0; c < maxConn; c++) {
const connection = potentialConnections[c];
const j = connection.index;
const nodeB = this.nodes[j];
const distance = connection.distance;
// Create weighted connection (closer = stronger)
const connectionStrength = Math.max(0, 1 - distance / this.config.connectionDistance);
const connOpacity = connectionStrength * this.config.connectionOpacity;
// Check if connection already exists
if (!this.connections.some(conn =>
(conn.from === i && conn.to === j) || (conn.from === j && conn.to === i)
)) {
// Neue Verbindung startet mit progress=0 für animierten Aufbau
this.connections.push({
from: i,
to: j,
distance: distance,
opacity: connOpacity,
strength: connectionStrength,
hasFlow: false,
lastActivated: 0,
progress: 0 // Animationsfortschritt für Verbindungsaufbau
});
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();
// Simulate neural firing
for (let i = 0; i < this.nodes.length; i++) {
const node = this.nodes[i];
// Check if node should fire based on its firing rate
if (now - node.lastFired > node.firingRate) {
node.isActive = true;
node.lastFired = now;
// Activate connected nodes with probability based on connection strength
for (const connIndex of node.connections) {
// Find the connection
const conn = this.connections.find(c =>
(c.from === i && c.to === connIndex) || (c.from === connIndex && c.to === i)
);
if (conn) {
// Mark connection as recently activated
conn.lastActivated = now;
// Create a flow along this connection
if (Math.random() < conn.strength * 0.8) {
this.flows.push({
connection: conn,
progress: 0,
direction: conn.from === i, // Flow from activated node
length: this.config.flowLength + Math.random() * 0.1,
intensity: 0.7 + Math.random() * 0.3 // Random intensity for variation
});
}
// Probability for connected node to activate
if (Math.random() < conn.strength * 0.5) {
this.nodes[connIndex].isActive = true;
this.nodes[connIndex].lastFired = now - Math.random() * 500; // Slight variation
}
}
}
} else if (now - node.lastFired > 300) { // Deactivate after short period
node.isActive = false;
}
}
// Animierter Verbindungsaufbau: progress inkrementieren
for (const connection of this.connections) {
if (connection.progress < 1) {
// Langsamer Aufbau: Geschwindigkeit kann angepasst werden
connection.progress += 0.012; // Sehr langsam, für subtilen Effekt
if (connection.progress > 1) connection.progress = 1;
}
}
// Update flows
this.updateFlows();
// Occasionally create new flows along connections
if (Math.random() < this.config.flowDensity) {
this.createNewFlow();
}
// Recalculate connections occasionally for a living network
if (Math.random() < 0.01) { // Only recalculate 1% of the time for performance
this.createConnections();
}
// Render
if (this.useWebGL) {
this.renderWebGL();
} else {
this.renderCanvas();
}
// Continue animation
this.animationFrameId = requestAnimationFrame(this.animate.bind(this));
}
// New method to update flow animations
updateFlows() {
// Update existing flows
const now = Date.now();
for (let i = this.flows.length - 1; i >= 0; i--) {
const flow = this.flows[i];
// Update flow progress
flow.progress += this.config.flowSpeed / flow.connection.distance;
// Update line progress (langsamer als der Blitz)
flow.lineProgress = Math.min(flow.progress * 1.5, 1.0);
// Remove completed flows
if (flow.progress > 1.0) {
this.flows.splice(i, 1);
}
}
// Generate new flows selten und kontrolliert
if (Math.random() < this.config.flowDensity &&
(!this.config.sequentialFlows ||
now - this.lastFlowTime >= this.flowInterval)) {
this.createNewFlow();
}
}
// New method to create flow animations
createNewFlow() {
if (this.connections.length === 0) return;
// Überprüfen, ob maximale Anzahl gleichzeitiger Flüsse erreicht ist
if (this.config.sequentialFlows &&
this.flows.length >= this.config.maxSimultaneousFlows) {
return;
}
// Überprüfen, ob genügend Zeit seit dem letzten Fluss vergangen ist
const now = Date.now();
if (this.config.sequentialFlows &&
now - this.lastFlowTime < this.flowInterval) {
return;
}
// Zeit des letzten Flusses aktualisieren
this.lastFlowTime = now;
// 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];
// 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
lineProgress: 0 // Fortschritt der unterliegenden Strichlinie (startet bei 0)
});
}
renderWebGL() {
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();
// Draw flows on top of connections
this.renderFlowsWebGL();
// Draw nodes
this.renderNodesWebGL();
}
renderNodesWebGL() {
// Prepare node positions for WebGL
const positions = new Float32Array(this.nodes.length * 2);
const sizes = new Float32Array(this.nodes.length);
const now = Date.now();
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;
// Maximales Pulsieren und sehr große Knoten
let pulse = Math.sin(node.pulsePhase) * 0.38 + 1.35;
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 - more visible with active highlighting
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 brighter color
if (node.isActive) {
r = (r + nodePulseColor.r / 255) / 2;
g = (g + nodePulseColor.g / 255) / 2;
b = (b + nodePulseColor.b / 255) / 2;
}
// Sehr sichtbare, maximal leuchtende Knoten
this.gl.uniform4f(
this.programInfo.uniformLocations.color,
r, g, b,
node.isActive ? 1.0 : 0.92 // Maximal sichtbar
);
// Draw each node individually for better control
this.gl.drawArrays(this.gl.POINTS, i, 1);
}
}
renderConnectionsWebGL() {
// Permanente Verbindungsstriche werden nicht mehr gezeichnet
// Diese Methode bleibt leer, damit keine Linien mehr erscheinen
}
// New method to render the flowing animations
renderFlowsWebGL() {
// Für jeden Flow zuerst die unterliegende Strichlinie zeichnen, dann den Blitz
for (const flow of this.flows) {
const connection = flow.connection;
const fromNode = this.nodes[connection.from];
const toNode = this.nodes[connection.to];
const direction = flow.direction ? 1 : -1;
// 1. Die unterliegende Strichlinie zeichnen
if (this.config.lineUnderFlow) {
const lineStart = direction > 0 ? fromNode : toNode;
const lineEnd = direction > 0 ? toNode : fromNode;
// Strichlinie als gerade Linie mit voller Länge, aber mit Fortschritt
const lineEndPoint = {
x: lineStart.x + (lineEnd.x - lineStart.x) * flow.lineProgress,
y: lineStart.y + (lineEnd.y - lineStart.y) * flow.lineProgress
};
const positions = new Float32Array([
lineStart.x, lineStart.y,
lineEndPoint.x, lineEndPoint.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);
// Sanftere, transparentere Strichlinie
const colorObj = this.isDarkMode ? this.darkModeColors : this.lightModeColors;
const lineColor = this.hexToRgb(colorObj.lineUnderFlowColor || colorObj.connectionColor);
this.gl.uniform4f(
this.programInfo.uniformLocations.color,
lineColor.r / 255,
lineColor.g / 255,
lineColor.b / 255,
this.config.lineUnderFlowOpacity // Sehr subtile Linie
);
this.gl.enable(this.gl.BLEND);
this.gl.blendFunc(this.gl.SRC_ALPHA, this.gl.ONE_MINUS_SRC_ALPHA); // Normales Alpha-Blending
this.gl.lineWidth(this.config.lineUnderFlowWidth); // Dünne Linie
this.gl.drawArrays(this.gl.LINES, 0, 2);
}
// 2. Den Blitz selbst zeichnen
const startProgress = flow.progress;
const endProgress = Math.min(1, startProgress + flow.length);
if (startProgress >= 1 || endProgress <= 0) continue;
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 (weniger ausgeprägt)
const zigzag = this.generateZigZagPoints(p1, p2, 3, 5); // Weniger Segmente, kleinere 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, subtilerer Blitz
const colorObj = this.isDarkMode ? this.darkModeColors : this.lightModeColors;
const flowColor = this.hexToRgb(colorObj.flowColor);
this.gl.uniform4f(
this.programInfo.uniformLocations.color,
flowColor.r / 255,
flowColor.g / 255,
flowColor.b / 255,
0.45 // Geringere Sichtbarkeit
);
this.gl.enable(this.gl.BLEND);
this.gl.blendFunc(this.gl.SRC_ALPHA, this.gl.ONE);
this.gl.lineWidth(1.0); // Dünnerer Blitz für subtileren Effekt
this.gl.drawArrays(this.gl.LINES, 0, 2);
}
// Funken erzeugen - weniger und subtilere elektrische Funken
const sparks = this.generateSparkPoints(zigzag, 2 + Math.floor(Math.random() * 2)); // Weniger Funken
for (const spark of sparks) {
// Sanfteres Weiß-Blau für subtilere elektrische Funken
this.gl.uniform4f(
this.programInfo.uniformLocations.color,
0.85, 0.9, 0.95, 0.4 * spark.intensity // Geringere Intensitä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);
// Punktgröße setzen
const sizes = new Float32Array([spark.size * 2.0]); // Kleinere Funken
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() {
if (!this.ctx) return;
const { background, nodeColor, connectionColor } = this.isDarkMode ? this.darkModeColors : this.lightModeColors;
// Hintergrund sanft löschen mit leichter Transparenz für Nachleuchten
this.ctx.fillStyle = background;
this.ctx.globalAlpha = 0.22; // Sehr subtile Überblendung für sanfte Bewegung
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
this.ctx.globalAlpha = 1.0;
// Verbindungen zeichnen mit distanzabhängiger Transparenz
this.connections.forEach(connection => {
const { source, target, strength } = connection;
const dx = target.x - source.x;
const dy = target.y - source.y;
const distance = Math.sqrt(dx * dx + dy * dy);
// Berechne Opazität basierend auf Distanz - je weiter entfernt, desto transparenter
const maxOpacity = 0.02; // Sehr subtile maximale Opazität
const opacityFactor = 1 - (distance / this.config.connectionDistance);
let opacity = maxOpacity * opacityFactor * strength * this.config.connectionOpacity;
// Subtilere Linien
this.ctx.beginPath();
this.ctx.moveTo(source.x, source.y);
this.ctx.lineTo(target.x, target.y);
this.ctx.lineWidth = 0.4; // Sehr dünne Linie
this.ctx.strokeStyle = connectionColor;
this.ctx.globalAlpha = opacity;
this.ctx.stroke();
this.ctx.globalAlpha = 1.0;
});
// Knoten zeichnen mit subtilerem Glühen und Pulsieren
this.nodes.forEach(node => {
const nodeBaseSize = node.size * this.config.nodeSize;
const pulse = Math.sin(this.time * this.config.pulseSpeed * node.pulseRate) * 0.1 + 1; // Reduzierte Pulsamplitude
const glowSize = nodeBaseSize * 1.5; // Kleinerer Glowradius
// Aktive Knoten speziell hervorheben, aber subtiler
if (node.active) {
// Subtiles Glühen für aktive Knoten
const gradient = this.ctx.createRadialGradient(
node.x, node.y, 0,
node.x, node.y, glowSize * 2
);
this.ctx.globalAlpha = 0.09; // Sehr niedrige Opazität für subtiles Glühen
gradient.addColorStop(0, 'rgba(255, 255, 255, 0.15)');
gradient.addColorStop(1, 'rgba(255, 255, 255, 0)');
this.ctx.fillStyle = gradient;
this.ctx.beginPath();
this.ctx.arc(node.x, node.y, glowSize * 2, 0, Math.PI * 2);
this.ctx.fill();
// Zeichne aktiven Knoten
this.ctx.globalAlpha = 0.9;
this.ctx.fillStyle = nodeColor;
this.ctx.beginPath();
this.ctx.arc(node.x, node.y, nodeBaseSize * pulse, 0, Math.PI * 2);
this.ctx.fill();
} else {
// Inaktive Knoten sanfter darstellen
this.ctx.globalAlpha = 0.8;
this.ctx.fillStyle = nodeColor;
this.ctx.beginPath();
this.ctx.arc(node.x, node.y, nodeBaseSize * pulse * 0.8, 0, Math.PI * 2);
this.ctx.fill();
}
this.ctx.globalAlpha = 1.0;
});
}
renderFlowsCanvas() {
if (!this.ctx) return;
const { flowColor } = this.isDarkMode ? this.darkModeColors : this.lightModeColors;
this.flows.forEach(flow => {
if (flow.progress > 1) return;
const { source, target, progress } = flow;
// Berechne aktuelle Position
const dx = target.x - source.x;
const dy = target.y - source.y;
const currentX = source.x + dx * progress;
const currentY = source.y + dy * progress;
// Zick-Zack-Effekt reduzieren für sanftere Bewegung
const segmentCount = 4; // Weniger Segmente
const amplitude = 2; // Geringere Amplitude
const waveLength = 0.2;
let prevX = source.x;
let prevY = source.y;
this.ctx.beginPath();
this.ctx.moveTo(prevX, prevY);
// Sanftere "Elektrizitäts"-Linie
for (let i = 0; i <= segmentCount; i++) {
const segmentProgress = i / segmentCount;
if (segmentProgress > progress) break;
const segX = source.x + dx * segmentProgress;
const segY = source.y + dy * segmentProgress;
// Sanftere Wellen
const perpX = -dy * Math.sin(segmentProgress * Math.PI * 10) * amplitude;
const perpY = dx * Math.sin(segmentProgress * Math.PI * 10) * amplitude;
const pointX = segX + perpX * Math.sin(this.time * 0.01);
const pointY = segY + perpY * Math.sin(this.time * 0.01);
this.ctx.lineTo(pointX, pointY);
prevX = pointX;
prevY = pointY;
}
// Linienausrichtung und -stil
this.ctx.lineCap = 'round';
this.ctx.lineWidth = 1.2; // Dünnere Linie
// Subtiler Gloweffekt
this.ctx.shadowColor = flowColor;
this.ctx.shadowBlur = 6; // Reduzierter Unschärferadius
this.ctx.strokeStyle = flowColor;
this.ctx.globalAlpha = 0.7 - 0.5 * Math.abs(progress - 0.5); // Sanfteres Ein- und Ausblenden
this.ctx.stroke();
this.ctx.globalAlpha = 1;
this.ctx.shadowBlur = 0;
// Funkeneffekte am Ende der Linie, aber weniger und kleiner
if (progress > 0.9 && Math.random() < 0.3) { // Weniger Funken generieren
const sparkCount = Math.floor(Math.random() * 2) + 1; // Maximal 3 Funken
for (let i = 0; i < sparkCount; i++) {
const sparkSize = Math.random() * 0.8 + 0.4; // Kleinere Funken
const sparkAngle = Math.random() * Math.PI * 2;
const sparkDistance = Math.random() * 5 + 2;
const sparkX = currentX + Math.cos(sparkAngle) * sparkDistance;
const sparkY = currentY + Math.sin(sparkAngle) * sparkDistance;
// Weniger intensive Funken
this.ctx.beginPath();
this.ctx.arc(sparkX, sparkY, sparkSize, 0, Math.PI * 2);
this.ctx.fillStyle = flowColor;
this.ctx.globalAlpha = 0.4; // Reduzierte Opazität
this.ctx.fill();
this.ctx.globalAlpha = 1;
}
}
});
}
// 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() {
if (this.animationFrameId) {
cancelAnimationFrame(this.animationFrameId);
}
window.removeEventListener('resize', this.resizeCanvas.bind(this));
if (this.canvas && this.canvas.parentNode) {
this.canvas.parentNode.removeChild(this.canvas);
}
if (this.gl) {
// Clean up WebGL resources
this.gl.deleteBuffer(this.positionBuffer);
this.gl.deleteBuffer(this.sizeBuffer);
this.gl.deleteProgram(this.shaderProgram);
}
}
// Hilfsfunktion: Erzeuge Zickzack-Punkte für einen Blitz
generateZigZagPoints(start, end, segments = 5, amplitude = 8) {
const points = [start];
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;
// Versatz für Zickzack
const angle = Math.atan2(end.y - start.y, end.x - start.x) + Math.PI / 2;
const offset = (Math.random() - 0.5) * amplitude * (i % 2 === 0 ? 1 : -1);
points.push({
x: x + Math.cos(angle) * offset,
y: y + Math.sin(angle) * offset
});
}
points.push(end);
return points;
}
// Hilfsfunktion: Erzeuge Funkenpunkte entlang eines Zickzack-Blitzes
generateSparkPoints(zigzag, sparkCount = 4) {
const sparks = [];
// Moderatere Anzahl an Funken für subtileren Effekt
const actualSparkCount = sparkCount + Math.floor(Math.random() * 2);
for (let i = 0; i < actualSparkCount; i++) {
// Entlang des gesamten Blitzes verteilen
const seg = i * (zigzag.length - 1) / actualSparkCount;
const segIndex = Math.floor(seg);
const t = seg - segIndex;
// Basis-Position
const x = zigzag[segIndex].x + (zigzag[segIndex + 1].x - zigzag[segIndex].x) * t;
const y = zigzag[segIndex].y + (zigzag[segIndex + 1].y - zigzag[segIndex].y) * t;
// Sanfterer Versatz vom Blitz
const angle = Math.random() * Math.PI * 2;
const distance = 2 + Math.random() * 4; // Reduzierter Abstand für subtileres Sprühen
sparks.push({
x: x + Math.cos(angle) * distance,
y: y + Math.sin(angle) * distance,
size: 0.5 + Math.random() * 1 // Kleinere Funken
});
}
return sparks;
}
onResize() {
// Verhindere mehrfache Resize-Events in kurzer Zeit
if (this.resizeTimeout) clearTimeout(this.resizeTimeout);
this.resizeTimeout = setTimeout(() => {
this.resizeCanvas();
// Knoten und Verbindungen neu erstellen, um sie an die neue Größe anzupassen
this.createNodes();
this.createConnections();
}, 200); // Warte 200ms, bevor die Größenanpassung durchgeführt wird
}
setDarkMode(isDarkMode) {
this.isDarkMode = isDarkMode;
// WebGL-Hintergrundfarbe aktualisieren
if (this.useWebGL && this.gl) {
const bgColor = this.hexToRgb(
this.isDarkMode ? this.darkModeColors.background : this.lightModeColors.background
);
this.gl.clearColor(bgColor.r/255, bgColor.g/255, bgColor.b/255, 1.0);
}
}
}
// 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();
}
});
// Clean up when window is closed
window.addEventListener('beforeunload', () => {
if (window.neuralNetworkBackground) {
window.neuralNetworkBackground.destroy();
}
});