loading
Just a moment.

3D Photo Carousel

November 2024

img
img
img
img
img
img
img
img
img
img
img
img
img
img
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;
 };
 */