99 lines
2.6 KiB
TypeScript
99 lines
2.6 KiB
TypeScript
"use client";
|
|
|
|
import { useRef, useEffect, useState } from "react";
|
|
import {
|
|
motion,
|
|
useAnimationFrame,
|
|
useMotionValue,
|
|
useTransform,
|
|
useSpring,
|
|
useScroll,
|
|
} from "framer-motion";
|
|
|
|
interface ScrollVelocityTextProps {
|
|
text: string;
|
|
baseVelocity?: number;
|
|
className?: string;
|
|
}
|
|
|
|
function wrap(min: number, max: number, v: number) {
|
|
const rangeSize = max - min;
|
|
return ((((v - min) % rangeSize) + rangeSize) % rangeSize) + min;
|
|
}
|
|
|
|
export default function ScrollVelocityText({
|
|
text,
|
|
baseVelocity = 2,
|
|
className = "",
|
|
}: ScrollVelocityTextProps) {
|
|
const baseX = useMotionValue(0);
|
|
const { scrollY } = useScroll();
|
|
const scrollVelocity = useMotionValue(0);
|
|
const smoothVelocity = useSpring(scrollVelocity, {
|
|
damping: 50,
|
|
stiffness: 300,
|
|
});
|
|
const velocityFactor = useTransform(smoothVelocity, [0, 1000], [0, 5], {
|
|
clamp: false,
|
|
});
|
|
|
|
const [repetitions, setRepetitions] = useState(4);
|
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
|
|
useEffect(() => {
|
|
const handleResize = () => {
|
|
setRepetitions(Math.ceil(window.innerWidth / 400) + 2);
|
|
};
|
|
handleResize();
|
|
window.addEventListener("resize", handleResize);
|
|
return () => window.removeEventListener("resize", handleResize);
|
|
}, []);
|
|
|
|
// Track scroll velocity
|
|
const prevScrollY = useRef(0);
|
|
useEffect(() => {
|
|
return scrollY.on("change", (latest) => {
|
|
const delta = latest - prevScrollY.current;
|
|
prevScrollY.current = latest;
|
|
scrollVelocity.set(Math.abs(delta));
|
|
});
|
|
}, [scrollY, scrollVelocity]);
|
|
|
|
const directionFactor = useRef(1);
|
|
|
|
const x = useTransform(baseX, (v) => `${wrap(-25, 0, v)}%`);
|
|
|
|
useAnimationFrame((_, delta) => {
|
|
let moveBy = directionFactor.current * baseVelocity * (delta / 1000);
|
|
|
|
const vf = velocityFactor.get();
|
|
if (vf < 0) {
|
|
directionFactor.current = -1;
|
|
} else if (vf > 0) {
|
|
directionFactor.current = 1;
|
|
}
|
|
|
|
moveBy += directionFactor.current * moveBy * vf;
|
|
baseX.set(baseX.get() + moveBy);
|
|
});
|
|
|
|
return (
|
|
<div
|
|
ref={containerRef}
|
|
className={`overflow-hidden whitespace-nowrap ${className}`}
|
|
>
|
|
<motion.div className="inline-flex" style={{ x }}>
|
|
{[...Array(repetitions)].map((_, i) => (
|
|
<span
|
|
key={i}
|
|
className="mr-8 inline-block font-display text-[clamp(3rem,10vw,8rem)] font-extrabold uppercase leading-none tracking-[-0.04em]"
|
|
>
|
|
{text}
|
|
<span className="mx-8 inline-block text-[var(--slash)]">•</span>
|
|
</span>
|
|
))}
|
|
</motion.div>
|
|
</div>
|
|
);
|
|
}
|