feat(web): redesign config screen with animated particle background and unified auth layout
Some checks failed
ci / build-test (push) Has been cancelled
ci / docker-build (push) Has been cancelled
Desktop App / Build Linux (amd64) (push) Has been cancelled
Desktop App / Attach to Release (push) Has been cancelled

This commit is contained in:
2026-07-13 23:12:16 +02:00
parent 35ff3f37e1
commit 62a337f3cc
2 changed files with 295 additions and 80 deletions

View File

@@ -0,0 +1,171 @@
<script lang="ts">
import { onMount } from 'svelte'
import { mode } from 'mode-watcher'
interface Particle {
x: number
y: number
vx: number
vy: number
r: number
phase: number
pulse: number
}
const COUNT = 90
const CONNECT_DIST = 160
const MOUSE_RADIUS = 200
const MOUSE_FORCE = 0.012
let host = $state<HTMLDivElement | null>(null)
let canvas = $state<HTMLCanvasElement | null>(null)
let particles: Particle[] = []
let mouse = { x: -500, y: -500 }
let w = 0, h = 0, dpr = 1
let timer: ReturnType<typeof setTimeout> | 0 = 0
function spawn() {
particles = Array.from({ length: COUNT }, () => ({
x: Math.random() * w,
y: Math.random() * h,
vx: (Math.random() - 0.5) * 0.3,
vy: (Math.random() - 0.5) * 0.3,
r: 1 + Math.random() * 2,
phase: Math.random() * Math.PI * 2,
pulse: 0.4 + Math.random() * 0.6
}))
}
function resize() {
if (!host || !canvas) return
dpr = Math.min(window.devicePixelRatio || 1, 2)
w = host.clientWidth
h = host.clientHeight
canvas.width = Math.round(w * dpr)
canvas.height = Math.round(h * dpr)
if (particles.length === 0) spawn()
}
function onPointerMove(e: PointerEvent) {
if (!host) return
const rect = host.getBoundingClientRect()
mouse.x = e.clientX - rect.left
mouse.y = e.clientY - rect.top
}
function onPointerLeave() {
mouse.x = -500
mouse.y = -500
}
function draw(ts: number) {
timer = setTimeout(() => draw(performance.now()), 33)
if (!canvas || particles.length === 0) return
const ctx = canvas.getContext('2d')
if (!ctx) return
const t = ts / 1000
const dark = mode.current !== 'light'
ctx.setTransform(dpr, 0, 0, dpr, 0, 0)
ctx.clearRect(0, 0, w, h)
// update + draw particles
for (const p of particles) {
// autonomous drift
p.vx += (Math.sin(t * 0.4 + p.phase) * 0.003) * 0.15
p.vy += (Math.cos(t * 0.35 + p.phase) * 0.003) * 0.15
// mouse interaction
const dx = p.x - mouse.x
const dy = p.y - mouse.y
const dist = Math.sqrt(dx * dx + dy * dy)
if (dist < MOUSE_RADIUS && dist > 0) {
const force = (MOUSE_RADIUS - dist) / MOUSE_RADIUS * MOUSE_FORCE
p.vx += (dx / dist) * force * 0.6
p.vy += (dy / dist) * force * 0.6
}
// friction + random nudge
p.vx *= 0.995
p.vy *= 0.995
if (Math.random() < 0.003) {
p.vx += (Math.random() - 0.5) * 0.04
p.vy += (Math.random() - 0.5) * 0.04
}
// wrap
p.x += p.vx
p.y += p.vy
if (p.x < -40) p.x = w + 40
if (p.x > w + 40) p.x = -40
if (p.y < -40) p.y = h + 40
if (p.y > h + 40) p.y = -40
// pulse brightness
const alpha = p.pulse * (0.35 + 0.15 * Math.sin(t * 1.2 + p.phase))
ctx.fillStyle = dark
? `rgba(140,175,230,${alpha})`
: `rgba(60,90,140,${alpha})`
ctx.beginPath()
ctx.arc(p.x, p.y, p.r, 0, Math.PI * 2)
ctx.fill()
}
// connections between nearby particles
ctx.lineWidth = 0.6
for (let i = 0; i < particles.length; i++) {
for (let j = i + 1; j < particles.length; j++) {
const a = particles[i]
const b = particles[j]
const dx = a.x - b.x
const dy = a.y - b.y
const dist = dx * dx + dy * dy
if (dist < CONNECT_DIST * CONNECT_DIST) {
const alpha = (1 - Math.sqrt(dist) / CONNECT_DIST) * 0.18
ctx.strokeStyle = dark
? `rgba(140,175,230,${alpha})`
: `rgba(60,90,140,${alpha})`
ctx.beginPath()
ctx.moveTo(a.x, a.y)
ctx.lineTo(b.x, b.y)
ctx.stroke()
}
}
}
// radial scrim to keep center legible
const cx = w / 2, cy = h / 2
const scrim = ctx.createRadialGradient(cx, cy, 0, cx, cy, Math.hypot(cx, cy))
const base = dark ? '13,17,23' : '255,255,255'
scrim.addColorStop(0, `rgba(${base},0.72)`)
scrim.addColorStop(0.35, `rgba(${base},0.40)`)
scrim.addColorStop(0.65, `rgba(${base},0.08)`)
scrim.addColorStop(1, 'rgba(0,0,0,0)')
ctx.fillStyle = scrim
ctx.fillRect(0, 0, w, h)
}
onMount(() => {
resize()
spawn()
const ro = new ResizeObserver(() => {
resize()
spawn()
})
if (host) ro.observe(host)
window.addEventListener('pointermove', onPointerMove)
window.addEventListener('pointerleave', onPointerLeave)
timer = setTimeout(() => draw(performance.now()), 33)
return () => {
clearTimeout(timer)
ro.disconnect()
window.removeEventListener('pointermove', onPointerMove)
window.removeEventListener('pointerleave', onPointerLeave)
}
})
</script>
<div bind:this={host} class="absolute inset-0 overflow-hidden bg-background">
<canvas bind:this={canvas} class="h-full w-full"></canvas>
</div>