Files
entscheidomat/components/scroll-flourishes.tsx
2026-08-02 00:06:11 +02:00

390 lines
12 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { useEffect, useRef } from "react";
import { motion } from "framer-motion";
import { gsap } from "@/lib/gsap";
/**
* Botanisch präzise verschnörkelte Ranken:
* Das Wachstum ist exakt auf den gesamten Scrollverlauf abgestimmt (kein zu schnelles Wachsen).
* Die Ranken entfalten sich genau synchron zur Scroll-Position und erreichen den
* untersten Rand erst, wenn man auch ganz unten angekommen ist.
*/
// Botanisch geformtes Blatt mit spitzer Silhouetten-Form, Blattader & dezentem Hover-Wiegeln
function Leaf({
x,
y,
rotate = 0,
scale = 1,
className = "",
}: {
x: number;
y: number;
rotate?: number;
scale?: number;
className?: string;
}) {
return (
<motion.g
transform={`translate(${x}, ${y}) rotate(${rotate}) scale(${scale})`}
whileHover={{ scale: scale * 1.18, rotate: rotate + 6 }}
transition={{ type: "spring", stiffness: 160, damping: 16 }}
className={`vine-leaf pointer-events-auto cursor-pointer ${className}`}
>
{/* Äußere Blattsilhouette edles Gelb-Grün / Smaragd */}
<path
d="M 0 0 C 14 -20 36 -12 42 0 C 36 12 14 20 0 0 Z"
fill="#0d7a57"
fillOpacity="0.92"
className="transition-all duration-300 hover:fill-[#109b6f]"
/>
{/* Feine Blattader */}
<path
d="M 0 0 C 15 -3 28 -2 39 0"
stroke="#ffffff"
strokeOpacity="0.75"
strokeWidth="1.2"
strokeLinecap="round"
/>
</motion.g>
);
}
// Offene Spiral-Tendril (Ranken-Kringel)
function Tendril({ d, className = "" }: { d: string; className?: string }) {
return (
<path
className={`vine-tendril ${className}`}
d={d}
fill="none"
stroke="#161513"
strokeWidth="1.4"
strokeLinecap="round"
vectorEffect="non-scaling-stroke"
/>
);
}
// Ranke 1 (Links-Oben nach Mitte-Rechts - Hero Bereich)
const STEM_1 =
"M -40 20 C 120 70, 240 160, 320 300 C 400 440, 280 600, 420 740 C 520 860, 680 940, 820 1060";
const TENDRILS_1 = [
"M 240 160 C 275 135, 295 160, 275 185 C 255 195, 240 178, 252 165",
"M 420 740 C 465 710, 485 745, 460 770 C 435 785, 415 755, 428 738",
];
const LEAF_NODES_1 = [
{ x: 120, y: 70, rotate: -25, scale: 0.9 },
{ x: 240, y: 160, rotate: 42, scale: 1.1 },
{ x: 320, y: 300, rotate: -45, scale: 1.0 },
{ x: 400, y: 440, rotate: 32, scale: 0.95 },
{ x: 420, y: 740, rotate: -35, scale: 1.05 },
{ x: 680, y: 940, rotate: 50, scale: 0.9 },
];
// Ranke 2 (Rechts-Oben edel schwebend - Mittlerer Bereich)
const STEM_2 =
"M 920 30 C 760 150, 620 310, 710 490 C 800 680, 620 830, 740 1020 C 860 1180, 720 1340, 840 1500";
const TENDRILS_2 = [
"M 710 490 C 665 460, 640 495, 665 520 C 690 535, 710 505, 695 488",
"M 740 1020 C 695 990, 670 1025, 695 1050 C 720 1065, 740 1035, 725 1018",
];
const LEAF_NODES_2 = [
{ x: 760, y: 150, rotate: 155, scale: 1.0 },
{ x: 710, y: 490, rotate: -130, scale: 1.15 },
{ x: 620, y: 830, rotate: 140, scale: 0.9 },
{ x: 740, y: 1020, rotate: -150, scale: 1.05 },
{ x: 840, y: 1500, rotate: 135, scale: 1.1 },
];
// Ranke 3 (Untere Ranke Links Wächst erst in der 2. Seitenhälfte bis in den Footer)
const STEM_3 =
"M -30 50 C 210 180, 440 240, 520 430 C 600 620, 420 810, 610 1000 C 780 1170, 690 1340, 840 1550 C 920 1720, 780 1880, 910 2050";
const TENDRILS_3 = [
"M 520 430 C 565 400, 590 435, 565 460 C 540 475, 518 445, 532 428",
"M 610 1000 C 655 970, 680 1005, 655 1030 C 630 1045, 608 1015, 622 998",
"M 840 1550 C 885 1520, 910 1555, 885 1580 C 860 1595, 838 1565, 852 1548",
];
const LEAF_NODES_3 = [
{ x: 210, y: 180, rotate: -30, scale: 1.05 },
{ x: 440, y: 240, rotate: 35, scale: 0.9 },
{ x: 520, y: 430, rotate: -40, scale: 1.1 },
{ x: 420, y: 810, rotate: 45, scale: 1.0 },
{ x: 610, y: 1000, rotate: -35, scale: 1.15 },
{ x: 780, y: 1170, rotate: 30, scale: 0.95 },
{ x: 840, y: 1550, rotate: -25, scale: 1.1 },
{ x: 880, y: 1860, rotate: 40, scale: 0.95 },
];
// Ranke 4 (Untere Ranke Rechts Wächst erst in der 2. Seitenhälfte bis in den Footer)
const STEM_4 =
"M 900 40 C 740 220, 620 400, 700 600 C 780 800, 600 1000, 720 1220 C 840 1440, 680 1680, 780 1920 C 880 2080, 750 2200, 850 2320";
const TENDRILS_4 = [
"M 700 600 C 655 570, 630 605, 655 630 C 680 645, 700 615, 685 598",
"M 720 1220 C 675 1190, 650 1225, 675 1250 C 700 1265, 720 1235, 705 1218",
];
const LEAF_NODES_4 = [
{ x: 740, y: 220, rotate: 145, scale: 1.0 },
{ x: 700, y: 600, rotate: 125, scale: 0.95 },
{ x: 600, y: 1000, rotate: -140, scale: 1.1 },
{ x: 720, y: 1220, rotate: 135, scale: 1.05 },
{ x: 780, y: 1920, rotate: -130, scale: 0.95 },
];
export default function ScrollFlourishes() {
const rootRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const root = rootRef.current;
if (!root) return;
const mm = gsap.matchMedia();
mm.add("(prefers-reduced-motion: no-preference)", () => {
// 1. Dasharray & Offset für alle Linienpfade vorbereiten
const strokePaths = root.querySelectorAll<SVGPathElement>(
".vine-stem, .vine-tendril",
);
strokePaths.forEach((p) => {
const len = p.getTotalLength();
p.style.strokeDasharray = `${len}`;
p.style.strokeDashoffset = `${len}`;
});
const leaves = root.querySelectorAll<SVGGElement>(".vine-leaf");
gsap.set(leaves, { opacity: 0, scale: 0, transformOrigin: "0px 0px" });
// 2. Perfekt getaktete ScrollTrigger Timeline (Spannweite von 0 bis 1.0 des gesamten Dokument-Scrolls)
const tl = gsap.timeline({
scrollTrigger: {
trigger: document.body,
start: "top top",
end: "bottom bottom",
scrub: 0.7,
},
});
// Ranke 1 (Hero): Wächst im oberen Drittel (0.0 bis 0.38)
tl.to(
root.querySelectorAll(".vine-stem-1"),
{ strokeDashoffset: 0, ease: "none", duration: 0.38 },
0,
);
tl.to(
root.querySelectorAll(".vine-tendril-1"),
{ strokeDashoffset: 0, ease: "none", duration: 0.08 },
0.25,
);
// Ranke 2 (Mitte-Rechts): Wächst im mittleren Bereich (0.22 bis 0.68)
tl.to(
root.querySelectorAll(".vine-stem-2"),
{ strokeDashoffset: 0, ease: "none", duration: 0.46 },
0.22,
);
tl.to(
root.querySelectorAll(".vine-tendril-2"),
{ strokeDashoffset: 0, ease: "none", duration: 0.08 },
0.48,
);
// Ranke 3 (Untere Ranke Links): Wächst in der 2. Hälfte bis ganz nach unten (0.45 bis 1.0)
tl.to(
root.querySelectorAll(".vine-stem-3"),
{ strokeDashoffset: 0, ease: "none", duration: 0.55 },
0.45,
);
tl.to(
root.querySelectorAll(".vine-tendril-3"),
{ strokeDashoffset: 0, ease: "none", duration: 0.08 },
0.72,
);
// Ranke 4 (Untere Ranke Rechts): Wächst in der 2. Hälfte bis ganz nach unten (0.50 bis 1.0)
tl.to(
root.querySelectorAll(".vine-stem-4"),
{ strokeDashoffset: 0, ease: "none", duration: 0.5 },
0.5,
);
tl.to(
root.querySelectorAll(".vine-tendril-4"),
{ strokeDashoffset: 0, ease: "none", duration: 0.08 },
0.78,
);
// Blätter entfalten sich organisch zur jeweiligen Wachstumszeit
tl.to(
leaves,
{
opacity: 1,
scale: 1,
ease: "back.out(1.5)",
duration: 0.1,
stagger: 0.02,
},
0.1,
);
// Subtiler, ruhiger Parallax-Versatz (Ohne hektische Maus-Swayings)
tl.to(
".vine-container-1",
{ yPercent: 6, scale: 1.06, ease: "none", duration: 1 },
0,
);
tl.to(
".vine-container-2",
{ yPercent: 8, scale: 0.95, ease: "none", duration: 1 },
0,
);
tl.to(
".vine-container-3",
{ yPercent: -4, scale: 1.05, ease: "none", duration: 1 },
0,
);
tl.to(
".vine-container-4",
{ yPercent: 5, scale: 1.04, ease: "none", duration: 1 },
0,
);
});
return () => mm.revert();
}, []);
return (
<div
ref={rootRef}
aria-hidden
className="pointer-events-none fixed inset-0 -z-10 overflow-hidden select-none"
>
{/* Ranke 1: Links-Oben nach Mitte-Rechts (Hero) */}
<div className="vine-container-1 absolute left-[-4vw] top-[-2vh] w-[50vw] max-w-[750px] min-w-[320px] opacity-35 transition-opacity duration-500">
<svg
viewBox="0 0 850 1100"
fill="none"
strokeLinecap="round"
className="w-full h-auto pointer-events-auto"
>
<path
className="vine-stem vine-stem-1"
d={STEM_1}
stroke="#161513"
strokeWidth="1.6"
vectorEffect="non-scaling-stroke"
/>
{TENDRILS_1.map((d, i) => (
<Tendril key={i} className="vine-tendril-1" d={d} />
))}
{LEAF_NODES_1.map((node, i) => (
<Leaf
key={i}
x={node.x}
y={node.y}
rotate={node.rotate}
scale={node.scale}
/>
))}
</svg>
</div>
{/* Ranke 2: Rechts-Oben (Mittlerer Bereich) */}
<div className="vine-container-2 absolute right-[-4vw] top-[18vh] w-[54vw] max-w-[820px] min-w-[340px] opacity-30 transition-opacity duration-500">
<svg
viewBox="0 0 950 1550"
fill="none"
strokeLinecap="round"
className="w-full h-auto pointer-events-auto"
>
<path
className="vine-stem vine-stem-2"
d={STEM_2}
stroke="#161513"
strokeWidth="1.6"
vectorEffect="non-scaling-stroke"
/>
{TENDRILS_2.map((d, i) => (
<Tendril key={i} className="vine-tendril-2" d={d} />
))}
{LEAF_NODES_2.map((node, i) => (
<Leaf
key={i}
x={node.x}
y={node.y}
rotate={node.rotate}
scale={node.scale}
/>
))}
</svg>
</div>
{/* Ranke 3: Untere Ranke Links Wächst erst in der 2. Seitenhälfte bis in den Footer */}
<div className="vine-container-3 absolute left-[-3vw] top-[42vh] w-[58vw] max-w-[920px] min-w-[360px] opacity-35 transition-opacity duration-500">
<svg
viewBox="0 0 950 2100"
fill="none"
strokeLinecap="round"
className="w-full h-auto pointer-events-auto"
>
<path
className="vine-stem vine-stem-3"
d={STEM_3}
stroke="#161513"
strokeWidth="1.6"
vectorEffect="non-scaling-stroke"
/>
{TENDRILS_3.map((d, i) => (
<Tendril key={i} className="vine-tendril-3" d={d} />
))}
{LEAF_NODES_3.map((node, i) => (
<Leaf
key={i}
x={node.x}
y={node.y}
rotate={node.rotate}
scale={node.scale}
/>
))}
</svg>
</div>
{/* Ranke 4: Untere Ranke Rechts Wächst erst in der 2. Seitenhälfte bis in den Footer */}
<div className="vine-container-4 absolute right-[-3vw] top-[48vh] w-[56vw] max-w-[900px] min-w-[350px] opacity-35 transition-opacity duration-500">
<svg
viewBox="0 0 950 2350"
fill="none"
strokeLinecap="round"
className="w-full h-auto pointer-events-auto"
>
<path
className="vine-stem vine-stem-4"
d={STEM_4}
stroke="#161513"
strokeWidth="1.6"
vectorEffect="non-scaling-stroke"
/>
{TENDRILS_4.map((d, i) => (
<Tendril key={i} className="vine-tendril-4" d={d} />
))}
{LEAF_NODES_4.map((node, i) => (
<Leaf
key={i}
x={node.x}
y={node.y}
rotate={node.rotate}
scale={node.scale}
/>
))}
</svg>
</div>
</div>
);
}