240 lines
7.2 KiB
TypeScript
240 lines
7.2 KiB
TypeScript
/* eslint-disable react-hooks/exhaustive-deps */
|
||
import gsap from "gsap";
|
||
import { useState, useEffect } from "react";
|
||
import { isMobile } from "react-device-detect";
|
||
import { useSwipeable } from "react-swipeable";
|
||
import { Transition } from "react-transition-group";
|
||
import Button from "../Button";
|
||
import ActiveResizeIcon from "../icons/ActiveResizeIcon";
|
||
import LeftArrowSliderIcon from "../icons/LeftArrowSliderIcon";
|
||
import RightArrowSliderIcon from "../icons/RightArrowSliderIcon";
|
||
|
||
// const marks = [
|
||
// {
|
||
// id: "1",
|
||
// label: "Башня 1",
|
||
// icon: "tower1",
|
||
// x: [1210, 1095, 770, 660, 820, 1060],
|
||
// y: [675, 795, 790, 670, 590, 590],
|
||
// url: "/tower1",
|
||
// },
|
||
// {
|
||
// id: "2",
|
||
// label: "Башня 2",
|
||
// icon: "tower2",
|
||
// x: [650, 835, 1080, 1240, 1080, 720],
|
||
// y: [650, 565, 575, 670, 800, 780],
|
||
// url: "/tower2",
|
||
// },
|
||
// {
|
||
// id: "3",
|
||
// label: "Двор",
|
||
// icon: "walk",
|
||
// x: [930, 930, 930, 930, 930, 930],
|
||
// y: [930, 930, 930, 930, 930, 930],
|
||
// url: "/virtual-tour",
|
||
// },
|
||
// {
|
||
// id: "4",
|
||
// label: "Внешний двор",
|
||
// icon: "walk",
|
||
// x: [1310, 980, 600, -100, -100, 1185],
|
||
// y: [1075, 1280, 1130, -100, -100, 860],
|
||
// url: "/virtual-tour-2",
|
||
// },
|
||
// ];
|
||
|
||
const arrayLength = 360;
|
||
const keyframes: number[] = [51, 178, 249, 339];
|
||
|
||
interface SequenceSliderProps {
|
||
path: string;
|
||
}
|
||
|
||
function SequenceSlider({ path }: SequenceSliderProps) {
|
||
const [selectedImageRightIndex, setSelectedRightImageIndex] =
|
||
useState<number>(keyframes[3]);
|
||
const [selectedImageLeftIndex, setSelectedImageLeftIndex] = useState<number>(
|
||
arrayLength - keyframes[3]
|
||
);
|
||
const [isAnimate, setIsAnimate] = useState<boolean>(false);
|
||
|
||
const [keyframeIndex, setKeyframeIndex] = useState<number>(3);
|
||
const handlers = useSwipeable({
|
||
trackMouse: true,
|
||
onSwipedLeft: () => !isAnimate && next(),
|
||
onSwipedRight: () => !isAnimate && prev(),
|
||
});
|
||
|
||
const [loadedImages, setLoadedImages] = useState<number>(0);
|
||
|
||
function handleLoad() {
|
||
setLoadedImages((prev) => prev + 1);
|
||
}
|
||
|
||
function animateToRight(currentOffset: number) {
|
||
const obj = { selectedImageIndex: selectedImageRightIndex };
|
||
|
||
gsap.to(obj, {
|
||
selectedImageIndex: selectedImageRightIndex + currentOffset,
|
||
duration: 0.75,
|
||
ease: "power1.inOut",
|
||
onUpdate: () => {
|
||
const roundedIndex = Math.round(obj.selectedImageIndex);
|
||
if (roundedIndex >= arrayLength) {
|
||
setSelectedRightImageIndex(roundedIndex - arrayLength);
|
||
setSelectedImageLeftIndex(arrayLength - (roundedIndex - arrayLength));
|
||
} else {
|
||
setSelectedRightImageIndex(roundedIndex);
|
||
setSelectedImageLeftIndex(arrayLength - roundedIndex);
|
||
}
|
||
},
|
||
onComplete: () => {
|
||
setIsAnimate(false);
|
||
},
|
||
});
|
||
}
|
||
|
||
function animateToLeft(currentOffset: number) {
|
||
const obj = { selectedImageIndex: selectedImageLeftIndex };
|
||
|
||
gsap.to(obj, {
|
||
selectedImageIndex: selectedImageLeftIndex + currentOffset,
|
||
duration: 0.75,
|
||
ease: "power1.inOut",
|
||
onUpdate: () => {
|
||
const roundedIndex = Math.round(obj.selectedImageIndex);
|
||
if (roundedIndex > arrayLength) {
|
||
setSelectedRightImageIndex(
|
||
arrayLength - (roundedIndex - arrayLength)
|
||
);
|
||
setSelectedImageLeftIndex(roundedIndex - arrayLength);
|
||
} else {
|
||
setSelectedImageLeftIndex(roundedIndex);
|
||
setSelectedRightImageIndex(arrayLength - roundedIndex);
|
||
}
|
||
},
|
||
onComplete: () => {
|
||
setIsAnimate(false);
|
||
},
|
||
});
|
||
}
|
||
|
||
function next() {
|
||
const updatedKeyframeIndex =
|
||
keyframeIndex !== keyframes.length - 1 ? keyframeIndex + 1 : 0;
|
||
const currentOffset =
|
||
keyframeIndex !== keyframes.length - 1
|
||
? keyframes[updatedKeyframeIndex] - keyframes[keyframeIndex]
|
||
: 360 - keyframes[keyframeIndex] + keyframes[0];
|
||
|
||
setKeyframeIndex(updatedKeyframeIndex);
|
||
|
||
setIsAnimate(true);
|
||
animateToRight(currentOffset);
|
||
}
|
||
|
||
function prev() {
|
||
const updatedKeyframeIndex =
|
||
keyframeIndex !== 0 ? keyframeIndex - 1 : keyframes.length - 1;
|
||
const currentOffset =
|
||
keyframeIndex !== 0
|
||
? keyframes[keyframeIndex] - keyframes[updatedKeyframeIndex]
|
||
: 360 - keyframes[updatedKeyframeIndex] + keyframes[keyframeIndex];
|
||
|
||
setKeyframeIndex(updatedKeyframeIndex);
|
||
|
||
setIsAnimate(true);
|
||
animateToLeft(currentOffset);
|
||
}
|
||
|
||
const [width, setWidth] = useState<number>();
|
||
const [top, setTop] = useState<number>();
|
||
|
||
function handleResize() {
|
||
setWidth(window.innerWidth);
|
||
setTop(window.innerWidth / 2 - window.innerHeight / 2);
|
||
}
|
||
|
||
useEffect(() => {
|
||
handleResize();
|
||
window.addEventListener("resize", handleResize);
|
||
return () => window.removeEventListener("resize", handleResize);
|
||
}, []);
|
||
|
||
return (
|
||
<div
|
||
{...handlers}
|
||
className="absolute w-full h-full overflow-hidden bg-black top-0 left-0 z-10"
|
||
>
|
||
<div
|
||
className="absolute left-0"
|
||
style={{ width: `${width}px`, height: `${width}px`, top: `-${top}px` }}
|
||
>
|
||
{Array.from({ length: arrayLength }).map((_, index) => (
|
||
<>
|
||
<img
|
||
key={index}
|
||
width={`${width}px`}
|
||
height={`${width}px`}
|
||
src={`${path}/${index + 1}.${isMobile ? "jpg" : "jpg"}`}
|
||
alt=""
|
||
className={`absolute t select-none pointer-events-none ${
|
||
index === selectedImageRightIndex ? "opacity-100" : "opacity-0"
|
||
}`}
|
||
onLoad={handleLoad}
|
||
/>
|
||
</>
|
||
))}
|
||
</div>
|
||
<Button
|
||
className={`p-3 rounded-full transition-colors absolute left-6 top-[calc(50%-24px)] z-50 ${
|
||
isAnimate ? "opacity-50" : "opacity-100"
|
||
}`}
|
||
buttonType="fab"
|
||
icon={<LeftArrowSliderIcon />}
|
||
onClick={prev}
|
||
/>
|
||
<Button
|
||
className={`p-3 rounded-full transition-colors absolute right-6 top-[calc(50%-24px)] z-50 ${
|
||
isAnimate ? "opacity-50" : "opacity-100"
|
||
}`}
|
||
buttonType="fab"
|
||
icon={<RightArrowSliderIcon />}
|
||
onClick={next}
|
||
/>
|
||
<div
|
||
className="absolute left-0 z-40 select-none"
|
||
style={{ width: `${width}px`, height: `${width}px`, top: `-${top}px` }}
|
||
>
|
||
<div
|
||
className={`transition-opacity duration-75 ${
|
||
keyframes.includes(selectedImageRightIndex)
|
||
? "opacity-100"
|
||
: "opacity-0"
|
||
}`}
|
||
></div>
|
||
</div>
|
||
|
||
<Transition
|
||
in={loadedImages !== arrayLength}
|
||
timeout={300}
|
||
mountOnEnter
|
||
unmountOnExit
|
||
>
|
||
{(state) => (
|
||
<div
|
||
className={`absolute top-0 left-0 w-full h-full flex items-center justify-center bg-neutral-950 z-50 transition-opacity duration-300 ${state}`}
|
||
>
|
||
<h2 className="text-2xl font-tenor text-white whitespace-nowrap">
|
||
Загрузка... {Math.round((100 / arrayLength) * loadedImages)} %
|
||
</h2>
|
||
</div>
|
||
)}
|
||
</Transition>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
export default SequenceSlider;
|