kinetic-slider
Version:
A WebGL-powered kinetic slider component using PIXI.js
280 lines (277 loc) • 9.2 kB
JavaScript
import { useRef, useEffect } from 'react';
import { gsap } from 'gsap';
const logError = (context, error) => {
};
const useTextTilt = ({
sliderRef,
textContainersRef,
currentIndex,
cursorTextEffect,
maxContainerShiftFraction,
bgDispFilterRef,
cursorDispFilterRef,
cursorImgEffect,
resourceManager,
throttleTime = 50
}) => {
const lastMoveTimeRef = useRef(0);
const tiltTimeoutRef = useRef(null);
const animationStateRef = useRef({
isAnimating: false,
lastOffsetX: 0,
lastOffsetY: 0
});
const cancellationRef = useRef({ isCancelled: false });
const activeTweensRef = useRef([]);
useEffect(() => {
if (typeof window === "undefined") return;
if (!cursorTextEffect || !sliderRef.current) {
return;
}
cancellationRef.current.isCancelled = false;
const sliderElement = sliderRef.current;
const cleanupActiveTweens = () => {
try {
activeTweensRef.current.forEach((tween) => {
if (tween && tween.isActive()) {
tween.kill();
}
});
activeTweensRef.current = [];
} catch (error) {
}
};
const calculateTiltValues = (mouseX, mouseY) => {
try {
if (!sliderElement) {
logError("calculateTiltValues", "Slider element is undefined");
return {
containerShiftX: 0,
containerShiftY: 0,
titleShiftX: 0,
subtitleShiftX: 0,
centerX: 0,
centerY: 0
};
}
const containerWidth = sliderElement.clientWidth;
const containerHeight = sliderElement.clientHeight;
if (containerWidth <= 0 || containerHeight <= 0) {
logError("calculateTiltValues", "Invalid container dimensions");
return {
containerShiftX: 0,
containerShiftY: 0,
titleShiftX: 0,
subtitleShiftX: 0,
centerX: 0,
centerY: 0
};
}
const centerX = containerWidth / 2;
const centerY = containerHeight / 2;
const offsetX = centerX - mouseX;
const offsetY = centerY - mouseY;
animationStateRef.current.lastOffsetX = offsetX;
animationStateRef.current.lastOffsetY = offsetY;
const rawContainerShiftX = offsetX * 0.05;
const rawContainerShiftY = offsetY * 0.1;
const maxShiftX = containerWidth * maxContainerShiftFraction;
const maxShiftY = containerHeight * maxContainerShiftFraction;
const containerShiftX = Math.max(Math.min(rawContainerShiftX, maxShiftX), -maxShiftX);
const containerShiftY = Math.max(Math.min(rawContainerShiftY, maxShiftY), -maxShiftY);
const maxTitleShift = containerWidth * 0.1;
const titleRawShiftX = offsetX * 0.8;
const titleShiftX = Math.max(Math.min(titleRawShiftX, maxTitleShift), -maxTitleShift);
const maxSubtitleShift = containerWidth * 0.15;
const subtitleShiftX = Math.max(Math.min(offsetX, maxSubtitleShift), -maxSubtitleShift);
return {
containerShiftX,
containerShiftY,
titleShiftX,
subtitleShiftX,
centerX,
centerY
};
} catch (error) {
return {
containerShiftX: 0,
containerShiftY: 0,
titleShiftX: 0,
subtitleShiftX: 0,
centerX: sliderElement?.clientWidth / 2 || 0,
centerY: sliderElement?.clientHeight / 2 || 0
};
}
};
const applyTiltEffect = (tiltValues) => {
try {
if (cancellationRef.current.isCancelled) return;
const activeTextContainer = textContainersRef.current[currentIndex.current];
if (!activeTextContainer || activeTextContainer.children.length < 2) {
return;
}
cleanupActiveTweens();
const createTrackedTween = (target, props) => {
const tween = gsap.to(target, {
...props,
onComplete: () => {
if (resourceManager) {
resourceManager.trackDisplayObject(target);
}
}
});
if (resourceManager) {
resourceManager.trackAnimation(tween);
}
activeTweensRef.current.push(tween);
return tween;
};
createTrackedTween(activeTextContainer, {
x: tiltValues.centerX + tiltValues.containerShiftX,
y: tiltValues.centerY + tiltValues.containerShiftY,
duration: 0.5,
ease: "expo.out"
});
if (activeTextContainer.children[0]) {
createTrackedTween(activeTextContainer.children[0], {
x: tiltValues.titleShiftX,
duration: 0.5,
ease: "expo.out"
});
}
if (activeTextContainer.children[1]) {
createTrackedTween(activeTextContainer.children[1], {
x: tiltValues.subtitleShiftX,
duration: 0.5,
ease: "expo.out"
});
}
animationStateRef.current.isAnimating = true;
} catch (error) {
animationStateRef.current.isAnimating = false;
}
};
const resetTiltEffect = () => {
try {
if (cancellationRef.current.isCancelled) return;
const activeContainer = textContainersRef.current[currentIndex.current];
if (!activeContainer) return;
cleanupActiveTweens();
const centerX = sliderElement.clientWidth / 2;
const centerY = sliderElement.clientHeight / 2;
const createResetTween = (target, props) => {
const tween = gsap.to(target, {
...props,
duration: 1,
ease: "expo.inOut",
onComplete: () => {
if (resourceManager) {
resourceManager.trackDisplayObject(target);
}
}
});
if (resourceManager) {
resourceManager.trackAnimation(tween);
}
activeTweensRef.current.push(tween);
return tween;
};
createResetTween(activeContainer, {
x: centerX,
y: centerY,
onComplete: () => {
animationStateRef.current.isAnimating = false;
}
});
if (activeContainer.children[0]) {
createResetTween(activeContainer.children[0], { x: 0 });
}
if (activeContainer.children[1]) {
createResetTween(activeContainer.children[1], { x: 0 });
}
const resetFilterTween = (filterRef) => {
if (filterRef.current) {
const tween = gsap.to(filterRef.current.scale, {
x: 0,
y: 0,
duration: 1,
ease: "expo.inOut",
onComplete: () => {
if (resourceManager && filterRef.current) {
resourceManager.trackFilter(filterRef.current);
}
}
});
if (resourceManager) {
resourceManager.trackAnimation(tween);
}
activeTweensRef.current.push(tween);
}
};
resetFilterTween(bgDispFilterRef);
if (cursorImgEffect) {
resetFilterTween(cursorDispFilterRef);
}
} catch (error) {
animationStateRef.current.isAnimating = false;
}
};
const handleTextTilt = (e) => {
try {
if (cancellationRef.current.isCancelled) return;
const now = Date.now();
if (now - lastMoveTimeRef.current < throttleTime) {
return;
}
lastMoveTimeRef.current = now;
const tiltValues = calculateTiltValues(e.clientX, e.clientY);
applyTiltEffect(tiltValues);
if (tiltTimeoutRef.current !== null) {
if (resourceManager) {
resourceManager.clearTimeout(tiltTimeoutRef.current);
} else {
clearTimeout(tiltTimeoutRef.current);
}
tiltTimeoutRef.current = null;
}
const setTimeoutFn = () => {
resetTiltEffect();
tiltTimeoutRef.current = null;
};
if (resourceManager) {
tiltTimeoutRef.current = resourceManager.setTimeout(setTimeoutFn, 300);
} else {
tiltTimeoutRef.current = window.setTimeout(setTimeoutFn, 300);
}
} catch (error) {
}
};
sliderElement.addEventListener("mousemove", handleTextTilt, { passive: true });
return () => {
cancellationRef.current.isCancelled = true;
sliderElement.removeEventListener("mousemove", handleTextTilt);
cleanupActiveTweens();
if (tiltTimeoutRef.current !== null) {
if (resourceManager) {
resourceManager.clearTimeout(tiltTimeoutRef.current);
} else {
clearTimeout(tiltTimeoutRef.current);
}
tiltTimeoutRef.current = null;
}
};
}, [
sliderRef,
textContainersRef,
currentIndex,
cursorTextEffect,
maxContainerShiftFraction,
bgDispFilterRef,
cursorDispFilterRef,
cursorImgEffect,
resourceManager,
throttleTime
]);
};
export { useTextTilt as default };
//# sourceMappingURL=useTextTilt.js.map