UNPKG

react-gsap-animation-library

Version:

A comprehensive React component library that leverages GSAP for animations with official GSAP-React integration

306 lines (274 loc) 9.18 kB
import React, { useRef } from 'react'; import { useGSAP } from '@gsap/react'; import gsap from 'gsap'; import ScrollTrigger from 'gsap/ScrollTrigger'; import { useAnimationSettings } from '../../context/AnimationContext'; /** * TextReveal component for creating text reveal animations with mask effects * * @param {Object} props - Component props * @param {React.ReactNode} props.children - Text content to reveal * @param {number} props.duration - Animation duration in seconds * @param {number} props.delay - Animation delay in seconds * @param {string} props.ease - GSAP easing function * @param {string} props.direction - Reveal direction ('left', 'right', 'top', 'bottom') * @param {string} props.backgroundColor - Background color for the reveal mask * @param {string} props.textColor - Text color * @param {string} props.maskStyle - Additional style for the mask ('solid', 'gradient', 'split') * @param {string} props.trigger - Animation trigger type ('scroll', 'load', 'none', 'hover', 'click') * @param {number} props.threshold - Viewport threshold to trigger animation (0-1) * @param {function} props.onStart - Callback when animation starts * @param {function} props.onComplete - Callback when animation completes * @param {string} props.className - Additional CSS class names * @param {Object} props.style - Custom styles */ const TextReveal = ({ children, duration = 1.2, delay = 0, ease = "power4.inOut", direction = 'left', backgroundColor = '#000', textColor = 'inherit', maskStyle = 'solid', trigger = 'scroll', threshold = 0.2, onStart = () => {}, onComplete = () => {}, className = '', style = {}, ...otherProps }) => { // References to DOM elements const containerRef = useRef(null); const textRef = useRef(null); const maskRef = useRef(null); const { disableAllAnimations, disableScrollAnimations } = useAnimationSettings(); // Use GSAP's React hook const { contextSafe } = useGSAP(() => { if (!containerRef.current || !textRef.current || !maskRef.current) return; // Skip if animations disabled if (disableAllAnimations || (trigger === 'scroll' && disableScrollAnimations)) { // Make text visible and hide mask gsap.set(textRef.current, { opacity: 1 }); gsap.set(maskRef.current, { display: 'none' }); return; } // Only set up automatic animations for load and scroll triggers if (trigger !== 'load' && trigger !== 'scroll') return; // Set initial states gsap.set(textRef.current, { opacity: 0 }); // Determine animation properties based on direction and style const isHorizontal = direction === 'left' || direction === 'right'; // Create a timeline for the animation sequence const tl = gsap.timeline({ paused: trigger === 'scroll', onStart, onComplete }); // Set up mask animation based on maskStyle if (maskStyle === 'split') { // Split reveal: translate the mask out instead of scaling tl.to(maskRef.current, { [isHorizontal ? 'x' : 'y']: isHorizontal ? (direction === 'left' ? '100%' : '-100%') : (direction === 'top' ? '100%' : '-100%'), duration: duration * 0.6, ease }); } else { // Scale reveal: shrink the mask in the appropriate direction tl.to(maskRef.current, { [isHorizontal ? 'scaleX' : 'scaleY']: 0, duration: duration * 0.6, ease, transformOrigin: direction === 'left' ? 'right center' : direction === 'right' ? 'left center' : direction === 'top' ? 'center bottom' : 'center top' }); } // Fade in text slightly before mask is fully gone tl.to(textRef.current, { opacity: 1, duration: duration * 0.4, ease: 'power2.out' }, '-=0.25'); // Start a bit before the mask animation completes // Set up scroll trigger if needed if (trigger === 'scroll' && !disableScrollAnimations) { ScrollTrigger.create({ trigger: containerRef.current, start: `top bottom-=${threshold * 100}%`, onEnter: () => tl.play() }); } else if (trigger === 'load') { // Play immediately for load trigger tl.play(); } return () => { // Clean up ScrollTrigger instances if (trigger === 'scroll') { ScrollTrigger.getAll() .filter(st => st.vars.trigger === containerRef.current) .forEach(st => st.kill()); } }; }, { scope: containerRef, dependencies: [ duration, delay, ease, direction, maskStyle, trigger, threshold, disableAllAnimations, disableScrollAnimations ] }); // Play animation function for manual triggers const playAnimation = contextSafe(() => { if (disableAllAnimations || !containerRef.current || !textRef.current || !maskRef.current) return; // Reset to initial state gsap.set(textRef.current, { opacity: 0 }); gsap.set(maskRef.current, { display: 'block', scaleX: direction === 'left' || direction === 'right' ? 1 : undefined, scaleY: direction === 'top' || direction === 'bottom' ? 1 : undefined, x: 0, y: 0 }); // Create a new timeline const tl = gsap.timeline({ onStart, onComplete: () => { onComplete(); gsap.set(maskRef.current, { display: 'none' }); } }); // Determine animation properties const isHorizontal = direction === 'left' || direction === 'right'; // Add mask animation if (maskStyle === 'split') { tl.to(maskRef.current, { [isHorizontal ? 'x' : 'y']: isHorizontal ? (direction === 'left' ? '100%' : '-100%') : (direction === 'top' ? '100%' : '-100%'), duration: duration * 0.6, ease }); } else { tl.to(maskRef.current, { [isHorizontal ? 'scaleX' : 'scaleY']: 0, duration: duration * 0.6, ease, transformOrigin: direction === 'left' ? 'right center' : direction === 'right' ? 'left center' : direction === 'top' ? 'center bottom' : 'center top' }); } // Add text reveal tl.to(textRef.current, { opacity: 1, duration: duration * 0.4, ease: 'power2.out' }, '-=0.25'); // Play the timeline tl.play(); }); // Get mask styles based on direction and maskStyle const getMaskStyles = () => { // Base styles const baseStyles = { position: 'absolute', background: backgroundColor, zIndex: 1, top: 0, right: 0, bottom: 0, left: 0 }; // Direction-specific transform origin const transformOrigin = direction === 'left' ? 'right center' : direction === 'right' ? 'left center' : direction === 'top' ? 'center bottom' : 'center top'; // Mask style variations if (maskStyle === 'gradient') { const gradientDirection = direction === 'left' ? 'to right' : direction === 'right' ? 'to left' : direction === 'top' ? 'to bottom' : 'to top'; return { ...baseStyles, transformOrigin, background: `linear-gradient(${gradientDirection}, ${backgroundColor} 0%, ${backgroundColor}CC 70%, ${backgroundColor}00 100%)` }; } return { ...baseStyles, transformOrigin }; }; // Get text styles const getTextStyles = () => { return { position: 'relative', zIndex: 2, color: textColor, opacity: 0 // Start hidden, will be revealed in animation }; }; // Get event handlers based on trigger type const getEventHandlers = () => { if (disableAllAnimations) return {}; if (trigger === 'hover') { return { onMouseEnter: playAnimation }; } if (trigger === 'click') { return { onClick: playAnimation }; } return {}; }; // Combined props for rendering const containerStyle = { position: 'relative', overflow: 'hidden', display: 'inline-block', ...style }; return ( <div ref={containerRef} className={`react-gsap-textreveal ${className}`} style={containerStyle} {...getEventHandlers()} {...otherProps} > <div ref={maskRef} className="react-gsap-textreveal-mask" style={getMaskStyles()} /> <div ref={textRef} className="react-gsap-textreveal-text" style={getTextStyles()} > {typeof children === 'function' ? children({ play: playAnimation }) : children } </div> </div> ); }; export default TextReveal;