Add three.js weather aura layer to dashboard map
This commit is contained in:
@@ -75,6 +75,46 @@
|
|||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.weatherAura {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 2;
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: 0.88;
|
||||||
|
mix-blend-mode: screen;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weatherAura :global(canvas) {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: block;
|
||||||
|
opacity: 0.94;
|
||||||
|
filter: saturate(1.05) blur(0.3px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.weatherAuraScrim {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background:
|
||||||
|
linear-gradient(
|
||||||
|
180deg,
|
||||||
|
rgba(5, 10, 24, 0.52) 0%,
|
||||||
|
rgba(6, 10, 22, 0.12) 24%,
|
||||||
|
rgba(5, 8, 19, 0.08) 54%,
|
||||||
|
rgba(4, 6, 15, 0.34) 100%
|
||||||
|
),
|
||||||
|
radial-gradient(
|
||||||
|
circle at 50% 60%,
|
||||||
|
rgba(92, 142, 255, 0.06) 0%,
|
||||||
|
rgba(92, 142, 255, 0) 48%
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
.weatherAura[data-reduced-motion="true"] :global(canvas) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
.root :global(.map .leaflet-tile),
|
.root :global(.map .leaflet-tile),
|
||||||
.root :global(.map .leaflet-marker-icon),
|
.root :global(.map .leaflet-marker-icon),
|
||||||
.root :global(.map .leaflet-marker-shadow),
|
.root :global(.map .leaflet-marker-shadow),
|
||||||
|
|||||||
@@ -21,6 +21,17 @@ const MapCanvas = dynamic(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const WeatherAuraLayer = dynamic(
|
||||||
|
() =>
|
||||||
|
import("@/components/dashboard/WeatherAuraLayer").then(
|
||||||
|
(module) => module.WeatherAuraLayer,
|
||||||
|
),
|
||||||
|
{
|
||||||
|
ssr: false,
|
||||||
|
loading: () => null,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
const HistoryModal = dynamic(
|
const HistoryModal = dynamic(
|
||||||
() =>
|
() =>
|
||||||
import("@/components/dashboard/HistoryModal").then(
|
import("@/components/dashboard/HistoryModal").then(
|
||||||
@@ -83,6 +94,7 @@ function DashboardScreen() {
|
|||||||
return (
|
return (
|
||||||
<div className={styles.root}>
|
<div className={styles.root}>
|
||||||
<MapCanvas />
|
<MapCanvas />
|
||||||
|
<WeatherAuraLayer />
|
||||||
<HeaderBar />
|
<HeaderBar />
|
||||||
<CitySidebar />
|
<CitySidebar />
|
||||||
<DetailPanel />
|
<DetailPanel />
|
||||||
|
|||||||
@@ -0,0 +1,229 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { CSSProperties, useEffect, useRef, useState } from "react";
|
||||||
|
import * as THREE from "three";
|
||||||
|
import { useDashboardStore } from "@/hooks/useDashboardStore";
|
||||||
|
import { usePrefersReducedMotion } from "@/hooks/usePrefersReducedMotion";
|
||||||
|
import { getWeatherAuraProfile } from "@/lib/weather-aura";
|
||||||
|
import styles from "./Dashboard.module.css";
|
||||||
|
|
||||||
|
function hexToRgba(hex: string, alpha: number) {
|
||||||
|
const sanitized = hex.replace("#", "");
|
||||||
|
const normalized =
|
||||||
|
sanitized.length === 3
|
||||||
|
? sanitized
|
||||||
|
.split("")
|
||||||
|
.map((char) => `${char}${char}`)
|
||||||
|
.join("")
|
||||||
|
: sanitized.padEnd(6, "0");
|
||||||
|
const numeric = Number.parseInt(normalized, 16);
|
||||||
|
const r = (numeric >> 16) & 255;
|
||||||
|
const g = (numeric >> 8) & 255;
|
||||||
|
const b = numeric & 255;
|
||||||
|
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WeatherAuraLayer() {
|
||||||
|
const store = useDashboardStore();
|
||||||
|
const prefersReducedMotion = usePrefersReducedMotion();
|
||||||
|
const [isDesktop, setIsDesktop] = useState(false);
|
||||||
|
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
|
const aura = getWeatherAuraProfile(store.selectedDetail, store.cities);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (typeof window === "undefined" || !window.matchMedia) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mediaQuery = window.matchMedia("(min-width: 1024px)");
|
||||||
|
const apply = () => {
|
||||||
|
setIsDesktop(mediaQuery.matches);
|
||||||
|
};
|
||||||
|
|
||||||
|
apply();
|
||||||
|
mediaQuery.addEventListener("change", apply);
|
||||||
|
return () => {
|
||||||
|
mediaQuery.removeEventListener("change", apply);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const host = containerRef.current;
|
||||||
|
if (!host || !isDesktop || prefersReducedMotion) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderer = new THREE.WebGLRenderer({
|
||||||
|
alpha: true,
|
||||||
|
antialias: true,
|
||||||
|
powerPreference: "low-power",
|
||||||
|
});
|
||||||
|
renderer.setClearColor(0x000000, 0);
|
||||||
|
renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 1.5));
|
||||||
|
|
||||||
|
const scene = new THREE.Scene();
|
||||||
|
const camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0.1, 10);
|
||||||
|
camera.position.z = 2;
|
||||||
|
|
||||||
|
const clock = new THREE.Clock();
|
||||||
|
const particleGroups: Array<{
|
||||||
|
geometry: THREE.BufferGeometry;
|
||||||
|
positions: Float32Array;
|
||||||
|
baseY: Float32Array;
|
||||||
|
drift: Float32Array;
|
||||||
|
phase: Float32Array;
|
||||||
|
}> = [];
|
||||||
|
|
||||||
|
function createParticleField(
|
||||||
|
count: number,
|
||||||
|
pointSize: number,
|
||||||
|
opacity: number,
|
||||||
|
depthShift: number,
|
||||||
|
) {
|
||||||
|
const geometry = new THREE.BufferGeometry();
|
||||||
|
const positions = new Float32Array(count * 3);
|
||||||
|
const colors = new Float32Array(count * 3);
|
||||||
|
const baseY = new Float32Array(count);
|
||||||
|
const drift = new Float32Array(count);
|
||||||
|
const phase = new Float32Array(count);
|
||||||
|
const primaryColor = new THREE.Color(aura.primary);
|
||||||
|
const secondaryColor = new THREE.Color(aura.secondary);
|
||||||
|
const tertiaryColor = new THREE.Color(aura.tertiary);
|
||||||
|
|
||||||
|
for (let index = 0; index < count; index += 1) {
|
||||||
|
const offset = index * 3;
|
||||||
|
const x = Math.random() * 2.6 - 1.3;
|
||||||
|
const y = Math.random() * 1.8 - 0.9;
|
||||||
|
const z = (Math.random() * 0.8 - 0.4) + depthShift;
|
||||||
|
positions[offset] = x;
|
||||||
|
positions[offset + 1] = y;
|
||||||
|
positions[offset + 2] = z;
|
||||||
|
baseY[index] = y;
|
||||||
|
drift[index] = (0.00045 + Math.random() * 0.0012) * aura.drift;
|
||||||
|
phase[index] = Math.random() * Math.PI * 2;
|
||||||
|
|
||||||
|
const mixedColor = primaryColor
|
||||||
|
.clone()
|
||||||
|
.lerp(secondaryColor, Math.random() * 0.65)
|
||||||
|
.lerp(tertiaryColor, Math.random() * 0.4);
|
||||||
|
colors[offset] = mixedColor.r;
|
||||||
|
colors[offset + 1] = mixedColor.g;
|
||||||
|
colors[offset + 2] = mixedColor.b;
|
||||||
|
}
|
||||||
|
|
||||||
|
geometry.setAttribute("position", new THREE.BufferAttribute(positions, 3));
|
||||||
|
geometry.setAttribute("color", new THREE.BufferAttribute(colors, 3));
|
||||||
|
|
||||||
|
const material = new THREE.PointsMaterial({
|
||||||
|
size: pointSize,
|
||||||
|
transparent: true,
|
||||||
|
opacity,
|
||||||
|
vertexColors: true,
|
||||||
|
depthWrite: false,
|
||||||
|
blending: THREE.AdditiveBlending,
|
||||||
|
sizeAttenuation: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const points = new THREE.Points(geometry, material);
|
||||||
|
scene.add(points);
|
||||||
|
particleGroups.push({ geometry, positions, baseY, drift, phase });
|
||||||
|
}
|
||||||
|
|
||||||
|
createParticleField(90, 0.018, aura.particleOpacity * 0.9, -0.1);
|
||||||
|
createParticleField(60, 0.026, aura.particleOpacity * 0.65, 0.08);
|
||||||
|
|
||||||
|
const resize = () => {
|
||||||
|
const width = host.clientWidth || window.innerWidth;
|
||||||
|
const height = host.clientHeight || window.innerHeight;
|
||||||
|
renderer.setSize(width, height, false);
|
||||||
|
};
|
||||||
|
|
||||||
|
resize();
|
||||||
|
host.appendChild(renderer.domElement);
|
||||||
|
|
||||||
|
let frameId = 0;
|
||||||
|
let lastFrameAt = 0;
|
||||||
|
|
||||||
|
const renderFrame = (timestamp: number) => {
|
||||||
|
frameId = window.requestAnimationFrame(renderFrame);
|
||||||
|
if (timestamp - lastFrameAt < 40) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
lastFrameAt = timestamp;
|
||||||
|
|
||||||
|
const elapsed = clock.getElapsedTime();
|
||||||
|
for (const field of particleGroups) {
|
||||||
|
for (let index = 0; index < field.baseY.length; index += 1) {
|
||||||
|
const offset = index * 3;
|
||||||
|
let nextX = field.positions[offset] + field.drift[index];
|
||||||
|
if (nextX > 1.35) {
|
||||||
|
nextX = -1.35;
|
||||||
|
field.baseY[index] = Math.random() * 1.8 - 0.9;
|
||||||
|
}
|
||||||
|
field.positions[offset] = nextX;
|
||||||
|
field.positions[offset + 1] =
|
||||||
|
field.baseY[index] +
|
||||||
|
Math.sin(elapsed * 0.45 + field.phase[index] + nextX * 2.4) *
|
||||||
|
0.06 *
|
||||||
|
aura.intensity;
|
||||||
|
}
|
||||||
|
|
||||||
|
field.geometry.attributes.position.needsUpdate = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderer.render(scene, camera);
|
||||||
|
};
|
||||||
|
|
||||||
|
frameId = window.requestAnimationFrame(renderFrame);
|
||||||
|
window.addEventListener("resize", resize);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.cancelAnimationFrame(frameId);
|
||||||
|
window.removeEventListener("resize", resize);
|
||||||
|
for (const child of [...scene.children]) {
|
||||||
|
scene.remove(child);
|
||||||
|
}
|
||||||
|
for (const field of particleGroups) {
|
||||||
|
field.geometry.dispose();
|
||||||
|
}
|
||||||
|
renderer.dispose();
|
||||||
|
if (renderer.domElement.parentNode === host) {
|
||||||
|
host.removeChild(renderer.domElement);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [
|
||||||
|
aura.drift,
|
||||||
|
aura.intensity,
|
||||||
|
aura.particleOpacity,
|
||||||
|
aura.primary,
|
||||||
|
aura.secondary,
|
||||||
|
aura.tertiary,
|
||||||
|
isDesktop,
|
||||||
|
prefersReducedMotion,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!isDesktop) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const overlayStyle = {
|
||||||
|
backgroundImage: [
|
||||||
|
`radial-gradient(circle at 18% 22%, ${hexToRgba(aura.primary, 0.18 * aura.intensity)}, transparent 32%)`,
|
||||||
|
`radial-gradient(circle at 78% 20%, ${hexToRgba(aura.secondary, 0.14 * aura.intensity)}, transparent 34%)`,
|
||||||
|
`radial-gradient(circle at 52% 78%, ${hexToRgba(aura.tertiary, 0.12 * aura.intensity)}, transparent 38%)`,
|
||||||
|
].join(", "),
|
||||||
|
} as CSSProperties;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={containerRef}
|
||||||
|
aria-hidden="true"
|
||||||
|
className={styles.weatherAura}
|
||||||
|
data-reduced-motion={prefersReducedMotion ? "true" : "false"}
|
||||||
|
style={overlayStyle}
|
||||||
|
>
|
||||||
|
<div className={styles.weatherAuraScrim} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
export function usePrefersReducedMotion() {
|
||||||
|
const [prefersReducedMotion, setPrefersReducedMotion] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (typeof window === "undefined" || !window.matchMedia) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mediaQuery = window.matchMedia("(prefers-reduced-motion: reduce)");
|
||||||
|
const apply = () => {
|
||||||
|
setPrefersReducedMotion(mediaQuery.matches);
|
||||||
|
};
|
||||||
|
|
||||||
|
apply();
|
||||||
|
mediaQuery.addEventListener("change", apply);
|
||||||
|
return () => {
|
||||||
|
mediaQuery.removeEventListener("change", apply);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return prefersReducedMotion;
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import { CityDetail, CityListItem } from "@/lib/dashboard-types";
|
||||||
|
|
||||||
|
export interface WeatherAuraProfile {
|
||||||
|
primary: string;
|
||||||
|
secondary: string;
|
||||||
|
tertiary: string;
|
||||||
|
intensity: number;
|
||||||
|
drift: number;
|
||||||
|
particleOpacity: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const RISK_AURA: Record<string, Omit<WeatherAuraProfile, "intensity" | "drift">> =
|
||||||
|
{
|
||||||
|
high: {
|
||||||
|
primary: "#ff7c2a",
|
||||||
|
secondary: "#ffcf66",
|
||||||
|
tertiary: "#56c7ff",
|
||||||
|
particleOpacity: 0.42,
|
||||||
|
},
|
||||||
|
medium: {
|
||||||
|
primary: "#f6c453",
|
||||||
|
secondary: "#5eead4",
|
||||||
|
tertiary: "#7dd3fc",
|
||||||
|
particleOpacity: 0.34,
|
||||||
|
},
|
||||||
|
low: {
|
||||||
|
primary: "#38bdf8",
|
||||||
|
secondary: "#22d3ee",
|
||||||
|
tertiary: "#34d399",
|
||||||
|
particleOpacity: 0.28,
|
||||||
|
},
|
||||||
|
default: {
|
||||||
|
primary: "#6ee7ff",
|
||||||
|
secondary: "#7c8dff",
|
||||||
|
tertiary: "#60a5fa",
|
||||||
|
particleOpacity: 0.3,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function clamp(value: number, min: number, max: number) {
|
||||||
|
return Math.min(Math.max(value, min), max);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getWeatherAuraProfile(
|
||||||
|
detail: CityDetail | null,
|
||||||
|
cities: CityListItem[],
|
||||||
|
): WeatherAuraProfile {
|
||||||
|
const dominantRisk =
|
||||||
|
String(detail?.risk?.level || "").toLowerCase() ||
|
||||||
|
String(cities.find((city) => city.risk_level)?.risk_level || "").toLowerCase() ||
|
||||||
|
"default";
|
||||||
|
|
||||||
|
const base = RISK_AURA[dominantRisk] || RISK_AURA.default;
|
||||||
|
const currentTemp = Number(detail?.current?.temp);
|
||||||
|
const validTemp = Number.isFinite(currentTemp) ? currentTemp : 18;
|
||||||
|
const intensity = clamp(0.7 + validTemp / 40, 0.72, 1.45);
|
||||||
|
const drift = clamp(0.45 + validTemp / 30, 0.55, 1.35);
|
||||||
|
const particleOpacity = clamp(base.particleOpacity + (intensity - 1) * 0.08, 0.22, 0.48);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...base,
|
||||||
|
intensity,
|
||||||
|
drift,
|
||||||
|
particleOpacity,
|
||||||
|
};
|
||||||
|
}
|
||||||
Generated
+129
-3
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "polyweather-frontend",
|
"name": "polyweather-frontend",
|
||||||
"version": "0.1.0",
|
"version": "1.5.1",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "polyweather-frontend",
|
"name": "polyweather-frontend",
|
||||||
"version": "0.1.0",
|
"version": "1.5.1",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@radix-ui/react-slot": "^1.1.2",
|
"@radix-ui/react-slot": "^1.1.2",
|
||||||
"@supabase/ssr": "^0.5.2",
|
"@supabase/ssr": "^0.5.2",
|
||||||
@@ -23,13 +23,15 @@
|
|||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"react-leaflet": "^5.0.0",
|
"react-leaflet": "^5.0.0",
|
||||||
"tailwind-merge": "^3.3.0"
|
"tailwind-merge": "^3.3.0",
|
||||||
|
"three": "^0.183.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/leaflet": "^1.9.20",
|
"@types/leaflet": "^1.9.20",
|
||||||
"@types/node": "^22.15.21",
|
"@types/node": "^22.15.21",
|
||||||
"@types/react": "^19.0.10",
|
"@types/react": "^19.0.10",
|
||||||
"@types/react-dom": "^19.0.4",
|
"@types/react-dom": "^19.0.4",
|
||||||
|
"@types/three": "^0.183.1",
|
||||||
"@types/trusted-types": "^2.0.7",
|
"@types/trusted-types": "^2.0.7",
|
||||||
"autoprefixer": "^10.4.20",
|
"autoprefixer": "^10.4.20",
|
||||||
"postcss": "^8.5.3",
|
"postcss": "^8.5.3",
|
||||||
@@ -109,6 +111,12 @@
|
|||||||
"zod": "^3.24.4"
|
"zod": "^3.24.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@dimforge/rapier3d-compat": {
|
||||||
|
"version": "0.12.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@dimforge/rapier3d-compat/-/rapier3d-compat-0.12.0.tgz",
|
||||||
|
"integrity": "sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"node_modules/@emnapi/runtime": {
|
"node_modules/@emnapi/runtime": {
|
||||||
"version": "1.8.1",
|
"version": "1.8.1",
|
||||||
"resolved": "https://registry.npmmirror.com/@emnapi/runtime/-/runtime-1.8.1.tgz",
|
"resolved": "https://registry.npmmirror.com/@emnapi/runtime/-/runtime-1.8.1.tgz",
|
||||||
@@ -2857,6 +2865,12 @@
|
|||||||
"tslib": "^2.8.0"
|
"tslib": "^2.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@tweenjs/tween.js": {
|
||||||
|
"version": "23.1.3",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@tweenjs/tween.js/-/tween.js-23.1.3.tgz",
|
||||||
|
"integrity": "sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"node_modules/@types/connect": {
|
"node_modules/@types/connect": {
|
||||||
"version": "3.4.38",
|
"version": "3.4.38",
|
||||||
"resolved": "https://registry.npmmirror.com/@types/connect/-/connect-3.4.38.tgz",
|
"resolved": "https://registry.npmmirror.com/@types/connect/-/connect-3.4.38.tgz",
|
||||||
@@ -2917,6 +2931,27 @@
|
|||||||
"@types/react": "^19.2.0"
|
"@types/react": "^19.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/stats.js": {
|
||||||
|
"version": "0.17.4",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@types/stats.js/-/stats.js-0.17.4.tgz",
|
||||||
|
"integrity": "sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"node_modules/@types/three": {
|
||||||
|
"version": "0.183.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@types/three/-/three-0.183.1.tgz",
|
||||||
|
"integrity": "sha512-f2Pu5Hrepfgavttdye3PsH5RWyY/AvdZQwIVhrc4uNtvF7nOWJacQKcoVJn0S4f0yYbmAE6AR+ve7xDcuYtMGw==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@dimforge/rapier3d-compat": "~0.12.0",
|
||||||
|
"@tweenjs/tween.js": "~23.1.3",
|
||||||
|
"@types/stats.js": "*",
|
||||||
|
"@types/webxr": ">=0.5.17",
|
||||||
|
"@webgpu/types": "*",
|
||||||
|
"fflate": "~0.8.2",
|
||||||
|
"meshoptimizer": "~1.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/trusted-types": {
|
"node_modules/@types/trusted-types": {
|
||||||
"version": "2.0.7",
|
"version": "2.0.7",
|
||||||
"resolved": "https://registry.npmmirror.com/@types/trusted-types/-/trusted-types-2.0.7.tgz",
|
"resolved": "https://registry.npmmirror.com/@types/trusted-types/-/trusted-types-2.0.7.tgz",
|
||||||
@@ -2928,6 +2963,12 @@
|
|||||||
"integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==",
|
"integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==",
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/webxr": {
|
||||||
|
"version": "0.5.24",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@types/webxr/-/webxr-0.5.24.tgz",
|
||||||
|
"integrity": "sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"node_modules/@types/ws": {
|
"node_modules/@types/ws": {
|
||||||
"version": "8.18.1",
|
"version": "8.18.1",
|
||||||
"resolved": "https://registry.npmmirror.com/@types/ws/-/ws-8.18.1.tgz",
|
"resolved": "https://registry.npmmirror.com/@types/ws/-/ws-8.18.1.tgz",
|
||||||
@@ -3475,6 +3516,12 @@
|
|||||||
"resolved": "https://registry.npmmirror.com/tslib/-/tslib-1.14.1.tgz",
|
"resolved": "https://registry.npmmirror.com/tslib/-/tslib-1.14.1.tgz",
|
||||||
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
|
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
|
||||||
},
|
},
|
||||||
|
"node_modules/@webgpu/types": {
|
||||||
|
"version": "0.1.69",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@webgpu/types/-/types-0.1.69.tgz",
|
||||||
|
"integrity": "sha512-RPmm6kgRbI8e98zSD3RVACvnuktIja5+yLgDAkTmxLr90BEwdTXRQWNLF3ETTTyH/8mKhznZuN5AveXYFEsMGQ==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"node_modules/abitype": {
|
"node_modules/abitype": {
|
||||||
"version": "1.0.6",
|
"version": "1.0.6",
|
||||||
"resolved": "https://registry.npmmirror.com/abitype/-/abitype-1.0.6.tgz",
|
"resolved": "https://registry.npmmirror.com/abitype/-/abitype-1.0.6.tgz",
|
||||||
@@ -4308,6 +4355,12 @@
|
|||||||
"reusify": "^1.0.4"
|
"reusify": "^1.0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/fflate": {
|
||||||
|
"version": "0.8.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/fflate/-/fflate-0.8.2.tgz",
|
||||||
|
"integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"node_modules/fill-range": {
|
"node_modules/fill-range": {
|
||||||
"version": "7.1.1",
|
"version": "7.1.1",
|
||||||
"resolved": "https://registry.npmmirror.com/fill-range/-/fill-range-7.1.1.tgz",
|
"resolved": "https://registry.npmmirror.com/fill-range/-/fill-range-7.1.1.tgz",
|
||||||
@@ -4901,6 +4954,12 @@
|
|||||||
"node": ">= 8"
|
"node": ">= 8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/meshoptimizer": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/meshoptimizer/-/meshoptimizer-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-Vix+QlA1YYT3FwmBBZ+49cE5y/b+pRrcXKqGpS5ouh33d3lSp2PoTpCw19E0cKDFWalembrHnIaZetf27a+W2g==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"node_modules/micromatch": {
|
"node_modules/micromatch": {
|
||||||
"version": "4.0.8",
|
"version": "4.0.8",
|
||||||
"resolved": "https://registry.npmmirror.com/micromatch/-/micromatch-4.0.8.tgz",
|
"resolved": "https://registry.npmmirror.com/micromatch/-/micromatch-4.0.8.tgz",
|
||||||
@@ -6016,6 +6075,11 @@
|
|||||||
"real-require": "^0.2.0"
|
"real-require": "^0.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/three": {
|
||||||
|
"version": "0.183.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/three/-/three-0.183.2.tgz",
|
||||||
|
"integrity": "sha512-di3BsL2FEQ1PA7Hcvn4fyJOlxRRgFYBpMTcyOgkwJIaDOdJMebEFPA+t98EvjuljDx4hNulAGwF6KIjtwI5jgQ=="
|
||||||
|
},
|
||||||
"node_modules/tinyglobby": {
|
"node_modules/tinyglobby": {
|
||||||
"version": "0.2.15",
|
"version": "0.2.15",
|
||||||
"resolved": "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.15.tgz",
|
"resolved": "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.15.tgz",
|
||||||
@@ -6642,6 +6706,12 @@
|
|||||||
"zod": "^3.24.4"
|
"zod": "^3.24.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@dimforge/rapier3d-compat": {
|
||||||
|
"version": "0.12.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@dimforge/rapier3d-compat/-/rapier3d-compat-0.12.0.tgz",
|
||||||
|
"integrity": "sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"@emnapi/runtime": {
|
"@emnapi/runtime": {
|
||||||
"version": "1.8.1",
|
"version": "1.8.1",
|
||||||
"resolved": "https://registry.npmmirror.com/@emnapi/runtime/-/runtime-1.8.1.tgz",
|
"resolved": "https://registry.npmmirror.com/@emnapi/runtime/-/runtime-1.8.1.tgz",
|
||||||
@@ -8349,6 +8419,12 @@
|
|||||||
"tslib": "^2.8.0"
|
"tslib": "^2.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@tweenjs/tween.js": {
|
||||||
|
"version": "23.1.3",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@tweenjs/tween.js/-/tween.js-23.1.3.tgz",
|
||||||
|
"integrity": "sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"@types/connect": {
|
"@types/connect": {
|
||||||
"version": "3.4.38",
|
"version": "3.4.38",
|
||||||
"resolved": "https://registry.npmmirror.com/@types/connect/-/connect-3.4.38.tgz",
|
"resolved": "https://registry.npmmirror.com/@types/connect/-/connect-3.4.38.tgz",
|
||||||
@@ -8407,6 +8483,27 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {}
|
"requires": {}
|
||||||
},
|
},
|
||||||
|
"@types/stats.js": {
|
||||||
|
"version": "0.17.4",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@types/stats.js/-/stats.js-0.17.4.tgz",
|
||||||
|
"integrity": "sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"@types/three": {
|
||||||
|
"version": "0.183.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@types/three/-/three-0.183.1.tgz",
|
||||||
|
"integrity": "sha512-f2Pu5Hrepfgavttdye3PsH5RWyY/AvdZQwIVhrc4uNtvF7nOWJacQKcoVJn0S4f0yYbmAE6AR+ve7xDcuYtMGw==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"@dimforge/rapier3d-compat": "~0.12.0",
|
||||||
|
"@tweenjs/tween.js": "~23.1.3",
|
||||||
|
"@types/stats.js": "*",
|
||||||
|
"@types/webxr": ">=0.5.17",
|
||||||
|
"@webgpu/types": "*",
|
||||||
|
"fflate": "~0.8.2",
|
||||||
|
"meshoptimizer": "~1.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"@types/trusted-types": {
|
"@types/trusted-types": {
|
||||||
"version": "2.0.7",
|
"version": "2.0.7",
|
||||||
"resolved": "https://registry.npmmirror.com/@types/trusted-types/-/trusted-types-2.0.7.tgz",
|
"resolved": "https://registry.npmmirror.com/@types/trusted-types/-/trusted-types-2.0.7.tgz",
|
||||||
@@ -8418,6 +8515,12 @@
|
|||||||
"integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==",
|
"integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==",
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
|
"@types/webxr": {
|
||||||
|
"version": "0.5.24",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@types/webxr/-/webxr-0.5.24.tgz",
|
||||||
|
"integrity": "sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"@types/ws": {
|
"@types/ws": {
|
||||||
"version": "8.18.1",
|
"version": "8.18.1",
|
||||||
"resolved": "https://registry.npmmirror.com/@types/ws/-/ws-8.18.1.tgz",
|
"resolved": "https://registry.npmmirror.com/@types/ws/-/ws-8.18.1.tgz",
|
||||||
@@ -8838,6 +8941,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@webgpu/types": {
|
||||||
|
"version": "0.1.69",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@webgpu/types/-/types-0.1.69.tgz",
|
||||||
|
"integrity": "sha512-RPmm6kgRbI8e98zSD3RVACvnuktIja5+yLgDAkTmxLr90BEwdTXRQWNLF3ETTTyH/8mKhznZuN5AveXYFEsMGQ==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"abitype": {
|
"abitype": {
|
||||||
"version": "1.0.6",
|
"version": "1.0.6",
|
||||||
"resolved": "https://registry.npmmirror.com/abitype/-/abitype-1.0.6.tgz",
|
"resolved": "https://registry.npmmirror.com/abitype/-/abitype-1.0.6.tgz",
|
||||||
@@ -9423,6 +9532,12 @@
|
|||||||
"reusify": "^1.0.4"
|
"reusify": "^1.0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"fflate": {
|
||||||
|
"version": "0.8.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/fflate/-/fflate-0.8.2.tgz",
|
||||||
|
"integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"fill-range": {
|
"fill-range": {
|
||||||
"version": "7.1.1",
|
"version": "7.1.1",
|
||||||
"resolved": "https://registry.npmmirror.com/fill-range/-/fill-range-7.1.1.tgz",
|
"resolved": "https://registry.npmmirror.com/fill-range/-/fill-range-7.1.1.tgz",
|
||||||
@@ -9835,6 +9950,12 @@
|
|||||||
"integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
|
"integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"meshoptimizer": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/meshoptimizer/-/meshoptimizer-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-Vix+QlA1YYT3FwmBBZ+49cE5y/b+pRrcXKqGpS5ouh33d3lSp2PoTpCw19E0cKDFWalembrHnIaZetf27a+W2g==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"micromatch": {
|
"micromatch": {
|
||||||
"version": "4.0.8",
|
"version": "4.0.8",
|
||||||
"resolved": "https://registry.npmmirror.com/micromatch/-/micromatch-4.0.8.tgz",
|
"resolved": "https://registry.npmmirror.com/micromatch/-/micromatch-4.0.8.tgz",
|
||||||
@@ -10534,6 +10655,11 @@
|
|||||||
"real-require": "^0.2.0"
|
"real-require": "^0.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"three": {
|
||||||
|
"version": "0.183.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/three/-/three-0.183.2.tgz",
|
||||||
|
"integrity": "sha512-di3BsL2FEQ1PA7Hcvn4fyJOlxRRgFYBpMTcyOgkwJIaDOdJMebEFPA+t98EvjuljDx4hNulAGwF6KIjtwI5jgQ=="
|
||||||
|
},
|
||||||
"tinyglobby": {
|
"tinyglobby": {
|
||||||
"version": "0.2.15",
|
"version": "0.2.15",
|
||||||
"resolved": "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.15.tgz",
|
"resolved": "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.15.tgz",
|
||||||
|
|||||||
@@ -24,13 +24,15 @@
|
|||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"react-leaflet": "^5.0.0",
|
"react-leaflet": "^5.0.0",
|
||||||
"tailwind-merge": "^3.3.0"
|
"tailwind-merge": "^3.3.0",
|
||||||
|
"three": "^0.183.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/leaflet": "^1.9.20",
|
"@types/leaflet": "^1.9.20",
|
||||||
"@types/node": "^22.15.21",
|
"@types/node": "^22.15.21",
|
||||||
"@types/react": "^19.0.10",
|
"@types/react": "^19.0.10",
|
||||||
"@types/react-dom": "^19.0.4",
|
"@types/react-dom": "^19.0.4",
|
||||||
|
"@types/three": "^0.183.1",
|
||||||
"@types/trusted-types": "^2.0.7",
|
"@types/trusted-types": "^2.0.7",
|
||||||
"autoprefixer": "^10.4.20",
|
"autoprefixer": "^10.4.20",
|
||||||
"postcss": "^8.5.3",
|
"postcss": "^8.5.3",
|
||||||
|
|||||||
Reference in New Issue
Block a user