diff --git a/static/js/theme-toggle.js b/static/js/theme-toggle.js new file mode 100644 index 0000000..9c5a1e6 --- /dev/null +++ b/static/js/theme-toggle.js @@ -0,0 +1,191 @@ +/** + * Systades Theme Toggle - Verbessertes Dark/Light Mode Switching + * + * Dieses Skript verwaltet die Theme-Umschaltung zwischen Light und Dark Mode + * mit sanften Übergängen und persistenter Speicherung der Benutzereinstellung. + */ + +document.addEventListener('DOMContentLoaded', () => { + // Initialisiere den Theme-Modus basierend auf gespeicherter Einstellung + initializeTheme(); + + // Führe eine Animation beim Umschalten der Themes aus + setupThemeTransition(); +}); + +/** + * Initialisiere das Theme basierend auf der gespeicherten Benutzereinstellung. + * Wenn keine Einstellung gefunden wird, verwende die Systemeinstellung. + */ +function initializeTheme() { + // Prüfe zuerst die gespeicherte Benutzereinstellung + const storedTheme = localStorage.getItem('darkMode'); + + if (storedTheme) { + // Verwende die gespeicherte Einstellung + const isDarkMode = storedTheme === 'dark'; + applyTheme(isDarkMode); + updateAlpineJsState(isDarkMode); + } else { + // Wenn keine Einstellung gefunden wurde, prüfe die Systemeinstellung + const prefersDarkMode = window.matchMedia('(prefers-color-scheme: dark)').matches; + applyTheme(prefersDarkMode); + updateAlpineJsState(prefersDarkMode); + + // Speichere die initiale Einstellung + localStorage.setItem('darkMode', prefersDarkMode ? 'dark' : 'light'); + } +} + +/** + * Wendet das ausgewählte Theme auf die Seite an + * @param {boolean} isDarkMode - Ob der Dark Mode aktiviert werden soll + */ +function applyTheme(isDarkMode) { + // Toggle der 'dark' Klasse auf dem HTML-Element + document.documentElement.classList.toggle('dark', isDarkMode); + + // Meta-Theme-Color für mobile Browser aktualisieren + const metaThemeColor = document.querySelector('meta[name="theme-color"]'); + if (metaThemeColor) { + metaThemeColor.setAttribute( + 'content', + isDarkMode ? '#111827' : '#f9fafb' + ); + } + + // Zusätzliche Stile für sanfte Übergänge + document.body.style.transition = 'background-color 0.5s ease, color 0.3s ease'; +} + +/** + * Aktualisiert den Dark Mode Status in Alpine.js + * @param {boolean} isDarkMode - Ob der Dark Mode aktiviert ist + */ +function updateAlpineJsState(isDarkMode) { + // Wenn Alpine.js verfügbar ist, aktualisiere den State + if (window.Alpine) { + // Finde alle Alpine Components, die ein darkMode Property haben + document.querySelectorAll('[x-data]').forEach(el => { + const alpineComponent = Alpine.$data(el); + if (alpineComponent && 'darkMode' in alpineComponent) { + alpineComponent.darkMode = isDarkMode; + } + }); + } +} + +/** + * Richte Event-Listener und Animationen für Themeübergänge ein + */ +function setupThemeTransition() { + // Animationseffekt für Themenwechsel + const createRippleEffect = (isDarkMode) => { + // Entferne bestehende Ripple-Elemente + const existingRipples = document.querySelectorAll('.theme-transition-ripple'); + existingRipples.forEach(ripple => ripple.remove()); + + // Erstelle neues Ripple-Element + const ripple = document.createElement('div'); + ripple.classList.add('theme-transition-ripple'); + + // Positioniere das Ripple-Element in der oberen rechten Ecke (wo der Toggle-Button ist) + ripple.style.position = 'fixed'; + ripple.style.top = '60px'; + ripple.style.right = '80px'; + ripple.style.width = '10px'; + ripple.style.height = '10px'; + ripple.style.borderRadius = '50%'; + ripple.style.backgroundColor = isDarkMode ? '#111827' : '#f9fafb'; + ripple.style.transform = 'scale(0)'; + ripple.style.transition = 'transform 1.2s ease-out'; + ripple.style.zIndex = '9999'; + ripple.style.pointerEvents = 'none'; + + // Füge das Ripple-Element zum Body hinzu + document.body.appendChild(ripple); + + // Trigger Animation + setTimeout(() => { + // Berechne die Größe, um den gesamten Bildschirm abzudecken + const maxDimension = Math.max(window.innerWidth, window.innerHeight) * 2.5; + ripple.style.transform = `scale(${maxDimension})`; + + // Entferne das Element nach Abschluss der Animation + setTimeout(() => { + ripple.remove(); + }, 1000); + }, 50); + }; + + // Event-Listener für den Theme-Toggle-Button + const themeToggleButtons = document.querySelectorAll('[data-toggle-theme]'); + themeToggleButtons.forEach(button => { + button.addEventListener('click', () => { + const isDarkMode = document.documentElement.classList.contains('dark'); + const newMode = !isDarkMode; + + // Erstelle den Ripple-Effekt + createRippleEffect(newMode); + + // Wende das neue Theme an + applyTheme(newMode); + + // Aktualisiere Alpine.js State + updateAlpineJsState(newMode); + + // Speichere die Einstellung + localStorage.setItem('darkMode', newMode ? 'dark' : 'light'); + }); + }); + + // Füge CSS für den Ripple-Effekt hinzu + const style = document.createElement('style'); + style.textContent = ` + .theme-transition-ripple { + position: fixed; + border-radius: 50%; + z-index: 9999; + pointer-events: none; + transition: transform 1.2s cubic-bezier(0.22, 1, 0.36, 1); + } + `; + document.head.appendChild(style); +} + +/** + * Öffentliche API für das Theme-Management + */ +window.ThemeManager = { + /** + * Schaltet zwischen Light und Dark Mode um + */ + toggleDarkMode() { + const isDarkMode = document.documentElement.classList.contains('dark'); + const newMode = !isDarkMode; + + // Wende das neue Theme an + applyTheme(newMode); + + // Aktualisiere Alpine.js State + updateAlpineJsState(newMode); + + // Speichere die Einstellung + localStorage.setItem('darkMode', newMode ? 'dark' : 'light'); + }, + + /** + * Setzt das Theme auf einen bestimmten Modus + * @param {boolean} isDarkMode - Ob der Dark Mode aktiviert werden soll + */ + setDarkMode(isDarkMode) { + // Wende das gewünschte Theme an + applyTheme(isDarkMode); + + // Aktualisiere Alpine.js State + updateAlpineJsState(isDarkMode); + + // Speichere die Einstellung + localStorage.setItem('darkMode', isDarkMode ? 'dark' : 'light'); + } +}; \ No newline at end of file diff --git a/static/neural-network-background.js b/static/neural-network-background.js index ec40938..e89531b 100644 --- a/static/neural-network-background.js +++ b/static/neural-network-background.js @@ -252,178 +252,122 @@ class NeuralNetworkBackground { 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]; + // 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); - // 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) { + const randomIndex = Math.floor(Math.random() * availableConnections.length); + const connection = availableConnections[randomIndex]; - 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; + // 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 (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); + this.activeConnections.add(connection.id); } } } 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 + // Aktualisiere Farben 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]; - // 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'; + 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(); } - // Verbindungen zeichnen - for (let i = 0; i < this.connections.length; i++) { - const connection = this.connections[i]; + // 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; - if (connection.opacity <= 0) continue; + const fromNode = this.nodes[connection.from]; + const toNode = this.nodes[connection.to]; - const nodeA = this.nodes[connection.from]; - const nodeB = this.nodes[connection.to]; + // Glühen-Effekt + this.ctx.globalAlpha = Math.sin(connection.flowProgress * Math.PI) * 0.8; - this.ctx.strokeStyle = this.currentColors.connectionColor; - this.ctx.globalAlpha = connection.opacity; + // Linie zeichnen this.ctx.beginPath(); - this.ctx.moveTo(nodeA.x, nodeA.y); - this.ctx.lineTo(nodeB.x, nodeB.y); + this.ctx.moveTo(fromNode.x, fromNode.y); + this.ctx.lineTo(toNode.x, toNode.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(); - } + // 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 (let i = 0; i < this.nodes.length; i++) { - const node = this.nodes[i]; - const isPulsing = now - node.lastPulse < 300; + for (const node of this.nodes) { + // Pulsierende Knoten + const timeSinceLastPulse = now - node.lastPulse; + const isPulsing = timeSinceLastPulse < 800; + const pulseProgress = isPulsing ? timeSinceLastPulse / 800 : 0; - // 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; + // Knoten selbst + this.ctx.globalAlpha = 1; + this.ctx.fillStyle = isPulsing + ? colors.nodePulse + : colors.nodeColor; - // 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.arc(node.x, node.y, node.size + (isPulsing ? 1 * Math.sin(pulseProgress * Math.PI) : 0), 0, Math.PI * 2); this.ctx.fill(); - // Knoten selbst zeichnen - this.ctx.fillStyle = nodeColor; - this.ctx.globalAlpha = 0.8; + // Wenn pulsierend, füge einen Glow-Effekt hinzu + if (isPulsing) { + this.ctx.globalAlpha = 0.5 * (1 - pulseProgress); this.ctx.beginPath(); - this.ctx.arc(node.x, node.y, node.size, 0, Math.PI * 2); + this.ctx.arc(node.x, node.y, node.size + 5 * pulseProgress, 0, Math.PI * 2); this.ctx.fill(); + } } - // Zurücksetzen der Globalwerte this.ctx.globalAlpha = 1; - this.ctx.filter = 'none'; } destroy() { @@ -447,9 +391,6 @@ 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), @@ -457,11 +398,6 @@ 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 c924b0b..84e1d9e 100644 --- a/templates/base.html +++ b/templates/base.html @@ -401,7 +401,8 @@ @click="darkMode = !darkMode; document.documentElement.classList.toggle('dark', darkMode); localStorage.setItem('darkMode', darkMode ? 'dark' : 'light');" class="p-2 ml-3 rounded-full flex items-center justify-center transition-all duration-300 group focus:outline-none focus:ring-2 focus:ring-indigo-400/50" :class="darkMode ? 'bg-gray-800 hover:bg-gray-700' : 'bg-white/90 hover:bg-white shadow-md'" - aria-label="Themen-Modus wechseln"> + aria-label="Themen-Modus wechseln" + data-toggle-theme>