feat: nds render system
This commit is contained in:
45
.gitignore
vendored
45
.gitignore
vendored
@@ -1,32 +1,27 @@
|
||||
# Temporary
|
||||
_old
|
||||
# temporary
|
||||
__old
|
||||
|
||||
# Nuxt dev/build outputs
|
||||
.output
|
||||
.data
|
||||
.nuxt
|
||||
.nitro
|
||||
.cache
|
||||
dist
|
||||
|
||||
# Node dependencies
|
||||
node_modules
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
# Misc
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
.fleet
|
||||
.idea
|
||||
|
||||
# ESLint
|
||||
.eslintcache
|
||||
|
||||
_old
|
||||
# Local env files
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
21
LICENSE.md
21
LICENSE.md
@@ -1,21 +0,0 @@
|
||||
Copyright (c) 2025 Pihkaal <hello@pihkaal.me>
|
||||
Permission is hereby granted, free of charge, to any person
|
||||
obtaining a copy of this software and associated documentation
|
||||
files (the "Software"), to deal in the Software without
|
||||
restriction, including without limitation the rights to use,
|
||||
copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following
|
||||
conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
OTHER DEALINGS IN THE SOFTWARE.
|
||||
96
README.md
96
README.md
@@ -1,39 +1,75 @@
|
||||
<h1 align="center">
|
||||
<br>
|
||||
<img src="https://i.imgur.com/8xRqYCS.png" alt="Pihkaal Profile Picture" width="200">
|
||||
<br>
|
||||
pihkaal.me
|
||||
<br>
|
||||
</h1>
|
||||
# Nuxt Minimal Starter
|
||||
|
||||
<h4 align="center">My <a href="https://pihkaal.me">personnal website</a>.</h4>
|
||||
Look at the [Nuxt documentation](https://nuxt.com/docs/getting-started/introduction) to learn more.
|
||||
|
||||
<p align="center">
|
||||
<a href="https://vitejs.dev">
|
||||
<img src="https://img.shields.io/badge/vite-906dfe?style=for-the-badge&logo=vite&logoColor=white">
|
||||
</a>
|
||||
<a href="https://typescriptlang.org">
|
||||
<img src="https://img.shields.io/badge/TypeScript-007acc?style=for-the-badge&logo=typescript&logoColor=white">
|
||||
</a>
|
||||
<a href="https://react.dev">
|
||||
<img src="https://img.shields.io/badge/react-017fa5?style=for-the-badge&logo=react&logoColor=white">
|
||||
</a>
|
||||
</p>
|
||||
## Setup
|
||||
|
||||
<p align="center" id="links">
|
||||
<a href="#description">Description</a> •
|
||||
<a href="https://pihkaal.me">Visit it</a> •
|
||||
<a href="#license">License</a>
|
||||
</p>
|
||||
Make sure to install dependencies:
|
||||
|
||||
<br>
|
||||
```bash
|
||||
# npm
|
||||
npm install
|
||||
|
||||
## Description
|
||||
# pnpm
|
||||
pnpm install
|
||||
|
||||
This is my personnal website, built with Vite, React and TypeScript. It's the recreation of my desktop, with [neovim](https://neovim.io/), [spotify-player](https://github.com/aome510/spotify-player) (that is synchronized with my spotify activity), and [cava](https://github.com/karlstav/cava).
|
||||
# yarn
|
||||
yarn install
|
||||
|
||||
<br>
|
||||
# bun
|
||||
bun install
|
||||
```
|
||||
|
||||
## License
|
||||
## Development Server
|
||||
|
||||
This project is <a href="https://opensource.org/licenses/MIT">MIT</a> licensed.
|
||||
Start the development server on `http://localhost:3000`:
|
||||
|
||||
```bash
|
||||
# npm
|
||||
npm run dev
|
||||
|
||||
# pnpm
|
||||
pnpm dev
|
||||
|
||||
# yarn
|
||||
yarn dev
|
||||
|
||||
# bun
|
||||
bun run dev
|
||||
```
|
||||
|
||||
## Production
|
||||
|
||||
Build the application for production:
|
||||
|
||||
```bash
|
||||
# npm
|
||||
npm run build
|
||||
|
||||
# pnpm
|
||||
pnpm build
|
||||
|
||||
# yarn
|
||||
yarn build
|
||||
|
||||
# bun
|
||||
bun run build
|
||||
```
|
||||
|
||||
Locally preview production build:
|
||||
|
||||
```bash
|
||||
# npm
|
||||
npm run preview
|
||||
|
||||
# pnpm
|
||||
pnpm preview
|
||||
|
||||
# yarn
|
||||
yarn preview
|
||||
|
||||
# bun
|
||||
bun run preview
|
||||
```
|
||||
|
||||
Check out the [deployment documentation](https://nuxt.com/docs/getting-started/deployment) for more information.
|
||||
|
||||
20
app/app.vue
Normal file
20
app/app.vue
Normal file
@@ -0,0 +1,20 @@
|
||||
<script setup lang="ts">
|
||||
import Background from "./components/screen/Background.vue";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="wrapper">
|
||||
<Screen>
|
||||
<Background />
|
||||
<ScreenStats />
|
||||
</Screen>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.wrapper {
|
||||
width: fit-content;
|
||||
height: fit-content;
|
||||
border: 1px solid red;
|
||||
}
|
||||
</style>
|
||||
6
app/components/screen/Background.vue
Normal file
6
app/components/screen/Background.vue
Normal file
@@ -0,0 +1,6 @@
|
||||
<script lang="ts" setup>
|
||||
useRender((ctx) => {
|
||||
ctx.fillStyle = "green";
|
||||
ctx.fillRect(0, 0, 100, 100);
|
||||
});
|
||||
</script>
|
||||
70
app/components/screen/Screen.vue
Normal file
70
app/components/screen/Screen.vue
Normal file
@@ -0,0 +1,70 @@
|
||||
<script lang="ts" setup>
|
||||
const canvas = useTemplateRef("canvas");
|
||||
|
||||
const updateCallbacks = new Set<UpdateCallback>();
|
||||
const renderCallbacks = new Set<RenderCallback>();
|
||||
|
||||
let ctx: CanvasRenderingContext2D | null = null;
|
||||
let animationFrameId: number | null = null;
|
||||
let lastFrameTime = 0;
|
||||
let lastRealFrameTime = 0;
|
||||
|
||||
const registerUpdateCallback = (callback: UpdateCallback) => {
|
||||
updateCallbacks.add(callback);
|
||||
return () => updateCallbacks.delete(callback);
|
||||
};
|
||||
|
||||
const registerRenderCallback = (callback: RenderCallback) => {
|
||||
renderCallbacks.add(callback);
|
||||
return () => renderCallbacks.delete(callback);
|
||||
};
|
||||
|
||||
const renderFrame = (timestamp: number) => {
|
||||
if (!ctx) return;
|
||||
|
||||
const deltaTime = timestamp - lastFrameTime;
|
||||
lastFrameTime = timestamp;
|
||||
|
||||
const start = Date.now();
|
||||
|
||||
// update
|
||||
for (const callback of updateCallbacks) {
|
||||
callback(deltaTime, lastRealFrameTime);
|
||||
}
|
||||
|
||||
// render
|
||||
ctx.clearRect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT);
|
||||
|
||||
for (const callback of renderCallbacks) {
|
||||
callback(ctx);
|
||||
}
|
||||
|
||||
lastRealFrameTime = Date.now() - start;
|
||||
|
||||
animationFrameId = requestAnimationFrame(renderFrame);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
if (!canvas.value) throw new Error("Missing canvas");
|
||||
|
||||
ctx = canvas.value.getContext("2d");
|
||||
if (!ctx) throw new Error("Missing 2d context");
|
||||
|
||||
provide("registerUpdateCallback", registerUpdateCallback);
|
||||
provide("registerRenderCallback", registerRenderCallback);
|
||||
|
||||
animationFrameId = requestAnimationFrame(renderFrame);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (animationFrameId !== null) {
|
||||
cancelAnimationFrame(animationFrameId);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<canvas ref="canvas" :width="SCREEN_WIDTH" :height="SCREEN_HEIGHT" />
|
||||
|
||||
<slot v-if="canvas" />
|
||||
</template>
|
||||
68
app/components/screen/Stats.vue
Normal file
68
app/components/screen/Stats.vue
Normal file
@@ -0,0 +1,68 @@
|
||||
<script lang="ts" setup>
|
||||
const props = defineProps({
|
||||
x: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
y: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
});
|
||||
|
||||
const SAMPLES = 60;
|
||||
let average = { deltaTime: 0, realDeltaTime: 0 };
|
||||
const lastFrames: (typeof average)[] = [];
|
||||
|
||||
useUpdate((deltaTime, realDeltaTime) => {
|
||||
lastFrames.push({ deltaTime, realDeltaTime });
|
||||
|
||||
if (lastFrames.length > SAMPLES) {
|
||||
lastFrames.shift();
|
||||
}
|
||||
|
||||
if (lastFrames.length > 0) {
|
||||
average = {
|
||||
deltaTime:
|
||||
lastFrames.reduce((acc, v) => acc + v.deltaTime, 0) /
|
||||
lastFrames.length,
|
||||
realDeltaTime:
|
||||
lastFrames.reduce((acc, v) => acc + v.realDeltaTime, 0) /
|
||||
lastFrames.length,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
useRender((ctx) => {
|
||||
const LINE_COUNT = 5;
|
||||
const LINE_HEIGHT = 12;
|
||||
|
||||
ctx.fillStyle = "red";
|
||||
ctx.fillRect(props.x - 2, props.y, 140, LINE_COUNT * LINE_HEIGHT + 3);
|
||||
|
||||
let textY = props.y;
|
||||
|
||||
ctx.fillStyle = "black";
|
||||
ctx.fillText("[avg on 60 frames]", props.x, (textY += LINE_HEIGHT));
|
||||
ctx.fillText(
|
||||
`fps=${(1000 / average.deltaTime).toFixed()}`,
|
||||
props.x,
|
||||
(textY += LINE_HEIGHT),
|
||||
);
|
||||
ctx.fillText(
|
||||
`frame_time=${average.deltaTime.toFixed(2)}ms`,
|
||||
props.x,
|
||||
(textY += LINE_HEIGHT),
|
||||
);
|
||||
ctx.fillText(
|
||||
`real_fps=${(1000 / average.realDeltaTime).toFixed()}`,
|
||||
props.x,
|
||||
(textY += LINE_HEIGHT),
|
||||
);
|
||||
ctx.fillText(
|
||||
`real_frame_time=${average.realDeltaTime.toFixed(2)}ms`,
|
||||
props.x,
|
||||
(textY += LINE_HEIGHT),
|
||||
);
|
||||
});
|
||||
</script>
|
||||
18
app/composables/useRender.ts
Normal file
18
app/composables/useRender.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
export type RenderCallback = (ctx: CanvasRenderingContext2D) => void;
|
||||
|
||||
export const useRender = (callback: RenderCallback) => {
|
||||
const registerRenderCallback = inject<
|
||||
(callback: RenderCallback) => () => void
|
||||
>("registerRenderCallback");
|
||||
|
||||
onMounted(() => {
|
||||
if (!registerRenderCallback) {
|
||||
throw new Error(
|
||||
"Missing registerRenderCallback - useRender must be used within a Screen component",
|
||||
);
|
||||
}
|
||||
|
||||
const unregister = registerRenderCallback(callback);
|
||||
onUnmounted(unregister);
|
||||
});
|
||||
};
|
||||
18
app/composables/useUpdate.ts
Normal file
18
app/composables/useUpdate.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
export type UpdateCallback = (deltaTime: number, realFrameTime: number) => void;
|
||||
|
||||
export const useUpdate = (callback: UpdateCallback) => {
|
||||
const registerUpdateCallback = inject<
|
||||
(callback: UpdateCallback) => () => void
|
||||
>("registerUpdateCallback");
|
||||
|
||||
onMounted(() => {
|
||||
if (!registerUpdateCallback) {
|
||||
throw new Error(
|
||||
"Missing registerUpdateCallback - useUpdate must be used within a Screen component",
|
||||
);
|
||||
}
|
||||
|
||||
const unregister = registerUpdateCallback(callback);
|
||||
onUnmounted(unregister);
|
||||
});
|
||||
};
|
||||
2
app/utils/screen.ts
Normal file
2
app/utils/screen.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export const SCREEN_WIDTH = 256;
|
||||
export const SCREEN_HEIGHT = 192;
|
||||
@@ -1,28 +0,0 @@
|
||||
import js from "@eslint/js";
|
||||
import globals from "globals";
|
||||
import reactHooks from "eslint-plugin-react-hooks";
|
||||
import reactRefresh from "eslint-plugin-react-refresh";
|
||||
import tseslint from "typescript-eslint";
|
||||
|
||||
export default tseslint.config(
|
||||
{ ignores: ["dist"] },
|
||||
{
|
||||
extends: [js.configs.recommended, ...tseslint.configs.recommended],
|
||||
files: ["**/*.{ts,tsx}"],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2020,
|
||||
globals: globals.browser,
|
||||
},
|
||||
plugins: {
|
||||
"react-hooks": reactHooks,
|
||||
"react-refresh": reactRefresh,
|
||||
},
|
||||
rules: {
|
||||
...reactHooks.configs.recommended.rules,
|
||||
"react-refresh/only-export-components": [
|
||||
"warn",
|
||||
{ allowConstantExport: true },
|
||||
],
|
||||
},
|
||||
},
|
||||
);
|
||||
12
index.html
12
index.html
@@ -1,12 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>pihkaal.me</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
5
nuxt.config.ts
Normal file
5
nuxt.config.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
// https://nuxt.com/docs/api/configuration/nuxt-config
|
||||
export default defineNuxtConfig({
|
||||
compatibilityDate: '2025-07-15',
|
||||
devtools: { enabled: true }
|
||||
})
|
||||
38
package.json
38
package.json
@@ -1,35 +1,17 @@
|
||||
{
|
||||
"name": "pihkaal.me",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"name": "pihkaal-me",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc -b && vite build",
|
||||
"lint": "eslint --cache .",
|
||||
"preview": "vite preview",
|
||||
"format": "prettier --write --cache ."
|
||||
"build": "nuxt build",
|
||||
"dev": "nuxt dev",
|
||||
"generate": "nuxt generate",
|
||||
"preview": "nuxt preview",
|
||||
"postinstall": "nuxt prepare"
|
||||
},
|
||||
"dependencies": {
|
||||
"@react-three/drei": "^10.0.8",
|
||||
"@react-three/fiber": "^9.1.2",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"three": "^0.176.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.25.0",
|
||||
"@types/react": "^19.1.2",
|
||||
"@types/react-dom": "^19.1.2",
|
||||
"@types/three": "^0.176.0",
|
||||
"@vitejs/plugin-react-swc": "^3.9.0",
|
||||
"eslint": "^9.25.0",
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.19",
|
||||
"globals": "^16.0.0",
|
||||
"prettier": "^3.5.3",
|
||||
"typescript": "~5.8.3",
|
||||
"typescript-eslint": "^8.30.1",
|
||||
"vite": "^6.3.5"
|
||||
"nuxt": "^4.2.1",
|
||||
"vue": "^3.5.22",
|
||||
"vue-router": "^4.6.3"
|
||||
}
|
||||
}
|
||||
|
||||
8127
pnpm-lock.yaml
generated
8127
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
3
pnpm-workspace.yaml
Normal file
3
pnpm-workspace.yaml
Normal file
@@ -0,0 +1,3 @@
|
||||
onlyBuiltDependencies:
|
||||
- '@parcel/watcher'
|
||||
- esbuild
|
||||
2
public/robots.txt
Normal file
2
public/robots.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
User-Agent: *
|
||||
Disallow:
|
||||
Binary file not shown.
Binary file not shown.
BIN
public/room.glb
BIN
public/room.glb
Binary file not shown.
10
src/App.css
10
src/App.css
@@ -1,10 +0,0 @@
|
||||
body {
|
||||
margin: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
#root {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
125
src/App.tsx
125
src/App.tsx
@@ -1,125 +0,0 @@
|
||||
// TODO: handle moving the camera vs actual click
|
||||
|
||||
import { useRef, useState, type ComponentRef } from "react";
|
||||
import {
|
||||
Canvas,
|
||||
useFrame,
|
||||
useThree,
|
||||
type ThreeEvent,
|
||||
useLoader,
|
||||
} from "@react-three/fiber";
|
||||
import { OrbitControls } from "@react-three/drei";
|
||||
import { GLTFLoader } from "three/examples/jsm/Addons.js";
|
||||
import { Vector3 } from "three";
|
||||
|
||||
import "./App.css";
|
||||
import { state, anim } from "./utils/animator";
|
||||
import { useAnimator } from "./hooks/useAnimator";
|
||||
|
||||
const Room = () => {
|
||||
const controls = useRef<ComponentRef<typeof OrbitControls>>(null);
|
||||
const room = useLoader(GLTFLoader, "/room.glb");
|
||||
|
||||
const windowAnimator = useAnimator(
|
||||
room.animations,
|
||||
[
|
||||
state(anim("Cube.010Action", false), anim("ShadeHandleAction", false)),
|
||||
state(anim("LowerWindowAction", false)),
|
||||
state(anim("LowerWindowAction", true)),
|
||||
state(anim("Cube.010Action", true), anim("Cube.010Action", true)),
|
||||
],
|
||||
room.scene,
|
||||
);
|
||||
|
||||
const [focus, setFocus] = useState<string | null>(null);
|
||||
|
||||
const cameraPosition = useRef<Vector3 | null>(null);
|
||||
const cameraTarget = useRef<Vector3>(new Vector3(0, 1, 0));
|
||||
|
||||
const pointerDownCameraPos = useRef(new Vector3());
|
||||
|
||||
const { camera } = useThree();
|
||||
|
||||
useFrame(() => {
|
||||
if (!controls.current) return;
|
||||
|
||||
if (cameraPosition.current) {
|
||||
camera.position.lerp(cameraPosition.current, 0.09);
|
||||
if (camera.position.distanceTo(cameraPosition.current) < 0.012) {
|
||||
if (!focus) {
|
||||
controls.current.enableZoom = true;
|
||||
controls.current.enableRotate = true;
|
||||
}
|
||||
|
||||
cameraPosition.current = null;
|
||||
}
|
||||
}
|
||||
|
||||
controls.current.target.lerp(cameraTarget.current, 0.1);
|
||||
});
|
||||
|
||||
const handlePointerUp = (e: ThreeEvent<MouseEvent>) => {
|
||||
if (!controls.current) return;
|
||||
if (pointerDownCameraPos.current.distanceTo(camera.position) >= 0.05)
|
||||
return;
|
||||
|
||||
e.stopPropagation();
|
||||
|
||||
if (e.object.name === "WindowClickTarget") {
|
||||
windowAnimator.play();
|
||||
}
|
||||
|
||||
if (e.object.name.includes("Poster") && e.object.name !== focus) {
|
||||
const objectPos = new Vector3();
|
||||
const objectDir = new Vector3();
|
||||
e.object.getWorldPosition(objectPos);
|
||||
e.object.getWorldDirection(objectDir);
|
||||
|
||||
cameraPosition.current = objectPos
|
||||
.clone()
|
||||
.add(objectDir.multiplyScalar(-0.65));
|
||||
cameraTarget.current = objectPos;
|
||||
|
||||
controls.current.enableZoom = false;
|
||||
controls.current.enableRotate = false;
|
||||
|
||||
setFocus(e.object.name);
|
||||
} else if (focus) {
|
||||
cameraPosition.current = new Vector3(3, 3, -3);
|
||||
cameraTarget.current = new Vector3(0, 1, 0);
|
||||
|
||||
setFocus(null);
|
||||
}
|
||||
|
||||
controls.current.update();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<ambientLight intensity={Math.PI / 2} color={"#ffffff"} />
|
||||
<directionalLight intensity={5} position={[5, 10, 5]} castShadow />
|
||||
<OrbitControls
|
||||
ref={controls}
|
||||
target={[0, 1, 0]}
|
||||
minPolarAngle={Math.PI / 5}
|
||||
maxPolarAngle={Math.PI / 2}
|
||||
enablePan={false}
|
||||
/>
|
||||
|
||||
<mesh
|
||||
onPointerDown={() => pointerDownCameraPos.current.copy(camera.position)}
|
||||
onPointerUp={handlePointerUp}
|
||||
>
|
||||
<primitive object={room.scene} />
|
||||
</mesh>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<Canvas camera={{ position: [3, 3, -3] }}>
|
||||
<Room />
|
||||
</Canvas>
|
||||
);
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
import { useAnimations } from "@react-three/drei";
|
||||
import { LoopOnce, type AnimationClip } from "three";
|
||||
import type { AnimationAction, Object3D } from "three";
|
||||
import type { Animation, AnimatorState } from "../utils/animator";
|
||||
import { useState } from "react";
|
||||
|
||||
// NOTE: maybe root is not necessary
|
||||
|
||||
export const useAnimator = (
|
||||
clips: AnimationClip[],
|
||||
states: AnimatorState[],
|
||||
root?: React.RefObject<Object3D | undefined | null> | Object3D,
|
||||
) => {
|
||||
const [stateIndex, setStateIndex] = useState(0);
|
||||
const { mixer } = useAnimations(clips, root);
|
||||
|
||||
const getAction = (name: string): AnimationAction => {
|
||||
const clip = clips.find((x) => x.name === name);
|
||||
if (!clip) throw `Animation not found: '${name}'`;
|
||||
return mixer.clipAction(clip);
|
||||
};
|
||||
|
||||
const playAnimation = (animation: Animation): void => {
|
||||
const action = getAction(animation.name);
|
||||
|
||||
action.clampWhenFinished = true;
|
||||
action.setLoop(LoopOnce, 1);
|
||||
action.timeScale = (animation.reverse ? -1 : 1) * 1.5;
|
||||
if (animation.reverse) {
|
||||
action.paused = false;
|
||||
action.time = action.getClip().duration;
|
||||
} else {
|
||||
action.reset();
|
||||
}
|
||||
action.play();
|
||||
};
|
||||
|
||||
return {
|
||||
play() {
|
||||
if (this.isPlaying()) return;
|
||||
for (const animation of states[stateIndex]) {
|
||||
playAnimation(animation);
|
||||
}
|
||||
|
||||
setStateIndex((stateIndex + 1) % states.length);
|
||||
},
|
||||
|
||||
isPlaying(): boolean {
|
||||
for (const state of states) {
|
||||
for (const animation of state) {
|
||||
const action = getAction(animation.name);
|
||||
if (action.isRunning()) return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -1,68 +0,0 @@
|
||||
:root {
|
||||
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
|
||||
color-scheme: light dark;
|
||||
color: rgba(255, 255, 255, 0.87);
|
||||
background-color: #242424;
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
a {
|
||||
font-weight: 500;
|
||||
color: #646cff;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
a:hover {
|
||||
color: #535bf2;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
place-items: center;
|
||||
min-width: 320px;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 3.2em;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 8px;
|
||||
border: 1px solid transparent;
|
||||
padding: 0.6em 1.2em;
|
||||
font-size: 1em;
|
||||
font-weight: 500;
|
||||
font-family: inherit;
|
||||
background-color: #1a1a1a;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.25s;
|
||||
}
|
||||
button:hover {
|
||||
border-color: #646cff;
|
||||
}
|
||||
button:focus,
|
||||
button:focus-visible {
|
||||
outline: 4px auto -webkit-focus-ring-color;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root {
|
||||
color: #213547;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
a:hover {
|
||||
color: #747bff;
|
||||
}
|
||||
button {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { StrictMode } from "react";
|
||||
import App from "./App";
|
||||
|
||||
createRoot(document.getElementById("root") as HTMLElement).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>,
|
||||
);
|
||||
@@ -1,20 +0,0 @@
|
||||
import type { AnimationAction } from "three";
|
||||
|
||||
export type Animation = {
|
||||
name: string;
|
||||
reverse: boolean;
|
||||
};
|
||||
|
||||
export type AnimatorState = Animation[];
|
||||
|
||||
export type Animator = {
|
||||
actions: Map<string, AnimationAction>;
|
||||
states: AnimatorState[];
|
||||
};
|
||||
|
||||
export const state = (...animations: Animation[]): Animation[] => animations;
|
||||
|
||||
export const anim = (name: string, reverse: boolean): Animation => ({
|
||||
name,
|
||||
reverse,
|
||||
});
|
||||
1
src/vite-env.d.ts
vendored
1
src/vite-env.d.ts
vendored
@@ -1 +0,0 @@
|
||||
/// <reference types="vite/client" />
|
||||
@@ -1,27 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
@@ -1,7 +1,18 @@
|
||||
{
|
||||
// https://nuxt.com/docs/guide/concepts/typescript
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
{ "path": "./tsconfig.node.json" }
|
||||
{
|
||||
"path": "./.nuxt/tsconfig.app.json"
|
||||
},
|
||||
{
|
||||
"path": "./.nuxt/tsconfig.server.json"
|
||||
},
|
||||
{
|
||||
"path": "./.nuxt/tsconfig.shared.json"
|
||||
},
|
||||
{
|
||||
"path": "./.nuxt/tsconfig.node.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||
"target": "ES2022",
|
||||
"lib": ["ES2023"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
import { defineConfig } from "vite";
|
||||
import react from "@vitejs/plugin-react-swc";
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
});
|
||||
Reference in New Issue
Block a user