326 lines
11 KiB
TypeScript
326 lines
11 KiB
TypeScript
"use client";
|
||
|
||
import { useEffect, useRef, useState } from "react";
|
||
import {
|
||
motion,
|
||
useMotionValue,
|
||
useScroll,
|
||
useSpring,
|
||
useTransform,
|
||
} from "framer-motion";
|
||
import { ArrowDown } from "lucide-react";
|
||
import { gsap } from "@/lib/gsap";
|
||
|
||
export default function Hero() {
|
||
const sectionRef = useRef<HTMLElement>(null);
|
||
|
||
const { scrollY } = useScroll();
|
||
|
||
// Scroll-Transformationen (Reversibel)
|
||
const ringY = useTransform(scrollY, [0, 1000], [0, 180]);
|
||
const ringScale = useTransform(scrollY, [0, 1000], [1, 1.18]);
|
||
const ringOpacity = useTransform(scrollY, [0, 400, 1000, 1500], [1, 1, 0.5, 0]);
|
||
const ringRotate = useTransform(scrollY, [0, 1000], [0, 10]);
|
||
|
||
// Interaktive Cursor-Bewegung (Subtile Spring-Physik für elegantes Hovering)
|
||
const rawMouseX = useMotionValue(0);
|
||
const rawMouseY = useMotionValue(0);
|
||
|
||
const smoothMouseX = useSpring(rawMouseX, { stiffness: 60, damping: 28 });
|
||
const smoothMouseY = useSpring(rawMouseY, { stiffness: 60, damping: 28 });
|
||
|
||
// Subtile magnetische Linienverschiebung
|
||
const magnetX = useTransform(smoothMouseX, [-0.5, 0.5], [-10, 10]);
|
||
const magnetY = useTransform(smoothMouseY, [-0.5, 0.5], [-8, 8]);
|
||
|
||
// SVG-Glow-Position (in viewBox 1550 x 950 Koordinaten)
|
||
const [svgGlow, setSvgGlow] = useState({ x: 775, y: 475 });
|
||
|
||
const handleMouseMove = (e: React.MouseEvent<HTMLElement>) => {
|
||
const rect = sectionRef.current?.getBoundingClientRect();
|
||
if (!rect) return;
|
||
|
||
// Normalisierte Mauskoordinaten (-0.5 bis 0.5)
|
||
const normX = (e.clientX - rect.left) / rect.width - 0.5;
|
||
const normY = (e.clientY - rect.top) / rect.height - 0.5;
|
||
rawMouseX.set(normX);
|
||
rawMouseY.set(normY);
|
||
|
||
// Umrechnung auf SVG-ViewBox-Koordinaten für das sanfte Aufleuchten
|
||
const svgX = Math.round(((e.clientX - rect.left) / rect.width) * 1550);
|
||
const svgY = Math.round(((e.clientY - rect.top) / rect.height) * 950);
|
||
setSvgGlow({ x: svgX, y: svgY });
|
||
};
|
||
|
||
// Text-Intro startet erst, wenn der Preloader das "app:reveal"-Event feuert
|
||
useEffect(() => {
|
||
// Der Hero ist das LCP-Element. Auf Mobilgeräten bleibt er deshalb sofort
|
||
// sichtbar, statt seine endgültige Darstellung durch eine Timeline zu verzögern.
|
||
if (window.matchMedia("(max-width: 767px)").matches) return;
|
||
|
||
let ctx: gsap.Context | undefined;
|
||
let fallback: ReturnType<typeof setTimeout> | undefined;
|
||
|
||
const start = () => {
|
||
if (ctx) return;
|
||
ctx = gsap.context(() => {
|
||
gsap.fromTo(
|
||
".hero-el",
|
||
{ y: 34, opacity: 0 },
|
||
{
|
||
y: 0,
|
||
opacity: 1,
|
||
duration: 1,
|
||
stagger: 0.1,
|
||
ease: "power3.out",
|
||
},
|
||
);
|
||
}, sectionRef);
|
||
};
|
||
|
||
const mm = gsap.matchMedia();
|
||
mm.add("(prefers-reduced-motion: no-preference)", () => {
|
||
window.addEventListener("app:reveal", start, { once: true });
|
||
|
||
// Beim gecachten bzw. mobilen Preloader ist die Seite schon sichtbar,
|
||
// bevor dieser Effect registriert wird. Dann sofort animieren statt auf
|
||
// den Fallback-Timer zu warten.
|
||
if (document.documentElement.dataset.preloader !== "active") {
|
||
start();
|
||
} else {
|
||
fallback = setTimeout(start, 1200);
|
||
}
|
||
});
|
||
|
||
return () => {
|
||
mm.revert();
|
||
window.removeEventListener("app:reveal", start);
|
||
if (fallback) clearTimeout(fallback);
|
||
ctx?.revert();
|
||
};
|
||
}, []);
|
||
|
||
return (
|
||
<section
|
||
id="top"
|
||
ref={sectionRef}
|
||
onMouseMove={handleMouseMove}
|
||
className="relative flex min-h-[100svh] flex-col justify-center pt-16 cursor-default"
|
||
>
|
||
{/* Weicher Spot in der Mitte: Fadet am Rand und unten zu 100% transparent in den Seitenhintergrund aus */}
|
||
<div
|
||
aria-hidden
|
||
className="absolute inset-0 bg-[radial-gradient(ellipse_75%_55%_at_50%_35%,#ffffff_0%,transparent_100%)]"
|
||
/>
|
||
|
||
{/* Multischichtiges Ring-Design: Kreise fließen flüssig über die Sektionsgrenze nach unten */}
|
||
<motion.div
|
||
initial={{ opacity: 0 }}
|
||
animate={{ opacity: 1 }}
|
||
transition={{ duration: 1.5, ease: "easeOut" }}
|
||
style={{
|
||
y: ringY,
|
||
x: magnetX,
|
||
scale: ringScale,
|
||
opacity: ringOpacity,
|
||
rotate: ringRotate,
|
||
}}
|
||
className="pointer-events-none absolute left-1/2 top-1/2 w-[min(100vw,1550px)] h-[min(115vh,1100px)] -translate-x-1/2 -translate-y-1/2"
|
||
>
|
||
<motion.div style={{ y: magnetY }} className="w-full h-full">
|
||
<svg
|
||
aria-hidden
|
||
className="w-full h-full"
|
||
viewBox="0 0 1550 1100"
|
||
fill="none"
|
||
>
|
||
<defs>
|
||
{/* Subtiler radialer Glow-Verlauf unter dem Cursor */}
|
||
<radialGradient
|
||
id="cursorLineGlow"
|
||
cx={svgGlow.x}
|
||
cy={svgGlow.y}
|
||
r="280"
|
||
gradientUnits="userSpaceOnUse"
|
||
>
|
||
<stop offset="0%" stopColor="#0d7a57" stopOpacity="0.32" />
|
||
<stop offset="50%" stopColor="#0d7a57" stopOpacity="0.12" />
|
||
<stop offset="100%" stopColor="#0d7a57" stopOpacity="0" />
|
||
</radialGradient>
|
||
</defs>
|
||
|
||
{/* 1. Basis-Ellipsen mit dezentem Ruhe-Opacity */}
|
||
<ellipse
|
||
cx="775"
|
||
cy="490"
|
||
rx="290"
|
||
ry="180"
|
||
stroke="#111412"
|
||
strokeOpacity="0.13"
|
||
strokeWidth="1"
|
||
/>
|
||
<ellipse
|
||
cx="775"
|
||
cy="490"
|
||
rx="440"
|
||
ry="275"
|
||
stroke="#111412"
|
||
strokeOpacity="0.10"
|
||
strokeWidth="1"
|
||
/>
|
||
<ellipse
|
||
cx="775"
|
||
cy="490"
|
||
rx="610"
|
||
ry="370"
|
||
stroke="#111412"
|
||
strokeOpacity="0.11"
|
||
strokeWidth="1"
|
||
/>
|
||
<ellipse
|
||
cx="775"
|
||
cy="490"
|
||
rx="770"
|
||
ry="460"
|
||
stroke="#111412"
|
||
strokeOpacity="0.08"
|
||
strokeWidth="1"
|
||
strokeDasharray="5 5"
|
||
/>
|
||
<ellipse
|
||
cx="775"
|
||
cy="490"
|
||
rx="920"
|
||
ry="550"
|
||
stroke="#111412"
|
||
strokeOpacity="0.06"
|
||
strokeWidth="1"
|
||
/>
|
||
<ellipse
|
||
cx="775"
|
||
cy="490"
|
||
rx="1080"
|
||
ry="640"
|
||
stroke="#111412"
|
||
strokeOpacity="0.05"
|
||
strokeWidth="1"
|
||
/>
|
||
|
||
{/* Dynamisch kreuzende Orbit-Bögen – reichen flüssig bis in die nächste Sektion */}
|
||
<circle
|
||
cx="690"
|
||
cy="440"
|
||
r="620"
|
||
stroke="#111412"
|
||
strokeOpacity="0.07"
|
||
strokeWidth="1"
|
||
/>
|
||
<circle
|
||
cx="860"
|
||
cy="570"
|
||
r="680"
|
||
stroke="#111412"
|
||
strokeOpacity="0.06"
|
||
strokeWidth="1"
|
||
/>
|
||
<ellipse
|
||
cx="775"
|
||
cy="490"
|
||
rx="1040"
|
||
ry="440"
|
||
transform="rotate(-11 775 490)"
|
||
stroke="#111412"
|
||
strokeOpacity="0.06"
|
||
strokeWidth="1"
|
||
/>
|
||
|
||
{/* Feine Horizon-Linie hinter den Buttons */}
|
||
<line
|
||
x1="390"
|
||
y1="690"
|
||
x2="1160"
|
||
y2="690"
|
||
stroke="#111412"
|
||
strokeOpacity="0.09"
|
||
strokeWidth="1"
|
||
/>
|
||
|
||
{/* 2. Subtile Aufleuchten-Ebene unter dem Cursor */}
|
||
<g stroke="url(#cursorLineGlow)" strokeWidth="1.3">
|
||
<ellipse cx="775" cy="490" rx="290" ry="180" />
|
||
<ellipse cx="775" cy="490" rx="440" ry="275" />
|
||
<ellipse cx="775" cy="490" rx="610" ry="370" />
|
||
<ellipse cx="775" cy="490" rx="770" ry="460" strokeDasharray="5 5" />
|
||
<ellipse cx="775" cy="490" rx="920" ry="550" />
|
||
<ellipse cx="775" cy="490" rx="1080" ry="640" />
|
||
<circle cx="690" cy="440" r="620" />
|
||
<circle cx="860" cy="570" r="680" />
|
||
<ellipse
|
||
cx="775"
|
||
cy="490"
|
||
rx="1040"
|
||
ry="440"
|
||
transform="rotate(-11 775 490)"
|
||
/>
|
||
<line x1="390" y1="690" x2="1160" y2="690" />
|
||
</g>
|
||
</svg>
|
||
</motion.div>
|
||
</motion.div>
|
||
|
||
<div className="bg-noise pointer-events-none absolute inset-0" aria-hidden />
|
||
|
||
<div className="relative mx-auto flex w-full max-w-5xl flex-col items-center px-5 py-16 text-center sm:px-8">
|
||
<h1 className="font-sans text-[11vw] font-medium leading-[1.02] tracking-[-0.03em] sm:text-6xl lg:text-7xl">
|
||
<span className="hero-el block">Kostenloser</span>
|
||
<span className="hero-el block font-display text-[1.06em] font-normal italic tracking-normal text-[#0d7a57]">
|
||
Zufallsgenerator:
|
||
</span>
|
||
<span className="hero-el block">Entscheide in Sekunden</span>
|
||
</h1>
|
||
|
||
<p className="hero-el mt-8 max-w-md text-base leading-relaxed text-ink-soft sm:text-lg">
|
||
Ob Kopf oder Zahl, Glücksrad oder Namensauslosung: Wähle dein Tool und
|
||
starte sofort – kostenlos, ohne Anmeldung, direkt im Browser.
|
||
</p>
|
||
|
||
<div className="hero-el mt-10 flex flex-wrap items-center justify-center gap-3">
|
||
<motion.a
|
||
href="#tools"
|
||
whileHover={{ y: -2, scale: 1.02 }}
|
||
whileTap={{ scale: 0.97 }}
|
||
className="inline-flex items-center rounded-full bg-[#0d7a57] px-8 py-3.5 text-sm font-semibold text-white shadow-[0_12px_28px_rgba(13,122,87,0.30)] transition-colors duration-200 hover:bg-[#095c41]"
|
||
>
|
||
Passendes Tool wählen
|
||
</motion.a>
|
||
<motion.a
|
||
href="#features"
|
||
whileHover={{ y: -2 }}
|
||
whileTap={{ scale: 0.97 }}
|
||
className="inline-flex items-center rounded-full border border-ink/20 bg-surface/80 px-8 py-3.5 text-sm font-semibold text-ink transition-colors duration-200 hover:border-[#0d7a57] hover:text-[#0d7a57]"
|
||
>
|
||
So funktioniert's
|
||
</motion.a>
|
||
</div>
|
||
|
||
<p className="hero-el mt-8 inline-flex items-center rounded-full border border-[#0d7a57]/20 bg-[#e6f7f0] px-4 py-1.5 text-xs font-semibold uppercase tracking-[0.2em] text-[#0d7a57]">
|
||
<span className="inline-block h-1.5 w-1.5 rounded-full bg-[#0d7a57] mr-2.5" />
|
||
7 Tools · Kostenlos · Ohne Anmeldung
|
||
</p>
|
||
</div>
|
||
|
||
{/* Scroll-Hinweis */}
|
||
<a
|
||
href="#tools"
|
||
aria-label="Zu den Tools scrollen"
|
||
className="absolute bottom-6 left-1/2 z-10 hidden -translate-x-1/2 flex-col items-center gap-1 text-ink-soft transition-colors hover:text-ink md:flex"
|
||
>
|
||
<span className="text-xs font-semibold uppercase tracking-[0.15em]">
|
||
Scroll
|
||
</span>
|
||
<ArrowDown className="animate-bob h-4 w-4" />
|
||
</a>
|
||
</section>
|
||
);
|
||
}
|