From 83afb6e942273891eae8151407e1aeabaee65e0d Mon Sep 17 00:00:00 2001 From: bender Date: Tue, 3 Mar 2026 04:54:55 +0000 Subject: [PATCH] Switch to version 3: modified src/components/cardStack/hooks/useDepth3DAnimation.ts --- .../cardStack/hooks/useDepth3DAnimation.ts | 133 +++++++++++++++--- 1 file changed, 112 insertions(+), 21 deletions(-) diff --git a/src/components/cardStack/hooks/useDepth3DAnimation.ts b/src/components/cardStack/hooks/useDepth3DAnimation.ts index 89dfd74..1966225 100644 --- a/src/components/cardStack/hooks/useDepth3DAnimation.ts +++ b/src/components/cardStack/hooks/useDepth3DAnimation.ts @@ -1,27 +1,118 @@ -import { useEffect } from "react"; -import gsap from "gsap"; +import { useEffect, useState, useRef, RefObject } from "react"; -export const useDepth3DAnimation = (containerRef: React.RefObject) => { +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; + perspectiveRef?: RefObject; + isEnabled: boolean; +} + +export const useDepth3DAnimation = ({ + itemRefs, + containerRef, + perspectiveRef, + isEnabled, +}: UseDepth3DAnimationProps) => { + const [isMobile, setIsMobile] = useState(false); + + // Detect mobile viewport useEffect(() => { - if (!containerRef.current) return; + const checkMobile = () => { + setIsMobile(window.innerWidth < MOBILE_BREAKPOINT); + }; - const container = containerRef.current; - const cards = container.querySelectorAll("[data-card]"); + checkMobile(); + window.addEventListener("resize", checkMobile); - // Add scroll-triggered 3D animations - cards.forEach((card, index) => { - gsap.from(card, { - opacity: 0, - y: 50, - rotationX: 10, - duration: 0.8, - delay: index * 0.1, - scrollTrigger: { - trigger: card, - start: "top 80%", end: "top 20%", scrub: 1, - markers: false - } + return () => { + window.removeEventListener("resize", checkMobile); + }; + }, []); + + // 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)`; }); - }); - }, [containerRef]); + + 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 }; };