magicuidesign / magicui

UI Library for Design Engineers. Animated components and effects you can copy and paste into your apps. Free. Open Source.
https://magicui.design
MIT License
8.7k stars 337 forks source link

[feat]: /docs/components/scroll-based-velocity #264

Open ssaintx opened 1 month ago

ssaintx commented 1 month ago

Is your feature request related to a problem? Please describe. The amount of Parallax text is strongly 2(user can change it manually, but for reusable component it is NOT that flexible)

Describe the solution you'd like I recomend to add new prop as "amount" and if user type 2 in the amount, it will render 2, if 3 than 3, and baseVelocity should not be in the same directory.

Here is a solution i suggest

"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;
  amount: number; // added new property
  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 function VelocityScroll({
  text,
  default_velocity = 5,
  className,
  amount,
}: VelocityScrollProps) {
  function ParallaxText({
    children,
    baseVelocity = 100,
    className,
  }: ParallaxProps) {
    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 = React.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>
    );
  }
// use this property to render a text, and direction is not repetetive, and every second directory will dynamically changing
  return (
    <section className="relative w-full">
      {Array.from({ length: amount }).map((_, index) => (
        <ParallaxText key={index} baseVelocity={index % 2 === 0 ? -default_velocity : default_velocity} className={className}>
          {text}
        </ParallaxText>
      ))}
    </section>
  );
}

This is how can you use it

<VelocityScroll
text="string"
amount={number}
default_velocity={number}
className="your-style"
/>
itsarghyadas commented 3 weeks ago

I will review it and inform you. Then you can create a PR.