feat(branding): add global dynamic theme system and fix 2fa mail

This commit is contained in:
DanielS
2026-08-14 15:57:29 +02:00
parent 009b167cbd
commit 9ecdc645e8
22 changed files with 1764 additions and 313 deletions

View File

@@ -2,29 +2,6 @@
import React, { useEffect, useRef } from 'react'
interface OKLCHColor {
l: number
c: number
h: number
}
// Monochromes Farbkonzept im OKLCH-Raum
const BASE_BG = { l: 0.145, c: 0.01, h: 148 }
const BASE_DIM = { l: 0.50, c: 0.01, h: 148 }
const HIGHLIGHT_BRIGHT = { l: 0.95, c: 0.01, h: 148 }
function lerp(a: number, b: number, t: number): number {
return a + (b - a) * t
}
function lerpOKLCH(c1: OKLCHColor, c2: OKLCHColor, t: number): OKLCHColor {
return {
l: lerp(c1.l, c2.l, t),
c: lerp(c1.c, c2.c, t),
h: lerp(c1.h, c2.h, t)
}
}
export function AsciiShaderBackground({ className = '' }: { className?: string }) {
const canvasRef = useRef<HTMLCanvasElement | null>(null)
@@ -47,6 +24,32 @@ export function AsciiShaderBackground({ className = '' }: { className?: string }
let scrollProgress = 0
let targetScrollProgress = 0
const getDynamicHue = (): number => {
try {
const hex = getComputedStyle(document.documentElement).getPropertyValue('--primary-custom').trim() || '#2563eb'
let c = hex.replace('#', '')
if (c.length === 3) c = c.split('').map(x => x + x).join('')
const r = parseInt(c.substring(0, 2), 16) / 255
const g = parseInt(c.substring(2, 4), 16) / 255
const b = parseInt(c.substring(4, 6), 16) / 255
const max = Math.max(r, g, b)
const min = Math.min(r, g, b)
let h = 0
if (max !== min) {
const d = max - min
switch (max) {
case r: h = (g - b) / d + (g < b ? 6 : 0); break
case g: h = (b - r) / d + 2; break
case b: h = (r - g) / d + 4; break
}
h /= 6
}
return Math.round(h * 360)
} catch (e) {
return 217
}
}
const resize = () => {
const width = window.innerWidth
const height = window.innerHeight
@@ -99,54 +102,40 @@ export function AsciiShaderBackground({ className = '' }: { className?: string }
return
}
// Scroll-gesteuerter Dimmer
const scrollDimFactor = (1 - scrollProgress * 0.4) * scrollFade
const currentDimColor = {
...BASE_DIM,
l: BASE_DIM.l * scrollDimFactor
}
ctx.font = `${fontSize}px monospace`
ctx.textAlign = 'center'
ctx.textBaseline = 'middle'
const timeSec = time * 0.001
const activeHue = getDynamicHue()
for (let r = 0; r < rows; r++) {
for (let c = 0; c < cols; c++) {
const x = c * fontSize + fontSize / 2
const y = r * fontSize + fontSize / 2
// Abstandsvektor zur gedämpften Mausposition
const dx = x - mouse.x
const dy = y - mouse.y
const dist = Math.sqrt(dx * dx + dy * dy)
// Maus-Störung (Vektorfeld-Ablenkung): Phase & Amplitude lokal beeinflussen
const mouseInfluence = Math.exp(-dist / 250)
const mouseDistortion = (Math.atan2(dy, dx) + (dx + dy) * 0.003) * mouseInfluence * 3.0
// Diagonale 2D-Strömung / Wellen-Synthese aus Sinus & Kosinus
const waveX = Math.sin(c * 0.08 + timeSec * 0.8 + mouseDistortion)
const waveY = Math.cos(r * 0.08 + timeSec * 0.6 + mouseDistortion)
const flowValue = Math.sin(waveX + waveY + (c + r) * 0.04 + timeSec * 0.4)
// Zeichenauswahl basierend auf Strömungsfeld
const normalizedFlow = (flowValue + 1) * 0.5 // Range 0..1
const normalizedFlow = (flowValue + 1) * 0.5
const charIndex = Math.floor(normalizedFlow * chars.length) % chars.length
const char = chars[charIndex]
// Leerzeichen für ruhiges Raster überspringen
if (char === ' ') continue
// Helligkeits-Highlighting durch Maus-Störung und Wellenkamm
const blendFactor = Math.min(Math.max(mouseInfluence * 0.85 + normalizedFlow * 0.15, 0), 1)
const currentColor = lerpOKLCH(currentDimColor, HIGHLIGHT_BRIGHT, blendFactor)
const alpha = (0.35 + blendFactor * 0.6) * scrollFade
const lightness = 40 + Math.round(blendFactor * 50)
// Stärkerer Kontrast: min 0.35, max 0.95 Deckkraft
const alpha = (0.35 + blendFactor * 0.6) * scrollDimFactor
ctx.fillStyle = `oklch(${currentColor.l.toFixed(3)} ${currentColor.c.toFixed(3)} ${currentColor.h.toFixed(1)} / ${alpha.toFixed(2)})`
ctx.fillStyle = `hsl(${activeHue} 80% ${lightness}% / ${alpha.toFixed(2)})`
ctx.fillText(char, x, y)
}
}
@@ -166,7 +155,7 @@ export function AsciiShaderBackground({ className = '' }: { className?: string }
return (
<div
className={`absolute top-0 left-0 right-0 h-[500px] z-0 overflow-hidden bg-background pointer-events-none ${className}`}
className={`fixed inset-0 pointer-events-none z-0 overflow-hidden ${className}`}
style={{
maskImage: 'linear-gradient(to bottom, rgba(0,0,0,1) 0%, rgba(0,0,0,1) 250px, rgba(0,0,0,0) 500px)',
WebkitMaskImage: 'linear-gradient(to bottom, rgba(0,0,0,1) 0%, rgba(0,0,0,1) 250px, rgba(0,0,0,0) 500px)'
@@ -181,8 +170,8 @@ export function AsciiShaderBackground({ className = '' }: { className?: string }
style={{
backgroundSize: '40px 40px',
backgroundImage: `
linear-gradient(to right, oklch(0.31 0.012 148 / 0.4) 1px, transparent 1px),
linear-gradient(to bottom, oklch(0.31 0.012 148 / 0.4) 1px, transparent 1px)
linear-gradient(to right, var(--card-border-glow, rgba(37, 99, 235, 0.15)) 1px, transparent 1px),
linear-gradient(to bottom, var(--card-border-glow, rgba(37, 99, 235, 0.15)) 1px, transparent 1px)
`
}}
/>