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

479 lines
15 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
if (document.body.firstChild) {
document.body.insertBefore(this.canvas, document.body.firstChild);
} else {
document.body.appendChild(this.canvas);
}
// 2D Context
this.ctx = this.canvas.getContext('2d');
// Eigenschaften
this.nodes = [];
this.connections = [];
this.activeConnections = new Set();
this.animationFrameId = null;
this.isDestroying = false;
// Farben für Dark/Light Mode
this.colors = {
dark: {
background: '#040215',
nodeColor: '#6a5498',
nodePulse: '#9c7fe0',
connectionColor: '#4a3870',
flowColor: '#b47fea'
},
light: {
background: '#f9fafb',
nodeColor: '#8b5cf6',
nodePulse: '#7c3aed',
connectionColor: '#c4b5fd',
flowColor: '#6d28d9'
}
};
// Aktuelle Farbpalette basierend auf Theme
this.currentColors = document.documentElement.classList.contains('dark')
? this.colors.dark
: this.colors.light;
// Konfiguration
this.config = {
nodeCount: 80, // Anzahl der Knoten
nodeSize: 2.5, // Größe der Knoten
connectionDistance: 150, // Maximale Verbindungsdistanz
connectionOpacity: 0.5, // Erhöht von 0.3 auf 0.5 - Deckkraft der ständigen Verbindungen
animationSpeed: 0.15, // Geschwindigkeit der Animation
flowDensity: 2, // Anzahl aktiver Verbindungen
maxFlowsPerNode: 2, // Maximale Anzahl aktiver Verbindungen pro Knoten
flowDuration: [2000, 5000], // Min/Max Dauer des Flows in ms
nodePulseFrequency: 0.01 // Wie oft Knoten pulsieren
};
// Initialisieren
this.init();
// Event-Listener
window.addEventListener('resize', this.resizeCanvas.bind(this));
console.log('Vereinfachter Neural Network Background initialized');
}
init() {
this.resizeCanvas();
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.ctx) {
this.ctx.scale(pixelRatio, pixelRatio);
}
// Neuberechnung der Knotenpositionen nach Größenänderung
if (this.nodes.length) {
this.createNodes();
this.createConnections();
}
}
createNodes() {
this.nodes = [];
const width = this.canvas.width / (window.devicePixelRatio || 1);
const height = this.canvas.height / (window.devicePixelRatio || 1);
// Cluster-Zentren für realistisches neuronales Netzwerk
const clusterCount = Math.floor(6 + Math.random() * 4);
const clusters = [];
for (let i = 0; i < clusterCount; i++) {
clusters.push({
x: Math.random() * width,
y: Math.random() * height,
radius: 100 + Math.random() * 150
});
}
// Knoten erstellen
for (let i = 0; i < this.config.nodeCount; i++) {
// Wähle zufällig ein Cluster
const cluster = clusters[Math.floor(Math.random() * clusters.length)];
// Erstelle einen Knoten innerhalb des Clusters mit zufälligem Offset
const angle = Math.random() * Math.PI * 2;
const distance = Math.random() * cluster.radius;
const node = {
id: i,
x: cluster.x + Math.cos(angle) * distance,
y: cluster.y + Math.sin(angle) * distance,
size: this.config.nodeSize * (0.8 + Math.random() * 0.4),
speed: {
x: (Math.random() - 0.5) * 0.2,
y: (Math.random() - 0.5) * 0.2
},
lastPulse: 0,
pulseInterval: 5000 + Math.random() * 10000, // Zufälliges Pulsieren
connections: []
};
this.nodes.push(node);
}
}
createConnections() {
this.connections = [];
// Verbindungen zwischen Knoten erstellen
for (let i = 0; i < this.nodes.length; i++) {
const nodeA = this.nodes[i];
for (let j = i + 1; j < this.nodes.length; j++) {
const nodeB = this.nodes[j];
const dx = nodeA.x - nodeB.x;
const dy = nodeA.y - nodeB.y;
const distance = Math.sqrt(dx * dx + dy * dy);
if (distance < this.config.connectionDistance) {
const connection = {
id: `${i}-${j}`,
from: i,
to: j,
distance: distance,
opacity: Math.max(0.05, 1 - (distance / this.config.connectionDistance)),
active: false,
flowProgress: 0,
flowDuration: 0,
flowStart: 0
};
this.connections.push(connection);
nodeA.connections.push(connection);
nodeB.connections.push(connection);
}
}
}
}
startAnimation() {
this.animate();
}
animate() {
this.animationFrameId = requestAnimationFrame(this.animate.bind(this));
const now = Date.now();
this.updateNodes(now);
this.updateConnections(now);
this.render(now);
}
updateNodes(now) {
const width = this.canvas.width / (window.devicePixelRatio || 1);
const height = this.canvas.height / (window.devicePixelRatio || 1);
// Knoten bewegen
for (let i = 0; i < this.nodes.length; i++) {
const node = this.nodes[i];
node.x += node.speed.x;
node.y += node.speed.y;
// Begrenzung am Rand
if (node.x < 0 || node.x > width) {
node.speed.x *= -1;
}
if (node.y < 0 || node.y > height) {
node.speed.y *= -1;
}
// Zufällig Richtung ändern
if (Math.random() < 0.01) {
node.speed.x = (Math.random() - 0.5) * 0.2;
node.speed.y = (Math.random() - 0.5) * 0.2;
}
// Zufälliges Pulsieren
if (Math.random() < this.config.nodePulseFrequency && now - node.lastPulse > node.pulseInterval) {
node.lastPulse = now;
}
}
}
updateConnections(now) {
// Update aktive Verbindungen
for (const connectionId of this.activeConnections) {
const connection = this.connections.find(c => c.id === connectionId);
if (!connection) continue;
// Aktualisiere den Flow-Fortschritt
const elapsed = now - connection.flowStart;
const progress = elapsed / connection.flowDuration;
if (progress >= 1) {
// Flow beenden
connection.active = false;
connection.flowProgress = 0;
this.activeConnections.delete(connectionId);
} else {
connection.flowProgress = progress;
}
}
// Neue aktive Verbindungen starten
if (this.activeConnections.size < this.config.flowDensity && Math.random() < 0.05) {
// Zufälligen Knoten auswählen
const nodeIndex = Math.floor(Math.random() * this.nodes.length);
const node = this.nodes[nodeIndex];
// Anzahl der aktiven Verbindungen für diesen Knoten zählen
const activeConnectionsCount = Array.from(this.activeConnections)
.filter(id => {
const [from, to] = id.split('-').map(Number);
return from === nodeIndex || to === nodeIndex;
}).length;
// Nur neue Verbindung aktivieren, wenn Knoten noch nicht zu viele aktive hat
if (activeConnectionsCount < this.config.maxFlowsPerNode) {
// Verfügbare Verbindungen für diesen Knoten finden
const availableConnections = node.connections.filter(conn => !conn.active);
if (availableConnections.length > 0) {
// Zufällige Verbindung auswählen
const connection = availableConnections[Math.floor(Math.random() * availableConnections.length)];
// Verbindung aktivieren
connection.active = true;
connection.flowProgress = 0;
connection.flowStart = now;
connection.flowDuration = this.config.flowDuration[0] +
Math.random() * (this.config.flowDuration[1] - this.config.flowDuration[0]);
this.activeConnections.add(connection.id);
}
}
}
// Verbindungsdistanzen neu berechnen
for (let i = 0; i < this.connections.length; i++) {
const connection = this.connections[i];
const nodeA = this.nodes[connection.from];
const nodeB = this.nodes[connection.to];
const dx = nodeA.x - nodeB.x;
const dy = nodeA.y - nodeB.y;
const distance = Math.sqrt(dx * dx + dy * dy);
connection.distance = distance;
// Bei zu großer Distanz Verbindung deaktivieren
if (distance > this.config.connectionDistance) {
connection.opacity = 0;
if (connection.active) {
connection.active = false;
connection.flowProgress = 0;
this.activeConnections.delete(connection.id);
}
} else {
// Verbesserte Berechnung der Opazität für einen natürlicheren Look
const opacityFactor = document.documentElement.classList.contains('dark') ? 1.0 : 0.85;
connection.opacity = Math.max(0.08, (1 - (distance / this.config.connectionDistance)) * this.config.connectionOpacity * opacityFactor);
}
}
}
render(now) {
// Canvas löschen
this.ctx.clearRect(0, 0, this.canvas.width / (window.devicePixelRatio || 1), this.canvas.height / (window.devicePixelRatio || 1));
// Aktualisiere Farbpalette basierend auf aktuellem Theme
this.currentColors = document.documentElement.classList.contains('dark')
? this.colors.dark
: this.colors.light;
// Light Mode mit zusätzlichem Blur-Effekt für weicheres Erscheinungsbild
if (!document.documentElement.classList.contains('dark')) {
this.ctx.filter = 'blur(0.5px)';
} else {
this.ctx.filter = 'none';
}
// Verbindungen zeichnen
for (let i = 0; i < this.connections.length; i++) {
const connection = this.connections[i];
if (connection.opacity <= 0) continue;
const nodeA = this.nodes[connection.from];
const nodeB = this.nodes[connection.to];
this.ctx.strokeStyle = this.currentColors.connectionColor;
this.ctx.globalAlpha = connection.opacity;
this.ctx.beginPath();
this.ctx.moveTo(nodeA.x, nodeA.y);
this.ctx.lineTo(nodeB.x, nodeB.y);
this.ctx.stroke();
// Aktive Verbindungen mit Fluss darstellen
if (connection.active) {
const fromX = nodeA.x;
const fromY = nodeA.y;
const toX = nodeB.x;
const toY = nodeB.y;
// Position des Flusspunkts
const x = fromX + (toX - fromX) * connection.flowProgress;
const y = fromY + (toY - fromY) * connection.flowProgress;
// Fluss-Effekt zeichnen
const pulseSize = document.documentElement.classList.contains('dark') ? 3 : 4;
const pulseOpacity = document.documentElement.classList.contains('dark') ? 0.8 : 0.85;
// Pulse-Effekt
this.ctx.fillStyle = this.currentColors.flowColor;
this.ctx.globalAlpha = pulseOpacity;
this.ctx.beginPath();
this.ctx.arc(x, y, pulseSize, 0, Math.PI * 2);
this.ctx.fill();
// Glow-Effekt
const gradient = this.ctx.createRadialGradient(x, y, 0, x, y, pulseSize * 2);
gradient.addColorStop(0, this.hexToRgba(this.currentColors.flowColor, 0.4));
gradient.addColorStop(1, this.hexToRgba(this.currentColors.flowColor, 0));
this.ctx.fillStyle = gradient;
this.ctx.globalAlpha = 0.8;
this.ctx.beginPath();
this.ctx.arc(x, y, pulseSize * 2, 0, Math.PI * 2);
this.ctx.fill();
}
}
// Knoten zeichnen
for (let i = 0; i < this.nodes.length; i++) {
const node = this.nodes[i];
const isPulsing = now - node.lastPulse < 300;
// Erhöhte Helligkeit für pulsierende Knoten
const nodeColor = isPulsing ? this.currentColors.nodePulse : this.currentColors.nodeColor;
const glowSize = isPulsing ? node.size * 2.5 : node.size * 1.5;
// Glow-Effekt
const gradient = this.ctx.createRadialGradient(
node.x, node.y, 0,
node.x, node.y, glowSize
);
gradient.addColorStop(0, this.hexToRgba(nodeColor, isPulsing ? 0.6 : 0.3));
gradient.addColorStop(1, this.hexToRgba(nodeColor, 0));
this.ctx.fillStyle = gradient;
this.ctx.globalAlpha = document.documentElement.classList.contains('dark') ? 0.7 : 0.5;
this.ctx.beginPath();
this.ctx.arc(node.x, node.y, glowSize, 0, Math.PI * 2);
this.ctx.fill();
// Knoten selbst zeichnen
this.ctx.fillStyle = nodeColor;
this.ctx.globalAlpha = 0.8;
this.ctx.beginPath();
this.ctx.arc(node.x, node.y, node.size, 0, Math.PI * 2);
this.ctx.fill();
}
// Zurücksetzen der Globalwerte
this.ctx.globalAlpha = 1;
this.ctx.filter = 'none';
}
destroy() {
if (this.isDestroying) return;
this.isDestroying = true;
// Animation stoppen
if (this.animationFrameId) {
cancelAnimationFrame(this.animationFrameId);
}
// Canvas ausblenden
this.canvas.style.opacity = '0';
// Nach Übergang entfernen
setTimeout(() => {
if (this.canvas && this.canvas.parentNode) {
this.canvas.parentNode.removeChild(this.canvas);
}
}, 3500);
}
hexToRgb(hex) {
const shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i;
hex = hex.replace(shorthandRegex, (m, r, g, b) => r + r + g + g + b + b);
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result ? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16)
} : null;
}
hexToRgba(hex, alpha) {
const rgb = this.hexToRgb(hex);
return rgb ? `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${alpha})` : `rgba(0, 0, 0, ${alpha})`;
}
}
// Initialisiert den Hintergrund, sobald die Seite geladen ist
document.addEventListener('DOMContentLoaded', () => {
window.neuralBackground = new NeuralNetworkBackground();
// Theme-Wechsel-Event-Listener
document.addEventListener('theme-changed', () => {
if (window.neuralBackground) {
window.neuralBackground.currentColors = document.documentElement.classList.contains('dark')
? window.neuralBackground.colors.dark
: window.neuralBackground.colors.light;
}
});
});