From 96e41161b49417f619c51257060f5d6ca41b5e35 Mon Sep 17 00:00:00 2001 From: Huh Hyeongjun Date: Tue, 25 Feb 2025 17:49:34 +0900 Subject: [PATCH] feat: add car --- index.html | 1 - src/components/nav/TopNav.tsx | 25 +++++- src/state/carStore.ts | 12 +++ src/three/Car.tsx | 151 ++++++++++++++++++++++++++++++++++ src/three/Space.tsx | 4 +- src/ui/App.tsx | 2 +- 6 files changed, 190 insertions(+), 5 deletions(-) create mode 100644 src/state/carStore.ts create mode 100644 src/three/Car.tsx diff --git a/index.html b/index.html index 455df7c..cb942d6 100644 --- a/index.html +++ b/index.html @@ -2,7 +2,6 @@ - map3d diff --git a/src/components/nav/TopNav.tsx b/src/components/nav/TopNav.tsx index 0034eba..35774a9 100644 --- a/src/components/nav/TopNav.tsx +++ b/src/components/nav/TopNav.tsx @@ -1,3 +1,4 @@ +import { useCarStore } from "@/state/carStore"; import { css } from "@emotion/react"; const TOP_PANEL_HEIGHT = "3rem"; @@ -6,7 +7,9 @@ const BORDER_COLOR = "#ededf290"; const breakpoints = [768]; const mq = breakpoints.map((bp) => `@media (max-width: ${bp}px)`); -export function TopNav() { +export function TopNav({ step }: { step: number }) { + const setThirdMode = useCarStore((state) => state.setThirdMode); + return (
+ > + + ); } diff --git a/src/state/carStore.ts b/src/state/carStore.ts new file mode 100644 index 0000000..97b1d6f --- /dev/null +++ b/src/state/carStore.ts @@ -0,0 +1,12 @@ +import { create } from "zustand"; + +type CarStore = { + thirdMode: boolean; + + setThirdMode: (thirdMode: boolean) => void; +}; + +export const useCarStore = create((set) => ({ + thirdMode: false, + setThirdMode: (thirdMode) => set(() => ({ thirdMode: thirdMode })), +})); diff --git a/src/three/Car.tsx b/src/three/Car.tsx new file mode 100644 index 0000000..c8a9ee5 --- /dev/null +++ b/src/three/Car.tsx @@ -0,0 +1,151 @@ +import React, { useRef, useEffect, useCallback } from "react"; +import { useFrame, useThree } from "@react-three/fiber"; +import { OrbitControls, Html } from "@react-three/drei"; +import { css } from "@emotion/react"; +import * as THREE from "three"; +import { useCarStore } from "@/state/carStore"; + +const Car = () => { + const carRef = useRef(); + const { camera } = useThree(); + const thirdMode = useCarStore((state) => state.thirdMode); + const setThirdMode = useCarStore((state) => state.setThirdMode); + const keys = useRef({ w: false, s: false, a: false, d: false }); + const velocity = useRef(0); + + const handleKeyDown = useCallback((e) => { + switch (e.key.toLowerCase()) { + case "w": + keys.current.w = true; + break; + case "s": + keys.current.s = true; + break; + case "a": + keys.current.a = true; + break; + case "d": + keys.current.d = true; + break; + case "escape": + setThirdMode(false); + if (document.exitPointerLock) { + document.exitPointerLock(); + } + break; + default: + break; + } + }, []); + + const handleKeyUp = useCallback((e) => { + switch (e.key.toLowerCase()) { + case "w": + keys.current.w = false; + break; + case "s": + keys.current.s = false; + break; + case "a": + keys.current.a = false; + break; + case "d": + keys.current.d = false; + break; + default: + break; + } + }, []); + + useEffect(() => { + window.addEventListener("keydown", handleKeyDown); + window.addEventListener("keyup", handleKeyUp); + return () => { + window.removeEventListener("keydown", handleKeyDown); + window.removeEventListener("keyup", handleKeyUp); + }; + }, [handleKeyDown, handleKeyUp]); + + useEffect(() => { + if (thirdMode) { + const handleClick = () => { + if (document.pointerLockElement !== document.body) { + document.body.requestPointerLock(); + } + }; + window.addEventListener("click", handleClick); + return () => window.removeEventListener("click", handleClick); + } + }, [thirdMode]); + + useEffect(() => { + if (thirdMode) { + const onMouseMove = (event) => { + if (document.pointerLockElement === document.body && carRef.current) { + carRef.current.rotation.y -= event.movementX * 0.002; + } + }; + document.addEventListener("mousemove", onMouseMove); + return () => document.removeEventListener("mousemove", onMouseMove); + } + }, [thirdMode]); + + useFrame((state, delta) => { + if (carRef.current) { + const accelerationRate = 0.2; + const maxSpeed = 3.0; + const decelerationRate = 1.0; + if (keys.current.w) { + velocity.current = Math.min( + maxSpeed, + velocity.current + accelerationRate * delta + ); + } else if (keys.current.s) { + velocity.current = Math.max( + -maxSpeed, + velocity.current - accelerationRate * delta + ); + } else { + if (velocity.current > 0) { + velocity.current = Math.max( + 0, + velocity.current - decelerationRate * delta + ); + } else if (velocity.current < 0) { + velocity.current = Math.min( + 0, + velocity.current + decelerationRate * delta + ); + } + } + if (keys.current.a) carRef.current.rotation.y += 0.02; + if (keys.current.d) carRef.current.rotation.y -= 0.02; + const forward = new THREE.Vector3(0, 0, -1); + forward.applyQuaternion(carRef.current.quaternion); + carRef.current.position.addScaledVector(forward, velocity.current); + } + if (thirdMode && carRef.current) { + const carPos = carRef.current.position; + const offset = new THREE.Vector3(0, 1, 2); + offset.applyAxisAngle( + new THREE.Vector3(0, 1, 0), + carRef.current.rotation.y + ); + const desiredPosition = carPos.clone().add(offset); + camera.position.lerp(desiredPosition, 0.1); + camera.lookAt(carPos); + } + }); + + return ( + <> + + + + + {!thirdMode && } + + ); +}; + +export default Car; diff --git a/src/three/Space.tsx b/src/three/Space.tsx index f4b7516..f6854b4 100644 --- a/src/three/Space.tsx +++ b/src/three/Space.tsx @@ -5,6 +5,7 @@ import { OrbitControls, Html, Sky, Environment } from "@react-three/drei"; import * as THREE from "three"; import { useActionStore } from "@/state/exportStore"; import { GLTFExporter } from "three/examples/jsm/Addons.js"; +import Car from "./Car"; function Building({ shape, @@ -217,6 +218,7 @@ export function Space() { }; const buildingsData = areaData(); + return ( @@ -238,7 +240,7 @@ export function Space() { - + - +