frondend/components/TextGradientScrollContext.tsx

144 lines
3.5 KiB
TypeScript
Raw Normal View History

2024-10-05 15:04:17 +08:00
"use client";
import React, { createContext, useContext, useRef } from "react";
import { useScroll, useTransform, motion, MotionValue } from "framer-motion";
import { cn } from "./utils";
type TextOpacityEnum = "none" | "soft" | "medium";
type ViewTypeEnum = "word" | "letter";
type TextGradientScrollType = {
text: string;
type?: ViewTypeEnum;
className?: string;
textOpacity?: TextOpacityEnum;
};
type LetterType = {
children: React.ReactNode | string;
progress: MotionValue<number>;
range: number[];
};
type WordType = {
children: React.ReactNode;
progress: MotionValue<number>;
range: number[];
};
type CharType = {
children: React.ReactNode;
progress: MotionValue<number>;
range: number[];
};
type TextGradientScrollContextType = {
textOpacity?: TextOpacityEnum;
type?: ViewTypeEnum;
};
const TextGradientScrollContext = createContext<TextGradientScrollContextType>(
{}
);
function useGradientScroll() {
const context = useContext(TextGradientScrollContext);
return context;
}
export default function TextGradientScroll({
text,
className,
type = "letter",
textOpacity = "soft",
}: TextGradientScrollType) {
const ref = useRef<HTMLParagraphElement>(null);
const { scrollYProgress } = useScroll({
target: ref,
offset: ["start center", "end center"],
});
const words = text.split(" ");
return (
<TextGradientScrollContext.Provider value={{ textOpacity, type }}>
<p ref={ref} className={cn("relative flex m-0 flex-wrap", className)}>
{words.map((word, i) => {
const start = i / words.length;
const end = start + 1 / words.length;
return type === "word" ? (
<Word key={i} progress={scrollYProgress} range={[start, end]}>
{word}
</Word>
) : (
<Letter key={i} progress={scrollYProgress} range={[start, end]}>
{word}
</Letter>
);
})}
</p>
</TextGradientScrollContext.Provider>
);
}
const Word = ({ children, progress, range }: WordType) => {
const opacity = useTransform(progress, range, [0, 1]);
return (
<span className="relative me-2 mt-2">
<span style={{ position: "absolute", opacity: 0.1 }}>{children}</span>
<motion.span style={{ transition: "all .5s", opacity: opacity }}>
{children}
</motion.span>
</span>
);
};
const Letter = ({ children, progress, range }: LetterType) => {
if (typeof children === "string") {
const amount = range[1] - range[0];
const step = amount / children.length;
return (
<span className="relative me-2 mt-2">
{children.split("").map((char: string, i: number) => {
const start = range[0] + i * step;
const end = range[0] + (i + 1) * step;
return (
<Char key={`c_${i}`} progress={progress} range={[start, end]}>
{char}
</Char>
);
})}
</span>
);
}
};
const Char = ({ children, progress, range }: CharType) => {
const opacity = useTransform(progress, range, [0, 1]);
const { textOpacity } = useGradientScroll();
return (
<span>
<span
className={cn("absolute", {
"opacity-0": textOpacity == "none",
"opacity-10": textOpacity == "soft",
"opacity-30": textOpacity == "medium",
})}
>
{children}
</span>
<motion.span
style={{
transition: "all .5s",
opacity: opacity,
}}
>
{children}
</motion.span>
</span>
);
};