kinetic-slider
Version:
A WebGL-powered kinetic slider component using PIXI.js
302 lines (299 loc) • 11.3 kB
JavaScript
import { useRef, useCallback, useEffect } from 'react';
import { gsap } from 'gsap';
// Development environment check
const isDevelopment = "production" === 'development';
/**
* Hook to handle mouse drag interactions for slides
* Fully optimized with:
* - Batch animation processing
* - Comprehensive error handling
* - Memory leak prevention
* - Performance optimizations
* - Throttled event handling
*/
const useMouseDrag = ({ sliderRef, slidesRef, currentIndex, swipeScaleIntensity, swipeDistance, onSwipeLeft, onSwipeRight, resourceManager }) => {
// Track drag state with a ref to avoid re-renders
const dragStateRef = useRef({
isDragging: false,
startX: 0,
endX: 0,
isAnimating: false,
lastThrottleTime: 0,
activeAnimations: []
});
// Flag to track component mount state
const isMountedRef = useRef(true);
// Process a batch of animations using ResourceManager
const processBatchAnimations = useCallback(() => {
try {
const { activeAnimations } = dragStateRef.current;
// Skip if no resource manager or no animations
if (!resourceManager || activeAnimations.length === 0)
return;
// Track all animations in batch
resourceManager.trackAnimationBatch(activeAnimations);
// Clear the array by setting length to 0 (more efficient than creating a new array)
activeAnimations.length = 0;
if (isDevelopment) ;
}
catch (error) {
// Clear array even on error
dragStateRef.current.activeAnimations = [];
}
}, [resourceManager]);
// Clean up any active animations
const cleanupAnimations = useCallback(() => {
try {
const { activeAnimations } = dragStateRef.current;
// Kill all active animations
activeAnimations.forEach(tween => {
if (tween && tween.isActive()) {
tween.kill();
}
});
// Clear the animations array
activeAnimations.length = 0;
// Reset animation state
dragStateRef.current.isAnimating = false;
}
catch (error) {
}
}, []);
// Handle mouse drag effects
const handleDragEffect = useCallback((deltaX) => {
try {
if (!isMountedRef.current)
return;
const currentSlide = slidesRef.current[currentIndex.current];
if (!currentSlide)
return;
// Calculate normalized scale factor based on drag distance
const normalizedFactor = Math.min(Math.abs(deltaX) / swipeDistance, 1);
const newScale = 1 + normalizedFactor * swipeScaleIntensity;
// Clean up any existing animations
cleanupAnimations();
// Create and add the new animation to our active animations
const dragTween = gsap.to(currentSlide.scale, {
x: currentSlide.baseScale * newScale,
y: currentSlide.baseScale * newScale,
duration: 0.1,
ease: "power2.out",
onComplete: () => {
// Re-track the sprite after animation
if (resourceManager && currentSlide) {
resourceManager.trackDisplayObject(currentSlide);
}
}
});
// Add to animations array
dragStateRef.current.activeAnimations.push(dragTween);
// Process animations in batch
processBatchAnimations();
}
catch (error) {
}
}, [
slidesRef,
currentIndex,
swipeScaleIntensity,
swipeDistance,
cleanupAnimations,
processBatchAnimations,
resourceManager
]);
// Reset slide scale after drag
const resetSlideScale = useCallback(() => {
try {
if (!isMountedRef.current)
return;
const currentSlide = slidesRef.current[currentIndex.current];
if (!currentSlide)
return;
// Clean up any existing animations
cleanupAnimations();
// Create and add the reset animation
const resetTween = gsap.to(currentSlide.scale, {
x: currentSlide.baseScale,
y: currentSlide.baseScale,
duration: 0.2,
ease: "power2.out",
onComplete: () => {
// Re-track the sprite after animation
if (resourceManager && currentSlide) {
resourceManager.trackDisplayObject(currentSlide);
}
// Update state
dragStateRef.current.isAnimating = false;
}
});
// Add to animations array
dragStateRef.current.activeAnimations.push(resetTween);
// Process animations in batch
processBatchAnimations();
}
catch (error) {
// Ensure animation state is reset even on error
dragStateRef.current.isAnimating = false;
}
}, [
slidesRef,
currentIndex,
cleanupAnimations,
processBatchAnimations,
resourceManager
]);
// Event handlers with memoization
const handleMouseDown = useCallback((event) => {
try {
const e = event;
e.preventDefault();
// Set dragging state
dragStateRef.current.isDragging = true;
dragStateRef.current.startX = e.clientX;
dragStateRef.current.endX = e.clientX;
}
catch (error) {
}
}, []);
const handleMouseMove = useCallback((event) => {
try {
// Skip if not dragging
if (!dragStateRef.current.isDragging)
return;
const e = event;
e.preventDefault();
// Apply throttling for performance (limit to 60fps)
const now = Date.now();
const throttleDelay = 16; // ~60fps
if (now - dragStateRef.current.lastThrottleTime < throttleDelay)
return;
dragStateRef.current.lastThrottleTime = now;
// Update drag end position
dragStateRef.current.endX = e.clientX;
// Calculate drag distance
const deltaX = dragStateRef.current.endX - dragStateRef.current.startX;
// Apply drag effect
handleDragEffect(deltaX);
}
catch (error) {
}
}, [handleDragEffect]);
const handleMouseUp = useCallback((event) => {
try {
// Skip if not dragging
if (!dragStateRef.current.isDragging)
return;
const e = event;
e.preventDefault();
// Update state
dragStateRef.current.isDragging = false;
// Calculate final drag distance
const deltaX = dragStateRef.current.endX - dragStateRef.current.startX;
// Reset slide scale first
resetSlideScale();
// Handle swipe if distance is sufficient
if (Math.abs(deltaX) > swipeDistance) {
// Set small timeout to allow scale reset to complete
setTimeout(() => {
if (!isMountedRef.current)
return;
if (deltaX < 0) {
onSwipeLeft();
}
else {
onSwipeRight();
}
}, 50);
}
}
catch (error) {
// Reset dragging state even on error
dragStateRef.current.isDragging = false;
}
}, [resetSlideScale, swipeDistance, onSwipeLeft, onSwipeRight]);
const handleMouseLeave = useCallback((event) => {
try {
// Skip if not dragging
if (!dragStateRef.current.isDragging)
return;
const e = event;
e.preventDefault();
// Update state
dragStateRef.current.isDragging = false;
// Reset slide scale
resetSlideScale();
}
catch (error) {
// Reset dragging state even on error
dragStateRef.current.isDragging = false;
}
}, [resetSlideScale]);
// Set up event listeners with batch handling
useEffect(() => {
// Skip during server-side rendering
if (typeof window === 'undefined')
return;
// Skip if slider reference is not available
if (!sliderRef.current)
return;
try {
const slider = sliderRef.current;
// Update mounted ref
isMountedRef.current = true;
// Create event handler maps for batch registration
const listeners = new Map();
// Maps for all our event types
listeners.set('mousedown', [handleMouseDown]);
listeners.set('mousemove', [handleMouseMove]);
listeners.set('mouseup', [handleMouseUp]);
listeners.set('mouseleave', [handleMouseLeave]);
// Register event listeners - use batch if available
if (resourceManager) {
// Batch register all listeners
resourceManager.addEventListenerBatch(slider, listeners);
}
else {
// Fall back to individual registration
slider.addEventListener('mousedown', handleMouseDown);
slider.addEventListener('mousemove', handleMouseMove);
slider.addEventListener('mouseup', handleMouseUp);
slider.addEventListener('mouseleave', handleMouseLeave);
}
// Cleanup on unmount
return () => {
// Update mounted state immediately
isMountedRef.current = false;
try {
// Clean up animations first
cleanupAnimations();
// ResourceManager will handle its own cleanup
if (!resourceManager) {
// Manual cleanup for each event
slider.removeEventListener('mousedown', handleMouseDown);
slider.removeEventListener('mousemove', handleMouseMove);
slider.removeEventListener('mouseup', handleMouseUp);
slider.removeEventListener('mouseleave', handleMouseLeave);
}
}
catch (cleanupError) {
if (isDevelopment) ;
}
};
}
catch (error) {
// Return empty cleanup function
return () => { };
}
}, [
sliderRef,
handleMouseDown,
handleMouseMove,
handleMouseUp,
handleMouseLeave,
cleanupAnimations,
resourceManager
]);
// No return value needed - hook works internally
};
export { useMouseDrag as default };
//# sourceMappingURL=useMouseDrag.js.map