Scroll Velocity
Scroll velocity animations create a dynamic and engaging effect as elements move on the screen. This technique can be used to draw attention to specific content or enhance the overall user experience.
Installation
npm install framer-motion
Copy and paste the following code into your project.
components/ui/scrollbasedvelocity.tsx
"use client";
import React, { useEffect, useRef, useState } from "react";
import {
motion,
useAnimationFrame,
useMotionValue,
useScroll,
useSpring,
useTransform,
useVelocity,
} from "framer-motion";
import { cn } from "@/lib/utils";
interface VelocityScrollProps {
text: string;
default_velocity?: number;
className?: string;
}
interface ParallaxProps {
children: string;
baseVelocity: number;
className?: string;
}
export const wrap = (min: number, max: number, v: number) => {
const rangeSize = max - min;
return ((((v - min) % rangeSize) + rangeSize) % rangeSize) + min;
};
export const VelocityScroll: React.FC<VelocityScrollProps> = ({
text,
default_velocity = 5,
className,
}) => {
const ParallaxText: React.FC<ParallaxProps> = ({
children,
baseVelocity = 100,
className,
}) => {
const baseX = useMotionValue(0);
const { scrollY } = useScroll();
const scrollVelocity = useVelocity(scrollY);
const smoothVelocity = useSpring(scrollVelocity, {
damping: 50,
stiffness: 400,
});
const velocityFactor = useTransform(smoothVelocity, [0, 1000], [0, 5], {
clamp: false,
});
const [repetitions, setRepetitions] = useState(1);
const containerRef = useRef<HTMLDivElement>(null);
const textRef = useRef<HTMLSpanElement>(null);
useEffect(() => {
const calculateRepetitions = () => {
if (containerRef.current && textRef.current) {
const containerWidth = containerRef.current.offsetWidth;
const textWidth = textRef.current.offsetWidth;
const newRepetitions = Math.ceil(containerWidth / textWidth) + 2;
setRepetitions(newRepetitions);
}
};
calculateRepetitions();
window.addEventListener("resize", calculateRepetitions);
return () => window.removeEventListener("resize", calculateRepetitions);
}, [children]);
const x = useTransform(baseX, (v) => `${wrap(-100 / repetitions, 0, v)}%`);
const directionFactor = useRef<number>(1);
useAnimationFrame((t, delta) => {
let moveBy = directionFactor.current * baseVelocity * (delta / 1000);
if (velocityFactor.get() < 0) {
directionFactor.current = -1;
} else if (velocityFactor.get() > 0) {
directionFactor.current = 1;
}
moveBy += directionFactor.current * moveBy * velocityFactor.get();
baseX.set(baseX.get() + moveBy);
});
return (
<div
className="w-full overflow-hidden whitespace-nowrap"
ref={containerRef}
>
<motion.div className={cn("inline-block", className)} style={{ x }}>
{Array.from({ length: repetitions }).map((_, i) => (
<span key={i} ref={i === 0 ? textRef : null}>
{children}{" "}
</span>
))}
</motion.div>
</div>
);
};
return (
<section className="relative w-full">
<ParallaxText baseVelocity={default_velocity} className={className}>
{text}
</ParallaxText>
<ParallaxText baseVelocity={-default_velocity} className={className}>
{text}
</ParallaxText>
</section>
);
};
Props
Prop | Type | Default |
---|---|---|
text | string | Hello World! |
default_velocity | number | 5 |
className | string | - |
Last updated on