reze-engine 0.9.4 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -5
- package/dist/animation.d.ts +26 -13
- package/dist/animation.d.ts.map +1 -1
- package/dist/animation.js +95 -35
- package/dist/engine.d.ts +5 -1
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +78 -23
- package/dist/ik-solver.d.ts.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/model.d.ts +8 -11
- package/dist/model.d.ts.map +1 -1
- package/dist/model.js +55 -38
- package/package.json +1 -1
- package/src/animation.ts +124 -40
- package/src/engine.ts +121 -69
- package/src/ik-solver.ts +7 -7
- package/src/index.ts +9 -5
- package/src/model.ts +64 -42
package/dist/model.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Mat4, Quat, Vec3 } from "./math";
|
|
2
2
|
import { Rigidbody, Joint } from "./physics";
|
|
3
|
-
import {
|
|
3
|
+
import { AnimationClip, AnimationPlayOptions, AnimationProgress } from "./animation";
|
|
4
4
|
export interface Texture {
|
|
5
5
|
path: string;
|
|
6
6
|
name: string;
|
|
@@ -145,26 +145,23 @@ export declare class Model {
|
|
|
145
145
|
setMorphWeight(name: string, weight: number, durationMs?: number): void;
|
|
146
146
|
private applyMorphs;
|
|
147
147
|
private buildClipFromVmdKeyFrames;
|
|
148
|
-
loadAnimation(animationName: string,
|
|
148
|
+
loadAnimation(animationName: string, source: string): Promise<void>;
|
|
149
|
+
loadAnimation(animationName: string, source: AnimationClip): void;
|
|
149
150
|
resetAllBones(): void;
|
|
150
151
|
resetAllMorphs(): void;
|
|
151
|
-
|
|
152
|
+
getAnimationClip(name: string): AnimationClip | null;
|
|
152
153
|
play(): void;
|
|
153
154
|
play(name: string): boolean;
|
|
155
|
+
play(name: string, options?: AnimationPlayOptions): boolean;
|
|
154
156
|
show(name: string): void;
|
|
155
157
|
playAnimation(): void;
|
|
156
158
|
pause(): void;
|
|
157
159
|
pauseAnimation(): void;
|
|
158
160
|
stop(): void;
|
|
159
161
|
stopAnimation(): void;
|
|
160
|
-
seek(
|
|
161
|
-
seekAnimation(
|
|
162
|
-
getAnimationProgress():
|
|
163
|
-
current: number;
|
|
164
|
-
duration: number;
|
|
165
|
-
percentage: number;
|
|
166
|
-
animationName: string | null;
|
|
167
|
-
};
|
|
162
|
+
seek(seconds: number): void;
|
|
163
|
+
seekAnimation(seconds: number): void;
|
|
164
|
+
getAnimationProgress(): AnimationProgress;
|
|
168
165
|
private static upperBound;
|
|
169
166
|
private findKeyframeIndex;
|
|
170
167
|
private applyPoseFromClip;
|
package/dist/model.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"model.d.ts","sourceRoot":"","sources":["../src/model.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAA;AAEzC,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,WAAW,CAAA;AAG5C,OAAO,
|
|
1
|
+
{"version":3,"file":"model.d.ts","sourceRoot":"","sources":["../src/model.ts"],"names":[],"mappings":"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"}
|
package/dist/model.js
CHANGED
|
@@ -3,7 +3,6 @@ import { Engine } from "./engine";
|
|
|
3
3
|
import { IKSolverSystem } from "./ik-solver";
|
|
4
4
|
import { VMDLoader } from "./vmd-loader";
|
|
5
5
|
import { AnimationState, interpolateControlPoints, rawInterpolationToBoneInterpolation, } from "./animation";
|
|
6
|
-
const VMD_FPS = 30;
|
|
7
6
|
const VERTEX_STRIDE = 8;
|
|
8
7
|
export class Model {
|
|
9
8
|
get name() {
|
|
@@ -255,7 +254,7 @@ export class Model {
|
|
|
255
254
|
getMorphWeights() {
|
|
256
255
|
return this.runtimeMorph.weights;
|
|
257
256
|
}
|
|
258
|
-
// ------- Bone helpers (
|
|
257
|
+
// ------- Bone helpers (API) -------
|
|
259
258
|
rotateBones(boneRotations, durationMs) {
|
|
260
259
|
const state = this.tweenState;
|
|
261
260
|
// Clone and normalize to avoid mutating input
|
|
@@ -548,7 +547,6 @@ export class Model {
|
|
|
548
547
|
rotation: kf.rotation,
|
|
549
548
|
translation: kf.translation,
|
|
550
549
|
interpolation: kf.interpolation,
|
|
551
|
-
time: kf.frame / VMD_FPS,
|
|
552
550
|
})));
|
|
553
551
|
}
|
|
554
552
|
const morphTracksByMorph = {};
|
|
@@ -567,24 +565,28 @@ export class Model {
|
|
|
567
565
|
morphName: name,
|
|
568
566
|
frame: kf.frame,
|
|
569
567
|
weight: kf.weight,
|
|
570
|
-
time: kf.frame / VMD_FPS,
|
|
571
568
|
})));
|
|
572
569
|
}
|
|
573
|
-
let
|
|
570
|
+
let maxFrame = 0;
|
|
574
571
|
for (const frames of boneTracks.values()) {
|
|
575
572
|
if (frames.length > 0)
|
|
576
|
-
|
|
573
|
+
maxFrame = Math.max(maxFrame, frames[frames.length - 1].frame);
|
|
577
574
|
}
|
|
578
575
|
for (const frames of morphTracks.values()) {
|
|
579
576
|
if (frames.length > 0)
|
|
580
|
-
|
|
577
|
+
maxFrame = Math.max(maxFrame, frames[frames.length - 1].frame);
|
|
581
578
|
}
|
|
582
|
-
return { boneTracks, morphTracks,
|
|
579
|
+
return { boneTracks, morphTracks, frameCount: maxFrame };
|
|
583
580
|
}
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
581
|
+
loadAnimation(animationName, source) {
|
|
582
|
+
if (typeof source !== "string") {
|
|
583
|
+
this.animationState.loadAnimation(animationName, source);
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
return VMDLoader.load(source).then((vmdKeyFrames) => {
|
|
587
|
+
const clip = this.buildClipFromVmdKeyFrames(vmdKeyFrames);
|
|
588
|
+
this.animationState.loadAnimation(animationName, clip);
|
|
589
|
+
});
|
|
588
590
|
}
|
|
589
591
|
resetAllBones() {
|
|
590
592
|
for (let boneIdx = 0; boneIdx < this.skeleton.bones.length; boneIdx++) {
|
|
@@ -603,17 +605,21 @@ export class Model {
|
|
|
603
605
|
this.morphsDirty = true;
|
|
604
606
|
this.applyMorphs();
|
|
605
607
|
}
|
|
606
|
-
|
|
607
|
-
return this.animationState;
|
|
608
|
+
getAnimationClip(name) {
|
|
609
|
+
return this.animationState.getAnimationClip(name);
|
|
608
610
|
}
|
|
609
|
-
play(name) {
|
|
611
|
+
play(name, options) {
|
|
610
612
|
if (name === undefined) {
|
|
611
613
|
this.animationState.play();
|
|
612
614
|
return;
|
|
613
615
|
}
|
|
614
|
-
|
|
616
|
+
this.resetAllBones();
|
|
617
|
+
this.resetAllMorphs();
|
|
618
|
+
return this.animationState.play(name, options);
|
|
615
619
|
}
|
|
616
620
|
show(name) {
|
|
621
|
+
this.resetAllBones();
|
|
622
|
+
this.resetAllMorphs();
|
|
617
623
|
this.animationState.show(name);
|
|
618
624
|
}
|
|
619
625
|
// @deprecated Use model.play()
|
|
@@ -634,42 +640,51 @@ export class Model {
|
|
|
634
640
|
stopAnimation() {
|
|
635
641
|
this.animationState.stop();
|
|
636
642
|
}
|
|
637
|
-
|
|
638
|
-
|
|
643
|
+
// Seek by absolute timeline seconds, not frame index.
|
|
644
|
+
seek(seconds) {
|
|
645
|
+
this.animationState.seek(seconds);
|
|
639
646
|
}
|
|
640
647
|
// @deprecated Use model.seek()
|
|
641
|
-
seekAnimation(
|
|
642
|
-
this.animationState.seek(
|
|
648
|
+
seekAnimation(seconds) {
|
|
649
|
+
this.animationState.seek(seconds);
|
|
643
650
|
}
|
|
644
651
|
getAnimationProgress() {
|
|
645
652
|
const p = this.animationState.getProgress();
|
|
646
|
-
return {
|
|
653
|
+
return {
|
|
654
|
+
current: p.current,
|
|
655
|
+
duration: p.duration,
|
|
656
|
+
percentage: p.percentage,
|
|
657
|
+
animationName: p.animationName,
|
|
658
|
+
looping: p.looping,
|
|
659
|
+
playing: p.playing,
|
|
660
|
+
paused: p.paused,
|
|
661
|
+
};
|
|
647
662
|
}
|
|
648
|
-
static upperBound(
|
|
663
|
+
static upperBound(frame, keyFrames, startIdx = 0) {
|
|
649
664
|
let left = startIdx, right = keyFrames.length;
|
|
650
665
|
while (left < right) {
|
|
651
666
|
const mid = Math.floor((left + right) / 2);
|
|
652
|
-
if (keyFrames[mid].
|
|
667
|
+
if (keyFrames[mid].frame <= frame)
|
|
653
668
|
left = mid + 1;
|
|
654
669
|
else
|
|
655
670
|
right = mid;
|
|
656
671
|
}
|
|
657
672
|
return left;
|
|
658
673
|
}
|
|
659
|
-
findKeyframeIndex(
|
|
674
|
+
findKeyframeIndex(frame, keyFrames, cachedIdx) {
|
|
660
675
|
if (keyFrames.length === 0)
|
|
661
676
|
return -1;
|
|
662
677
|
if (cachedIdx >= 0 && cachedIdx < keyFrames.length) {
|
|
663
|
-
const
|
|
664
|
-
const
|
|
665
|
-
if (
|
|
678
|
+
const currentFrame = keyFrames[cachedIdx].frame;
|
|
679
|
+
const nextFrame = cachedIdx + 1 < keyFrames.length ? keyFrames[cachedIdx + 1].frame : Infinity;
|
|
680
|
+
if (frame >= currentFrame && frame < nextFrame) {
|
|
666
681
|
return cachedIdx;
|
|
667
682
|
}
|
|
668
683
|
}
|
|
669
|
-
const idx = Model.upperBound(
|
|
684
|
+
const idx = Model.upperBound(frame, keyFrames, 0) - 1;
|
|
670
685
|
return idx;
|
|
671
686
|
}
|
|
672
|
-
applyPoseFromClip(clip,
|
|
687
|
+
applyPoseFromClip(clip, frame) {
|
|
673
688
|
if (!clip)
|
|
674
689
|
return;
|
|
675
690
|
if (clip !== this.lastAppliedClip) {
|
|
@@ -681,8 +696,8 @@ export class Model {
|
|
|
681
696
|
if (keyFrames.length === 0)
|
|
682
697
|
continue;
|
|
683
698
|
const cachedIdx = this.boneTrackIndices.get(boneName) ?? -1;
|
|
684
|
-
const
|
|
685
|
-
const idx = this.findKeyframeIndex(
|
|
699
|
+
const clampedFrame = Math.max(keyFrames[0].frame, Math.min(keyFrames[keyFrames.length - 1].frame, frame));
|
|
700
|
+
const idx = this.findKeyframeIndex(clampedFrame, keyFrames, cachedIdx);
|
|
686
701
|
if (idx < 0)
|
|
687
702
|
continue;
|
|
688
703
|
this.boneTrackIndices.set(boneName, idx);
|
|
@@ -700,8 +715,8 @@ export class Model {
|
|
|
700
715
|
localTrans.set(localTranslation);
|
|
701
716
|
}
|
|
702
717
|
else {
|
|
703
|
-
const
|
|
704
|
-
const gradient = (
|
|
718
|
+
const frameDelta = frameB.frame - frameA.frame;
|
|
719
|
+
const gradient = frameDelta > 0 ? (clampedFrame - frameA.frame) / frameDelta : 0;
|
|
705
720
|
const interp = frameB.interpolation;
|
|
706
721
|
const rotT = interpolateControlPoints(interp.rotation, gradient);
|
|
707
722
|
const rotation = Quat.slerp(frameA.rotation, frameB.rotation, rotT);
|
|
@@ -718,8 +733,8 @@ export class Model {
|
|
|
718
733
|
if (keyFrames.length === 0)
|
|
719
734
|
continue;
|
|
720
735
|
const cachedIdx = this.morphTrackIndices.get(morphName) ?? -1;
|
|
721
|
-
const
|
|
722
|
-
const idx = this.findKeyframeIndex(
|
|
736
|
+
const clampedFrame = Math.max(keyFrames[0].frame, Math.min(keyFrames[keyFrames.length - 1].frame, frame));
|
|
737
|
+
const idx = this.findKeyframeIndex(clampedFrame, keyFrames, cachedIdx);
|
|
723
738
|
if (idx < 0)
|
|
724
739
|
continue;
|
|
725
740
|
this.morphTrackIndices.set(morphName, idx);
|
|
@@ -731,7 +746,9 @@ export class Model {
|
|
|
731
746
|
const weight = frameB
|
|
732
747
|
? frameA.weight +
|
|
733
748
|
(frameB.weight - frameA.weight) *
|
|
734
|
-
(
|
|
749
|
+
(keyFrames[idx + 1].frame > keyFrames[idx].frame
|
|
750
|
+
? (clampedFrame - keyFrames[idx].frame) / (keyFrames[idx + 1].frame - keyFrames[idx].frame)
|
|
751
|
+
: 0)
|
|
735
752
|
: frameA.weight;
|
|
736
753
|
this.runtimeMorph.weights[morphIdx] = weight;
|
|
737
754
|
this.morphsDirty = true; // Mark as dirty when animation sets morph weights
|
|
@@ -745,9 +762,9 @@ export class Model {
|
|
|
745
762
|
const tweensChangedMorphs = this.updateTweens();
|
|
746
763
|
this.animationState.update(deltaTime);
|
|
747
764
|
const clip = this.animationState.getCurrentClip();
|
|
748
|
-
const
|
|
765
|
+
const frame = this.animationState.getCurrentFrame();
|
|
749
766
|
if (clip !== null) {
|
|
750
|
-
this.applyPoseFromClip(clip,
|
|
767
|
+
this.applyPoseFromClip(clip, frame);
|
|
751
768
|
}
|
|
752
769
|
// Apply morphs if tweens changed morphs or animation changed morphs
|
|
753
770
|
const verticesChanged = this.morphsDirty || tweensChangedMorphs;
|
package/package.json
CHANGED
package/src/animation.ts
CHANGED
|
@@ -18,52 +18,82 @@ export interface BoneKeyframe {
|
|
|
18
18
|
rotation: Quat
|
|
19
19
|
translation: Vec3
|
|
20
20
|
interpolation: BoneInterpolation
|
|
21
|
-
time: number
|
|
22
21
|
}
|
|
23
22
|
|
|
24
23
|
export interface MorphKeyframe {
|
|
25
24
|
morphName: string
|
|
26
25
|
frame: number
|
|
27
26
|
weight: number
|
|
28
|
-
time: number
|
|
29
27
|
}
|
|
30
28
|
|
|
31
29
|
export interface AnimationClip {
|
|
32
30
|
boneTracks: Map<string, BoneKeyframe[]>
|
|
33
31
|
morphTracks: Map<string, MorphKeyframe[]>
|
|
32
|
+
frameCount: number // last keyframe frame index
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface AnimationPlayOptions {
|
|
36
|
+
priority?: number // Higher number = higher priority. Default: 0.
|
|
37
|
+
loop?: boolean // When true, timeline wraps at end. Default: false.
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Wall-clock playback progress; `current`/`duration` are seconds (clip span uses `AnimationClip.frameCount`, not `duration`). */
|
|
41
|
+
export interface AnimationProgress {
|
|
42
|
+
animationName: string | null
|
|
43
|
+
current: number
|
|
34
44
|
duration: number
|
|
35
|
-
|
|
45
|
+
percentage: number
|
|
46
|
+
looping: boolean
|
|
47
|
+
/** True while the timeline is advancing (not idle at end, not paused). */
|
|
48
|
+
playing: boolean
|
|
49
|
+
paused: boolean
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export const FPS = 30
|
|
53
|
+
|
|
54
|
+
interface QueuedAnimationRequest {
|
|
55
|
+
name: string
|
|
56
|
+
priority: number
|
|
57
|
+
loop: boolean
|
|
36
58
|
}
|
|
37
59
|
|
|
38
|
-
//
|
|
60
|
+
// Priority-aware playback: higher priority preempts, otherwise latest request is queued.
|
|
39
61
|
export class AnimationState {
|
|
40
62
|
private animations = new Map<string, AnimationClip>()
|
|
41
63
|
private currentAnimationName: string | null = null
|
|
42
|
-
private
|
|
64
|
+
private currentFrame = 0
|
|
65
|
+
private currentPriority = 0
|
|
66
|
+
private currentLoop = false
|
|
43
67
|
private isPlaying = false
|
|
44
68
|
private isPaused = false
|
|
45
|
-
private
|
|
69
|
+
private nextAnimation: QueuedAnimationRequest | null = null
|
|
46
70
|
private onEnd: ((animationName: string) => void) | null = null
|
|
47
71
|
|
|
48
72
|
loadAnimation(name: string, clip: AnimationClip): void {
|
|
49
|
-
this.animations.set(name,
|
|
73
|
+
this.animations.set(name, {
|
|
74
|
+
boneTracks: clip.boneTracks,
|
|
75
|
+
morphTracks: clip.morphTracks,
|
|
76
|
+
frameCount: clip.frameCount,
|
|
77
|
+
})
|
|
50
78
|
}
|
|
51
79
|
|
|
52
80
|
removeAnimation(name: string): void {
|
|
53
81
|
this.animations.delete(name)
|
|
54
82
|
if (this.currentAnimationName === name) {
|
|
55
83
|
this.currentAnimationName = null
|
|
56
|
-
this.
|
|
84
|
+
this.currentFrame = 0
|
|
85
|
+
this.currentPriority = 0
|
|
86
|
+
this.currentLoop = false
|
|
57
87
|
this.isPlaying = false
|
|
58
|
-
this.
|
|
59
|
-
} else if (this.
|
|
60
|
-
this.
|
|
88
|
+
this.nextAnimation = this.nextAnimation?.name === name ? null : this.nextAnimation
|
|
89
|
+
} else if (this.nextAnimation?.name === name) {
|
|
90
|
+
this.nextAnimation = null
|
|
61
91
|
}
|
|
62
92
|
}
|
|
63
93
|
|
|
64
|
-
play(name: string): boolean
|
|
94
|
+
play(name: string, options?: AnimationPlayOptions): boolean
|
|
65
95
|
play(): void
|
|
66
|
-
play(name?: string): boolean | void {
|
|
96
|
+
play(name?: string, options?: AnimationPlayOptions): boolean | void {
|
|
67
97
|
if (name === undefined) {
|
|
68
98
|
if (this.currentAnimationName && this.animations.has(this.currentAnimationName)) {
|
|
69
99
|
this.isPaused = false
|
|
@@ -72,15 +102,39 @@ export class AnimationState {
|
|
|
72
102
|
return
|
|
73
103
|
}
|
|
74
104
|
if (!this.animations.has(name)) return false
|
|
105
|
+
const priority = options?.priority ?? 0
|
|
106
|
+
const loop = options?.loop ?? false
|
|
107
|
+
|
|
108
|
+
if (this.currentAnimationName === name) {
|
|
109
|
+
this.currentFrame = 0
|
|
110
|
+
this.currentPriority = priority
|
|
111
|
+
this.currentLoop = loop
|
|
112
|
+
this.isPlaying = true
|
|
113
|
+
this.isPaused = false
|
|
114
|
+
return true
|
|
115
|
+
}
|
|
116
|
+
|
|
75
117
|
if (this.isPlaying && !this.isPaused) {
|
|
76
|
-
this.
|
|
118
|
+
if (priority > this.currentPriority) {
|
|
119
|
+
this.currentAnimationName = name
|
|
120
|
+
this.currentFrame = 0
|
|
121
|
+
this.currentPriority = priority
|
|
122
|
+
this.currentLoop = loop
|
|
123
|
+
this.isPlaying = true
|
|
124
|
+
this.isPaused = false
|
|
125
|
+
this.nextAnimation = null
|
|
126
|
+
return true
|
|
127
|
+
}
|
|
128
|
+
this.nextAnimation = { name, priority, loop }
|
|
77
129
|
return true
|
|
78
130
|
}
|
|
79
131
|
this.currentAnimationName = name
|
|
80
|
-
this.
|
|
132
|
+
this.currentFrame = 0
|
|
133
|
+
this.currentPriority = priority
|
|
134
|
+
this.currentLoop = loop
|
|
81
135
|
this.isPlaying = true
|
|
82
136
|
this.isPaused = false
|
|
83
|
-
this.
|
|
137
|
+
this.nextAnimation = null
|
|
84
138
|
return true
|
|
85
139
|
}
|
|
86
140
|
|
|
@@ -91,22 +145,30 @@ export class AnimationState {
|
|
|
91
145
|
const clip = this.animations.get(this.currentAnimationName)
|
|
92
146
|
if (!clip) return { ended: false, animationName: this.currentAnimationName }
|
|
93
147
|
|
|
94
|
-
|
|
95
|
-
|
|
148
|
+
const frameCount = clip.frameCount
|
|
149
|
+
if (frameCount <= 0 || !Number.isFinite(frameCount)) {
|
|
150
|
+
return { ended: false, animationName: this.currentAnimationName }
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
this.currentFrame += deltaTime * FPS
|
|
96
154
|
|
|
97
|
-
if (this.
|
|
98
|
-
this.
|
|
99
|
-
|
|
100
|
-
|
|
155
|
+
if (this.currentFrame >= frameCount) {
|
|
156
|
+
if (this.currentLoop) {
|
|
157
|
+
while (this.currentFrame >= frameCount) {
|
|
158
|
+
this.currentFrame -= frameCount
|
|
159
|
+
}
|
|
101
160
|
return { ended: false, animationName: this.currentAnimationName }
|
|
102
161
|
}
|
|
162
|
+
this.currentFrame = frameCount
|
|
103
163
|
const finishedName = this.currentAnimationName
|
|
104
164
|
this.onEnd?.(finishedName)
|
|
105
|
-
if (this.
|
|
106
|
-
const next = this.
|
|
107
|
-
this.
|
|
108
|
-
this.currentAnimationName = next
|
|
109
|
-
this.
|
|
165
|
+
if (this.nextAnimation !== null) {
|
|
166
|
+
const next = this.nextAnimation
|
|
167
|
+
this.nextAnimation = null
|
|
168
|
+
this.currentAnimationName = next.name
|
|
169
|
+
this.currentFrame = 0
|
|
170
|
+
this.currentPriority = next.priority
|
|
171
|
+
this.currentLoop = next.loop
|
|
110
172
|
this.isPlaying = true
|
|
111
173
|
this.isPaused = false
|
|
112
174
|
return { ended: true, animationName: finishedName }
|
|
@@ -124,42 +186,62 @@ export class AnimationState {
|
|
|
124
186
|
stop(): void {
|
|
125
187
|
this.isPlaying = false
|
|
126
188
|
this.isPaused = false
|
|
127
|
-
this.
|
|
128
|
-
this.
|
|
189
|
+
this.currentFrame = 0
|
|
190
|
+
this.currentPriority = 0
|
|
191
|
+
this.currentLoop = false
|
|
192
|
+
this.nextAnimation = null
|
|
129
193
|
}
|
|
130
194
|
|
|
131
|
-
|
|
195
|
+
// Seek by absolute timeline seconds, not frame index.
|
|
196
|
+
seek(seconds: number): void {
|
|
132
197
|
const clip = this.getCurrentClip()
|
|
133
|
-
if (!clip) return
|
|
134
|
-
|
|
198
|
+
if (!clip || clip.frameCount <= 0 || !Number.isFinite(clip.frameCount)) return
|
|
199
|
+
const targetFrame = seconds * FPS
|
|
200
|
+
this.currentFrame = Math.max(0, Math.min(targetFrame, clip.frameCount))
|
|
135
201
|
}
|
|
136
202
|
|
|
137
203
|
getCurrentClip(): AnimationClip | null {
|
|
138
204
|
return this.currentAnimationName !== null ? this.animations.get(this.currentAnimationName) ?? null : null
|
|
139
205
|
}
|
|
140
206
|
|
|
207
|
+
getAnimationClip(name: string): AnimationClip | null {
|
|
208
|
+
return this.animations.get(name) ?? null
|
|
209
|
+
}
|
|
210
|
+
|
|
141
211
|
getCurrentAnimation(): string | null {
|
|
142
212
|
return this.currentAnimationName
|
|
143
213
|
}
|
|
144
214
|
|
|
145
215
|
getCurrentTime(): number {
|
|
146
|
-
|
|
216
|
+
const clip = this.getCurrentClip()
|
|
217
|
+
if (!clip) return 0
|
|
218
|
+
return this.currentFrame / FPS
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
getCurrentFrame(): number {
|
|
222
|
+
return this.currentFrame
|
|
147
223
|
}
|
|
148
224
|
|
|
225
|
+
/** Clip length in seconds (`frameCount / FPS`). */
|
|
149
226
|
getDuration(): number {
|
|
150
227
|
const clip = this.getCurrentClip()
|
|
151
|
-
|
|
228
|
+
if (!clip || clip.frameCount <= 0 || !Number.isFinite(clip.frameCount)) return 0
|
|
229
|
+
return clip.frameCount / FPS
|
|
152
230
|
}
|
|
153
231
|
|
|
154
|
-
getProgress():
|
|
232
|
+
getProgress(): AnimationProgress {
|
|
155
233
|
const clip = this.getCurrentClip()
|
|
156
|
-
const duration = clip ? clip.
|
|
157
|
-
const
|
|
234
|
+
const duration = clip && clip.frameCount > 0 ? clip.frameCount / FPS : 0
|
|
235
|
+
const current = clip ? this.currentFrame / FPS : 0
|
|
236
|
+
const percentage = duration > 0 ? (current / duration) * 100 : 0
|
|
158
237
|
return {
|
|
159
238
|
animationName: this.currentAnimationName,
|
|
160
|
-
current
|
|
239
|
+
current,
|
|
161
240
|
duration,
|
|
162
241
|
percentage,
|
|
242
|
+
looping: this.currentLoop,
|
|
243
|
+
playing: this.isPlaying && !this.isPaused,
|
|
244
|
+
paused: this.isPaused,
|
|
163
245
|
}
|
|
164
246
|
}
|
|
165
247
|
|
|
@@ -174,10 +256,12 @@ export class AnimationState {
|
|
|
174
256
|
show(name: string): void {
|
|
175
257
|
if (!this.animations.has(name)) return
|
|
176
258
|
this.currentAnimationName = name
|
|
177
|
-
this.
|
|
259
|
+
this.currentFrame = 0
|
|
260
|
+
this.currentPriority = 0
|
|
261
|
+
this.currentLoop = false
|
|
178
262
|
this.isPlaying = false
|
|
179
263
|
this.isPaused = false
|
|
180
|
-
this.
|
|
264
|
+
this.nextAnimation = null
|
|
181
265
|
}
|
|
182
266
|
|
|
183
267
|
setOnEnd(callback: ((animationName: string) => void) | null): void {
|