479 lines
15 KiB
JavaScript
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;
|
|
}
|
|
});
|
|
});
|