diff --git a/public/room.blend b/public/room.blend index 1342fd4..75d3689 100644 Binary files a/public/room.blend and b/public/room.blend differ diff --git a/public/room.blend1 b/public/room.blend1 index 7cc3bd0..47dfd87 100644 Binary files a/public/room.blend1 and b/public/room.blend1 differ diff --git a/public/room.glb b/public/room.glb index a083327..dbaca57 100644 Binary files a/public/room.glb and b/public/room.glb differ diff --git a/src/App.tsx b/src/App.tsx index c74163d..d8bc302 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -8,22 +8,33 @@ import { type ThreeEvent, useLoader, } from "@react-three/fiber"; -import { OrbitControls, useAnimations } from "@react-three/drei"; +import { OrbitControls } from "@react-three/drei"; import { GLTFLoader } from "three/examples/jsm/Addons.js"; -import { LoopOnce, Vector3 } from "three"; +import { Vector3 } from "three"; import "./App.css"; +import { state, anim } from "./utils/animator"; +import { useAnimator } from "./hooks/useAnimator"; const Room = () => { const controls = useRef>(null); const room = useLoader(GLTFLoader, "/room.glb"); - const { mixer } = useAnimations(room.animations, room.scene); + + 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(null); const cameraPosition = useRef(null); const cameraTarget = useRef(new Vector3(0, 1, 0)); - const [windowState, setWindowState] = useState(false); - const [shadeState, setShadeState] = useState(false); const pointerDownCameraPos = useRef(new Vector3()); @@ -53,64 +64,9 @@ const Room = () => { return; e.stopPropagation(); - if (e.object.name.includes("Window")) { - const clip = room.animations.find((x) => x.name === "LowerWindowAction"); - if (!clip) throw "no animation"; - const action = mixer.clipAction(clip); - if (action.isRunning()) return; - action.clampWhenFinished = true; - action.setLoop(LoopOnce, 1); - action.timeScale = (windowState ? -1 : 1) * 1.5; - if (windowState) { - action.paused = false; - action.time = action.getClip().duration; - } else { - action.reset(); - } - action.play(); - - setWindowState(!windowState); - } - - if (e.object.name.includes("Shade")) { - { - const clip = room.animations.find((x) => x.name === "Cube.010Action"); - if (!clip) throw "no animation"; - - const action = mixer.clipAction(clip); - if (action.isRunning()) return; - action.clampWhenFinished = true; - action.setLoop(LoopOnce, 1); - action.timeScale = (shadeState ? -1 : 1) * 1.5; - if (shadeState) { - action.paused = false; - action.time = action.getClip().duration; - } else { - action.reset(); - } - action.play(); - } - { - const clip = room.animations.find( - (x) => x.name === "ShadeHandleAction", - ); - if (!clip) throw "no animation"; - - const action = mixer.clipAction(clip); - if (action.isRunning()) return; - action.clampWhenFinished = true; - action.setLoop(LoopOnce, 1); - action.timeScale = (shadeState ? -1 : 1) * 1.5; - if (shadeState) { - action.paused = false; - action.time = action.getClip().duration; - } else { - action.reset(); - } - action.play(); - } - setShadeState(!shadeState); + if (e.object.name === "WindowClickTarget") { + windowAnimator.play(); } if (e.object.name.includes("Poster") && e.object.name !== focus) { diff --git a/src/hooks/useAnimator.ts b/src/hooks/useAnimator.ts new file mode 100644 index 0000000..a55a2cf --- /dev/null +++ b/src/hooks/useAnimator.ts @@ -0,0 +1,59 @@ +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, +) => { + 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; + }, + }; +}; diff --git a/src/utils/animator.ts b/src/utils/animator.ts new file mode 100644 index 0000000..d8a3a07 --- /dev/null +++ b/src/utils/animator.ts @@ -0,0 +1,20 @@ +import type { AnimationAction } from "three"; + +export type Animation = { + name: string; + reverse: boolean; +}; + +export type AnimatorState = Animation[]; + +export type Animator = { + actions: Map; + states: AnimatorState[]; +}; + +export const state = (...animations: Animation[]): Animation[] => animations; + +export const anim = (name: string, reverse: boolean): Animation => ({ + name, + reverse, +});