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
JSX
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;