diff --git a/database/systades.db b/database/systades.db index 286c46f..129ea8d 100644 Binary files a/database/systades.db and b/database/systades.db differ diff --git a/static/css/neural-network-background.css b/static/css/neural-network-background.css index f5c6b76..990d5d6 100644 --- a/static/css/neural-network-background.css +++ b/static/css/neural-network-background.css @@ -103,4 +103,115 @@ body.dark footer { body:not(.dark) footer { background-color: rgba(249, 250, 251, 0.92) !important; border-top: 1px solid rgba(220, 220, 220, 0.8) !important; +} + +/* Neural Network Background für Dark & Light Mode */ +.neural-network-container { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: -1; + pointer-events: none; + overflow: hidden; + opacity: 0.8; + transition: opacity 0.5s ease; +} + +.neural-network-bg { + width: 100%; + height: 100%; +} + +/* Dark Mode Netzwerk-Hintergrund */ +.dark .neural-network-container { + opacity: 0.5; +} + +.dark .neural-network-bg .node { + fill: rgba(139, 92, 246, 0.4); + transition: fill 0.3s ease; +} + +.dark .neural-network-bg .link { + stroke: rgba(139, 92, 246, 0.15); + transition: stroke 0.3s ease; +} + +/* Light Mode Netzwerk-Hintergrund */ +body:not(.dark) .neural-network-container { + opacity: 0.25; +} + +body:not(.dark) .neural-network-bg .node { + fill: rgba(124, 58, 237, 0.35); + filter: drop-shadow(0 0 3px rgba(124, 58, 237, 0.2)); + transition: all 0.3s ease; +} + +body:not(.dark) .neural-network-bg .node:hover { + fill: rgba(124, 58, 237, 0.5); + filter: drop-shadow(0 0 5px rgba(124, 58, 237, 0.3)); +} + +body:not(.dark) .neural-network-bg .link { + stroke: rgba(124, 58, 237, 0.15); + stroke-width: 1.25; + filter: drop-shadow(0 0 1px rgba(124, 58, 237, 0.1)); + transition: all 0.3s ease; +} + +body:not(.dark) .neural-network-bg .link:hover { + stroke: rgba(124, 58, 237, 0.3); + stroke-width: 1.5; +} + +/* Animationen für das Netzwerk */ +@keyframes pulse { + 0% { opacity: 0.7; } + 50% { opacity: 1; } + 100% { opacity: 0.7; } +} + +@keyframes float { + 0% { transform: translateY(0); } + 50% { transform: translateY(-5px); } + 100% { transform: translateY(0); } +} + +.neural-network-bg .node { + animation: pulse 3s infinite ease-in-out; +} + +.neural-network-bg .link { + animation: pulse 4s infinite ease-in-out; +} + +/* Speziell für den Light Mode leicht pulsierende Knoten */ +body:not(.dark) .neural-network-bg .node { + animation: pulse 4s infinite ease-in-out, float 6s infinite ease-in-out; + animation-delay: calc(var(--node-index, 0) * 0.2s); +} + +/* Responsives Verhalten */ +@media (max-width: 768px) { + .neural-network-container { + opacity: 0.2; /* Auf mobilen Geräten etwas dezenter */ + } + + body:not(.dark) .neural-network-bg .link { + stroke-width: 1; + } +} + +/* Hover-Effekte für Desktop-Geräte */ +@media (min-width: 1024px) { + body:not(.dark) .neural-network-container:hover { + opacity: 0.35; + } + + .dark .neural-network-container:hover { + opacity: 0.6; + } } \ No newline at end of file diff --git a/static/neural-network-background.js b/static/neural-network-background.js index 01e7b09..ec40938 100644 --- a/static/neural-network-background.js +++ b/static/neural-network-background.js @@ -51,11 +51,11 @@ class NeuralNetworkBackground { flowColor: '#b47fea' }, light: { - background: '#f8f9fc', - nodeColor: '#8c6db5', - nodePulse: '#b094dd', - connectionColor: '#9882bd', - flowColor: '#7d5bb5' + background: '#f9fafb', + nodeColor: '#8b5cf6', + nodePulse: '#7c3aed', + connectionColor: '#c4b5fd', + flowColor: '#6d28d9' } }; @@ -252,122 +252,178 @@ class NeuralNetworkBackground { if (progress >= 1) { // Flow beenden connection.active = false; + connection.flowProgress = 0; this.activeConnections.delete(connectionId); } else { connection.flowProgress = progress; } } - // Neue Flows starten, wenn unter dem Limit - if (this.activeConnections.size < this.config.flowDensity) { - // Wähle eine zufällige Verbindung - const availableConnections = this.connections.filter(c => !c.active); + // 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]; - if (availableConnections.length > 0) { - const randomIndex = Math.floor(Math.random() * availableConnections.length); - const connection = availableConnections[randomIndex]; + // 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); - // Aktiviere die Verbindung - 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]); + 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; - this.activeConnections.add(connection.id); + 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) { - // Aktualisiere Farben basierend auf aktuellem Theme + // 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; - const colors = this.currentColors; - const width = this.canvas.width / (window.devicePixelRatio || 1); - const height = this.canvas.height / (window.devicePixelRatio || 1); - - // Hintergrund löschen - this.ctx.fillStyle = colors.background; - this.ctx.fillRect(0, 0, width, height); - - // Verbindungen zeichnen (statisch) - this.ctx.strokeStyle = colors.connectionColor; - this.ctx.lineWidth = 1.2; - - for (const connection of this.connections) { - const fromNode = this.nodes[connection.from]; - const toNode = this.nodes[connection.to]; - this.ctx.globalAlpha = connection.opacity * 0.5; - - this.ctx.beginPath(); - this.ctx.moveTo(fromNode.x, fromNode.y); - this.ctx.lineTo(toNode.x, toNode.y); - this.ctx.stroke(); + // 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'; } - // Aktive Verbindungen zeichnen (Flows) - this.ctx.strokeStyle = colors.flowColor; - this.ctx.lineWidth = 2.5; - - for (const connectionId of this.activeConnections) { - const connection = this.connections.find(c => c.id === connectionId); - if (!connection) continue; + // Verbindungen zeichnen + for (let i = 0; i < this.connections.length; i++) { + const connection = this.connections[i]; - const fromNode = this.nodes[connection.from]; - const toNode = this.nodes[connection.to]; + if (connection.opacity <= 0) continue; - // Glühen-Effekt - this.ctx.globalAlpha = Math.sin(connection.flowProgress * Math.PI) * 0.8; + const nodeA = this.nodes[connection.from]; + const nodeB = this.nodes[connection.to]; - // Linie zeichnen + this.ctx.strokeStyle = this.currentColors.connectionColor; + this.ctx.globalAlpha = connection.opacity; this.ctx.beginPath(); - this.ctx.moveTo(fromNode.x, fromNode.y); - this.ctx.lineTo(toNode.x, toNode.y); + this.ctx.moveTo(nodeA.x, nodeA.y); + this.ctx.lineTo(nodeB.x, nodeB.y); this.ctx.stroke(); - // Fließendes Partikel - const progress = connection.flowProgress; - const x = fromNode.x + (toNode.x - fromNode.x) * progress; - const y = fromNode.y + (toNode.y - fromNode.y) * progress; - - this.ctx.globalAlpha = 0.9; - this.ctx.fillStyle = colors.flowColor; - - this.ctx.beginPath(); - this.ctx.arc(x, y, 2, 0, Math.PI * 2); - this.ctx.fill(); - } - - // Knoten zeichnen - for (const node of this.nodes) { - // Pulsierende Knoten - const timeSinceLastPulse = now - node.lastPulse; - const isPulsing = timeSinceLastPulse < 800; - const pulseProgress = isPulsing ? timeSinceLastPulse / 800 : 0; - - // Knoten selbst - this.ctx.globalAlpha = 1; - this.ctx.fillStyle = isPulsing - ? colors.nodePulse - : colors.nodeColor; - - this.ctx.beginPath(); - this.ctx.arc(node.x, node.y, node.size + (isPulsing ? 1 * Math.sin(pulseProgress * Math.PI) : 0), 0, Math.PI * 2); - this.ctx.fill(); - - // Wenn pulsierend, füge einen Glow-Effekt hinzu - if (isPulsing) { - this.ctx.globalAlpha = 0.5 * (1 - pulseProgress); + // 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(node.x, node.y, node.size + 5 * pulseProgress, 0, Math.PI * 2); + 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() { @@ -391,6 +447,9 @@ class NeuralNetworkBackground { } 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), @@ -398,6 +457,11 @@ class NeuralNetworkBackground { 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 diff --git a/templates/base.html b/templates/base.html index eeaa1cc..c924b0b 100644 --- a/templates/base.html +++ b/templates/base.html @@ -397,12 +397,52 @@