refactor: factor out animator logic

This commit is contained in:
2025-05-30 19:13:51 +02:00
parent 89b80f49e6
commit ed8e35b1d7
6 changed files with 97 additions and 62 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -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<ComponentRef<typeof OrbitControls>>(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<string | null>(null);
const cameraPosition = useRef<Vector3 | null>(null);
const cameraTarget = useRef<Vector3>(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) {

59
src/hooks/useAnimator.ts Normal file
View File

@@ -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 | 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;
},
};
};

20
src/utils/animator.ts Normal file
View File

@@ -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<string, AnimationAction>;
states: AnimatorState[];
};
export const state = (...animations: Animation[]): Animation[] => animations;
export const anim = (name: string, reverse: boolean): Animation => ({
name,
reverse,
});