kinetic-slider
Version:
A WebGL-powered kinetic slider component using PIXI.js
422 lines (419 loc) • 17.7 kB
JavaScript
import { useRef, useCallback, useEffect } from 'react';
import { gsap } from 'gsap';
import { RenderScheduler } from '../managers/RenderScheduler.js';
import { UpdateType } from '../managers/UpdateTypes.js';
import { AnimationCoordinator, AnimationGroupType } from '../managers/AnimationCoordinator.js';
// Development environment check
const isDevelopment = "production" === 'development';
/**
* Hook to manage idle timer for resetting displacement effects
* Fully optimized with:
* - Batch resource management
* - Efficient timer handling
* - Comprehensive error handling
* - Memory leak prevention
* - Optimized animation management
* - Cancellation mechanisms
* - Animation coordination
*/
const useIdleTimer = ({ sliderRef, cursorActive, bgDispFilterRef, cursorDispFilterRef, cursorImgEffect, defaultBgFilterScale, defaultCursorFilterScale, idleTimeout = 300, resourceManager }) => {
// Store idle timer reference
const idleTimerRef = useRef(null);
// Store animation state with a ref to avoid re-renders
const animationStateRef = useRef({
isAnimating: false,
activeAnimations: [],
pendingBatchAnimations: []
});
// Flag to prevent operations during unmounting
const isUnmountingRef = useRef(false);
// Last animation operation timestamp for performance tracking
const lastAnimationOpRef = useRef(0);
// Get the animation coordinator
const animationCoordinator = AnimationCoordinator.getInstance();
/**
* Process pending animations in batch for better performance
*/
useCallback(() => {
try {
// Skip if unmounting or no resource manager
if (isUnmountingRef.current || !resourceManager)
return;
const { pendingBatchAnimations } = animationStateRef.current;
// Process animations in batch if any exist
if (pendingBatchAnimations.length > 0) {
if (isDevelopment) ;
resourceManager.trackAnimationBatch(pendingBatchAnimations);
// Clear the array after tracking (more efficient than creating a new array)
pendingBatchAnimations.length = 0;
// Record performance metrics
const now = performance.now();
const opTime = now - lastAnimationOpRef.current;
if (isDevelopment && lastAnimationOpRef.current > 0) ;
lastAnimationOpRef.current = now;
}
}
catch (error) {
// Clear pending animations even on error to avoid stuck state
animationStateRef.current.pendingBatchAnimations = [];
}
}, [resourceManager]);
/**
* Track an animation for batch processing
*/
useCallback((animation) => {
try {
// Skip if unmounting
if (isUnmountingRef.current)
return animation;
// Add to pending batch
animationStateRef.current.pendingBatchAnimations.push(animation);
return animation;
}
catch (error) {
return animation;
}
}, []);
/**
* Create animations for filter scale changes
* Now uses AnimationCoordinator for better coordination
*/
const createFilterAnimations = useCallback((targetScale, duration = 0.5, onComplete) => {
try {
// Skip if unmounting
if (isUnmountingRef.current)
return [];
// Record start time for performance tracking
lastAnimationOpRef.current = performance.now();
if (isDevelopment) ;
// Create animations array for batch tracking
const animations = [];
// Create background displacement filter animation
if (bgDispFilterRef.current) {
// Ensure we're using the correct target scale
const actualTargetScale = targetScale === 0 ? 0 : defaultBgFilterScale;
// Apply immediate scale if needed for visibility
if (targetScale > 0 && (bgDispFilterRef.current.scale.x === 0 || bgDispFilterRef.current.scale.y === 0)) {
bgDispFilterRef.current.scale.x = actualTargetScale;
bgDispFilterRef.current.scale.y = actualTargetScale;
if (isDevelopment) ;
}
const bgTween = gsap.to(bgDispFilterRef.current.scale, {
x: actualTargetScale,
y: actualTargetScale,
duration,
ease: "power2.out",
onComplete: () => {
// Re-track the filter after animation
if (resourceManager && bgDispFilterRef.current) {
resourceManager.trackFilter(bgDispFilterRef.current);
}
if (isDevelopment) ;
}
});
animations.push(bgTween);
}
// Create cursor displacement filter animation if enabled
if (cursorImgEffect && cursorDispFilterRef.current) {
const cursorScale = targetScale === 0 ? 0 : defaultCursorFilterScale;
// Apply immediate scale if needed for visibility
if (targetScale > 0 && (cursorDispFilterRef.current.scale.x === 0 || cursorDispFilterRef.current.scale.y === 0)) {
cursorDispFilterRef.current.scale.x = cursorScale;
cursorDispFilterRef.current.scale.y = cursorScale;
if (isDevelopment) ;
}
const cursorTween = gsap.to(cursorDispFilterRef.current.scale, {
x: cursorScale,
y: cursorScale,
duration,
ease: "power2.out",
onComplete: () => {
// Re-track the filter after animation
if (resourceManager && cursorDispFilterRef.current) {
resourceManager.trackFilter(cursorDispFilterRef.current);
}
if (isDevelopment) ;
}
});
animations.push(cursorTween);
}
// Use AnimationCoordinator to create a coordinated animation group
const groupType = targetScale === 0
? AnimationGroupType.IDLE_EFFECT
: AnimationGroupType.INTERACTION;
animationCoordinator.queueAnimationGroup({
id: `filter_animation_${Date.now()}`,
type: groupType,
animations,
onComplete
});
return animations;
}
catch (error) {
// Call completion handler even on error
if (onComplete)
onComplete();
return [];
}
}, [
bgDispFilterRef,
cursorDispFilterRef,
cursorImgEffect,
defaultCursorFilterScale,
defaultBgFilterScale,
resourceManager,
animationCoordinator
]);
/**
* Reset filters to idle state (no effect)
* Now uses AnimationCoordinator for better coordination
*/
const resetFilters = useCallback(() => {
try {
// Skip if unmounting
if (isUnmountingRef.current)
return;
if (isDevelopment) ;
// Cancel any active idle timer
if (idleTimerRef.current !== null) {
window.clearTimeout(idleTimerRef.current);
idleTimerRef.current = null;
}
// Skip if cursor is active
if (cursorActive.current) {
if (isDevelopment) ;
return;
}
// Cancel any existing animations of the same type
animationCoordinator.cancelAnimationsByType(AnimationGroupType.IDLE_EFFECT);
// Create animations to reset filters
createFilterAnimations(0, 0.5, () => {
if (isDevelopment) ;
});
// Schedule a render update
animationCoordinator.scheduleAnimationUpdate(AnimationGroupType.IDLE_EFFECT, () => {
if (isDevelopment) ;
}, 'idle_reset');
}
catch (error) {
}
}, [
cursorActive,
createFilterAnimations,
animationCoordinator
]);
/**
* Restore filters to active state
* Now uses AnimationCoordinator for better coordination
* @param immediate Whether to apply changes immediately without animation
*/
const restoreFilters = useCallback((immediate = false) => {
try {
// Skip if unmounting
if (isUnmountingRef.current)
return;
if (isDevelopment) ;
// Cancel any active idle timer
if (idleTimerRef.current !== null) {
window.clearTimeout(idleTimerRef.current);
idleTimerRef.current = null;
}
// Cancel any existing animations of the same type
animationCoordinator.cancelAnimationsByType(AnimationGroupType.INTERACTION);
// Ensure the cursor is marked as active
cursorActive.current = true;
// Apply immediate changes to ensure filters are visible right away
if (bgDispFilterRef.current) {
// Store original values for debugging
const originalX = bgDispFilterRef.current.scale.x;
const originalY = bgDispFilterRef.current.scale.y;
// Set to full scale values
bgDispFilterRef.current.scale.x = defaultBgFilterScale;
bgDispFilterRef.current.scale.y = defaultBgFilterScale;
if (isDevelopment) ;
}
if (cursorImgEffect && cursorDispFilterRef.current) {
// Store original values for debugging
const originalX = cursorDispFilterRef.current.scale.x;
const originalY = cursorDispFilterRef.current.scale.y;
// Set to full scale values
cursorDispFilterRef.current.scale.x = defaultCursorFilterScale;
cursorDispFilterRef.current.scale.y = defaultCursorFilterScale;
if (isDevelopment) ;
}
// Schedule an immediate render update to ensure changes are visible
RenderScheduler.getInstance().scheduleTypedUpdate('idleTimer', UpdateType.DISPLACEMENT_EFFECT, () => {
if (isDevelopment) ;
}, immediate ? 'critical' : 'high' // Use critical priority for immediate restoration
);
// If immediate is true, we've already set the filter scales directly
// For smoother transitions, still create animations but with shorter duration
const animationDuration = immediate ? 0.2 : 0.5;
// Create animations to restore filters (for smooth transition)
createFilterAnimations(defaultBgFilterScale, animationDuration, () => {
if (isDevelopment) ;
});
// Schedule a render update with appropriate priority
animationCoordinator.scheduleAnimationUpdate(AnimationGroupType.INTERACTION, () => {
if (isDevelopment) ;
}, immediate ? 'critical' : 'high');
}
catch (error) {
}
}, [
defaultBgFilterScale,
defaultCursorFilterScale,
bgDispFilterRef,
cursorDispFilterRef,
cursorImgEffect,
cursorActive,
createFilterAnimations,
animationCoordinator
]);
/**
* Handle mouse movement to reset idle timer
*/
const handleMouseMove = useCallback(() => {
try {
// Skip if unmounting
if (isUnmountingRef.current)
return;
// Cancel any active idle timer
if (idleTimerRef.current !== null) {
window.clearTimeout(idleTimerRef.current);
idleTimerRef.current = null;
}
// Set cursor as active
if (cursorActive.current !== true) {
cursorActive.current = true;
if (isDevelopment) ;
// Immediately restore filters when cursor becomes active after being inactive
// This ensures there's no delay in showing the displacement effect
const bgFilter = bgDispFilterRef.current;
const cursorFilter = cursorDispFilterRef.current;
if (bgFilter) {
// Apply full scale immediately for instant visibility
bgFilter.scale.x = defaultBgFilterScale;
bgFilter.scale.y = defaultBgFilterScale;
if (isDevelopment) ;
}
if (cursorImgEffect && cursorFilter) {
// Apply full scale immediately for instant visibility
cursorFilter.scale.x = defaultCursorFilterScale;
cursorFilter.scale.y = defaultCursorFilterScale;
if (isDevelopment) ;
}
// Schedule an immediate render update to ensure changes are visible
RenderScheduler.getInstance().scheduleTypedUpdate('idleTimer', UpdateType.DISPLACEMENT_EFFECT, () => {
if (isDevelopment) ;
}, 'critical' // Use critical priority to ensure immediate processing
);
// Then call restoreFilters to handle animations and other logic
restoreFilters(true); // Pass true to indicate immediate restoration
}
// Check if filters need to be restored
const bgFilter = bgDispFilterRef.current;
const cursorFilter = cursorDispFilterRef.current;
const bgFilterInactive = bgFilter && (bgFilter.scale.x === 0 || bgFilter.scale.y === 0);
const cursorFilterInactive = cursorImgEffect && cursorFilter &&
(cursorFilter.scale.x === 0 || cursorFilter.scale.y === 0);
if (bgFilterInactive || cursorFilterInactive) {
if (isDevelopment) ;
// Restore filters to active state immediately
restoreFilters(true); // Pass true to indicate immediate restoration
}
// Set a new idle timer
idleTimerRef.current = window.setTimeout(() => {
// Set cursor as inactive
cursorActive.current = false;
if (isDevelopment) ;
// Reset filters when idle
resetFilters();
// Clear timer reference
idleTimerRef.current = null;
}, idleTimeout);
}
catch (error) {
}
}, [
cursorActive,
bgDispFilterRef,
cursorDispFilterRef,
cursorImgEffect,
defaultBgFilterScale,
defaultCursorFilterScale,
idleTimeout,
resetFilters,
restoreFilters
]);
// Set up mouse movement tracking
useEffect(() => {
// Skip during server-side rendering
if (typeof window === 'undefined')
return;
// Skip if slider reference is not available
if (!sliderRef.current)
return;
// Reset unmounting flag
isUnmountingRef.current = false;
try {
const node = sliderRef.current;
// Register event listeners
if (resourceManager) {
// Batch registration with ResourceManager
const listeners = new Map();
listeners.set('mousemove', [handleMouseMove]);
resourceManager.addEventListenerBatch(node, listeners);
}
else {
// Direct registration
node.addEventListener('mousemove', handleMouseMove, { passive: true });
}
// Set initial idle timer
idleTimerRef.current = window.setTimeout(() => {
// Set cursor as inactive
cursorActive.current = false;
// Reset filters when idle
resetFilters();
// Clear timer reference
idleTimerRef.current = null;
}, idleTimeout);
// Cleanup on unmount
return () => {
// Update unmounting flag immediately
isUnmountingRef.current = true;
try {
// Cancel any active idle timer
if (idleTimerRef.current !== null) {
window.clearTimeout(idleTimerRef.current);
idleTimerRef.current = null;
}
// ResourceManager handles its own cleanup
if (!resourceManager) {
node.removeEventListener('mousemove', handleMouseMove);
}
}
catch (cleanupError) {
if (isDevelopment) ;
}
};
}
catch (error) {
// Return empty cleanup function
return () => { };
}
}, [
sliderRef,
handleMouseMove,
idleTimeout,
resetFilters,
resourceManager
]);
// Return methods for external control
return {
resetFilters,
restoreFilters
};
};
export { useIdleTimer as default };
//# sourceMappingURL=useIdleTimer.js.map