Implement Owen Animation System with core classes, loaders, and state handlers

- Added OwenSystemFactory for creating the animation system.
- Introduced OwenAnimationContext to manage animations and states.
- Created AnimationLoader and GLTFAnimationLoader for loading animations.
- Developed state handlers: WaitStateHandler, ReactStateHandler, TypeStateHandler, SleepStateHandler.
- Implemented StateFactory for managing state handlers.
- Defined constants for clip types, states, and emotions.
- Added type definitions for TypeScript support.
- Configured Vite for building and serving the project.
- Added licenses (dual) to project.
This commit is contained in:
2025-05-23 21:36:52 +02:00
parent 9e5f576b68
commit 658e1e64b2
29 changed files with 6902 additions and 907 deletions

View File

@ -0,0 +1,257 @@
/**
* @fileoverview Core animation classes for clip management and creation
* @module animation
*/
import * as THREE from 'three';
import { ClipTypes, Config } from '../constants.js';
/**
* Represents a single animation clip with metadata and Three.js action
* @class
*/
export class AnimationClip {
/**
* Create an animation clip
* @param {string} name - The name of the animation clip
* @param {THREE.AnimationClip} threeAnimation - The Three.js animation clip
* @param {Object} metadata - Parsed metadata from animation name
*/
constructor(name, threeAnimation, metadata) {
/**
* The name of the animation clip
* @type {string}
*/
this.name = name;
/**
* The Three.js animation clip
* @type {THREE.AnimationClip}
*/
this.animation = threeAnimation;
/**
* Parsed metadata about the animation
* @type {Object}
*/
this.metadata = metadata;
/**
* The Three.js animation action
* @type {THREE.AnimationAction|null}
*/
this.action = null;
/**
* The animation mixer
* @type {THREE.AnimationMixer|null}
*/
this.mixer = null;
}
/**
* Create and configure a Three.js action for this clip
* @param {THREE.AnimationMixer} mixer - The animation mixer
* @returns {THREE.AnimationAction} The created action
*/
createAction(mixer) {
this.mixer = mixer;
this.action = mixer.clipAction(this.animation);
// Configure based on type
if (
this.metadata.type === ClipTypes.LOOP ||
this.metadata.type === ClipTypes.NESTED_LOOP
) {
this.action.setLoop(THREE.LoopRepeat, Infinity);
} else {
this.action.setLoop(THREE.LoopOnce);
this.action.clampWhenFinished = true;
}
return this.action;
}
/**
* Play the animation with optional fade in
* @param {number} [fadeInDuration=0.3] - Fade in duration in seconds
* @returns {Promise<void>} Promise that resolves when fade in completes
*/
play(fadeInDuration = Config.DEFAULT_FADE_IN) {
if (this.action) {
this.action.reset();
this.action.fadeIn(fadeInDuration);
this.action.play();
}
}
/**
* Stop the animation with optional fade out
* @param {number} [fadeOutDuration=0.3] - Fade out duration in seconds
* @returns {Promise<void>} Promise that resolves when fade out completes
*/
stop(fadeOutDuration = Config.DEFAULT_FADE_OUT) {
if (this.action) {
this.action.fadeOut(fadeOutDuration);
setTimeout(() => {
if (this.action) {
this.action.stop();
}
}, fadeOutDuration * 1000);
}
}
/**
* Check if the animation is currently playing
* @returns {boolean} True if playing, false otherwise
*/
isPlaying() {
return this.action?.isRunning() || false;
}
}
/**
* Factory for creating animation clips with parsed metadata
* @class
*/
export class AnimationClipFactory {
/**
* Create an animation clip factory
* @param {AnimationLoader} animationLoader - The animation loader instance
*/
constructor(animationLoader) {
/**
* The animation loader for loading animation data
* @type {AnimationLoader}
*/
this.animationLoader = animationLoader;
/**
* Cache for created animation clips
* @type {Map<string, AnimationClip>}
*/
this.clipCache = new Map();
}
/**
* Parse animation name and create clip metadata
* Format: [state]_[action]_[type] or [state]_[action]2[toState]_[emotion]_T
* @param {string} name - The animation name to parse
* @returns {Object} Parsed metadata object
*/
parseAnimationName(name) {
const parts = name.split('_');
const state = parts[ 0 ];
const action = parts[ 1 ];
// Handle transitions with emotions
if (parts[ 2 ]?.includes('2') && parts[ 3 ] === ClipTypes.TRANSITION) {
const [ , toState ] = parts[ 2 ].split('2');
return {
state,
action,
toState,
emotion: parts[ 2 ] || '',
type: ClipTypes.TRANSITION,
isTransition: true,
hasEmotion: true,
};
}
// Handle regular transitions
if (parts[ 2 ] === ClipTypes.TRANSITION) {
return {
state,
action,
type: ClipTypes.TRANSITION,
isTransition: true,
};
}
// Handle nested animations
if (parts[ 2 ] === ClipTypes.NESTED_IN || parts[ 2 ] === ClipTypes.NESTED_OUT) {
return {
state,
action,
type: parts[ 2 ],
nestedType: parts[ 3 ],
isNested: true,
};
}
// Handle nested loops and quirks
if (
parts[ 3 ] === ClipTypes.NESTED_LOOP ||
parts[ 3 ] === ClipTypes.NESTED_QUIRK
) {
return {
state,
action,
subAction: parts[ 2 ],
type: parts[ 3 ],
isNested: true,
};
}
// Handle standard loops and quirks
return {
state,
action,
type: parts[ 2 ],
isStandard: true,
};
}
/**
* Create an animation clip from a name
* @param {string} name - The animation name
* @returns {Promise<AnimationClip>} The created animation clip
*/
async createClip(name) {
if (this.clipCache.has(name)) {
return this.clipCache.get(name);
}
const metadata = this.parseAnimationName(name);
const animation = await this.animationLoader.loadAnimation(name);
const clip = new AnimationClip(name, animation, metadata);
this.clipCache.set(name, clip);
return clip;
}
/**
* Create all animation clips from a model's animations
* @param {THREE.Object3D} model - The 3D model containing animations
* @returns {Promise<Map<string, AnimationClip>>} Map of animation name to clip
*/
async createClipsFromModel(model) {
const clips = new Map();
const animations = model.animations || [];
for (const animation of animations) {
const clip = await this.createClip(animation.name, model);
clips.set(animation.name, clip);
}
return clips;
}
/**
* Clear the clip cache
* @returns {void}
*/
clearCache() {
this.clipCache.clear();
}
/**
* Get cached clip by name
* @param {string} name - The animation name
* @returns {AnimationClip|undefined} The cached clip or undefined
*/
getCachedClip(name) {
return this.clipCache.get(name);
}
}