UNPKG

mylingo3d

Version:

Lingo3D is a React/Vue 3d game development framework that ships with a complete visual editor

149 lines (127 loc) 4.33 kB
import { Cancellable, Disposable } from "@lincode/promiselikes" import { Object3D, AnimationMixer, AnimationClip, NumberKeyframeTrack, AnimationAction, LoopRepeat, LoopOnce } from "three" import { AnimationData } from "../../../api/serializer/types" import { forceGet } from "@lincode/utils" import EventLoopItem from "../../../api/core/EventLoopItem" import { onBeforeRender } from "../../../events/onBeforeRender" import { dt } from "../../../engine/eventLoop" const targetMixerMap = new WeakMap<EventLoopItem | Object3D, AnimationMixer>() const mixerActionMap = new WeakMap<AnimationMixer, [AnimationAction, boolean]>() const mixerHandleMap = new WeakMap<AnimationMixer, Cancellable>() export type PlayOptions = { crossFade?: number repeat?: boolean onFinish?: () => void } export default class AnimationManager extends Disposable { private clip?: AnimationClip public name: string private mixer: AnimationMixer private action?: AnimationAction public constructor( nameOrClip: string | AnimationClip, target: EventLoopItem | Object3D ) { super() this.mixer = forceGet( targetMixerMap, target, () => new AnimationMixer(target as any) ) if (typeof nameOrClip === "string") this.name = nameOrClip else { this.name = nameOrClip.name this.loadClip(nameOrClip) } } public retarget(target: Object3D) { const newClip = this.clip!.clone() const targetName = target.name + "." newClip.tracks = newClip.tracks.filter((track) => track.name.startsWith(targetName) ) return new AnimationManager(newClip, target) } public override dispose() { if (this.done) return this super.dispose() this.stop() return this } public get duration() { return this.clip?.duration ?? 0 } private loadClip(clip: AnimationClip) { this.clip = clip this.action = this.mixer.clipAction(clip) } public setTracks(data: AnimationData) { const tracks = Object.entries(data).map( ([property, frames]) => new NumberKeyframeTrack( "." + property, Object.keys(frames).map((t) => Number(t)), Object.values(frames) ) ) this.clip && this.mixer.uncacheClip(this.clip) this.loadClip(new AnimationClip(this.name, -1, tracks)) } public play({ crossFade = 0.25, repeat = true, onFinish }: PlayOptions = {}) { const [prevAction, prevRepeat] = mixerActionMap.get(this.mixer) ?? [] if (prevAction?.isRunning() && this.action === prevAction) { repeat !== prevRepeat && prevAction.setLoop(repeat ? LoopRepeat : LoopOnce, Infinity) return } mixerHandleMap.get(this.mixer)?.cancel() const handle = this.watch( onBeforeRender(() => this.mixer.update(dt[0])) ) mixerHandleMap.set(this.mixer, handle) const { action } = this if (!action) return if (prevAction && crossFade) { action.time = 0 action.enabled = true action.crossFadeFrom(prevAction, crossFade, true) } else this.mixer.stopAllAction() mixerActionMap.set(this.mixer, [action, repeat]) action.setLoop(repeat ? LoopRepeat : LoopOnce, Infinity) action.clampWhenFinished = true const handleFinish = () => onFinish?.() this.mixer.addEventListener("finished", handleFinish) handle.then(() => this.mixer.removeEventListener("finished", handleFinish) ) action.paused && action.stop() action.play() } public stop() { this.action && (this.action.paused = true) mixerHandleMap.get(this.mixer)?.cancel() } public getPaused() { return this.action?.paused } public setPaused(val: boolean) { this.action && (this.action.paused = val) } public update(seconds: number) { this.mixer.time = 0 this.action && (this.action.time = 0) this.mixer.update(seconds) } }