diff --git a/.gitignore b/.gitignore index 64ce93f..6850046 100644 --- a/.gitignore +++ b/.gitignore @@ -40,3 +40,4 @@ yarn-error.log* # typescript *.tsbuildinfo next-env.d.ts +.bkit \ No newline at end of file diff --git a/bun.lock b/bun.lock index 700ee7e..61bca16 100644 --- a/bun.lock +++ b/bun.lock @@ -13,6 +13,7 @@ "clsx": "^2.1.1", "date-fns": "^4.1.0", "framer-motion": "^12.38.0", + "lucide-react": "^1.7.0", "next": "16.2.2", "react": "19.2.4", "react-dom": "19.2.4", @@ -21,15 +22,15 @@ "zustand": "^5.0.12", }, "devDependencies": { - "@tailwindcss/postcss": "^4", - "@types/node": "^20", - "@types/react": "^19", - "@types/react-dom": "^19", + "@tailwindcss/postcss": "^4.2.2", + "@types/node": "^20.19.39", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", "cpx2": "^8.0.1", - "eslint": "^9", + "eslint": "^9.39.4", "eslint-config-next": "16.2.2", - "tailwindcss": "^4", - "typescript": "^5", + "tailwindcss": "^4.2.2", + "typescript": "^5.9.3", }, }, }, @@ -794,6 +795,8 @@ "lru-cache": ["lru-cache@11.2.7", "", {}, "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA=="], + "lucide-react": ["lucide-react@1.7.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-yI7BeItCLZJTXikmK4KNUGCKoGzSvbKlfCvw44bU4fXAL6v3gYS4uHD1jzsLkfwODYwI6Drw5Tu9Z5ulDe0TSg=="], + "maath": ["maath@0.10.8", "", { "peerDependencies": { "@types/three": ">=0.134.0", "three": ">=0.134.0" } }, "sha512-tRvbDF0Pgqz+9XUa4jjfgAQ8/aPKmQdWXilFu2tMy4GWj4NOsx99HlULO4IeREfbO3a0sA145DZYyvXPkybm0g=="], "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], diff --git a/package.json b/package.json index f9b8888..4e93ef1 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "clsx": "^2.1.1", "date-fns": "^4.1.0", "framer-motion": "^12.38.0", + "lucide-react": "^1.7.0", "next": "16.2.2", "react": "19.2.4", "react-dom": "19.2.4", diff --git a/src/app/api/live/[slug]/places/route.ts b/src/app/api/live/[slug]/places/route.ts index 17a26a4..95c2d2e 100644 --- a/src/app/api/live/[slug]/places/route.ts +++ b/src/app/api/live/[slug]/places/route.ts @@ -3,6 +3,7 @@ import { createStaticSceneBootstrap } from "../../../../../shared/scene"; import { MVP_PLACES } from "../../../../../data/places"; import { toLiveStateCacheKey } from "../../../../../shared/cache"; import { validateLivePlaceSnapshot } from "../../../../../shared/contracts"; +import { normalizeLiveLevel } from "../../../../../shared/selectors"; export const runtime = "nodejs"; export const dynamic = "force-dynamic"; @@ -26,8 +27,7 @@ export async function GET( } const bootstrap = createStaticSceneBootstrap(place); - const raw = request.nextUrl.searchParams.get("density") ?? "medium"; - const density = raw === "low" || raw === "high" ? raw : "medium"; + const density = normalizeLiveLevel(request.nextUrl.searchParams.get("density")); const snapshot = validateLivePlaceSnapshot({ geometryId: bootstrap.geometryId, diff --git a/src/app/api/live/[slug]/traffic/route.ts b/src/app/api/live/[slug]/traffic/route.ts index 7c63ebf..ab3a848 100644 --- a/src/app/api/live/[slug]/traffic/route.ts +++ b/src/app/api/live/[slug]/traffic/route.ts @@ -3,6 +3,7 @@ import { createStaticSceneBootstrap } from "../../../../../shared/scene"; import { MVP_PLACES } from "../../../../../data/places"; import { toLiveStateCacheKey } from "../../../../../shared/cache"; import { validateLiveTrafficSnapshot } from "../../../../../shared/contracts"; +import { normalizeHour, toTrafficPolicy } from "../../../../../shared/domains"; export const runtime = "nodejs"; export const dynamic = "force-dynamic"; @@ -27,14 +28,14 @@ export async function GET( const bootstrap = createStaticSceneBootstrap(place); const hour = Number(request.nextUrl.searchParams.get("hour") ?? "12"); - const normalizedHour = Number.isFinite(hour) ? ((hour % 24) + 24) % 24 : 12; - const rushHour = (normalizedHour >= 8 && normalizedHour <= 10) || (normalizedHour >= 17 && normalizedHour <= 20); + const normalizedHour = normalizeHour(hour); + const trafficPolicy = toTrafficPolicy(normalizedHour); const payload = validateLiveTrafficSnapshot({ geometryId: bootstrap.geometryId, key: toLiveStateCacheKey(bootstrap.geometryId, "traffic"), - density: rushHour ? "high" : "medium", - speedKph: rushHour ? 18 : 32, + density: trafficPolicy.density, + speedKph: trafficPolicy.speedKph, capturedAtIso: new Date().toISOString(), }); diff --git a/src/app/api/live/[slug]/weather/route.ts b/src/app/api/live/[slug]/weather/route.ts index 54b1cc3..58d6e57 100644 --- a/src/app/api/live/[slug]/weather/route.ts +++ b/src/app/api/live/[slug]/weather/route.ts @@ -3,6 +3,7 @@ import { createStaticSceneBootstrap } from "../../../../../shared/scene"; import { MVP_PLACES } from "../../../../../data/places"; import { toLiveStateCacheKey } from "../../../../../shared/cache"; import { validateLiveWeatherSnapshot } from "../../../../../shared/contracts"; +import { buildWeatherPolicy, normalizeHour } from "../../../../../shared/domains"; export const runtime = "nodejs"; export const dynamic = "force-dynamic"; @@ -27,20 +28,14 @@ export async function GET( const bootstrap = createStaticSceneBootstrap(place); const hour = Number(request.nextUrl.searchParams.get("hour") ?? "12"); - const normalizedHour = Number.isFinite(hour) ? ((hour % 24) + 24) % 24 : 12; - - const condition = - normalizedHour >= 6 && normalizedHour < 17 - ? "clear" - : normalizedHour >= 17 && normalizedHour < 21 - ? "cloudy" - : "rain"; + const normalizedHour = normalizeHour(hour); + const weatherPolicy = buildWeatherPolicy(normalizedHour); const snapshot = validateLiveWeatherSnapshot({ geometryId: bootstrap.geometryId, key: toLiveStateCacheKey(bootstrap.geometryId, "weather"), - condition, - temperatureCelsius: condition === "rain" ? 10 : 18, + condition: weatherPolicy.condition, + temperatureCelsius: weatherPolicy.temperatureCelsius, capturedAtIso: new Date().toISOString(), }); diff --git a/src/app/globals.css b/src/app/globals.css index a2dc41e..a3a2664 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -20,7 +20,32 @@ } body { - background: var(--background); + background: #000; color: var(--foreground); - font-family: Arial, Helvetica, sans-serif; + font-family: var(--font-sans), system-ui, -apple-system, sans-serif; + overflow: hidden; +} + +@layer components { + .glass-panel { + @apply rounded-2xl border border-white/15 bg-black/45 backdrop-blur-xl; + box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.37); + } + + .glass-button { + @apply flex items-center justify-center rounded-xl border border-white/10 bg-white/5 transition-all hover:bg-white/15 active:scale-95 disabled:opacity-50; + } + + .glass-button-active { + @apply border-cyan-400/50 bg-cyan-500/20 text-cyan-300 ring-1 ring-cyan-500/30; + } +} + +/* Cesium UI Hiding */ +.cesium-viewer-bottom { + display: none !important; +} +.cesium-viewer-toolbar { + top: 20px !important; + right: 20px !important; } diff --git a/src/app/page.tsx b/src/app/page.tsx index 36730a0..d2cf2c4 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,18 +1,80 @@ import GlobeScene from "../globe/GlobeScene"; import { MVP_PLACES } from "../data/places"; +import { Globe, MapPin, Search } from "lucide-react"; +import Link from "next/link"; export default function Home() { return (
-
-

