257 lines
8.2 KiB
TypeScript
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;
|