kinetic-slider
Version:
A WebGL-powered kinetic slider component using PIXI.js
341 lines (338 loc) • 11.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';
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