- WorMap · Globe MVP -

-

장소를 클릭해 진입하세요

-

- 마커 3개(Shibuya / Times Square / Gangnam) 준비됨 + {/* Top Header */} +

+
+
+
+ +
+
+

WorMap Engine

+

GLOBAL DISCOVERY

+
+
+ +
+ + Search coordinates or places... +
+ +
+
+ System Status + Online +
+
+
+
+
+
+
+ + {/* Bottom Place Selector */} +
+
+ {MVP_PLACES.map((place, idx) => ( + +
+
+ Travel to {place.name} +
+
+ +
+
+
+
+ +
+
+

{place.city}

+

{place.name}

+
+
+
+ + ))} +
+
+ + {/* Info Badge */} +
+
+

+ Coordinates: 37.5665° N, 126.9780° E

diff --git a/src/components/hud/PlaybackHUD.tsx b/src/components/hud/PlaybackHUD.tsx index f77fba5..a8d7583 100644 --- a/src/components/hud/PlaybackHUD.tsx +++ b/src/components/hud/PlaybackHUD.tsx @@ -1,11 +1,26 @@ -"use client"; - +import { + Play, + Pause, + FastForward, + Cloud, + CloudRain, + Sun, + Clock, + Navigation, + Move, + ChevronLeft, + ChevronRight +} from "lucide-react"; import { usePlaybackStore } from "../../stores/playbackStore"; import { usePlaceStore } from "../../stores/placeStore"; import type { InputPreset } from "../../stores/placeStore"; const SPEED_OPTIONS = [1, 2, 4]; -const WEATHER_OPTIONS = ["clear", "cloudy", "rain"] as const; +const WEATHER_OPTIONS = [ + { value: "clear", icon: Sun }, + { value: "cloudy", icon: Cloud }, + { value: "rain", icon: CloudRain }, +] as const; const INPUT_PRESET_OPTIONS: InputPreset[] = ["precision", "balanced", "fast"]; function formatTime(hour: number) { @@ -24,87 +39,103 @@ export default function PlaybackHUD() { const setInputPreset = usePlaceStore((s) => s.setInputPreset); return ( -
-
- +
+
+ {/* Playback Controls */} +
+ -
- {SPEED_OPTIONS.map((s) => ( - - ))} +
+ {SPEED_OPTIONS.map((s) => ( + + ))} +
-
+
-
- {WEATHER_OPTIONS.map((w) => ( + {/* Weather Controls */} +
+ {WEATHER_OPTIONS.map(({ value, icon: Icon }) => ( ))}
-
- -
- Time - {formatTime(currentTime)} -
- -
- - +
+ + {/* Time Controls */} +
+
+ + + {formatTime(currentTime)} + +
+ +
+ + +
-
+
+ {/* View Mode Toggle */} -
+
-
+ {/* Sensitivity / Input Presets */} +
{INPUT_PRESET_OPTIONS.map((preset) => (
{viewMode === "walk" && ( -

- WASD 이동 · E 상승 / R 하강 · 마우스 좌클릭 드래그로 시야 회전 · V 탑뷰 전환 · ESC 복귀 · precision/balanced/fast 감도 -

+
+

+ WASD 이동 · E/R 상승/하강 · 드래그 회전 · V 탑뷰 · ESC 복귀 +

+
)}
); diff --git a/src/components/hud/SceneInfoHUD.tsx b/src/components/hud/SceneInfoHUD.tsx index ec36088..4b1b860 100644 --- a/src/components/hud/SceneInfoHUD.tsx +++ b/src/components/hud/SceneInfoHUD.tsx @@ -1,6 +1,5 @@ -"use client"; - import { useMemo } from "react"; +import { MapPin, Wind, Thermometer, Info } from "lucide-react"; import { usePlaceStore } from "../../stores/placeStore"; import { usePlaybackStore } from "../../stores/playbackStore"; @@ -25,14 +24,53 @@ export default function SceneInfoHUD() { }, [currentPlace]); return ( -
-

WorMap · Place MVP

-

{placeLabel}

-
- Mode: {viewMode === "top" ? "Top" : "Walk"} - Weather: {weather} - Time: {formatTime(currentTime)} - Playback: {isPlaying ? `${speed}x` : "Paused"} +
+
+
+
+ +
+

WorMap Simulation

+
+ +

{placeLabel}

+

{currentPlace?.city || "Discovery Mode"}

+ +
+
+ Status + + + {isPlaying ? `Active ${speed}x` : "Paused"} + +
+ +
+ +
+
+ Environment +
+ + {weather} +
+
+
+ Local Time +
+ + {formatTime(currentTime)} +
+
+
+
+
+ +
+ +

+ Mode: {viewMode} View +

); diff --git a/src/globe/GlobeScene.tsx b/src/globe/GlobeScene.tsx index cadb695..1c36a31 100644 --- a/src/globe/GlobeScene.tsx +++ b/src/globe/GlobeScene.tsx @@ -70,9 +70,25 @@ export default function GlobeScene({ places }: GlobeSceneProps) { navigationHelpButton: false, fullscreenButton: false, shouldAnimate: true, + skyAtmosphere: new Cesium.SkyAtmosphere(), + requestRenderMode: true, }); + // Google Earth 스타일 대기 및 안개 설정 viewer.scene.globe.enableLighting = true; + viewer.scene.globe.showGroundAtmosphere = true; + viewer.scene.fog.enabled = true; + viewer.scene.fog.density = 0.0001; + viewer.scene.fog.screenSpaceErrorFactor = 2.0; + + if (viewer.scene.moon) { + viewer.scene.moon.show = true; + } + + if (viewer.scene.sun) { + viewer.scene.sun.show = true; + } + viewerRef.current = viewer; slugMap.clear(); @@ -92,24 +108,27 @@ export default function GlobeScene({ places }: GlobeSceneProps) { name: place.name, position: Cesium.Cartesian3.fromDegrees(place.lng, place.lat, 0), point: { - pixelSize: APP_CONFIG.cesium.marker.pixelSize, - color: Cesium.Color.CYAN, - outlineColor: Cesium.Color.WHITE, - outlineWidth: APP_CONFIG.cesium.marker.outlineWidth, + pixelSize: 8, + color: Cesium.Color.CYAN.withAlpha(0.8), + outlineColor: Cesium.Color.WHITE.withAlpha(0.5), + outlineWidth: 2, disableDepthTestDistance: Number.POSITIVE_INFINITY, + scaleByDistance: new Cesium.NearFarScalar(1.5e2, 1.5, 8.0e6, 0.5), }, label: { text: place.name, - font: APP_CONFIG.cesium.marker.labelFont, + font: "14px 'Inter', system-ui, sans-serif", style: Cesium.LabelStyle.FILL_AND_OUTLINE, fillColor: Cesium.Color.WHITE, - outlineColor: Cesium.Color.BLACK, - outlineWidth: APP_CONFIG.cesium.marker.outlineWidth, - pixelOffset: new Cesium.Cartesian2( - APP_CONFIG.cesium.marker.labelOffset[0], - APP_CONFIG.cesium.marker.labelOffset[1], - ), + outlineColor: Cesium.Color.BLACK.withAlpha(0.5), + outlineWidth: 2, + pixelOffset: new Cesium.Cartesian2(0, -28), disableDepthTestDistance: Number.POSITIVE_INFINITY, + showBackground: true, + backgroundColor: Cesium.Color.BLACK.withAlpha(0.6), + backgroundPadding: new Cesium.Cartesian2(8, 4), + scaleByDistance: new Cesium.NearFarScalar(1.5e2, 1.0, 1.5e7, 0.5), + translucencyByDistance: new Cesium.NearFarScalar(1.5e2, 1.0, 1.5e7, 0.2), }, }); @@ -118,7 +137,24 @@ export default function GlobeScene({ places }: GlobeSceneProps) { placeNameMap.set(entityId, place.name); } - viewer.camera.flyHome(APP_CONFIG.cesium.marker.flyHomeDuration); + // 우주에서 빨려 들어가는 듯한 카메라 연출 (Google Earth Fly-in) + viewer.camera.setView({ + destination: Cesium.Cartesian3.fromDegrees(127.0276, 37.4979, 15000000), // 우주 고도 + orientation: { + heading: 0, + pitch: Cesium.Math.toRadians(-90), + roll: 0, + }, + }); + + // 1초 뒤 강남역 부근으로 빠르게 이동 후 서서히 안정화 + setTimeout(() => { + viewer.camera.flyTo({ + destination: Cesium.Cartesian3.fromDegrees(127.0276, 37.4979, 12000000), + duration: 3, + easingFunction: Cesium.EasingFunction.QUADRATIC_IN_OUT, + }); + }, 500); const handler = new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas); clickHandlerRef.current = handler; diff --git a/src/scene/place/PedestrianSystem.tsx b/src/scene/place/PedestrianSystem.tsx index 857a026..e753f6a 100644 --- a/src/scene/place/PedestrianSystem.tsx +++ b/src/scene/place/PedestrianSystem.tsx @@ -4,6 +4,7 @@ import { useMemo, useRef } from "react"; import { useFrame } from "@react-three/fiber"; import * as THREE from "three"; import { usePlaybackStore } from "../../stores/playbackStore"; +import { getPedestrianCountByLevel } from "../../shared/selectors"; const MAX_PEDESTRIANS = 20; const AREA = 44; @@ -20,12 +21,6 @@ function rand(min: number, max: number) { return min + Math.random() * (max - min); } -function countByLevel(level: "low" | "medium" | "high") { - if (level === "low") return 10; - if (level === "high") return 20; - return 15; -} - export default function PedestrianSystem() { const meshRef = useRef(null); const tempObj = useMemo(() => new THREE.Object3D(), []); @@ -48,7 +43,7 @@ export default function PedestrianSystem() { const mesh = meshRef.current; if (!mesh) return; - const activeCount = countByLevel(level); + const activeCount = getPedestrianCountByLevel(level); const walkers = walkersRef.current; for (let i = 0; i < MAX_PEDESTRIANS; i += 1) { diff --git a/src/scene/place/VehicleSystem.tsx b/src/scene/place/VehicleSystem.tsx index 8ade9db..76819d7 100644 --- a/src/scene/place/VehicleSystem.tsx +++ b/src/scene/place/VehicleSystem.tsx @@ -5,6 +5,7 @@ import { useFrame } from "@react-three/fiber"; import * as THREE from "three"; import { usePlaybackStore } from "../../stores/playbackStore"; import type { PlacePackage } from "../../data/placePackages"; +import { getVehicleCountByLevel } from "../../shared/selectors"; const MAX_VEHICLES = 8; @@ -19,12 +20,6 @@ function rand(min: number, max: number) { return min + Math.random() * (max - min); } -function countByLevel(level: "low" | "medium" | "high") { - if (level === "low") return 4; - if (level === "high") return 8; - return 6; -} - type VehicleSystemProps = { pkg: PlacePackage; }; @@ -50,7 +45,7 @@ export default function VehicleSystem({ pkg }: VehicleSystemProps) { const mesh = meshRef.current; if (!mesh) return; - const activeCount = countByLevel(level); + const activeCount = getVehicleCountByLevel(level); const vehicles = vehiclesRef.current; for (let i = 0; i < MAX_VEHICLES; i += 1) { diff --git a/src/shared/domains/index.ts b/src/shared/domains/index.ts new file mode 100644 index 0000000..98120c6 --- /dev/null +++ b/src/shared/domains/index.ts @@ -0,0 +1,3 @@ +export * from "./time"; +export * from "./weather"; +export * from "./traffic"; diff --git a/src/shared/domains/time.ts b/src/shared/domains/time.ts new file mode 100644 index 0000000..f041c1b --- /dev/null +++ b/src/shared/domains/time.ts @@ -0,0 +1,31 @@ +export type TimeOfDay = "day" | "dusk" | "night"; + +const HOURS_IN_DAY = 24; +const DEFAULT_HOUR = 12; + +export function normalizeHour(hour: number, fallback = DEFAULT_HOUR): number { + if (!Number.isFinite(hour)) { + return fallback; + } + + const mod = hour % HOURS_IN_DAY; + return mod < 0 ? mod + HOURS_IN_DAY : mod; +} + +export function toTimeOfDay(hour: number): TimeOfDay { + const normalizedHour = normalizeHour(hour); + + if (normalizedHour >= 6 && normalizedHour < 17) { + return "day"; + } + + if (normalizedHour >= 17 && normalizedHour < 20) { + return "dusk"; + } + + return "night"; +} + +export function isNightTime(hour: number): boolean { + return toTimeOfDay(hour) === "night"; +} diff --git a/src/shared/domains/traffic.ts b/src/shared/domains/traffic.ts new file mode 100644 index 0000000..3ebf576 --- /dev/null +++ b/src/shared/domains/traffic.ts @@ -0,0 +1,30 @@ +import { normalizeHour } from "./time"; + +export type LiveLevel = "low" | "medium" | "high"; + +export type TrafficSnapshotPolicy = { + density: LiveLevel; + speedKph: number; +}; + +function isRushHour(hour: number): boolean { + const normalizedHour = normalizeHour(hour); + const morningRush = normalizedHour >= 8 && normalizedHour <= 10; + const eveningRush = normalizedHour >= 17 && normalizedHour <= 20; + + return morningRush || eveningRush; +} + +export function toTrafficPolicy(hour: number): TrafficSnapshotPolicy { + if (isRushHour(hour)) { + return { + density: "high", + speedKph: 18, + }; + } + + return { + density: "medium", + speedKph: 32, + }; +} diff --git a/src/shared/domains/weather.ts b/src/shared/domains/weather.ts new file mode 100644 index 0000000..844253d --- /dev/null +++ b/src/shared/domains/weather.ts @@ -0,0 +1,43 @@ +import { normalizeHour } from "./time"; + +export type WeatherCondition = "clear" | "cloudy" | "rain" | "snow"; + +export type WeatherSnapshotPolicy = { + condition: WeatherCondition; + temperatureCelsius: number; +}; + +export function toWeatherConditionByHour(hour: number): WeatherCondition { + const normalizedHour = normalizeHour(hour); + + if (normalizedHour >= 6 && normalizedHour < 17) { + return "clear"; + } + + if (normalizedHour >= 17 && normalizedHour < 21) { + return "cloudy"; + } + + return "rain"; +} + +export function toTemperatureByCondition(condition: WeatherCondition): number { + if (condition === "rain") { + return 10; + } + + if (condition === "snow") { + return -2; + } + + return 18; +} + +export function buildWeatherPolicy(hour: number): WeatherSnapshotPolicy { + const condition = toWeatherConditionByHour(hour); + + return { + condition, + temperatureCelsius: toTemperatureByCondition(condition), + }; +} diff --git a/src/shared/selectors/density.ts b/src/shared/selectors/density.ts new file mode 100644 index 0000000..5656ed9 --- /dev/null +++ b/src/shared/selectors/density.ts @@ -0,0 +1,35 @@ +export type LiveLevel = "low" | "medium" | "high"; + +const DEFAULT_DENSITY: LiveLevel = "medium"; + +export function normalizeLiveLevel(level: string | null | undefined): LiveLevel { + if (level === "low" || level === "high") { + return level; + } + + return DEFAULT_DENSITY; +} + +export function getPedestrianCountByLevel(level: LiveLevel): number { + if (level === "low") { + return 10; + } + + if (level === "high") { + return 20; + } + + return 15; +} + +export function getVehicleCountByLevel(level: LiveLevel): number { + if (level === "low") { + return 4; + } + + if (level === "high") { + return 8; + } + + return 6; +} diff --git a/src/shared/selectors/index.ts b/src/shared/selectors/index.ts new file mode 100644 index 0000000..c6cdd5e --- /dev/null +++ b/src/shared/selectors/index.ts @@ -0,0 +1 @@ +export * from "./density"; diff --git a/src/stores/playbackStore.ts b/src/stores/playbackStore.ts index 66ed758..33660a4 100644 --- a/src/stores/playbackStore.ts +++ b/src/stores/playbackStore.ts @@ -1,4 +1,5 @@ import { create } from "zustand"; +import { isNightTime, normalizeHour, toTimeOfDay } from "../shared/domains"; export type WeatherMode = "clear" | "cloudy" | "rain" | "snow"; export type TimeOfDay = "day" | "dusk" | "night"; @@ -29,18 +30,6 @@ type PlaybackStore = { isNight: () => boolean; }; -function normalizeHour(hour: number) { - const mod = hour % 24; - return mod < 0 ? mod + 24 : mod; -} - -function hourToTimeOfDay(hour: number): TimeOfDay { - const h = normalizeHour(hour); - if (h >= 6 && h < 17) return "day"; - if (h >= 17 && h < 20) return "dusk"; - return "night"; -} - export const usePlaybackStore = create((set, get) => ({ currentTime: 12, setCurrentTime: (time) => set({ currentTime: normalizeHour(time) }), @@ -63,10 +52,10 @@ export const usePlaybackStore = create((set, get) => ({ getTimeOfDay: () => { const { currentTime } = get(); - return hourToTimeOfDay(currentTime); + return toTimeOfDay(currentTime); }, isNight: () => { const { currentTime } = get(); - return hourToTimeOfDay(currentTime) === "night"; + return isNightTime(currentTime); }, }));