"use client"; import { useEffect, useRef } from "react"; const LETTERS = ["D", "K", "0"] as const; // Ruhezustand und Spannweite der variablen Achsen von Bricolage Grotesque const REST = { wght: 800, wdth: 100 }; const NEAR = { wght: 220, wdth: 75 }; const REACH_PX = 520; /** * Das große „DK0“ im Plakat. Die Buchstaben werden dünner und schmaler, je näher * der Zeiger kommt, geglättet über eine kritisch gedämpfte Feder. * Ohne Maus oder bei reduzierter Bewegung bleibt es statisch. Das Markup ist * vollständig server-gerendert, JS fügt nur die Reaktion hinzu. */ export default function KineticWordmark() { const rootRef = useRef(null); const letterRefs = useRef<(HTMLSpanElement | null)[]>([]); useEffect(() => { const root = rootRef.current; const area = root?.closest("section"); if (!root || !area) return; const fine = window.matchMedia?.("(hover: hover) and (pointer: fine)")?.matches ?? false; const reduce = window.matchMedia?.("(prefers-reduced-motion: reduce)")?.matches ?? false; if (!fine || reduce) return; const state = LETTERS.map(() => ({ w: REST.wght, d: REST.wdth, vw: 0, vd: 0, tw: REST.wght, td: REST.wdth })); let raf = 0; const loop = () => { let moving = false; state.forEach((s, i) => { s.vw = (s.vw + (s.tw - s.w) * 0.08) * 0.78; s.w += s.vw; s.vd = (s.vd + (s.td - s.d) * 0.08) * 0.78; s.d += s.vd; if (Math.abs(s.vw) + Math.abs(s.vd) > 0.05) moving = true; const el = letterRefs.current[i]; if (el) el.style.fontVariationSettings = `"wght" ${s.w.toFixed(1)}, "wdth" ${s.d.toFixed(1)}`; }); raf = moving ? requestAnimationFrame(loop) : 0; }; const kick = () => { if (!raf) raf = requestAnimationFrame(loop); }; const onMove = (e: PointerEvent) => { letterRefs.current.forEach((el, i) => { if (!el) return; const r = el.getBoundingClientRect(); const dist = Math.hypot(e.clientX - (r.left + r.width / 2), e.clientY - (r.top + r.height / 2)); const f = Math.min(1, dist / REACH_PX); state[i].tw = NEAR.wght + f * (REST.wght - NEAR.wght); state[i].td = NEAR.wdth + f * (REST.wdth - NEAR.wdth); }); kick(); }; const onLeave = () => { state.forEach((s) => { s.tw = REST.wght; s.td = REST.wdth; }); kick(); }; area.addEventListener("pointermove", onMove); area.addEventListener("pointerleave", onLeave); return () => { cancelAnimationFrame(raf); area.removeEventListener("pointermove", onMove); area.removeEventListener("pointerleave", onLeave); }; }, []); return ( ); }