UNPKG

kinetic-slider

Version:

A WebGL-powered kinetic slider component using PIXI.js

341 lines (338 loc) 11.5 kB
import { useRef, useCallback, useEffect } from 'react'; import { gsap } from 'gsap'; import { RenderScheduler } from '../managers/RenderScheduler.js'; import { UpdateType } from '../managers/UpdateTypes.js'; const isDevelopment = false; const FILTER_COORDINATION_EVENT = "kinetic-slider:filter-update"; const useMouseTracking = ({ sliderRef, backgroundDisplacementSpriteRef, cursorDisplacementSpriteRef, backgroundDisplacementFilterRef, cursorDisplacementFilterRef, cursorImgEffect, cursorMomentum, resourceManager }) => { const activeAnimationsRef = useRef([]); const isMountedRef = useRef(true); const throttleStateRef = useRef({ lastThrottleTime: 0, throttleDelay: 16 // ~60fps }); const debounceStateRef = useRef({ debounceTimerId: 0, debounceDelay: 100, // 100ms debounce for non-critical updates lastDebounceTime: 0, pendingUpdate: false }); const lastMousePositionRef = useRef({ x: 0, y: 0, containerRect: null, intensity: 0, // Store calculated intensity for reuse timestamp: 0 // When this position was recorded }); const scheduler = RenderScheduler.getInstance(); const processBatchAnimations = useCallback(() => { try { const animations = activeAnimationsRef.current; if (!resourceManager || animations.length === 0) return; resourceManager.trackAnimationBatch(animations); animations.length = 0; if (isDevelopment) ; } catch (error) { activeAnimationsRef.current = []; } }, [resourceManager]); const cleanupAnimations = useCallback(() => { try { const animations = activeAnimationsRef.current; animations.forEach((tween) => { if (tween && tween.isActive()) { tween.kill(); } }); animations.length = 0; } catch (error) { activeAnimationsRef.current = []; } }, []); const calculateDisplacementIntensity = useCallback((mouseX, mouseY, rect) => { try { const centerX = rect.width / 2; const centerY = rect.height / 2; const distanceFromCenter = Math.sqrt( Math.pow(mouseX - centerX, 2) + Math.pow(mouseY - centerY, 2) ); const maxDistance = Math.sqrt( Math.pow(rect.width / 2, 2) + Math.pow(rect.height / 2, 2) ); return Math.min(1, distanceFromCenter / (maxDistance * 0.7)); } catch (error) { return 0.5; } }, []); 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) { } }, []); const animateDisplacementScheduled = useCallback(() => { try { if (!isMountedRef.current) return; const { x: mouseX, y: mouseY, containerRect, intensity: storedIntensity } = lastMousePositionRef.current; if (!containerRect) return; const displacementIntensity = storedIntensity || calculateDisplacementIntensity(mouseX, mouseY, containerRect); const backgroundSprite = backgroundDisplacementSpriteRef.current; const cursorSprite = cursorDisplacementSpriteRef.current; const bgFilter = backgroundDisplacementFilterRef?.current; const cursorFilter = cursorDisplacementFilterRef?.current; if (isDevelopment) ; cleanupAnimations(); const newAnimations = []; if (backgroundSprite) { backgroundSprite.x = mouseX; backgroundSprite.y = mouseY; const bgSpriteTween = gsap.to(backgroundSprite, { x: mouseX, y: mouseY, duration: cursorMomentum, ease: "power2.out" }); newAnimations.push(bgSpriteTween); if (bgFilter) { const intensity = displacementIntensity * 30; if (bgFilter.scale.x === 0 || bgFilter.scale.y === 0 || bgFilter.scale.x < intensity) { if (isDevelopment) ; bgFilter.scale.x = intensity; bgFilter.scale.y = intensity; } dispatchFilterUpdate("background-displacement", intensity, "critical"); const bgFilterTween = gsap.to(bgFilter.scale, { x: intensity, y: intensity, duration: cursorMomentum, ease: "power2.out" }); newAnimations.push(bgFilterTween); } } if (cursorImgEffect && cursorSprite) { cursorSprite.x = mouseX; cursorSprite.y = mouseY; const cursorSpriteTween = gsap.to(cursorSprite, { x: mouseX, y: mouseY, duration: cursorMomentum, ease: "power2.out" }); newAnimations.push(cursorSpriteTween); if (cursorFilter) { const intensity = displacementIntensity * 15; if (cursorFilter.scale.x === 0 || cursorFilter.scale.y === 0 || cursorFilter.scale.x < intensity) { if (isDevelopment) ; cursorFilter.scale.x = intensity; cursorFilter.scale.y = intensity; } dispatchFilterUpdate("cursor-displacement", intensity, "critical"); const cursorFilterTween = gsap.to(cursorFilter.scale, { x: intensity, y: intensity, duration: cursorMomentum, ease: "power2.out" }); newAnimations.push(cursorFilterTween); } } activeAnimationsRef.current.push(...newAnimations); processBatchAnimations(); scheduler.scheduleTypedUpdate( "mouseTracking", UpdateType.DISPLACEMENT_EFFECT, () => { if (isDevelopment) ; }, "high" ); debounceStateRef.current.pendingUpdate = false; } catch (error) { debounceStateRef.current.pendingUpdate = false; } }, [ backgroundDisplacementSpriteRef, cursorDisplacementSpriteRef, backgroundDisplacementFilterRef, cursorDisplacementFilterRef, cursorImgEffect, cursorMomentum, cleanupAnimations, processBatchAnimations, calculateDisplacementIntensity, dispatchFilterUpdate, scheduler ]); useCallback(() => { try { if (!isMountedRef.current) return; if (debounceStateRef.current.debounceTimerId) { window.clearTimeout(debounceStateRef.current.debounceTimerId); } debounceStateRef.current.debounceTimerId = window.setTimeout(() => { 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]); const handleMouseMove = useCallback((event) => { try { if (!isMountedRef.current) return; const mouseEvent = event; const slider = sliderRef.current; if (!slider) return; const rect = slider.getBoundingClientRect(); const mouseX = mouseEvent.clientX - rect.left; const mouseY = mouseEvent.clientY - rect.top; const lastPosition = lastMousePositionRef.current; lastPosition.x = mouseX; lastPosition.y = mouseY; lastPosition.containerRect = rect; lastPosition.timestamp = Date.now(); lastPosition.intensity = calculateDisplacementIntensity(mouseX, mouseY, rect); const now = Date.now(); const { lastThrottleTime, throttleDelay } = throttleStateRef.current; if (now - lastThrottleTime < throttleDelay) { return; } throttleStateRef.current.lastThrottleTime = now; scheduler.scheduleTypedUpdate( "mouseTracking", UpdateType.MOUSE_RESPONSE, animateDisplacementScheduled, "high" ); } catch (error) { } }, [sliderRef, animateDisplacementScheduled, calculateDisplacementIntensity, scheduler]); const handleMouseLeave = useCallback(() => { try { if (!isMountedRef.current) return; if (isDevelopment) ; const backgroundSprite = backgroundDisplacementSpriteRef.current; const cursorSprite = cursorDisplacementSpriteRef.current; const bgFilter = backgroundDisplacementFilterRef?.current; const cursorFilter = cursorDisplacementFilterRef?.current; const newAnimations = []; if (bgFilter) { const bgFilterTween = gsap.to(bgFilter.scale, { x: 0, y: 0, duration: 0.5, ease: "power2.out" }); newAnimations.push(bgFilterTween); dispatchFilterUpdate("background-displacement", 0, "high"); } if (cursorFilter) { const cursorFilterTween = gsap.to(cursorFilter.scale, { x: 0, y: 0, duration: 0.5, ease: "power2.out" }); newAnimations.push(cursorFilterTween); dispatchFilterUpdate("cursor-displacement", 0, "high"); } activeAnimationsRef.current.push(...newAnimations); processBatchAnimations(); scheduler.scheduleTypedUpdate( "mouseTracking", UpdateType.DISPLACEMENT_EFFECT, () => { if (isDevelopment) ; }, "high" ); } catch (error) { } }, [ backgroundDisplacementFilterRef, cursorDisplacementFilterRef, backgroundDisplacementSpriteRef, cursorDisplacementSpriteRef, dispatchFilterUpdate, processBatchAnimations, scheduler ]); useEffect(() => { if (typeof window === "undefined") return; if (!sliderRef.current) return; isMountedRef.current = true; try { const node = sliderRef.current; if (resourceManager) { const listeners = /* @__PURE__ */ new Map(); listeners.set("mousemove", [handleMouseMove]); listeners.set("mouseleave", [handleMouseLeave]); resourceManager.addEventListenerBatch(node, listeners); } else { node.addEventListener("mousemove", handleMouseMove, { passive: true }); node.addEventListener("mouseleave", handleMouseLeave, { passive: true }); } return () => { isMountedRef.current = false; try { cleanupAnimations(); if (debounceStateRef.current.debounceTimerId) { window.clearTimeout(debounceStateRef.current.debounceTimerId); debounceStateRef.current.debounceTimerId = 0; } scheduler.cancelTypedUpdate("mouseTracking", UpdateType.MOUSE_RESPONSE); scheduler.cancelTypedUpdate("mouseTracking", UpdateType.FILTER_UPDATE, "debounced"); if (!resourceManager) { node.removeEventListener("mousemove", handleMouseMove); node.removeEventListener("mouseleave", handleMouseLeave); } } catch (cleanupError) { if (isDevelopment) ; } }; } catch (error) { return () => { }; } }, [ sliderRef, handleMouseMove, handleMouseLeave, // Add handleMouseLeave to dependencies cleanupAnimations, resourceManager, scheduler ]); }; export { useMouseTracking as default }; //# sourceMappingURL=useMouseTracking.js.map