UNPKG

kinetic-slider

Version:

A WebGL-powered kinetic slider component using PIXI.js

422 lines (419 loc) 17.7 kB
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