Files
esengine/packages/mesh-3d/src/Animation3DComponent.ts

342 lines
9.1 KiB
TypeScript
Raw Normal View History

/**
* Animation3DComponent - 3D animation playback component.
* Animation3DComponent - 3D
*/
import { Component, ECSComponent, Property, Serializable, Serialize } from '@esengine/ecs-framework';
import type { IGLTFAnimationClip } from '@esengine/asset-system';
/**
* Animation play state.
*
*/
export enum AnimationPlayState {
/** Stopped - not playing. | 停止 - 未播放。 */
Stopped = 'stopped',
/** Playing forward. | 正向播放。 */
Playing = 'playing',
/** Paused. | 暂停。 */
Paused = 'paused'
}
/**
* Animation wrap mode.
*
*/
export enum AnimationWrapMode {
/** Play once and stop. | 播放一次后停止。 */
Once = 'once',
/** Loop continuously. | 连续循环。 */
Loop = 'loop',
/** Play forward then backward (ping-pong). | 往返播放。 */
PingPong = 'pingpong',
/** Clamp to last frame. | 停在最后一帧。 */
ClampForever = 'clampForever'
}
/**
* 3D Animation component for playing skeletal/node animations.
* / 3D
*
* Requires MeshComponent for animation data source.
* MeshComponent
*/
@ECSComponent('Animation3D', { requires: ['Mesh'] })
@Serializable({ version: 1, typeId: 'Animation3D' })
export class Animation3DComponent extends Component {
/**
*
* Default animation clip name
*/
@Serialize()
@Property({ type: 'string', label: 'Default Clip' })
public defaultClip: string = '';
/**
* 1.0 =
* Playback speed (1.0 = normal speed)
*/
@Serialize()
@Property({ type: 'number', label: 'Speed', min: 0, max: 10 })
public speed: number = 1.0;
/**
*
* Wrap mode
*/
@Serialize()
@Property({
type: 'enum',
label: 'Wrap Mode',
options: ['once', 'loop', 'pingpong', 'clampForever']
})
public wrapMode: AnimationWrapMode = AnimationWrapMode.Loop;
/**
*
* Whether to auto-play on start
*/
@Serialize()
@Property({ type: 'boolean', label: 'Play On Awake' })
public playOnAwake: boolean = true;
// ===== Runtime State | 运行时状态 =====
/**
*
* Current play state
*/
private _playState: AnimationPlayState = AnimationPlayState.Stopped;
/**
*
* Currently playing animation clip
*/
private _currentClip: IGLTFAnimationClip | null = null;
/**
*
* Current playback time (seconds)
*/
private _currentTime: number = 0;
/**
* 1 = -1 =
* Playback direction (1 = forward, -1 = backward)
*/
private _direction: number = 1;
/**
*
* Available animation clips
*/
private _clips: IGLTFAnimationClip[] = [];
/**
*
* Map of clip name to index
*/
private _clipNameToIndex: Map<string, number> = new Map();
// ===== Public Getters | 公共获取器 =====
/**
*
* Get current play state
*/
public get playState(): AnimationPlayState {
return this._playState;
}
/**
*
* Get currently playing clip
*/
public get currentClip(): IGLTFAnimationClip | null {
return this._currentClip;
}
/**
*
* Get current playback time
*/
public get currentTime(): number {
return this._currentTime;
}
/**
*
* Get duration of current clip
*/
public get duration(): number {
return this._currentClip?.duration ?? 0;
}
/**
* 0-1
* Get normalized time (0-1)
*/
public get normalizedTime(): number {
if (!this._currentClip || this._currentClip.duration <= 0) return 0;
return this._currentTime / this._currentClip.duration;
}
/**
*
* Whether playing
*/
public get isPlaying(): boolean {
return this._playState === AnimationPlayState.Playing;
}
/**
*
* Get all available clips
*/
public get clips(): readonly IGLTFAnimationClip[] {
return this._clips;
}
/**
*
* Get all clip names
*/
public get clipNames(): string[] {
return this._clips.map(c => c.name);
}
// ===== Public Methods | 公共方法 =====
/**
* Animation3DSystem
* Set animation clips (called by Animation3DSystem)
*/
public setClips(clips: IGLTFAnimationClip[]): void {
this._clips = clips;
this._clipNameToIndex.clear();
clips.forEach((clip, index) => {
this._clipNameToIndex.set(clip.name, index);
});
}
/**
*
* Play animation
*
* @param clipName - /
*/
public play(clipName?: string): void {
const name = clipName ?? this.defaultClip ?? (this._clips[0]?.name ?? '');
if (!name) {
console.warn('[Animation3DComponent] No clip to play');
return;
}
const index = this._clipNameToIndex.get(name);
if (index === undefined) {
console.warn(`[Animation3DComponent] Clip not found: ${name}`);
return;
}
this._currentClip = this._clips[index];
this._currentTime = 0;
this._direction = 1;
this._playState = AnimationPlayState.Playing;
}
/**
*
* Stop animation
*/
public stop(): void {
this._playState = AnimationPlayState.Stopped;
this._currentTime = 0;
}
/**
*
* Pause animation
*/
public pause(): void {
if (this._playState === AnimationPlayState.Playing) {
this._playState = AnimationPlayState.Paused;
}
}
/**
*
* Resume playback
*/
public resume(): void {
if (this._playState === AnimationPlayState.Paused) {
this._playState = AnimationPlayState.Playing;
}
}
/**
*
* Set playback time
*/
public setTime(time: number): void {
this._currentTime = Math.max(0, Math.min(time, this.duration));
}
/**
*
* Set normalized time
*/
public setNormalizedTime(t: number): void {
this._currentTime = Math.max(0, Math.min(t, 1)) * this.duration;
}
/**
* Animation3DSystem
* Update playback time (called by Animation3DSystem)
*
* @param deltaTime -
*/
public updateTime(deltaTime: number): void {
if (this._playState !== AnimationPlayState.Playing || !this._currentClip) {
return;
}
const scaledDelta = deltaTime * this.speed * this._direction;
this._currentTime += scaledDelta;
const duration = this._currentClip.duration;
if (duration <= 0) return;
// Handle wrap mode
// 处理循环模式
switch (this.wrapMode) {
case AnimationWrapMode.Once:
if (this._currentTime >= duration || this._currentTime < 0) {
this._currentTime = Math.max(0, Math.min(this._currentTime, duration));
this._playState = AnimationPlayState.Stopped;
}
break;
case AnimationWrapMode.Loop:
while (this._currentTime >= duration) {
this._currentTime -= duration;
}
while (this._currentTime < 0) {
this._currentTime += duration;
}
break;
case AnimationWrapMode.PingPong:
if (this._currentTime >= duration) {
this._currentTime = duration - (this._currentTime - duration);
this._direction = -1;
} else if (this._currentTime < 0) {
this._currentTime = -this._currentTime;
this._direction = 1;
}
break;
case AnimationWrapMode.ClampForever:
this._currentTime = Math.max(0, Math.min(this._currentTime, duration));
break;
}
}
/**
*
* Reset component
*/
reset(): void {
this.defaultClip = '';
this.speed = 1.0;
this.wrapMode = AnimationWrapMode.Loop;
this.playOnAwake = true;
this._playState = AnimationPlayState.Stopped;
this._currentClip = null;
this._currentTime = 0;
this._direction = 1;
this._clips = [];
this._clipNameToIndex.clear();
}
}