3D Photo Carousel
November 2024
TSX
import { PanInfo, motion, useAnimation, useMotionValue, useTransform } from 'framer-motion';
import { useMediaQuery } from '@/hook/useMediaQuery';
const IMGS = [
'https://images.unsplash.com/photo-1718119128153-645258b5b814?q=80&w=1887&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
'https://images.unsplash.com/photo-1718121279036-13650f74640b?q=80&w=1740&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
'https://images.unsplash.com/photo-1718113361290-35ca503e6092?q=80&w=1887&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
'https://images.unsplash.com/photo-1718113360777-8aadd1985ecd?q=80&w=1887&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
'https://images.unsplash.com/photo-1717844519228-40f041234a7d?q=80&w=1740&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
'https://images.unsplash.com/photo-1717844519137-62f09a0cbcc6?q=80&w=1887&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
'https://images.unsplash.com/photo-1718034363286-999f294f8523?q=80&w=1889&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
'https://images.unsplash.com/photo-1718046254440-77bb25734514?q=80&w=1752&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
'https://images.unsplash.com/photo-1718046254335-d9ff832c9c3c?q=80&w=1885&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
'https://images.unsplash.com/photo-1718070360743-d7103c38b266?q=80&w=1885&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
'https://images.unsplash.com/photo-1717960432608-b6faf49eaeb3?q=80&w=1887&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
'https://images.unsplash.com/photo-1717968368310-1110eae34644?q=80&w=1887&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
'https://images.unsplash.com/photo-1718058248054-5e7704c3c8ad?q=80&w=1893&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
'https://images.unsplash.com/photo-1716890385566-dee802c56d2d?q=80&w=1887&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
];
const ThreeDPhotoCarousel = () => {
const isScreenSizeSm = useMediaQuery('(max-width: 640px)');
const cylinderWidth = isScreenSizeSm ? 1100 : 1800;
const faceCount = IMGS.length;
const faceWidth = cylinderWidth / faceCount;
const dragFactor = 0.05;
const radius = cylinderWidth / (2 * Math.PI);
const rotation = useMotionValue(0);
const controls = useAnimation();
const handleDrag = (_: unknown, info: PanInfo) => {
rotation.set(rotation.get() + info.offset.x * dragFactor);
};
const handleDragEnd = (_: unknown, info: PanInfo) => {
controls.start({
rotateY: rotation.get() + info.velocity.x * dragFactor,
transition: { type: 'spring', stiffness: 100, damping: 30, mass: 0.1 },
});
};
const transform = useTransform(rotation, (value) => {
return `rotate3d(0, 1, 0, ${value}deg)`;
});
return (
<>
<div className="relative h-[500px] w-full overflow-hidden">
<div
className="pointer-events-none absolute left-0 top-0 z-10 h-full w-12"
style={{
background:
'linear-gradient(to left, rgba(0, 0, 0, 0) 0%, var(--background) 100%)',
}}
/>
<div
className="pointer-events-none absolute right-0 top-0 z-10 h-full w-12"
style={{
background:
'linear-gradient(to right, rgba(0, 0, 0, 0) 0%, var(--background) 100%)',
}}
/>
<div
className="flex h-full items-center justify-center bg-primary-dark-2"
style={{
perspective: '1000px',
transformStyle: 'preserve-3d',
transform: 'rotateX(0deg)',
}}
>
<motion.div
drag="x"
className="relative flex h-full origin-center cursor-grab justify-center active:cursor-grabbing"
style={{
transform: transform,
rotateY: rotation,
width: cylinderWidth,
transformStyle: 'preserve-3d',
}}
onDrag={handleDrag}
onDragEnd={handleDragEnd}
animate={controls}
>
{IMGS.map((url, i) => {
return (
<div
key={i}
className="absolute flex h-full origin-center items-center justify-center bg-primary-dark-2 p-2"
style={{
width: `${faceWidth}px`,
transform: `rotateY(${i * (360 / faceCount)}deg) translateZ(${radius}px)`,
}}
>
<img
src={url}
alt="img"
className="pointer-events-none h-12 w-full rounded-xl object-cover md:h-20"
/>
</div>
);
})}
</motion.div>
</div>
</div>
</>
);
};
export default ThreeDPhotoCarousel;
/*
@/hook/useMediaQuery.tsx
import React from "react";
import { useState, useCallback } from "react";
const useIsomorphicLayoutEffect =
typeof window !== "undefined" ? React.useLayoutEffect : React.useEffect;
export const useMediaQuery = (width: unknown) => {
const [targetReached, setTargetReached] = useState(false
const updateTarget = useCallback((e: { matches: boolean | ((prevState: boolean) => boolean); }) => {
setTargetReached(e.matches);
}, []);
useIsomorphicLayoutEffect(() => {
const media = window.matchMedia(`(max-width: ${width}px)`);
media.addListener(updateTarget);
if (media.matches) {
setTargetReached(true);
}
return () => media.removeListener(updateTarget);
}, [updateTarget, width]);
return targetReached;
};
*/