96ec2d867f
- New design tokens: signal red, ink and paper as RGB channels; ink and paper swap roles in dark mode so every component inverts cleanly - Fonts via next/font: Bricolage Grotesque (with wdth/opsz axes), Geist, Geist Mono; drop Inter and Playfair - Poster hero: kicker, giant DK0 wordmark that reacts to pointer distance (weight and width through a damped spring, desktop only, off for reduced motion), claim as h1, intro with thesis countdown, marquee strip - Header in poster style: transparent over the red hero, paper strip with ink rule when scrolled or on subpages; drawer menu restyled - Remove wavy SVG separators, CSS shader gradient, background blobs and unused providers that caused the pink/purple bands between sections - Drop the global p/span/div color override so colors inherit properly - New hero texts in de/en, tests and i18n key check updated Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
100 lines
3.3 KiB
TypeScript
100 lines
3.3 KiB
TypeScript
"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<HTMLDivElement>(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 (
|
|
<div
|
|
ref={rootRef}
|
|
aria-hidden="true"
|
|
className="flex select-none items-end justify-between font-display text-[clamp(8.5rem,33vw,31rem)] leading-[0.78] tracking-[-0.05em]"
|
|
>
|
|
{LETTERS.map((letter, i) => (
|
|
<span
|
|
key={letter}
|
|
ref={(el) => {
|
|
letterRefs.current[i] = el;
|
|
}}
|
|
className={`inline-block animate-wordmark-up ${letter === "0" ? "text-paper" : ""}`}
|
|
style={{ fontVariationSettings: `"wght" ${REST.wght}, "wdth" ${REST.wdth}`, animationDelay: `${i * 80}ms` }}
|
|
>
|
|
{letter}
|
|
</span>
|
|
))}
|
|
</div>
|
|
);
|
|
}
|