react-gsap-animation-library
Version:
A comprehensive React component library that leverages GSAP for animations with official GSAP-React integration
194 lines (163 loc) • 6.29 kB
JSX
import React, { useRef, useEffect, useState } from 'react';
import { gsap } from 'gsap';
const AnimatedCursor = ({
size = 20,
color = 'rgba(0, 0, 0, 0.5)',
hoverColor = 'rgba(0, 0, 0, 0.8)',
hoverScale = 1.5,
hoverSelectors = ['a', 'button', 'input', '.clickable'],
mixBlendMode = '',
speed = 0.1,
trailLength = 0,
trailSpacing = 10,
cursorStyle = {}
}) => {
const cursorRef = useRef(null);
const cursorTrailRefs = useRef([]);
const mousePosition = useRef({ x: 0, y: 0 });
const cursorPosition = useRef({ x: 0, y: 0 });
const [hovering, setHovering] = useState(false);
const animationRef = useRef(null);
useEffect(() => {
const cursor = cursorRef.current;
// Set initial position off-screen
mousePosition.current = { x: -100, y: -100 };
cursorPosition.current = { x: -100, y: -100 };
// Create cursor trails if needed
if (trailLength > 0) {
for (let i = 0; i < trailLength; i++) {
const trail = document.createElement('div');
trail.className = 'cursor-trail';
trail.style.width = `${size * (1 - i * 0.1)}px`;
trail.style.height = `${size * (1 - i * 0.1)}px`;
trail.style.backgroundColor = color;
trail.style.borderRadius = '50%';
trail.style.position = 'fixed';
trail.style.pointerEvents = 'none';
trail.style.zIndex = 9999 - i;
trail.style.opacity = 1 - i * (0.7 / trailLength);
trail.style.transform = 'translate(-50%, -50%)';
trail.style.mixBlendMode = mixBlendMode;
document.body.appendChild(trail);
cursorTrailRefs.current.push(trail);
}
}
// Update mouse position on move
const handleMouseMove = (e) => {
mousePosition.current = { x: e.clientX, y: e.clientY };
};
// Handle hover states
const handleElementHover = () => {
setHovering(true);
};
const handleElementLeave = () => {
setHovering(false);
};
// Add hover listeners to all matching elements
const addHoverListeners = () => {
const elements = document.querySelectorAll(hoverSelectors.join(', '));
elements.forEach(el => {
el.addEventListener('mouseenter', handleElementHover);
el.addEventListener('mouseleave', handleElementLeave);
});
return elements;
};
const elements = addHoverListeners();
// Animate cursor position
const animateCursor = () => {
// Smooth cursor follow with easing
cursorPosition.current.x += (mousePosition.current.x - cursorPosition.current.x) * speed;
cursorPosition.current.y += (mousePosition.current.y - cursorPosition.current.y) * speed;
// Apply to cursor
gsap.set(cursor, {
x: cursorPosition.current.x,
y: cursorPosition.current.y,
backgroundColor: hovering ? hoverColor : color,
width: hovering ? size * hoverScale : size,
height: hovering ? size * hoverScale : size
});
// Apply to trails with delay
if (trailLength > 0) {
// Store positions history for trail
const trailPositions = [{ ...cursorPosition.current }];
cursorTrailRefs.current.forEach((trail, index) => {
const delay = (index + 1) * trailSpacing;
// Wait until we have enough position history
if (trailPositions.length > delay) {
const pos = trailPositions[trailPositions.length - delay - 1];
gsap.set(trail, {
x: pos.x,
y: pos.y,
backgroundColor: hovering ? hoverColor : color,
width: hovering ? size * (1 - index * 0.1) * hoverScale : size * (1 - index * 0.1),
height: hovering ? size * (1 - index * 0.1) * hoverScale : size * (1 - index * 0.1)
});
}
});
}
animationRef.current = requestAnimationFrame(animateCursor);
};
document.addEventListener('mousemove', handleMouseMove);
animationRef.current = requestAnimationFrame(animateCursor);
// Re-add listeners when elements change
const mutationObserver = new MutationObserver(() => {
// Clean up old listeners
elements.forEach(el => {
el.removeEventListener('mouseenter', handleElementHover);
el.removeEventListener('mouseleave', handleElementLeave);
});
// Add new listeners
addHoverListeners();
});
mutationObserver.observe(document.body, { childList: true, subtree: true });
return () => {
document.removeEventListener('mousemove', handleMouseMove);
if (animationRef.current) {
cancelAnimationFrame(animationRef.current);
}
mutationObserver.disconnect();
// Clean up hover listeners
elements.forEach(el => {
el.removeEventListener('mouseenter', handleElementHover);
el.removeEventListener('mouseleave', handleElementLeave);
});
// Remove cursor trails
cursorTrailRefs.current.forEach(trail => {
if (trail && document.body.contains(trail)) {
document.body.removeChild(trail);
}
});
};
}, [size, color, hoverColor, hoverScale, hoverSelectors.join(','), mixBlendMode, speed, trailLength, trailSpacing]);
const defaultCursorStyle = {
width: size,
height: size,
borderRadius: '50%',
backgroundColor: color,
position: 'fixed',
pointerEvents: 'none',
zIndex: 10000,
transform: 'translate(-50%, -50%)',
transition: 'width 0.2s, height 0.2s, background-color 0.2s',
mixBlendMode,
...cursorStyle
};
return (
<>
<style>
{`
/* Hide default cursor */
body {
cursor: none;
}
/* Make sure the cursor works over images */
img, svg, video {
pointer-events: none;
}
`}
</style>
<div ref={cursorRef} style={defaultCursorStyle} />
</>
);
};
export default AnimatedCursor;