clouds completed, z-indices

This commit is contained in:
2025-04-18 17:02:04 +05:00
parent 7b41de5b8f
commit 0ef6009b57
6 changed files with 112 additions and 217 deletions
+1 -1
View File
@@ -10,7 +10,7 @@ function Compass({ imgStyle }: CompassProps) {
<img
src="/images/map/compass.png"
className={clsx(
"lg:w-[7.222vw] w-26 pointer-events-none absolute lg:left-[1.667vw] md:max-lg:bottom-4 left-4 lg:bottom-[1.667vw] max-md:hidden"
"lg:w-[7.222vw] w-26 pointer-events-none absolute lg:left-[1.667vw] md:max-lg:bottom-4 left-4 lg:bottom-[1.667vw] max-md:hidden z-10"
)}
style={imgStyle}
/>
+4 -1
View File
@@ -2,6 +2,7 @@ import { Dispatch, SetStateAction, useEffect } from "react";
import FullScreenIcon from "./icons/FullScreenIcon";
import Button from "./ui/Button";
import CloseFullscreenIcon from "./icons/CloseFullscreenIcon";
import { isMobileSafari } from "react-device-detect";
interface IFullScreenProps {
onFullScreenChange: Dispatch<SetStateAction<boolean>>;
@@ -26,12 +27,14 @@ function FullScreenButton({
);
}, [onFullScreenChange]);
if (isMobileSafari) return null;
return (
<Button
onlyIcon
size="small"
variant="secondary"
className="absolute lg:top-[1.667vw] lg:right-[1.667vw] top-4 right-4"
className="absolute lg:top-[1.667vw] lg:right-[1.667vw] top-4 right-4 z-10"
onClick={handleClick}
>
<span className="lg:w-[1.389vw] lg:h-[1.389vw] w-5 h-5">
+102 -195
View File
@@ -1,12 +1,5 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {
AnimatePresence,
motion,
animate,
AnimationPlaybackControlsWithThen,
useTransform,
useMotionValue,
} from "motion/react";
import { AnimatePresence, motion } from "motion/react";
import clsx from "clsx";
import { useRef, useState, useEffect } from "react";
import Marker from "./Marker";
@@ -80,7 +73,7 @@ const calculateMinZoom = (containerSize: Size, imageSize: Size): number => {
};
function Map({ maxZoom = 1 }: MapProps) {
const { height: windowHeight, width: windowWidth } = useWindowSize();
const { width: windowWidth } = useWindowSize();
const [isDragging, setIsDragging] = useState(false);
const [startPosition, setStartPosition] = useState<Position>({ x: 0, y: 0 });
@@ -99,7 +92,6 @@ function Map({ maxZoom = 1 }: MapProps) {
const markersContainerRef = useRef<HTMLDivElement>(null);
const [hoveredMarker, setHoveredMarker] = useState<IMarker | null>(null);
const [isImageLoaded, setIsImageLoaded] = useState(false);
const animationRef = useRef<AnimationPlaybackControlsWithThen | null>(null);
const [lastClickTime, setLastClickTime] = useState(0);
const [isShowInstruction, setIsShowInstruction] = useState(true);
const [lastHoveredMarker, setLastHoveredMarker] = useState<IMarker | null>(
@@ -111,37 +103,7 @@ function Map({ maxZoom = 1 }: MapProps) {
const [windSpeed, setWindSpeed] = useState(0);
const [windDirection, setWindDirection] = useState(0);
// const motionZoom = useMotionValue(0);
// const x = useMotionValue(0);
// const y = useMotionValue(0);
// const motionZoomValue = useSpring(motionZoom, {
// bounce: 0,
// });
// const motionXValue = useSpring(x, { bounce: 0 });
// const motionYValue = useSpring(y, { bounce: 0 });
const cloudsRef = useRef<HTMLDivElement>(null);
// const motionZoomValue = useTransform(motionZoom, [0, 1], [0, 1]);
// const motionXValue = useTransform(x.get);
// const motionYValue = useTransform(y.get);
// x,
// [0, containerSizeRef.current.width],
// [0, containerSizeRef.current.width]
// x,
// [0, containerSizeRef.current.height],
// [0, containerSizeRef.current.height]
// );
// useEffect(() => {
// motionZoom.set(zoom);
// }, [zoom]);
// useEffect(() => {
// x.set(position.x);
// y.set(position.y);
// }, [position]);
const handleHoverMarker = (marker: IMarker | null) => {
setHoveredMarker(marker);
@@ -149,57 +111,6 @@ function Map({ maxZoom = 1 }: MapProps) {
if (marker !== null) setLastHoveredMarker(marker);
};
const cloudsTranslateX = useMotionValue(0);
// const cloudsTranslateY = useMotionValue(0);
const cloudsX = useTransform(
() =>
cloudsTranslateX.get() * zoom +
// *
// Math.cos(((-90 - windDirection) / 180) * Math.PI)
position.x
);
const cloudsY = useTransform(
() => position.y
// +
// cloudsTranslateY.get() *
// zoom *
// Math.sin(((-90 - windDirection) / 180) * Math.PI)
);
useEffect(() => {
if (!cloudsRef.current) return;
const animation = animate(
cloudsTranslateX,
[-cloudsRef.current.clientWidth, 0],
{
duration: Math.round(3000 / windSpeed),
repeat: Infinity,
repeatDelay: 0,
ease: "linear",
}
);
return animation.stop;
}, [cloudsTranslateX, windSpeed]);
// useEffect(() => {
// if (!cloudsRef.current) return;
// const animation = animate(
// cloudsTranslateY,
// [-cloudsRef.current.clientHeight, 0],
// {
// duration: Math.round(30 / windSpeed),
// repeat: Infinity,
// repeatDelay: 0,
// ease: "linear",
// }
// );
// return animation.stop;
// }, [cloudsTranslateY, windSpeed]);
useEffect(() => {
if (!containerRef.current || !isImageLoaded || originalSize.width === 0)
return;
@@ -226,7 +137,6 @@ function Map({ maxZoom = 1 }: MapProps) {
function handleLoad() {
if (!mapRef.current || !containerRef.current) return;
// Создаем временное изображение для гарантированного получения размеров
const img = new Image();
img.src = mapRef.current.src;
@@ -238,7 +148,6 @@ function Map({ maxZoom = 1 }: MapProps) {
setOriginalSize(newOriginalSize);
// Рассчитываем минимальный зум после получения размеров
const containerRect = containerRef.current!.getBoundingClientRect();
const minZoom = calculateMinZoom(
{
@@ -254,14 +163,12 @@ function Map({ maxZoom = 1 }: MapProps) {
};
}
// Update container size and min zoom on resize
useEffect(() => {
const updateContainerSize = () => {
if (!containerRef.current) return;
const { width, height } = containerRef.current.getBoundingClientRect();
// Check if container size actually changed
if (
width === containerSizeRef.current.width &&
height === containerSizeRef.current.height
@@ -270,11 +177,9 @@ function Map({ maxZoom = 1 }: MapProps) {
containerSizeRef.current = { width, height };
// Recalculate min zoom when container size changes
const newMinZoom = calculateMinZoom({ width, height }, originalSize);
minZoomRef.current = newMinZoom;
// Reset zoom to new minimum zoom
setZoom(newMinZoom);
const scaledWidth = originalSize.width * zoom;
@@ -299,9 +204,7 @@ function Map({ maxZoom = 1 }: MapProps) {
window.addEventListener("resize", updateContainerSize);
return () => {
window.removeEventListener("resize", updateContainerSize);
};
return () => window.removeEventListener("resize", updateContainerSize);
}, [originalSize, zoom]);
useEffect(() => {
@@ -332,7 +235,6 @@ function Map({ maxZoom = 1 }: MapProps) {
const zoomToPoint = (point: Position, targetZoom: number) => {
if (!containerRef.current) return;
// Ensure zoom is within bounds
const boundedZoom = Math.min(
maxZoom,
Math.max(minZoomRef.current, targetZoom)
@@ -364,7 +266,6 @@ function Map({ maxZoom = 1 }: MapProps) {
if (isShowInstruction) setIsShowInstruction(false);
if (e.touches.length === 2) {
// Для щипка сразу сохраняем начальную дистанцию
const touch1 = e.touches[0];
const touch2 = e.touches[1];
const distance = Math.hypot(
@@ -383,7 +284,7 @@ function Map({ maxZoom = 1 }: MapProps) {
if (!containerRef.current) return;
if (e.touches.length === 2) {
e.preventDefault(); // Предотвращаем скролл страницы при щипке
e.preventDefault();
const touch1 = e.touches[0];
const touch2 = e.touches[1];
const distance = Math.hypot(
@@ -402,13 +303,11 @@ function Map({ maxZoom = 1 }: MapProps) {
return;
}
// Увеличиваем порог изменения для более плавного зума
const distanceChange = Math.abs(distance - previousTouchDistance.current);
const changePercentage =
(distanceChange / previousTouchDistance.current) * 100;
if (changePercentage >= 5) {
// Уменьшаем порог с 10 до 5 для более отзывчивого зума
const zoomFactor = distance > previousTouchDistance.current ? 1.1 : 0.9;
const centerX = (touch1.clientX + touch2.clientX) / 2;
const centerY = (touch1.clientY + touch2.clientY) / 2;
@@ -418,7 +317,6 @@ function Map({ maxZoom = 1 }: MapProps) {
Math.max(minZoomRef.current, zoom * zoomFactor)
);
// Проверяем, действительно ли изменился зум
if (Math.abs(newZoom - zoom) > 0.001) {
setZoom(newZoom);
@@ -442,7 +340,6 @@ function Map({ maxZoom = 1 }: MapProps) {
return;
}
// Обработка перетаскивания одним пальцем только если не в режиме щипка
if (isDragging && e.touches.length === 1) {
const { x, y } = getEventPosition(e);
const newPosition = {
@@ -531,6 +428,30 @@ function Map({ maxZoom = 1 }: MapProps) {
});
};
function smoothZoomTo(targetZoom: number, point: Position) {
const duration = 400;
const startZoom = zoom;
const startTime = performance.now();
function animateZoom(now: number) {
const elapsed = now - startTime;
const t = Math.min(elapsed / duration, 1);
const ease = t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t;
const currentZoom = startZoom + (targetZoom - startZoom) * ease;
zoomToPoint(point, currentZoom);
if (t < 1) {
requestAnimationFrame(animateZoom);
} else {
setZoom(targetZoom);
zoomToPoint(point, targetZoom);
}
}
requestAnimationFrame(animateZoom);
}
const handleClick = (
e: React.MouseEvent<HTMLElement> | React.TouchEvent<HTMLElement>
) => {
@@ -543,49 +464,11 @@ function Map({ maxZoom = 1 }: MapProps) {
return;
if (currentTime - lastClickTime < 200) {
if (animationRef.current) animationRef.current.stop();
const targetZoom =
Math.abs(zoom - maxZoom) < 0.01 ? minZoomRef.current : maxZoom;
const point = getEventPosition(e);
animationRef.current = animate(zoom, targetZoom, {
bounce: 0,
onUpdate(prev) {
zoomToPoint(point, prev);
},
});
// if (animationRef.current) {
// cancelAnimationFrame(animationRef.current);
// }
// const targetZoom =
// Math.abs(zoom - maxZoom) < 0.01 ? minZoomRef.current : maxZoom;
// const point = getEventPosition(e);
// const duration = 300; // длительность анимации в мс
// const startZoom = zoom;
// const startTime = performance.now();
// function animateZoom(currentTime: number) {
// const elapsed = currentTime - startTime;
// const t = Math.min(elapsed / duration, 1); // от 0 до 1
// // Можно использовать ease-функцию, например, easeInOutQuad
// const ease = t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t;
// const currentZoom = startZoom + (targetZoom - startZoom) * ease;
// zoomToPoint(point, currentZoom);
// if (t < 1) {
// animationRef.current = requestAnimationFrame(animateZoom);
// } else {
// animationRef.current = null;
// }
// }
// animationRef.current = requestAnimationFrame(animateZoom);
smoothZoomTo(targetZoom, point);
setLastClickTime(0);
} else {
@@ -598,24 +481,18 @@ function Map({ maxZoom = 1 }: MapProps) {
passive: false,
});
return () => {
return () =>
containerRef.current?.removeEventListener("wheel", handleWheel);
};
}, [isDragging, position]);
// Разделяем стили трансформации для изображения и контейнера маркеров
const imageStyle = {
transform: `translateX(${position.x}px) translateY(${position.y}px) scale(${zoom})`,
transform: `translateX(${position.x}px) translateY(${position.y}px) translateZ(0px) scale(${zoom})`,
...originalSize,
transformOrigin: "0 0",
};
const cloudStyle = {
x: cloudsX,
y: cloudsY,
scale: zoom,
...originalSize,
transformOrigin: "0 0",
...imageStyle,
opacity: opacityCloud,
};
@@ -623,19 +500,10 @@ function Map({ maxZoom = 1 }: MapProps) {
getWeather().then((data) => {
setTemperature(Math.round(data.temperature2m));
setWindSpeed(Math.round(data.windSpeed180m));
setWindDirection(45);
// setWindDirection(Math.round(data.windDirection180m));
setWindDirection(Math.round(data.windDirection180m));
});
}, []);
useEffect(() => {
console.log(position);
}, [position]);
useEffect(() => {
console.log("windDirection", windDirection);
}, [windDirection]);
const [isFullScreen, setIsFullScreen] = useState(false);
function handleFullScreenClick() {
@@ -645,20 +513,50 @@ function Map({ maxZoom = 1 }: MapProps) {
containerSizeRef.current = { width, height };
// Recalculate min zoom when container size changes
const newMinZoom = calculateMinZoom({ width, height }, originalSize);
minZoomRef.current = newMinZoom;
// Reset zoom to new minimum zoom
setZoom(newMinZoom);
setIsFullScreen((prev) => !prev);
if (isFullScreen) {
document.exitFullscreen();
} else {
containerRef.current.requestFullscreen();
}
if (isFullScreen) document.exitFullscreen();
else containerRef.current.requestFullscreen();
}
const cloudAnimationRef = useRef<number | null>(null);
const [cloudOffset, setCloudOffset] = useState(0);
const [cloudImageWidth, setCloudImageWidth] = useState(0);
const cloudImgRef = useRef<HTMLImageElement>(null);
useEffect(() => {
if (cloudImgRef.current && cloudImgRef.current.complete) {
setCloudImageWidth(cloudImgRef.current.naturalWidth);
setCloudOffset(-cloudImgRef.current.naturalWidth);
}
}, [windowWidth, originalSize]);
useEffect(() => {
if (!cloudImageWidth) return;
let lastTimestamp = performance.now();
function animateClouds(now: number) {
const delta = (now - lastTimestamp) / 100;
lastTimestamp = now;
setCloudOffset((prev) => {
let next = prev + (windSpeed * delta) / 10;
if (cloudImageWidth > 0 && next > 0) next -= cloudImageWidth;
return next;
});
cloudAnimationRef.current = requestAnimationFrame(animateClouds);
}
cloudAnimationRef.current = requestAnimationFrame(animateClouds);
return () => {
if (cloudAnimationRef.current)
cancelAnimationFrame(cloudAnimationRef.current);
};
}, [windSpeed, cloudImageWidth]);
return (
<div
ref={containerRef}
@@ -692,28 +590,6 @@ function Map({ maxZoom = 1 }: MapProps) {
/>
)}
<motion.div
style={cloudStyle}
ref={cloudsRef}
className={clsx(
`absolute w-full h-full pointer-events-none flex transition-[opacity] will-change-[opacity,scale,translate,transform]`,
hoveredMarker && "opacity-80"
)}
>
<img
src={`/images/map/clouds-${
windowWidth < 768 ? "mobile" : "desktop"
}.png`}
className="w-full"
/>
<img
src={`/images/map/clouds-${
windowWidth < 768 ? "mobile" : "desktop"
}.png`}
className="w-full"
/>
</motion.div>
<div
className={clsx(
"pointer-events-none absolute max-w-none transition-opacity duration-300 bg-black",
@@ -722,6 +598,37 @@ function Map({ maxZoom = 1 }: MapProps) {
style={imageStyle}
/>
<div
style={cloudStyle}
ref={cloudsRef}
className={clsx(
`absolute w-full h-full pointer-events-none transition-opacity duration-300 will-change-[opacity,scale,translate,transform]`,
hoveredMarker && "opacity-80"
)}
>
<div
className="h-full flex"
style={{
rotate: `${90 + windDirection}deg`,
transform: `translateX(${Math.round(
cloudOffset
)}px) translateZ(0px)`,
}}
>
<img
ref={cloudImgRef}
src={`/images/map/clouds-${
windowWidth < 768 ? "mobile" : "desktop"
}.png`}
/>
<img
src={`/images/map/clouds-${
windowWidth < 768 ? "mobile" : "desktop"
}.png`}
/>
</div>
</div>
<div ref={markersContainerRef} className="absolute" style={imageStyle}>
<div className="relative h-full">
{markers.map((marker) => (
@@ -746,7 +653,7 @@ function Map({ maxZoom = 1 }: MapProps) {
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="absolute inset-0 z-30 flex items-center justify-center pointer-events-none"
className="absolute inset-0 flex items-center justify-center pointer-events-none"
>
<div className="w-fit bg-[#0D1922]/40 rounded-lg backdrop-blur-sm space-y-3 p-4 text-white">
<div className="flex items-center justify-center gap-4">
+2 -4
View File
@@ -51,9 +51,7 @@ function SequenceSlider({ complexName }: SequenceSliderProps) {
setIsShowVideo(false);
}
function handleLoadVideo() {
console.log("handleLoadVideo");
}
function handleLoadVideo() {}
function animate(direction: "next" | "prev") {
setIsAnimating(true);
@@ -110,6 +108,7 @@ function SequenceSlider({ complexName }: SequenceSliderProps) {
<AnimatePresence>
{imageLoaded < FRAME_COUNT && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="absolute inset-0 z-10 flex flex-col items-center justify-center gap-2 bg-white"
@@ -228,7 +227,6 @@ function SequenceSlider({ complexName }: SequenceSliderProps) {
<BottomButton />
<PrivacyPolicyButton />
</div>
<BottomButton />
</div>
</div>
);
+1 -1
View File
@@ -18,7 +18,7 @@ export default function WeatherWidget({
const formattedTime = `${hours}:${minutes}`;
return (
<div className="absolute left-[1.667vw] top-[1.667vw] rounded-2xl space-y-4 min-w-50 w-[8.333vw] p-4 font-medium text-white bg-black/40 pointer-events-none max-[1440px]:hidden backdrop-blur-2xl">
<div className="absolute left-[1.667vw] top-[1.667vw] z-10 rounded-2xl space-y-4 min-w-50 w-[8.333vw] p-4 font-medium text-white bg-black/40 pointer-events-none max-[1440px]:hidden backdrop-blur-2xl">
<div>
<div className="flex justify-between">
<p>{day}</p>
+2 -15
View File
@@ -2,7 +2,7 @@
@import "tailwindcss";
@theme {
--breakpoint-lg: 1600px;
--breakpoint-lg: 1440px;
}
body {
@@ -16,10 +16,6 @@ button {
}
@layer utilities {
.cloud {
animation: clouds linear infinite;
}
.text-headline-l {
@apply lg:text-[clamp(56px,3.889vw,68px)] md:max-lg:text-[clamp(44px,6.366vw,56px)] text-[44px] leading-none;
}
@@ -45,19 +41,10 @@ button {
}
.text-caption-m {
@apply lg:text-[clamp(12px,0.833vw,14px)] md:max-lg:text-[clamp(10px,1.447vw,12px)] text-[10px] leading-[135%];
@apply lg:text-[clamp(12px,0.833vw,14px)] md:max-lg:text-[clamp(12px,1.563vw,18px)] text-[clamp(12px,3.333vw,18px)] leading-[135%];
}
.text-caption-s {
@apply lg:text-[clamp(10px,0.694vw,12px)] text-[10px] leading-[135%];
}
}
@keyframes clouds {
from {
transform: translate(-100%, 0%);
}
to {
transform: translate(0%, 0%);
}
}