Switch to version 3: modified src/components/cardStack/hooks/useDepth3DAnimation.ts

This commit is contained in:
2026-06-03 21:50:10 +00:00
parent 47cf4e068f
commit e78ba0f13f

View File

@@ -1,88 +1,118 @@
import { useEffect } from 'react';
import { useMotionValue, useSpring, useTransform } from 'framer-motion';
import { Variants } from '@/types/AnimatePresence';
import { useEffect, useState, useRef, RefObject } from "react";
interface Depth3DAnimationProps {
perspective?: number;
depthFactor?: number;
hoverScale?: number;
transition?: {
duration?: number;
ease?: string;
};
springOptions?: {
stiffness?: number;
damping?: number;
mass?: number;
};
rotationXRange?: number[];
rotationYRange?: number[];
const MOBILE_BREAKPOINT = 768;
const ANIMATION_SPEED = 0.05;
const ROTATION_SPEED = 0.1;
const MOUSE_MULTIPLIER = 0.5;
const ROTATION_MULTIPLIER = 0.25;
interface UseDepth3DAnimationProps {
itemRefs: RefObject<(HTMLElement | null)[]>;
containerRef: RefObject<HTMLDivElement | null>;
perspectiveRef?: RefObject<HTMLDivElement | null>;
isEnabled: boolean;
}
export const useDepth3DAnimation = ({
perspective = 2000,
depthFactor = 20,
hoverScale = 1.05,
transition = { duration: 0.8, ease: [0.6, 0.01, -0.05, 0.9] },
springOptions = { stiffness: 400, damping: 10, mass: 1 },
rotationXRange = [-10, 10],
rotationYRange = [-10, 10],
}: Depth3DAnimationProps) => {
const x = useMotionValue(0);
const y = useMotionValue(0);
const mouseXSpring = useSpring(x, springOptions);
const mouseYSpring = useSpring(y, springOptions);
const rotateX = useTransform(
mouseYSpring,
[-0.5, 0.5],
rotationXRange as Variants<number[]>,
);
const rotateY = useTransform(
mouseXSpring,
[-0.5, 0.5],
rotationYRange as Variants<number[]>,
);
const scale = useTransform(mouseXSpring, [-0.5, 0.5], [1, hoverScale]);
const handleMouseMove = (event: React.MouseEvent) => {
const rect = (event.target as HTMLElement).getBoundingClientRect();
const width = rect.width;
const height = rect.height;
const mouseX = event.clientX - rect.left;
const mouseY = event.clientY - rect.top;
const xPct = mouseX / width - 0.5;
const yPct = mouseY / height - 0.5;
x.set(xPct);
y.set(yPct);
};
const handleMouseLeave = () => {
x.set(0);
y.set(0);
};
itemRefs,
containerRef,
perspectiveRef,
isEnabled,
}: UseDepth3DAnimationProps) => {
const [isMobile, setIsMobile] = useState(false);
// Detect mobile viewport
useEffect(() => {
// Cleanup function if needed, though Framer Motion handles many subscriptions internally.
const checkMobile = () => {
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
};
checkMobile();
window.addEventListener("resize", checkMobile);
return () => {
// No explicit cleanup for motion values in this simple case, but good to keep in mind.
window.removeEventListener("resize", checkMobile);
};
}, []);
return {
style: {
perspective: perspective + 'px',
transformStyle: 'preserve-3d',
rotateX,
rotateY,
scale,
transition: transition as Variants<typeof transition>,
// The depth effect is implicitly handled by the perspective and rotation transform
// Additional translateZ can be added for more explicit depth if needed
translateZ: depthFactor + 'px',
},
onMouseMove: handleMouseMove,
onMouseLeave: handleMouseLeave,
};
// 3D mouse-tracking effect (desktop only)
useEffect(() => {
if (!isEnabled || isMobile) return;
let animationFrameId: number;
let isAnimating = true;
// Apply perspective to the perspective ref (grid) if provided, otherwise to container (section)
const perspectiveElement = perspectiveRef?.current || containerRef.current;
if (perspectiveElement) {
perspectiveElement.style.perspective = "1200px";
perspectiveElement.style.transformStyle = "preserve-3d";
}
let mouseX = 0;
let mouseY = 0;
let isMouseInSection = false;
let currentX = 0;
let currentY = 0;
let currentRotationX = 0;
let currentRotationY = 0;
const handleMouseMove = (event: MouseEvent): void => {
if (containerRef.current) {
const rect = containerRef.current.getBoundingClientRect();
isMouseInSection =
event.clientX >= rect.left &&
event.clientX <= rect.right &&
event.clientY >= rect.top &&
event.clientY <= rect.bottom;
}
if (isMouseInSection) {
mouseX = (event.clientX / window.innerWidth) * 100 - 50;
mouseY = (event.clientY / window.innerHeight) * 100 - 50;
}
};
const animate = (): void => {
if (!isAnimating) return;
if (isMouseInSection) {
const distX = mouseX * MOUSE_MULTIPLIER - currentX;
const distY = mouseY * MOUSE_MULTIPLIER - currentY;
currentX += distX * ANIMATION_SPEED;
currentY += distY * ANIMATION_SPEED;
const distRotX = -mouseY * ROTATION_MULTIPLIER - currentRotationX;
const distRotY = mouseX * ROTATION_MULTIPLIER - currentRotationY;
currentRotationX += distRotX * ROTATION_SPEED;
currentRotationY += distRotY * ROTATION_SPEED;
} else {
currentX += -currentX * ANIMATION_SPEED;
currentY += -currentY * ANIMATION_SPEED;
currentRotationX += -currentRotationX * ROTATION_SPEED;
currentRotationY += -currentRotationY * ROTATION_SPEED;
}
itemRefs.current?.forEach((ref) => {
if (!ref) return;
ref.style.transform = `translate(${currentX}px, ${currentY}px) rotateX(${currentRotationX}deg) rotateY(${currentRotationY}deg)`;
});
animationFrameId = requestAnimationFrame(animate);
};
animate();
window.addEventListener("mousemove", handleMouseMove);
return () => {
window.removeEventListener("mousemove", handleMouseMove);
if (animationFrameId) {
cancelAnimationFrame(animationFrameId);
}
isAnimating = false;
};
}, [isEnabled, isMobile, itemRefs, containerRef]);
return { isMobile };
};