feat: add car

This commit is contained in:
Huh Hyeongjun 2025-02-25 17:49:34 +09:00
parent 658741f889
commit 96e41161b4
6 changed files with 190 additions and 5 deletions

View File

@ -2,7 +2,6 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>map3d</title>
</head>

View File

@ -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 (
<div
css={css({
@ -65,7 +68,25 @@ export function TopNav() {
display: "none",
},
})}
></div>
>
<button
style={{
display: step == 2 ? "" : "none",
color: "#000000",
backgroundColor: "#ffffff96",
backdropFilter: "blur(8px)",
border: "none",
padding: "0.5rem 1rem",
borderRadius: "8px",
fontWeight: "300",
fontSize: "12px",
outline: "rgba(240, 240, 244, 0.51) solid 0.1rem",
}}
onClick={() => setThirdMode(true)}
>
Car Mode
</button>
</div>
</div>
);
}

12
src/state/carStore.ts Normal file
View File

@ -0,0 +1,12 @@
import { create } from "zustand";
type CarStore = {
thirdMode: boolean;
setThirdMode: (thirdMode: boolean) => void;
};
export const useCarStore = create<CarStore>((set) => ({
thirdMode: false,
setThirdMode: (thirdMode) => set(() => ({ thirdMode: thirdMode })),
}));

151
src/three/Car.tsx Normal file
View File

@ -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 (
<>
<mesh ref={carRef} position={[0, 0.5, 0]}>
<boxGeometry args={[0.2, 0.2, 0.4]} />
<meshStandardMaterial color="orange" />
</mesh>
{!thirdMode && <OrbitControls />}
</>
);
};
export default Car;

View File

@ -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 (
<Canvas camera={{ fov: 90, near: 0.1, far: 7000 }}>
<ambientLight intensity={Math.PI / 2} />
@ -238,7 +240,7 @@ export function Space() {
<Roads area={realCenter} />
<pointLight position={[-10, -10, -10]} decay={0} intensity={Math.PI} />
<OrbitControls />
<Car />
<Export />
<Sky
distance={450000}

View File

@ -76,7 +76,7 @@ function App() {
return (
<div css={css({ height: "100%", width: "100%" })}>
<TopNav />
<TopNav step={step} />
<FullscreenModal isOpen={steps[step] == "front"}>
<Column gap="1rem">