kinetic-slider
Version:
A WebGL-powered kinetic slider component using PIXI.js
428 lines (425 loc) • 18.5 kB
JavaScript
import { useRef, useCallback, useEffect } from 'react';
import { gsap } from 'gsap';
import { RenderScheduler } from '../managers/RenderScheduler.js';
import { UpdateType } from '../managers/UpdateTypes.js';
// Development environment check
const isDevelopment = "production" === 'development';
// Define a custom event for filter coordination
const FILTER_COORDINATION_EVENT = 'kinetic-slider:filter-update';
/**
* Hook to handle mouse movement tracking for displacement sprites
* Refactored to use RenderScheduler for batched updates
*/
const useMouseTracking = ({ sliderRef, backgroundDisplacementSpriteRef, cursorDisplacementSpriteRef, backgroundDisplacementFilterRef, cursorDisplacementFilterRef, cursorImgEffect, cursorMomentum, resourceManager }) => {
// Track active animations for batch processing
const activeAnimationsRef = useRef([]);
// Track component mount state
const isMountedRef = useRef(true);
// Track throttling state
const throttleStateRef = useRef({
lastThrottleTime: 0,
throttleDelay: 16 // ~60fps
});
// Track debouncing state for non-critical updates
const debounceStateRef = useRef({
debounceTimerId: 0,
debounceDelay: 100, // 100ms debounce for non-critical updates
lastDebounceTime: 0,
pendingUpdate: false
});
// Track last mouse position for use by scheduled updates
const lastMousePositionRef = useRef({
x: 0,
y: 0,
containerRect: null,
intensity: 0, // Store calculated intensity for reuse
timestamp: 0 // When this position was recorded
});
// Get the scheduler instance
const scheduler = RenderScheduler.getInstance();
// Process batch animations through ResourceManager
const processBatchAnimations = useCallback(() => {
try {
const animations = activeAnimationsRef.current;
// Skip if no ResourceManager or no animations
if (!resourceManager || animations.length === 0)
return;
// Track animations in batch
resourceManager.trackAnimationBatch(animations);
// Clear array by setting length to 0 (more efficient than creating new array)
animations.length = 0;
if (isDevelopment) ;
}
catch (error) {
// Clear array even on error to avoid stuck state
activeAnimationsRef.current = [];
}
}, [resourceManager]);
// Clean up active animations
const cleanupAnimations = useCallback(() => {
try {
const animations = activeAnimationsRef.current;
// Kill all animations
animations.forEach(tween => {
if (tween && tween.isActive()) {
tween.kill();
}
});
// Clear array
animations.length = 0;
}
catch (error) {
// Reset array even on error
activeAnimationsRef.current = [];
}
}, []);
// Calculate displacement intensity based on mouse position
const calculateDisplacementIntensity = useCallback((mouseX, mouseY, rect) => {
try {
// Calculate center point and distance
const centerX = rect.width / 2;
const centerY = rect.height / 2;
const distanceFromCenter = Math.sqrt(Math.pow(mouseX - centerX, 2) +
Math.pow(mouseY - centerY, 2));
// Calculate maximum possible distance
const maxDistance = Math.sqrt(Math.pow(rect.width / 2, 2) +
Math.pow(rect.height / 2, 2));
// Normalize intensity (0-1 range)
return Math.min(1, distanceFromCenter / (maxDistance * 0.7));
}
catch (error) {
// Return safe default value on error
return 0.5;
}
}, []);
/**
* Dispatch a custom event to coordinate with the filter system
* This allows for better integration with the filter batching system
* @param filterId The ID of the filter to update
* @param intensity The intensity value to set
* @param priority Optional priority level for the update (defaults to 'high')
*/
const dispatchFilterUpdate = useCallback((filterId, intensity, priority = 'high') => {
try {
if (typeof window === 'undefined')
return;
const detail = {
type: filterId,
intensity,
timestamp: Date.now(),
source: 'mouse-tracking',
priority
};
const event = new CustomEvent(FILTER_COORDINATION_EVENT, { detail });
window.dispatchEvent(event);
if (isDevelopment) ;
}
catch (error) {
}
}, []);
/**
* Animation function that gets scheduled by the RenderScheduler
* Creates and applies animations for mouse tracking displacement
*/
const animateDisplacementScheduled = useCallback(() => {
try {
if (!isMountedRef.current)
return;
// Get stored mouse position
const { x: mouseX, y: mouseY, containerRect, intensity: storedIntensity } = lastMousePositionRef.current;
if (!containerRect)
return;
// Use stored intensity if available, otherwise calculate it
const displacementIntensity = storedIntensity || calculateDisplacementIntensity(mouseX, mouseY, containerRect);
// Get current refs
const backgroundSprite = backgroundDisplacementSpriteRef.current;
const cursorSprite = cursorDisplacementSpriteRef.current;
const bgFilter = backgroundDisplacementFilterRef?.current;
const cursorFilter = cursorDisplacementFilterRef?.current;
if (isDevelopment) ;
// Clear existing animations
cleanupAnimations();
// Collect new animations
const newAnimations = [];
// Animate background displacement sprite
if (backgroundSprite) {
// Apply immediate position update for instant visibility
backgroundSprite.x = mouseX;
backgroundSprite.y = mouseY;
const bgSpriteTween = gsap.to(backgroundSprite, {
x: mouseX,
y: mouseY,
duration: cursorMomentum,
ease: 'power2.out'
});
newAnimations.push(bgSpriteTween);
// Animate background filter scale if available
if (bgFilter) {
const intensity = displacementIntensity * 30;
// Ensure the filter is visible by applying immediate scale
if (bgFilter.scale.x === 0 || bgFilter.scale.y === 0 || bgFilter.scale.x < intensity) {
if (isDevelopment) ;
bgFilter.scale.x = intensity;
bgFilter.scale.y = intensity;
}
// Dispatch filter update event for coordination with filter system
dispatchFilterUpdate('background-displacement', intensity, 'critical');
const bgFilterTween = gsap.to(bgFilter.scale, {
x: intensity,
y: intensity,
duration: cursorMomentum,
ease: 'power2.out'
});
newAnimations.push(bgFilterTween);
}
}
// Animate cursor displacement sprite if effect is enabled
if (cursorImgEffect && cursorSprite) {
// Apply immediate position update for instant visibility
cursorSprite.x = mouseX;
cursorSprite.y = mouseY;
const cursorSpriteTween = gsap.to(cursorSprite, {
x: mouseX,
y: mouseY,
duration: cursorMomentum,
ease: 'power2.out'
});
newAnimations.push(cursorSpriteTween);
// Animate cursor filter scale if available
if (cursorFilter) {
const intensity = displacementIntensity * 15;
// Ensure the filter is visible by applying immediate scale
if (cursorFilter.scale.x === 0 || cursorFilter.scale.y === 0 || cursorFilter.scale.x < intensity) {
if (isDevelopment) ;
cursorFilter.scale.x = intensity;
cursorFilter.scale.y = intensity;
}
// Dispatch filter update event for coordination with filter system
dispatchFilterUpdate('cursor-displacement', intensity, 'critical');
const cursorFilterTween = gsap.to(cursorFilter.scale, {
x: intensity,
y: intensity,
duration: cursorMomentum,
ease: 'power2.out'
});
newAnimations.push(cursorFilterTween);
}
}
// Add new animations to active animations ref
activeAnimationsRef.current.push(...newAnimations);
// Process animations in batch
processBatchAnimations();
// Schedule an immediate render update to ensure changes are visible
scheduler.scheduleTypedUpdate('mouseTracking', UpdateType.DISPLACEMENT_EFFECT, () => {
if (isDevelopment) ;
}, 'high');
// Reset pending update flag after processing
debounceStateRef.current.pendingUpdate = false;
}
catch (error) {
// Reset pending update flag even on error
debounceStateRef.current.pendingUpdate = false;
}
}, [
backgroundDisplacementSpriteRef,
cursorDisplacementSpriteRef,
backgroundDisplacementFilterRef,
cursorDisplacementFilterRef,
cursorImgEffect,
cursorMomentum,
cleanupAnimations,
processBatchAnimations,
calculateDisplacementIntensity,
dispatchFilterUpdate,
scheduler
]);
/**
* Debounced version of displacement animation for non-critical updates
* This is used when mouse movements are rapid but don't need immediate visual feedback
*/
useCallback(() => {
try {
if (!isMountedRef.current)
return;
// Clear any existing debounce timer
if (debounceStateRef.current.debounceTimerId) {
window.clearTimeout(debounceStateRef.current.debounceTimerId);
}
// Set a new debounce timer
debounceStateRef.current.debounceTimerId = window.setTimeout(() => {
// Only schedule if we have a pending update
if (debounceStateRef.current.pendingUpdate) {
scheduler.scheduleTypedUpdate('mouseTracking', UpdateType.FILTER_UPDATE, // Lower priority than direct mouse response
animateDisplacementScheduled, 'debounced');
}
}, debounceStateRef.current.debounceDelay);
}
catch (error) {
}
}, [animateDisplacementScheduled, scheduler]);
/**
* Handle mouse movement inside the slider
*/
const handleMouseMove = useCallback((event) => {
try {
if (!isMountedRef.current)
return;
// Cast to MouseEvent for accessing mouse-specific properties
const mouseEvent = event;
// Get current slider element
const slider = sliderRef.current;
if (!slider)
return;
// Get bounding rect
const rect = slider.getBoundingClientRect();
// Calculate mouse position relative to slider
const mouseX = mouseEvent.clientX - rect.left;
const mouseY = mouseEvent.clientY - rect.top;
// Store mouse position for use by scheduled updates
const lastPosition = lastMousePositionRef.current;
lastPosition.x = mouseX;
lastPosition.y = mouseY;
lastPosition.containerRect = rect;
lastPosition.timestamp = Date.now();
// Calculate displacement intensity only once and store it
lastPosition.intensity = calculateDisplacementIntensity(mouseX, mouseY, rect);
// Throttling for performance optimization
const now = Date.now();
const { lastThrottleTime, throttleDelay } = throttleStateRef.current;
if (now - lastThrottleTime < throttleDelay) {
// Skip update if we're throttling
return;
}
// Update throttle time
throttleStateRef.current.lastThrottleTime = now;
// Schedule the update with the render scheduler
scheduler.scheduleTypedUpdate('mouseTracking', UpdateType.MOUSE_RESPONSE, animateDisplacementScheduled, 'high');
}
catch (error) {
}
}, [sliderRef, animateDisplacementScheduled, calculateDisplacementIntensity, scheduler]);
/**
* Handle mouse leave event - fade out displacement effects
*/
const handleMouseLeave = useCallback(() => {
try {
if (!isMountedRef.current)
return;
if (isDevelopment) ;
// Get current refs
const backgroundSprite = backgroundDisplacementSpriteRef.current;
const cursorSprite = cursorDisplacementSpriteRef.current;
const bgFilter = backgroundDisplacementFilterRef?.current;
const cursorFilter = cursorDisplacementFilterRef?.current;
// Collect new animations
const newAnimations = [];
// Fade out background filter
if (bgFilter) {
const bgFilterTween = gsap.to(bgFilter.scale, {
x: 0,
y: 0,
duration: 0.5,
ease: "power2.out"
});
newAnimations.push(bgFilterTween);
// Dispatch filter update event for coordination with filter system
dispatchFilterUpdate('background-displacement', 0, 'high');
}
// Fade out cursor filter
if (cursorFilter) {
const cursorFilterTween = gsap.to(cursorFilter.scale, {
x: 0,
y: 0,
duration: 0.5,
ease: "power2.out"
});
newAnimations.push(cursorFilterTween);
// Dispatch filter update event for coordination with filter system
dispatchFilterUpdate('cursor-displacement', 0, 'high');
}
// Add new animations to active animations ref
activeAnimationsRef.current.push(...newAnimations);
// Process animations in batch
processBatchAnimations();
// Schedule an immediate render update
scheduler.scheduleTypedUpdate('mouseTracking', UpdateType.DISPLACEMENT_EFFECT, () => {
if (isDevelopment) ;
}, 'high');
}
catch (error) {
}
}, [
backgroundDisplacementFilterRef,
cursorDisplacementFilterRef,
backgroundDisplacementSpriteRef,
cursorDisplacementSpriteRef,
dispatchFilterUpdate,
processBatchAnimations,
scheduler
]);
// Set up mouse tracking
useEffect(() => {
// Skip during server-side rendering
if (typeof window === 'undefined')
return;
// Skip if slider reference is not available
if (!sliderRef.current)
return;
// Reset mounted state
isMountedRef.current = true;
try {
const node = sliderRef.current;
// Register event listeners
if (resourceManager) {
// Batch registration with ResourceManager
const listeners = new Map();
listeners.set('mousemove', [handleMouseMove]);
listeners.set('mouseleave', [handleMouseLeave]); // Add mouseleave handler
resourceManager.addEventListenerBatch(node, listeners);
}
else {
// Direct registration
node.addEventListener('mousemove', handleMouseMove, { passive: true });
node.addEventListener('mouseleave', handleMouseLeave, { passive: true }); // Add mouseleave handler
}
// Cleanup on unmount
return () => {
// Update mounted state immediately
isMountedRef.current = false;
try {
// Clean up animations
cleanupAnimations();
// Clear any debounce timer
if (debounceStateRef.current.debounceTimerId) {
window.clearTimeout(debounceStateRef.current.debounceTimerId);
debounceStateRef.current.debounceTimerId = 0;
}
// Cancel any scheduled updates
scheduler.cancelTypedUpdate('mouseTracking', UpdateType.MOUSE_RESPONSE);
scheduler.cancelTypedUpdate('mouseTracking', UpdateType.FILTER_UPDATE, 'debounced');
// ResourceManager handles its own cleanup
if (!resourceManager) {
node.removeEventListener('mousemove', handleMouseMove);
node.removeEventListener('mouseleave', handleMouseLeave); // Remove mouseleave handler
}
}
catch (cleanupError) {
if (isDevelopment) ;
}
};
}
catch (error) {
// Return empty cleanup function
return () => { };
}
}, [
sliderRef,
handleMouseMove,
handleMouseLeave, // Add handleMouseLeave to dependencies
cleanupAnimations,
resourceManager,
scheduler
]);
};
export { useMouseTracking as default };
//# sourceMappingURL=useMouseTracking.js.map