Files
irth-new-client/src/components/SequenceSlider.tsx
T

257 lines
8.2 KiB
TypeScript

// import { sequenceVideos } from "../data/sequenceVideos";
import { useState, useRef } from 'react';
import gsap from 'gsap';
import { useSwipeable } from 'react-swipeable';
import { motion, AnimatePresence } from 'motion/react';
import Button from './ui/Button';
import ArrowRightIcon from './icons/map/ArrowRightIcon';
import ArrowLeftIcon from './icons/ArrowLeftIcon';
import Compass from './Compass';
import { useNavigate } from 'react-router';
import InfoIcon from './icons/InfoIcon';
import FullScreenButton from './FullScreenButton';
import PrivacyPolicyButton from './PrivacyPolicyButton';
import DisclaimerButton from './DisclaimerButton';
import { masks } from '../data/masks';
interface SequenceSliderProps {
complexName: string;
}
const FRAME_COUNT = 360;
const FRAME_STEP = 90;
function SequenceSlider({ complexName }: SequenceSliderProps) {
const [currentIndex, setCurrentIndex] = useState(0);
const [isAnimating, setIsAnimating] = useState(false);
const handlers = useSwipeable({
onSwipedLeft: () => handleSwipe('next'),
onSwipedRight: () => handleSwipe('prev'),
preventScrollOnSwipe: true,
touchEventOptions: {
passive: false,
},
trackMouse: true,
});
const [imageLoaded, setImageLoaded] = useState(0);
const [isShowVideo, setIsShowVideo] = useState(true);
const directionRef = useRef<'next' | 'prev'>('next');
const [isFullScreen, setIsFullScreen] = useState(false);
function handleImageLoad() {
setImageLoaded((prev) => prev + 1);
}
function handleSwipe(direction: 'next' | 'prev') {
if (imageLoaded < FRAME_COUNT) return;
directionRef.current = direction;
setIsShowVideo(false);
}
function handleLoadVideo() {}
function animate(direction: 'next' | 'prev') {
setIsAnimating(true);
const targetIndex =
currentIndex + (direction === 'next' ? FRAME_STEP : -FRAME_STEP); // -1, -2
gsap.to(
{ value: currentIndex },
{
value: targetIndex,
duration: 1,
ease: 'power2.inOut',
onUpdate: function () {
const currentValue = Math.round(this.targets()[0].value);
if (currentValue > FRAME_COUNT - 1 || currentValue < 0) {
setCurrentIndex(FRAME_COUNT - Math.abs(currentValue));
} else {
setCurrentIndex(currentValue);
}
},
onComplete: () => {
setIsAnimating(false);
setIsShowVideo(true);
},
}
);
}
function handleFullScreenClick() {
if (!rootRef.current) return;
setIsFullScreen((prev) => !prev);
if (isFullScreen) {
document.exitFullscreen();
} else {
rootRef.current.requestFullscreen();
}
}
const rootRef = useRef<HTMLDivElement>(null);
const navigate = useNavigate();
return (
<div
{...handlers}
ref={(el) => {
handlers.ref(el);
rootRef.current = el;
}}
className='relative h-full overflow-hidden'
>
<AnimatePresence>
{imageLoaded < FRAME_COUNT && (
<motion.div
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className='absolute inset-0 z-10 flex flex-col items-center justify-center gap-2 bg-white'
>
<img
src={`/images/loader.png`}
alt=''
className='w-16 h-16 animate-spin'
/>
<p className='text-[#00BED7] text-m'>
{Math.round((imageLoaded / FRAME_COUNT) * 100)}%
</p>
</motion.div>
)}
</AnimatePresence>
{Array.from({ length: FRAME_COUNT }).map((_, index) => (
<img
key={index}
src={`/images/sequence/compressed-${
window.innerWidth < 768 ? 'mobile' : 'desktop'
}/${index}.jpg`}
alt=''
className='absolute object-cover w-full h-full pointer-events-none'
style={{
opacity: index === currentIndex ? 1 : 0,
}}
onLoad={handleImageLoad}
/>
))}
<AnimatePresence>
{isShowVideo && (
<motion.video
key={currentIndex}
src={`/videos/sequence/${
window.innerWidth < 768 ? 'mobile' : 'desktop'
}/${Math.floor(currentIndex / FRAME_STEP) + 1}.mp4`}
autoPlay
muted
loop
playsInline
className='absolute object-cover w-full h-full'
onLoad={handleLoadVideo}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2 }}
onAnimationComplete={({ opacity }: { opacity: number }) => {
if (!opacity) {
animate(directionRef.current);
}
}}
/>
)}
</AnimatePresence>
{!isAnimating && (
<svg
xmlns='http://www.w3.org/2000/svg'
viewBox='0 0 4096 1752'
className='absolute top-0 left-0 hidden w-full h-full md:block'
preserveAspectRatio='xMidYMid slice'
>
<path
d={`${
masks[complexName as keyof typeof masks][
Math.floor(currentIndex / FRAME_STEP)
]
}`}
className='fill-[#00BED7] cursor-pointer transition-opacity duration-300 opacity-0 hover:opacity-40'
onClick={() => navigate('floors')}
/>
</svg>
)}
{imageLoaded === FRAME_COUNT && (
<>
<div className='absolute flex 2xl:gap-[0.556vw] justify-between gap-2 2xl:left-[2.222vw] 2xl:right-[2.222vw] 2xl:top-[2.222vw] max-w-full md:max-2xl:left-6 md:max-2xl:right-6 md:max-2xl:top-6 left-4 right-4 top-4'>
<Button
variant='secondary'
className='!bg-white'
onClick={() => navigate('/')}
>
<span className='2xl:w-[1.389vw] 2xl:h-[1.389vw] w-5 h-5'>
<ArrowLeftIcon />
</span>
<span className='max-md:hidden'>Map</span>
</Button>
<Button
variant='secondary'
size='small'
className='max-md:hidden'
onClick={() => navigate('./about')}
>
<span className='2xl:w-[1.389vw] 2xl:h-[1.389vw] w-5 h-5'>
<InfoIcon />
</span>
<span>About</span>
</Button>
</div>
<p className='absolute text-xl font-bold text-white -translate-x-1/2 select-none left-1/2 top-9 max-md:hidden'>
ROVE Home Marasi Drive
</p>
<Button
onlyIcon
variant='secondary'
className='absolute top-1/2 -translate-y-1/2 2xl:left-[31.111vw] md:max-2xl:left-[8.854vw] left-4 !bg-[#0D1922]/40 backdrop-blur-md'
roundedFull
disabled={isAnimating || !isShowVideo}
onClick={() => handleSwipe('prev')}
>
<span className='2xl:w-[1.111vw] 2xl:h-[1.111vw] w-4 h-4 text-white'>
<ArrowLeftIcon />
</span>
</Button>
<Button
onlyIcon
variant='secondary'
className='absolute top-1/2 -translate-y-1/2 2xl:right-[31.111vw] md:max-2xl:right-[8.854vw] right-4 !bg-[#0D1922]/40 backdrop-blur-md'
roundedFull
disabled={isAnimating || !isShowVideo}
onClick={() => handleSwipe('next')}
>
<span className='2xl:w-[1.111vw] 2xl:h-[1.111vw] w-4 h-4 text-white'>
<ArrowRightIcon />
</span>
</Button>
<Compass imgStyle={{ transform: `rotate(${currentIndex}deg)` }} />
<div className='absolute 2xl:bottom-[2.222vw] 2xl:right-[2.222vw] 2xl:left-[2.222vw] max-w-full flex justify-end items-center 2xl:gap-[0.556vw] gap-2 md:max-2xl:bottom-6 md:max-2xl:left-6 md:max-2xl:right-6 bottom-4 left-4 right-4'>
<DisclaimerButton />
<PrivacyPolicyButton />
<FullScreenButton
isFullScreen={isFullScreen}
onFullScreenChange={setIsFullScreen}
onClick={handleFullScreenClick}
/>
</div>
</>
)}
</div>
);
}
export default SequenceSlider;