feat(login): Netzwerk-Muster driftet langsam

Knoten laufen auf eigenen Lissajous-Bahnen, Linien folgen den Knoten.
Positionen werden per requestAnimationFrame direkt am DOM gesetzt, damit
React nicht pro Frame neu rendert. Bei prefers-reduced-motion bleibt das
Muster statisch.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sascha Wolf
2026-08-11 22:14:04 +02:00
co-authored by Claude Opus 5
parent 41e9f6898f
commit 30164e0ae7
+95 -13
View File
@@ -1,30 +1,112 @@
import React, { useState } from 'react'; import React, { useEffect, useRef, useState } from 'react';
import { useAuth } from '../lib/auth'; import { useAuth } from '../lib/auth';
/**
* Knoten des dekorativen Netzwerks. Jeder Knoten driftet auf einer eigenen
* Lissajous-Bahn, damit sich das Muster nie exakt wiederholt.
*/
const NETWORK_NODES = [
{ x: 80, y: 300, r: 9, ax: 10, ay: 7, sx: 0.055, sy: 0.041, px: 0.0, py: 1.9 },
{ x: 150, y: 210, r: 13, ax: 8, ay: 11, sx: 0.037, sy: 0.049, px: 1.2, py: 0.4 },
{ x: 240, y: 250, r: 10, ax: 12, ay: 8, sx: 0.045, sy: 0.033, px: 2.7, py: 3.3 },
{ x: 310, y: 150, r: 12, ax: 9, ay: 12, sx: 0.031, sy: 0.052, px: 4.1, py: 1.1 },
{ x: 250, y: 80, r: 7, ax: 13, ay: 9, sx: 0.048, sy: 0.036, px: 5.4, py: 2.6 },
{ x: 160, y: 340, r: 6, ax: 11, ay: 10, sx: 0.042, sy: 0.057, px: 0.8, py: 4.8 },
{ x: 360, y: 240, r: 8, ax: 7, ay: 13, sx: 0.058, sy: 0.029, px: 3.5, py: 0.2 },
];
/** Verbindungen zwischen den Knoten (Indizes in NETWORK_NODES). */
const NETWORK_EDGES: Array<[number, number]> = [
[0, 1],
[1, 2],
[2, 3],
[3, 4],
[4, 1],
[0, 5],
[5, 2],
[3, 6],
[6, 2],
];
/** Dekoratives Netzwerk-Muster - greift das Knoten-Motiv des perinco Logos auf. */ /** Dekoratives Netzwerk-Muster - greift das Knoten-Motiv des perinco Logos auf. */
const NetworkPattern: React.FC = () => ( const NetworkPattern: React.FC = () => {
const circleRefs = useRef<Array<SVGCircleElement | null>>([]);
const lineRefs = useRef<Array<SVGLineElement | null>>([]);
useEffect(() => {
const reduced = window.matchMedia('(prefers-reduced-motion: reduce)');
if (reduced.matches) return;
let frame = 0;
const start = performance.now();
const tick = (now: number) => {
const t = (now - start) / 1000;
const positions = NETWORK_NODES.map((node) => ({
x: node.x + Math.sin(t * node.sx * Math.PI * 2 + node.px) * node.ax,
y: node.y + Math.cos(t * node.sy * Math.PI * 2 + node.py) * node.ay,
}));
positions.forEach((pos, index) => {
const circle = circleRefs.current[index];
if (!circle) return;
circle.setAttribute('cx', pos.x.toFixed(2));
circle.setAttribute('cy', pos.y.toFixed(2));
});
NETWORK_EDGES.forEach(([from, to], index) => {
const line = lineRefs.current[index];
if (!line) return;
line.setAttribute('x1', positions[from].x.toFixed(2));
line.setAttribute('y1', positions[from].y.toFixed(2));
line.setAttribute('x2', positions[to].x.toFixed(2));
line.setAttribute('y2', positions[to].y.toFixed(2));
});
frame = requestAnimationFrame(tick);
};
frame = requestAnimationFrame(tick);
return () => cancelAnimationFrame(frame);
}, []);
return (
<svg <svg
className="pointer-events-none absolute inset-0 h-full w-full opacity-20" className="pointer-events-none absolute inset-0 h-full w-full opacity-20"
viewBox="0 0 400 400" viewBox="0 0 400 400"
fill="none" fill="none"
aria-hidden="true" aria-hidden="true"
> >
<g stroke="white" strokeWidth="1.2"> <g stroke="white" strokeWidth="1.2" strokeLinecap="round">
<path d="M80 300 L150 210 L240 250 L310 150 L250 80 L150 210" /> {NETWORK_EDGES.map(([from, to], index) => (
<path d="M80 300 L160 340 L240 250" /> <line
<path d="M310 150 L360 240 L240 250" /> key={`${from}-${to}`}
ref={(element) => {
lineRefs.current[index] = element;
}}
x1={NETWORK_NODES[from].x}
y1={NETWORK_NODES[from].y}
x2={NETWORK_NODES[to].x}
y2={NETWORK_NODES[to].y}
/>
))}
</g> </g>
<g fill="white"> <g fill="white">
<circle cx="80" cy="300" r="9" /> {NETWORK_NODES.map((node, index) => (
<circle cx="150" cy="210" r="13" /> <circle
<circle cx="240" cy="250" r="10" /> key={`${node.x}-${node.y}`}
<circle cx="310" cy="150" r="12" /> ref={(element) => {
<circle cx="250" cy="80" r="7" /> circleRefs.current[index] = element;
<circle cx="160" cy="340" r="6" /> }}
<circle cx="360" cy="240" r="8" /> cx={node.x}
cy={node.y}
r={node.r}
/>
))}
</g> </g>
</svg> </svg>
); );
};
export const LoginScreen: React.FC = () => { export const LoginScreen: React.FC = () => {
const { login } = useAuth(); const { login } = useAuth();