UNPKG

lightswind

Version:

A professionally designed animate react component library & templates market that brings together functionality, accessibility, and beautiful aesthetics for modern applications.

171 lines (170 loc) 8.92 kB
"use client"; import { jsx as _jsx } from "react/jsx-runtime"; import { useEffect, useRef, useState, useMemo } from "react"; import { motion, AnimatePresence, useMotionValue, easeOut } from "framer-motion"; import { cn } from "../lib/utils"; // Assuming you have this utility for class names import { animate } from "framer-motion"; export function Draggable3DImageRing({ images, width = 300, perspective = 2000, imageDistance = 500, initialRotation = 180, animationDuration = 1.5, staggerDelay = 0.1, hoverOpacity = 0.5, containerClassName, ringClassName, imageClassName, backgroundColor, draggable = true, ease = "easeOut", mobileBreakpoint = 768, mobileScaleFactor = 0.8, inertiaPower = 0.8, // Default power for inertia inertiaTimeConstant = 300, // Default time constant for inertia inertiaVelocityMultiplier = 20, // Default multiplier for initial spin }) { const containerRef = useRef(null); const ringRef = useRef(null); const rotationY = useMotionValue(initialRotation); const startX = useRef(0); const currentRotationY = useRef(initialRotation); const isDragging = useRef(false); const velocity = useRef(0); // To track drag velocity const [currentScale, setCurrentScale] = useState(1); const [showImages, setShowImages] = useState(false); const angle = useMemo(() => 360 / images.length, [images.length]); const getBgPos = (imageIndex, currentRot, scale) => { const scaledImageDistance = imageDistance * scale; const effectiveRotation = currentRot - 180 - imageIndex * angle; const parallaxOffset = ((effectiveRotation % 360 + 360) % 360) / 360; return `${-(parallaxOffset * (scaledImageDistance / 1.5))}px 0px`; }; useEffect(() => { const unsubscribe = rotationY.on("change", (latestRotation) => { if (ringRef.current) { Array.from(ringRef.current.children).forEach((imgElement, i) => { imgElement.style.backgroundPosition = getBgPos(i, latestRotation, currentScale); }); } currentRotationY.current = latestRotation; }); return () => unsubscribe(); }, [rotationY, images.length, imageDistance, currentScale, angle]); useEffect(() => { const handleResize = () => { const viewportWidth = window.innerWidth; const newScale = viewportWidth <= mobileBreakpoint ? mobileScaleFactor : 1; setCurrentScale(newScale); }; window.addEventListener("resize", handleResize); handleResize(); return () => window.removeEventListener("resize", handleResize); }, [mobileBreakpoint, mobileScaleFactor]); useEffect(() => { setShowImages(true); }, []); const handleDragStart = (event) => { if (!draggable) return; isDragging.current = true; const clientX = "touches" in event ? event.touches[0].clientX : event.clientX; startX.current = clientX; // Stop any ongoing animation instantly when drag starts rotationY.stop(); velocity.current = 0; // Reset velocity if (ringRef.current) { ringRef.current.style.cursor = "grabbing"; } // Attach global move and end listeners to document when dragging starts document.addEventListener("mousemove", handleDrag); document.addEventListener("mouseup", handleDragEnd); document.addEventListener("touchmove", handleDrag); document.addEventListener("touchend", handleDragEnd); }; const handleDrag = (event) => { // Only proceed if dragging is active if (!draggable || !isDragging.current) return; const clientX = "touches" in event ? event.touches[0].clientX : event.clientX; const deltaX = clientX - startX.current; // Update velocity based on deltaX velocity.current = -deltaX * 0.5; // Factor of 0.5 to control sensitivity rotationY.set(currentRotationY.current + velocity.current); startX.current = clientX; }; const handleDragEnd = () => { isDragging.current = false; if (ringRef.current) { ringRef.current.style.cursor = "grab"; currentRotationY.current = rotationY.get(); } document.removeEventListener("mousemove", handleDrag); document.removeEventListener("mouseup", handleDragEnd); document.removeEventListener("touchmove", handleDrag); document.removeEventListener("touchend", handleDragEnd); const initial = rotationY.get(); const velocityBoost = velocity.current * inertiaVelocityMultiplier; const target = initial + velocityBoost; // Animate with inertia manually using `animate()` animate(initial, target, { type: "inertia", velocity: velocityBoost, power: inertiaPower, timeConstant: inertiaTimeConstant, restDelta: 0.5, modifyTarget: (target) => Math.round(target / angle) * angle, onUpdate: (latest) => { rotationY.set(latest); }, }); velocity.current = 0; }; // Corrected imageVariants: no function for 'visible' state const imageVariants = { hidden: { y: 200, opacity: 0 }, visible: { y: 0, opacity: 1, // Transition properties will be defined directly on the motion.div using `custom` prop }, }; return (_jsx("div", { ref: containerRef, className: cn("w-full h-full overflow-hidden select-none relative", containerClassName), style: { backgroundColor, transform: `scale(${currentScale})`, transformOrigin: "center center", }, // Attach initial drag start listeners only onMouseDown: draggable ? handleDragStart : undefined, onTouchStart: draggable ? handleDragStart : undefined, children: _jsx("div", { style: { perspective: `${perspective}px`, width: `${width}px`, height: `${width * 1.33}px`, position: "absolute", left: "50%", top: "50%", transform: "translate(-50%, -50%)", }, children: _jsx(motion.div, { ref: ringRef, className: cn("w-full h-full absolute", ringClassName), style: { transformStyle: "preserve-3d", rotateY: rotationY, cursor: draggable ? "grab" : "default", }, children: _jsx(AnimatePresence, { children: showImages && images.map((imageUrl, index) => (_jsx(motion.div, { className: cn("w-full h-full absolute", imageClassName), style: { transformStyle: "preserve-3d", backgroundImage: `url(${imageUrl})`, backgroundSize: "cover", backgroundRepeat: "no-repeat", backfaceVisibility: "hidden", rotateY: index * -angle, z: -imageDistance * currentScale, transformOrigin: `50% 50% ${imageDistance * currentScale}px`, backgroundPosition: getBgPos(index, currentRotationY.current, currentScale), }, initial: "hidden", animate: "visible", exit: "hidden", variants: imageVariants, custom: index, transition: { delay: index * staggerDelay, // Use index directly in transition duration: animationDuration, ease: easeOut, // Apply ease for entrance animation }, whileHover: { opacity: 1, transition: { duration: 0.15 } }, onHoverStart: () => { // Prevent hover effects while dragging if (isDragging.current) return; if (ringRef.current) { Array.from(ringRef.current.children).forEach((imgEl, i) => { if (i !== index) { imgEl.style.opacity = `${hoverOpacity}`; } }); } }, onHoverEnd: () => { // Prevent hover effects while dragging if (isDragging.current) return; if (ringRef.current) { Array.from(ringRef.current.children).forEach((imgEl) => { imgEl.style.opacity = `1`; }); } } }, index))) }) }) }) })); } export default Draggable3DImageRing;