react-gsap-animation-library
Version:
A comprehensive React component library that leverages GSAP for animations with official GSAP-React integration
412 lines (362 loc) • 10.8 kB
JavaScript
/**
* Animation fallback utilities
* Provides CSS-based fallbacks for common animations when GSAP is not available
*/
/**
* Creates CSS keyframes animation definitions
* @returns {string} CSS keyframes rules as a string
*/
export const generateCSSKeyframes = () => {
return `
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translate3d(0, 30px, 0);
}
to {
opacity: 1;
transform: translate3d(0, 0, 0);
}
}
@keyframes fadeInDown {
from {
opacity: 0;
transform: translate3d(0, -30px, 0);
}
to {
opacity: 1;
transform: translate3d(0, 0, 0);
}
}
@keyframes fadeInLeft {
from {
opacity: 0;
transform: translate3d(-30px, 0, 0);
}
to {
opacity: 1;
transform: translate3d(0, 0, 0);
}
}
@keyframes fadeInRight {
from {
opacity: 0;
transform: translate3d(30px, 0, 0);
}
to {
opacity: 1;
transform: translate3d(0, 0, 0);
}
}
@keyframes slideInUp {
from {
transform: translate3d(0, 100%, 0);
visibility: visible;
}
to {
transform: translate3d(0, 0, 0);
}
}
@keyframes slideInDown {
from {
transform: translate3d(0, -100%, 0);
visibility: visible;
}
to {
transform: translate3d(0, 0, 0);
}
}
@keyframes slideInLeft {
from {
transform: translate3d(-100%, 0, 0);
visibility: visible;
}
to {
transform: translate3d(0, 0, 0);
}
}
@keyframes slideInRight {
from {
transform: translate3d(100%, 0, 0);
visibility: visible;
}
to {
transform: translate3d(0, 0, 0);
}
}
@keyframes zoomIn {
from {
opacity: 0;
transform: scale3d(0.3, 0.3, 0.3);
}
50% {
opacity: 1;
}
}
@keyframes pulse {
from {
transform: scale3d(1, 1, 1);
}
50% {
transform: scale3d(1.05, 1.05, 1.05);
}
to {
transform: scale3d(1, 1, 1);
}
}
@keyframes typewriter {
from { width: 0; }
to { width: 100%; }
}
@keyframes blinkCursor {
from { border-right-color: rgba(0,0,0,.75); }
to { border-right-color: transparent; }
}
@keyframes reveal {
from { clip-path: inset(0 100% 0 0); }
to { clip-path: inset(0 0 0 0); }
}
@keyframes revealUp {
from { clip-path: inset(100% 0 0 0); }
to { clip-path: inset(0 0 0 0); }
}
@keyframes revealDown {
from { clip-path: inset(0 0 100% 0); }
to { clip-path: inset(0 0 0 0); }
}
@keyframes bounce {
from, 20%, 53%, 80%, to {
animation-timing-function: cubic-bezier(0.215, 0.610, 0.355, 1.000);
transform: translate3d(0, 0, 0);
}
40%, 43% {
animation-timing-function: cubic-bezier(0.755, 0.050, 0.855, 0.060);
transform: translate3d(0, -30px, 0);
}
70% {
animation-timing-function: cubic-bezier(0.755, 0.050, 0.855, 0.060);
transform: translate3d(0, -15px, 0);
}
90% {
transform: translate3d(0, -4px, 0);
}
}
@keyframes ripple {
0% {
transform: scale(0);
opacity: 1;
}
100% {
transform: scale(1);
opacity: 0;
}
}
@keyframes shine {
from {
background-position: -200% 0;
}
to {
background-position: 200% 0;
}
}
`;
};
/**
* Dynamically injects CSS keyframes into the document head
* Only injects them once even if called multiple times
*/
export const injectCSSKeyframes = () => {
// Check if we've already injected keyframes
if (typeof document !== 'undefined' && !document.getElementById('react-gsap-keyframes')) {
const styleElement = document.createElement('style');
styleElement.id = 'react-gsap-keyframes';
styleElement.textContent = generateCSSKeyframes();
document.head.appendChild(styleElement);
}
};
/**
* Maps GSAP easing functions to CSS equivalents
* @param {string} gsapEase - GSAP easing function name
* @returns {string} CSS easing equivalent
*/
export const mapEasing = (gsapEase = 'power2.out') => {
const easingMap = {
'none': 'linear',
'power1.in': 'cubic-bezier(0.550, 0.085, 0.680, 0.530)',
'power1.out': 'cubic-bezier(0.250, 0.460, 0.450, 0.940)',
'power1.inOut': 'cubic-bezier(0.455, 0.030, 0.515, 0.955)',
'power2.in': 'cubic-bezier(0.550, 0.055, 0.675, 0.190)',
'power2.out': 'cubic-bezier(0.215, 0.610, 0.355, 1.000)',
'power2.inOut': 'cubic-bezier(0.645, 0.045, 0.355, 1.000)',
'power3.in': 'cubic-bezier(0.895, 0.030, 0.685, 0.220)',
'power3.out': 'cubic-bezier(0.165, 0.840, 0.440, 1.000)',
'power3.inOut': 'cubic-bezier(0.770, 0.000, 0.175, 1.000)',
'power4.in': 'cubic-bezier(0.895, 0.030, 0.685, 0.220)',
'power4.out': 'cubic-bezier(0.165, 0.840, 0.440, 1.000)',
'power4.inOut': 'cubic-bezier(0.770, 0.000, 0.175, 1.000)',
'back.in': 'cubic-bezier(0.600, -0.280, 0.735, 0.045)',
'back.out': 'cubic-bezier(0.175, 0.885, 0.320, 1.275)',
'back.inOut': 'cubic-bezier(0.680, -0.550, 0.265, 1.550)',
'elastic.in': 'cubic-bezier(0.700, 0.000, 0.835, 0.000)',
'elastic.out': 'cubic-bezier(0.165, 0.840, 0.440, 1.000)',
'elastic.inOut': 'cubic-bezier(0.770, 0.000, 0.175, 1.000)',
'circ.in': 'cubic-bezier(0.600, 0.040, 0.980, 0.335)',
'circ.out': 'cubic-bezier(0.075, 0.820, 0.165, 1.000)',
'circ.inOut': 'cubic-bezier(0.785, 0.135, 0.150, 0.860)',
'expo.in': 'cubic-bezier(0.950, 0.050, 0.795, 0.035)',
'expo.out': 'cubic-bezier(0.190, 1.000, 0.220, 1.000)',
'expo.inOut': 'cubic-bezier(1.000, 0.000, 0.000, 1.000)',
'sine.in': 'cubic-bezier(0.470, 0.000, 0.745, 0.715)',
'sine.out': 'cubic-bezier(0.390, 0.575, 0.565, 1.000)',
'sine.inOut': 'cubic-bezier(0.445, 0.050, 0.550, 0.950)'
};
return easingMap[gsapEase] || 'ease-out';
};
/**
* Gets the CSS animation name for a GSAP-like animation
* @param {Object} props - Animation properties
* @returns {string} CSS animation name
*/
export const getAnimationName = (props) => {
const { x, y, opacity, scale } = props;
if (opacity === 0 && x === undefined && y === undefined && scale === undefined) {
return 'fadeIn';
}
if (opacity === 0 && y > 0) {
return 'fadeInUp';
}
if (opacity === 0 && y < 0) {
return 'fadeInDown';
}
if (opacity === 0 && x > 0) {
return 'fadeInLeft';
}
if (opacity === 0 && x < 0) {
return 'fadeInRight';
}
if (opacity === 0 && scale !== undefined && scale < 1) {
return 'zoomIn';
}
if (y > 50 && opacity === undefined) {
return 'slideInUp';
}
if (y < -50 && opacity === undefined) {
return 'slideInDown';
}
if (x > 50 && opacity === undefined) {
return 'slideInLeft';
}
if (x < -50 && opacity === undefined) {
return 'slideInRight';
}
return 'fadeIn'; // Default fallback
};
/**
* Applies CSS animation styles to an element
* @param {HTMLElement} element - DOM element to animate
* @param {Object} props - Animation properties
*/
export const applyCssAnimation = (element, props) => {
if (!element || !props) return;
const {
duration = 0.5,
delay = 0,
ease = 'power2.out',
repeat = 0,
yoyo = false,
onComplete
} = props;
// Inject keyframes if needed
injectCSSKeyframes();
// Get animation name based on properties
const animationName = getAnimationName(props);
// Map GSAP ease to CSS ease
const cssEase = mapEasing(ease);
// Set animation properties
element.style.animation = `${animationName} ${duration}s ${cssEase} ${delay}s ${repeat === -1 ? 'infinite' : repeat} ${yoyo ? 'alternate' : 'normal'} forwards`;
// Handle completion
if (onComplete && typeof onComplete === 'function') {
const animationEndHandler = () => {
onComplete();
element.removeEventListener('animationend', animationEndHandler);
};
element.addEventListener('animationend', animationEndHandler);
}
return {
element,
kill: () => {
element.style.animation = '';
}
};
};
/**
* Creates a test function to check if a feature is available in the current environment
* @param {Function} testFn - Function that tests feature availability
* @returns {boolean} Whether the feature is available
*/
export const canUseFeature = (testFn) => {
try {
return testFn();
} catch (e) {
return false;
}
};
/**
* Tests if GSAP is available
* @returns {boolean} Whether GSAP is available
*/
export const canUseGSAP = () => {
return canUseFeature(() => {
return typeof window !== 'undefined' && (
(window.gsap !== undefined) ||
(typeof require === 'function' && require('gsap') !== undefined)
);
});
};
/**
* Tests if Web Animation API is available
* @returns {boolean} Whether Web Animation API is available
*/
export const canUseWebAnimation = () => {
return canUseFeature(() => {
return typeof Element !== 'undefined' &&
typeof Element.prototype.animate === 'function';
});
};
/**
* Tests if ScrollTrigger is available
* @returns {boolean} Whether ScrollTrigger is available
*/
export const canUseScrollTrigger = () => {
return canUseFeature(() => {
const gsap = window.gsap || (typeof require === 'function' && require('gsap'));
return gsap && (
(gsap.plugins && gsap.plugins.ScrollTrigger) ||
window.ScrollTrigger ||
(typeof require === 'function' && require('gsap/ScrollTrigger'))
);
});
};
/**
* Tests if IntersectionObserver is available (as ScrollTrigger fallback)
* @returns {boolean} Whether IntersectionObserver is available
*/
export const canUseIntersectionObserver = () => {
return canUseFeature(() => {
return typeof IntersectionObserver === 'function';
});
};
export default {
injectCSSKeyframes,
mapEasing,
getAnimationName,
applyCssAnimation,
canUseGSAP,
canUseWebAnimation,
canUseScrollTrigger,
canUseIntersectionObserver
};