Implement multi-scheme animation name mapper for Owen Animation System
Some checks failed
CI/CD Pipeline / Test & Lint (16.x) (push) Has been cancelled
CI/CD Pipeline / Test & Lint (18.x) (push) Has been cancelled
CI/CD Pipeline / Test & Lint (20.x) (push) Has been cancelled
CI/CD Pipeline / Security Audit (push) Has been cancelled
CI/CD Pipeline / Release (push) Has been cancelled
Release / Validate Version (push) Has been cancelled
Release / Build and Test (push) Has been cancelled
Release / Create Release (push) Has been cancelled
Release / Publish to NPM (push) Has been cancelled
Release / Deploy Demo (push) Has been cancelled
Animation Processing Pipeline / Validate Animation Names (push) Has been cancelled
Animation Processing Pipeline / Process Blender Animation Assets (push) Has been cancelled
Animation Processing Pipeline / Update Animation Documentation (push) Has been cancelled
Animation Processing Pipeline / Deploy Animation Demo (push) Has been cancelled

- Added AnimationNameMapper class to handle conversion between different animation naming schemes (legacy, artist, hierarchical, semantic).
- Included methods for initialization, pattern matching, conversion, and validation of animation names.
- Developed comprehensive unit tests for the animation name converter and demo pages using Playwright.
- Created a Vite configuration for the demo application, including asset handling and optimization settings.
- Enhanced the demo with features for batch conversion, performance metrics, and responsive design.
This commit is contained in:
2025-05-24 05:18:13 +02:00
parent d513e80c07
commit ad8dbb95dd
55 changed files with 20060 additions and 6686 deletions

View File

@ -0,0 +1,280 @@
/**
* @fileoverview Animation constants with multi-scheme support for Owen Animation System
* @module animation/AnimationConstants
*/
import { AnimationNameMapper } from './AnimationNameMapper.js'
// Create a singleton instance of the name mapper
const nameMapper = new AnimationNameMapper()
/**
* Legacy animation names (backward compatibility)
* @constant
*/
export const LegacyAnimations = {
// Wait state animations
WAIT_IDLE_LOOP: 'wait_idle_L',
WAIT_PICK_NOSE_QUIRK: 'wait_pickNose_Q',
WAIT_STRETCH_QUIRK: 'wait_stretch_Q',
WAIT_YAWN_QUIRK: 'wait_yawn_Q',
// React state animations - neutral
REACT_IDLE_LOOP: 'react_idle_L',
REACT_ACKNOWLEDGE_TRANSITION: 'react_acknowledge_T',
REACT_NOD_TRANSITION: 'react_nod_T',
REACT_LISTENING_LOOP: 'react_listening_L',
// React state animations - angry
REACT_ANGRY_IDLE_LOOP: 'react_angry_L',
REACT_ANGRY_FROWN_TRANSITION: 'react_an2frown_T',
REACT_ANGRY_GRUMBLE_QUIRK: 'react_an2grumble_Q',
REACT_ANGRY_TO_TYPE_TRANSITION: 'react_an2type_T',
// React state animations - happy
REACT_HAPPY_IDLE_LOOP: 'react_happy_L',
REACT_HAPPY_SMILE_TRANSITION: 'react_hp2smile_T',
REACT_HAPPY_BOUNCE_QUIRK: 'react_hp2bounce_Q',
REACT_HAPPY_TO_TYPE_TRANSITION: 'react_hp2type_T',
// React state animations - sad
REACT_SAD_IDLE_LOOP: 'react_sad_L',
REACT_SAD_SIGH_TRANSITION: 'react_sd2sigh_T',
REACT_SAD_SLUMP_QUIRK: 'react_sd2slump_Q',
REACT_SAD_TO_TYPE_TRANSITION: 'react_sd2type_T',
// React state animations - shocked
REACT_SHOCKED_IDLE_LOOP: 'react_shocked_L',
REACT_SHOCKED_GASP_TRANSITION: 'react_sh2gasp_T',
REACT_SHOCKED_JUMP_QUIRK: 'react_sh2jump_Q',
REACT_SHOCKED_TO_TYPE_TRANSITION: 'react_sh2type_T',
// Type state animations
TYPE_IDLE_LOOP: 'type_idle_L',
TYPE_FAST_LOOP: 'type_fast_L',
TYPE_SLOW_LOOP: 'type_slow_L',
TYPE_THINKING_LOOP: 'type_thinking_L',
TYPE_TO_WAIT_TRANSITION: 'type2wait_T',
// Sleep state animations
SLEEP_LIGHT_LOOP: 'sleep_light_L',
SLEEP_DEEP_LOOP: 'sleep_deep_L',
SLEEP_DREAM_QUIRK: 'sleep_dream_Q',
SLEEP_WAKE_UP_TRANSITION: 'sleep2wake_T'
}
/**
* Artist-friendly animation names (Blender workflow)
* @constant
*/
export const ArtistAnimations = {
// Wait state animations
WAIT_IDLE: 'Owen_WaitIdle',
WAIT_PICK_NOSE: 'Owen_PickNose',
WAIT_STRETCH: 'Owen_Stretch',
WAIT_YAWN: 'Owen_Yawn',
// React state animations - neutral
REACT_IDLE: 'Owen_ReactIdle',
REACT_ACKNOWLEDGE: 'Owen_ReactAcknowledge',
REACT_NOD: 'Owen_ReactNod',
REACT_LISTENING: 'Owen_ReactListening',
// React state animations - angry
REACT_ANGRY_IDLE: 'Owen_ReactAngryIdle',
REACT_ANGRY_FROWN: 'Owen_ReactAngryFrown',
REACT_ANGRY_GRUMBLE: 'Owen_ReactAngryGrumble',
REACT_ANGRY_TO_TYPE: 'Owen_ReactAngryToType',
// React state animations - happy
REACT_HAPPY_IDLE: 'Owen_ReactHappyIdle',
REACT_HAPPY_SMILE: 'Owen_ReactHappySmile',
REACT_HAPPY_BOUNCE: 'Owen_ReactHappyBounce',
REACT_HAPPY_TO_TYPE: 'Owen_ReactHappyToType',
// React state animations - sad
REACT_SAD_IDLE: 'Owen_ReactSadIdle',
REACT_SAD_SIGH: 'Owen_ReactSadSigh',
REACT_SAD_SLUMP: 'Owen_ReactSadSlump',
REACT_SAD_TO_TYPE: 'Owen_ReactSadToType',
// React state animations - shocked
REACT_SHOCKED_IDLE: 'Owen_ReactShockedIdle',
REACT_SHOCKED_GASP: 'Owen_ReactShockedGasp',
REACT_SHOCKED_JUMP: 'Owen_ReactShockedJump',
REACT_SHOCKED_TO_TYPE: 'Owen_ReactShockedToType',
// Type state animations
TYPE_IDLE: 'Owen_TypeIdle',
TYPE_FAST: 'Owen_TypeFast',
TYPE_SLOW: 'Owen_TypeSlow',
TYPE_THINKING: 'Owen_TypeThinking',
TYPE_TO_WAIT: 'Owen_TypeToWait',
// Sleep state animations
SLEEP_LIGHT: 'Owen_SleepLight',
SLEEP_DEEP: 'Owen_SleepDeep',
SLEEP_DREAM: 'Owen_SleepDream',
SLEEP_WAKE_UP: 'Owen_SleepWakeUp'
}
/**
* Hierarchical animation names (organized structure)
* @constant
*/
export const HierarchicalAnimations = {
// Wait state animations
WAIT_IDLE: 'owen.state.wait.idle.loop',
WAIT_PICK_NOSE: 'owen.quirk.wait.picknose',
WAIT_STRETCH: 'owen.quirk.wait.stretch',
WAIT_YAWN: 'owen.quirk.wait.yawn',
// React state animations - neutral
REACT_IDLE: 'owen.state.react.idle.loop',
REACT_ACKNOWLEDGE: 'owen.state.react.acknowledge.transition',
REACT_NOD: 'owen.state.react.nod.transition',
REACT_LISTENING: 'owen.state.react.listening.loop',
// React state animations - angry
REACT_ANGRY_IDLE: 'owen.state.react.angry.idle.loop',
REACT_ANGRY_FROWN: 'owen.state.react.angry.frown.transition',
REACT_ANGRY_GRUMBLE: 'owen.quirk.react.angry.grumble',
REACT_ANGRY_TO_TYPE: 'owen.state.react.angry.totype.transition',
// React state animations - happy
REACT_HAPPY_IDLE: 'owen.state.react.happy.idle.loop',
REACT_HAPPY_SMILE: 'owen.state.react.happy.smile.transition',
REACT_HAPPY_BOUNCE: 'owen.quirk.react.happy.bounce',
REACT_HAPPY_TO_TYPE: 'owen.state.react.happy.totype.transition',
// React state animations - sad
REACT_SAD_IDLE: 'owen.state.react.sad.idle.loop',
REACT_SAD_SIGH: 'owen.state.react.sad.sigh.transition',
REACT_SAD_SLUMP: 'owen.quirk.react.sad.slump',
REACT_SAD_TO_TYPE: 'owen.state.react.sad.totype.transition',
// React state animations - shocked
REACT_SHOCKED_IDLE: 'owen.state.react.shocked.idle.loop',
REACT_SHOCKED_GASP: 'owen.state.react.shocked.gasp.transition',
REACT_SHOCKED_JUMP: 'owen.quirk.react.shocked.jump',
REACT_SHOCKED_TO_TYPE: 'owen.state.react.shocked.totype.transition',
// Type state animations
TYPE_IDLE: 'owen.state.type.idle.loop',
TYPE_FAST: 'owen.state.type.fast.loop',
TYPE_SLOW: 'owen.state.type.slow.loop',
TYPE_THINKING: 'owen.state.type.thinking.loop',
TYPE_TO_WAIT: 'owen.state.type.towait.transition',
// Sleep state animations
SLEEP_LIGHT: 'owen.state.sleep.light.loop',
SLEEP_DEEP: 'owen.state.sleep.deep.loop',
SLEEP_DREAM: 'owen.quirk.sleep.dream',
SLEEP_WAKE_UP: 'owen.state.sleep.wakeup.transition'
}
/**
* Semantic animation names (readable camelCase)
* @constant
*/
export const SemanticAnimations = {
// Wait state animations
WAIT_IDLE: 'OwenWaitIdleLoop',
WAIT_PICK_NOSE: 'OwenQuirkPickNose',
WAIT_STRETCH: 'OwenQuirkStretch',
WAIT_YAWN: 'OwenQuirkYawn',
// React state animations - neutral
REACT_IDLE: 'OwenReactIdleLoop',
REACT_ACKNOWLEDGE: 'OwenReactAcknowledgeTransition',
REACT_NOD: 'OwenReactNodTransition',
REACT_LISTENING: 'OwenReactListeningLoop',
// React state animations - angry
REACT_ANGRY_IDLE: 'OwenReactAngryIdleLoop',
REACT_ANGRY_FROWN: 'OwenReactAngryFrownTransition',
REACT_ANGRY_GRUMBLE: 'OwenQuirkAngryGrumble',
REACT_ANGRY_TO_TYPE: 'OwenReactAngryToTypeTransition',
// React state animations - happy
REACT_HAPPY_IDLE: 'OwenReactHappyIdleLoop',
REACT_HAPPY_SMILE: 'OwenReactHappySmileTransition',
REACT_HAPPY_BOUNCE: 'OwenQuirkHappyBounce',
REACT_HAPPY_TO_TYPE: 'OwenReactHappyToTypeTransition',
// React state animations - sad
REACT_SAD_IDLE: 'OwenReactSadIdleLoop',
REACT_SAD_SIGH: 'OwenReactSadSighTransition',
REACT_SAD_SLUMP: 'OwenQuirkSadSlump',
REACT_SAD_TO_TYPE: 'OwenReactSadToTypeTransition',
// React state animations - shocked
REACT_SHOCKED_IDLE: 'OwenReactShockedIdleLoop',
REACT_SHOCKED_GASP: 'OwenReactShockedGaspTransition',
REACT_SHOCKED_JUMP: 'OwenQuirkShockedJump',
REACT_SHOCKED_TO_TYPE: 'OwenReactShockedToTypeTransition',
// Type state animations
TYPE_IDLE: 'OwenTypeIdleLoop',
TYPE_FAST: 'OwenTypeFastLoop',
TYPE_SLOW: 'OwenTypeSlowLoop',
TYPE_THINKING: 'OwenTypeThinkingLoop',
TYPE_TO_WAIT: 'OwenTypeToWaitTransition',
// Sleep state animations
SLEEP_LIGHT: 'OwenSleepLightLoop',
SLEEP_DEEP: 'OwenSleepDeepLoop',
SLEEP_DREAM: 'OwenQuirkSleepDream',
SLEEP_WAKE_UP: 'OwenSleepWakeUpTransition'
}
/**
* Animation naming schemes enumeration
* @constant
*/
export const NamingSchemes = {
LEGACY: 'legacy',
ARTIST: 'artist',
HIERARCHICAL: 'hierarchical',
SEMANTIC: 'semantic'
}
/**
* Convert animation name between different schemes
* @param {string} name - The source animation name
* @param {string} targetScheme - The target naming scheme
* @returns {string} The converted animation name
*/
export function convertAnimationName (name, targetScheme) {
return nameMapper.convert(name, targetScheme)
}
/**
* Get all naming scheme variants for an animation
* @param {string} name - The source animation name
* @returns {Object} Object with all scheme variants
*/
export function getAllAnimationNames (name) {
return nameMapper.getAllNames(name)
}
/**
* Validate an animation name
* @param {string} name - The animation name to validate
* @returns {Object} Validation result
*/
export function validateAnimationName (name) {
return nameMapper.validateAnimationName(name)
}
/**
* Get animations by state and emotion
* @param {string} state - The state name
* @param {string} emotion - The emotion name (optional)
* @param {string} scheme - The naming scheme to return (default: 'semantic')
* @returns {string[]} Array of animation names
*/
export function getAnimationsByStateAndEmotion (state, emotion = '', scheme = 'semantic') {
const animations = nameMapper.getAnimationsByFilter({ state, emotion })
return animations.map(anim => anim[scheme] || anim.semantic)
}

View File

@ -0,0 +1,599 @@
/**
* @fileoverview Multi-scheme animation name mapper for Owen Animation System
* @module animation/AnimationNameMapper
*/
/**
* Multi-scheme animation name mapper for Owen Animation System
* Supports legacy, artist-friendly, and hierarchical naming schemes
* @class
*/
export class AnimationNameMapper {
constructor () {
// Mapping between different naming schemes
this.schemeMappings = new Map()
this.reverseMappings = new Map()
this.patterns = new Map()
this.initializeMappings()
}
/**
* Initialize all naming scheme mappings and patterns
* @private
*/
initializeMappings () {
// Core animation definitions with all naming scheme variants
const animations = [
// Wait state animations
{
legacy: 'wait_idle_L',
artist: 'Owen_WaitIdle',
hierarchical: 'owen.state.wait.idle.loop',
semantic: 'OwenWaitIdleLoop',
state: 'wait',
emotion: '',
type: 'loop',
category: 'state'
},
{
legacy: 'wait_pickNose_Q',
artist: 'Owen_PickNose',
hierarchical: 'owen.quirk.wait.picknose',
semantic: 'OwenQuirkPickNose',
state: 'wait',
emotion: '',
type: 'quirk',
category: 'quirk'
},
{
legacy: 'wait_wave_Q',
artist: 'Owen_Wave',
hierarchical: 'owen.quirk.wait.wave',
semantic: 'OwenQuirkWave',
state: 'wait',
emotion: '',
type: 'quirk',
category: 'quirk'
},
// React state animations
{
legacy: 'react_idle_L',
artist: 'Owen_ReactIdle',
hierarchical: 'owen.state.react.idle.loop',
semantic: 'OwenReactIdleLoop',
state: 'react',
emotion: '',
type: 'loop',
category: 'state'
},
{
legacy: 'react_an_L',
artist: 'Owen_ReactAngry',
hierarchical: 'owen.state.react.emotion.angry.loop',
semantic: 'OwenReactAngryLoop',
state: 'react',
emotion: 'angry',
type: 'loop',
category: 'state'
},
{
legacy: 'react_sh_L',
artist: 'Owen_ReactShocked',
hierarchical: 'owen.state.react.emotion.shocked.loop',
semantic: 'OwenReactShockedLoop',
state: 'react',
emotion: 'shocked',
type: 'loop',
category: 'state'
},
{
legacy: 'react_ha_L',
artist: 'Owen_ReactHappy',
hierarchical: 'owen.state.react.emotion.happy.loop',
semantic: 'OwenReactHappyLoop',
state: 'react',
emotion: 'happy',
type: 'loop',
category: 'state'
},
{
legacy: 'react_sa_L',
artist: 'Owen_ReactSad',
hierarchical: 'owen.state.react.emotion.sad.loop',
semantic: 'OwenReactSadLoop',
state: 'react',
emotion: 'sad',
type: 'loop',
category: 'state'
},
// Type state animations
{
legacy: 'type_idle_L',
artist: 'Owen_TypeIdle',
hierarchical: 'owen.state.type.idle.loop',
semantic: 'OwenTypeIdleLoop',
state: 'type',
emotion: '',
type: 'loop',
category: 'state'
},
{
legacy: 'type_an_L',
artist: 'Owen_TypeAngry',
hierarchical: 'owen.state.type.emotion.angry.loop',
semantic: 'OwenTypeAngryLoop',
state: 'type',
emotion: 'angry',
type: 'loop',
category: 'state'
},
{
legacy: 'type_sh_L',
artist: 'Owen_TypeShocked',
hierarchical: 'owen.state.type.emotion.shocked.loop',
semantic: 'OwenTypeShockedLoop',
state: 'type',
emotion: 'shocked',
type: 'loop',
category: 'state'
},
// Sleep state animations
{
legacy: 'sleep_idle_L',
artist: 'Owen_SleepIdle',
hierarchical: 'owen.state.sleep.idle.loop',
semantic: 'OwenSleepIdleLoop',
state: 'sleep',
emotion: '',
type: 'loop',
category: 'state'
},
// Transition animations
{
legacy: 'wait_2react_T',
artist: 'Owen_WaitToReact',
hierarchical: 'owen.transition.wait.to.react',
semantic: 'OwenTransitionWaitToReact',
fromState: 'wait',
toState: 'react',
emotion: '',
type: 'transition',
category: 'transition'
},
{
legacy: 'react_2type_T',
artist: 'Owen_ReactToType',
hierarchical: 'owen.transition.react.to.type',
semantic: 'OwenTransitionReactToType',
fromState: 'react',
toState: 'type',
emotion: '',
type: 'transition',
category: 'transition'
},
{
legacy: 'react_an2type_T',
artist: 'Owen_ReactAngryToType',
hierarchical: 'owen.transition.react.to.type.emotion.angry',
semantic: 'OwenTransitionReactToTypeAngry',
fromState: 'react',
toState: 'type',
emotion: 'angry',
type: 'transition',
category: 'transition'
},
{
legacy: 'type_2wait_T',
artist: 'Owen_TypeToWait',
hierarchical: 'owen.transition.type.to.wait',
semantic: 'OwenTransitionTypeToWait',
fromState: 'type',
toState: 'wait',
emotion: '',
type: 'transition',
category: 'transition'
},
{
legacy: 'sleep_2wait_T',
artist: 'Owen_SleepToWait',
hierarchical: 'owen.transition.sleep.to.wait',
semantic: 'OwenTransitionSleepToWait',
fromState: 'sleep',
toState: 'wait',
emotion: '',
type: 'transition',
category: 'transition'
}
]
// Build bidirectional mappings
animations.forEach(anim => {
const schemes = ['legacy', 'artist', 'hierarchical', 'semantic']
schemes.forEach(scheme1 => {
schemes.forEach(scheme2 => {
if (scheme1 !== scheme2) {
this.schemeMappings.set(anim[scheme1], anim[scheme2])
}
})
// Also map to animation definition
this.schemeMappings.set(anim[scheme1], anim)
})
})
// Initialize pattern matchers for auto-detection
this.initializePatterns()
}
/**
* Initialize pattern matchers for different naming schemes
* @private
*/
initializePatterns () {
// Pattern matchers for different naming schemes
this.patterns.set('legacy', [
{
regex: /^(\w+)_(\w+)_([LTQ])$/,
extract: (match) => ({
state: match[1],
action: match[2],
type: match[3] === 'L' ? 'loop' : match[3] === 'T' ? 'transition' : 'quirk'
})
},
{
regex: /^(\w+)_(\w{2})_([LTQ])$/,
extract: (match) => ({
state: match[1],
emotion: this.mapEmotionCode(match[2]),
type: match[3] === 'L' ? 'loop' : match[3] === 'T' ? 'transition' : 'quirk'
})
},
{
regex: /^(\w+)_(\w{2})?2(\w+)_T$/,
extract: (match) => ({
fromState: match[1],
emotion: match[2] ? this.mapEmotionCode(match[2]) : '',
toState: match[3],
type: 'transition'
})
},
{
regex: /^(\w+)_2(\w+)_T$/,
extract: (match) => ({
fromState: match[1],
toState: match[2],
type: 'transition'
})
}
])
this.patterns.set('artist', [
{
regex: /^Owen_(\w+)$/,
extract: (match) => ({
action: match[1],
scheme: 'artist'
})
},
{
regex: /^Owen_(\w+)To(\w+)$/,
extract: (match) => ({
fromState: match[1].toLowerCase(),
toState: match[2].toLowerCase(),
type: 'transition'
})
},
{
regex: /^Owen_(\w+)(Angry|Happy|Sad|Shocked)$/,
extract: (match) => ({
state: match[1].toLowerCase(),
emotion: match[2].toLowerCase(),
type: 'loop'
})
},
{
regex: /^Owen_(\w+)(Angry|Happy|Sad|Shocked)To(\w+)$/,
extract: (match) => ({
fromState: match[1].toLowerCase(),
emotion: match[2].toLowerCase(),
toState: match[3].toLowerCase(),
type: 'transition'
})
}
])
this.patterns.set('hierarchical', [
{
regex: /^owen\.(\w+)\.(\w+)\.(\w+)(?:\.(\w+))?(?:\.(\w+))?$/,
extract: (match) => ({
category: match[1],
subcategory: match[2],
action: match[3],
modifier: match[4],
type: match[5] || match[4]
})
}
])
this.patterns.set('semantic', [
{
regex: /^Owen(\w+)(\w+)(\w+)$/,
extract: (match) => ({
category: match[1].toLowerCase(),
action: match[2].toLowerCase(),
type: match[3].toLowerCase()
})
}
])
}
/**
* Map emotion codes to full names
* @private
* @param {string} code - Emotion code
* @returns {string} Full emotion name
*/
mapEmotionCode (code) {
const emotionMap = {
an: 'angry',
sh: 'shocked',
ha: 'happy',
sa: 'sad',
'': 'neutral'
}
return emotionMap[code] || code
}
/**
* Convert any animation name to any other scheme
* @param {string} fromName - Source animation name
* @param {string} targetScheme - Target naming scheme ('legacy', 'artist', 'hierarchical', 'semantic')
* @returns {string} Converted animation name
*/
convert (fromName, targetScheme = 'hierarchical') {
// Direct lookup first
const directMapping = this.schemeMappings.get(fromName)
if (directMapping && typeof directMapping === 'object') {
return directMapping[targetScheme] || fromName
}
// Pattern-based conversion
const detected = this.detectScheme(fromName)
if (detected) {
return this.generateName(detected.info, targetScheme)
}
console.warn(`Could not convert animation name: ${fromName}`)
return fromName
}
/**
* Detect which naming scheme is being used
* @param {string} name - Animation name to analyze
* @returns {Object|null} Detection result with scheme and extracted info
*/
detectScheme (name) {
for (const [scheme, patterns] of this.patterns) {
for (const pattern of patterns) {
const match = name.match(pattern.regex)
if (match) {
return {
scheme,
info: pattern.extract(match),
originalName: name
}
}
}
}
return null
}
/**
* Generate animation name in target scheme
* @private
* @param {Object} info - Animation information
* @param {string} targetScheme - Target naming scheme
* @returns {string} Generated animation name
*/
generateName (info, targetScheme) {
switch (targetScheme) {
case 'legacy':
return this.generateLegacyName(info)
case 'artist':
return this.generateArtistName(info)
case 'hierarchical':
return this.generateHierarchicalName(info)
case 'semantic':
return this.generateSemanticName(info)
default:
return null
}
}
/**
* Generate legacy format name
* @private
* @param {Object} info - Animation information
* @returns {string} Legacy format name
*/
generateLegacyName (info) {
const typeMap = { loop: 'L', transition: 'T', quirk: 'Q' }
const emotionMap = { angry: 'an', shocked: 'sh', happy: 'ha', sad: 'sa' }
if (info.type === 'transition' && info.fromState && info.toState) {
const emotionPart = info.emotion ? emotionMap[info.emotion] || '' : ''
return emotionPart
? `${info.fromState}_${emotionPart}2${info.toState}_T`
: `${info.fromState}_2${info.toState}_T`
}
const state = info.state || info.fromState || 'wait'
const action = info.action || (info.emotion ? emotionMap[info.emotion] : 'idle')
const type = typeMap[info.type] || 'L'
return `${state}_${action}_${type}`
}
/**
* Generate artist-friendly format name
* @private
* @param {Object} info - Animation information
* @returns {string} Artist format name
*/
generateArtistName (info) {
const parts = ['Owen']
if (info.type === 'transition') {
const from = this.capitalize(info.fromState || info.state)
const to = this.capitalize(info.toState)
if (info.emotion) {
parts.push(`${from}${this.capitalize(info.emotion)}To${to}`)
} else {
parts.push(`${from}To${to}`)
}
} else {
if (info.state) parts.push(this.capitalize(info.state))
if (info.action && info.action !== 'idle') parts.push(this.capitalize(info.action))
if (info.emotion) parts.push(this.capitalize(info.emotion))
}
return parts.join('_')
}
/**
* Generate hierarchical format name
* @private
* @param {Object} info - Animation information
* @returns {string} Hierarchical format name
*/
generateHierarchicalName (info) {
const parts = ['owen']
if (info.category) {
parts.push(info.category)
} else if (info.type === 'transition') {
parts.push('transition')
} else if (info.type === 'quirk') {
parts.push('quirk')
} else {
parts.push('state')
}
if (info.fromState && info.toState) {
// Transition
parts.push(info.fromState, 'to', info.toState)
} else if (info.state) {
parts.push(info.state)
}
if (info.action && info.action !== 'idle') parts.push(info.action)
if (info.emotion) parts.push('emotion', info.emotion)
if (info.type) parts.push(info.type)
return parts.join('.')
}
/**
* Generate semantic format name
* @private
* @param {Object} info - Animation information
* @returns {string} Semantic format name
*/
generateSemanticName (info) {
const parts = ['Owen']
if (info.type === 'transition') {
parts.push('Transition')
if (info.fromState) parts.push(this.capitalize(info.fromState))
parts.push('To')
if (info.toState) parts.push(this.capitalize(info.toState))
if (info.emotion) parts.push(this.capitalize(info.emotion))
} else {
if (info.type === 'quirk') parts.push('Quirk')
if (info.state) parts.push(this.capitalize(info.state))
if (info.action && info.action !== 'idle') parts.push(this.capitalize(info.action))
if (info.emotion) parts.push(this.capitalize(info.emotion))
if (info.type && info.type !== 'quirk') parts.push(this.capitalize(info.type))
}
return parts.join('')
}
/**
* Capitalize first letter of string
* @private
* @param {string} str - String to capitalize
* @returns {string} Capitalized string
*/
capitalize (str) {
return str.charAt(0).toUpperCase() + str.slice(1)
}
/**
* Get all possible names for an animation
* @param {string} animationName - Source animation name
* @returns {Object} Object with all naming scheme variants
*/
getAllNames (animationName) {
const schemes = ['legacy', 'artist', 'hierarchical', 'semantic']
const names = {}
schemes.forEach(scheme => {
names[scheme] = this.convert(animationName, scheme)
})
return names
}
/**
* Batch convert multiple animations
* @param {string[]} animations - Array of animation names
* @param {string} targetScheme - Target naming scheme
* @returns {Object} Mapping of original names to converted names
*/
convertBatch (animations, targetScheme) {
const converted = {}
animations.forEach(name => {
converted[name] = this.convert(name, targetScheme)
})
return converted
}
/**
* Validate animation name format
* @param {string} name - Animation name to validate
* @returns {Object} Validation result with issues and suggestions
*/
validateAnimationName (name) {
const issues = []
const suggestions = []
// Check for common issues
if (name.includes(' ')) {
issues.push(`❌ "${name}" contains spaces - may cause issues`)
suggestions.push(`💡 Suggestion: "${name.replace(/ /g, '_')}"`)
}
if (!/^[a-zA-Z0-9._-]+$/.test(name)) {
issues.push(`❌ "${name}" contains invalid characters`)
suggestions.push('💡 Use only letters, numbers, dots, underscores, and hyphens')
}
if (name.length > 50) {
issues.push(`⚠️ "${name}" is very long (${name.length} chars)`)
suggestions.push('💡 Consider shortening the name')
}
const detected = this.detectScheme(name)
if (!detected) {
issues.push(`⚠️ "${name}" doesn't match any known naming pattern`)
suggestions.push('💡 Consider using one of: legacy, artist, hierarchical, or semantic format')
} else {
suggestions.push(`✅ Detected as ${detected.scheme} scheme`)
}
return { issues, suggestions, detected }
}
}

View File

@ -32,13 +32,13 @@ export const ClipTypes = {
*/
export const States = {
/** Waiting/idle state */
WAITING: 'wait',
WAITING: 'wait',
/** Reacting to input state */
REACTING: 'react',
REACTING: 'react',
/** Typing response state */
TYPING: 'type',
TYPING: 'type',
/** Sleep/inactive state */
SLEEPING: 'sleep'
SLEEPING: 'sleep'
}
/**

View File

@ -4,6 +4,7 @@
*/
import { States, Emotions, Config } from '../constants.js'
import { AnimationNameMapper } from '../animation/AnimationNameMapper.js'
/**
* Main controller for the Owen animation system
@ -43,6 +44,12 @@ export class OwenAnimationContext {
*/
this.stateFactory = stateFactory
/**
* Multi-scheme animation name mapper
* @type {AnimationNameMapper}
*/
this.nameMapper = new AnimationNameMapper()
/**
* Map of animation clips by name
* @type {Map<string, AnimationClip>}
@ -59,7 +66,7 @@ export class OwenAnimationContext {
* Current active state
* @type {string}
*/
this.currentState = States.WAITING
this.currentState = States.WAITING
/**
* Current active state handler
@ -105,7 +112,7 @@ export class OwenAnimationContext {
this.initializeStates()
// Start in wait state
await this.transitionTo(States.WAITING)
await this.transitionTo(States.WAITING)
this.initialized = true
console.log('Owen Animation System initialized')
@ -167,8 +174,8 @@ export class OwenAnimationContext {
this.onUserActivity()
// If sleeping, wake up first
if (this.currentState === States.SLEEPING) {
await this.transitionTo(States.REACTING)
if (this.currentState === States.SLEEPING) {
await this.transitionTo(States.REACTING)
}
// Let current state handle the message
@ -177,10 +184,10 @@ export class OwenAnimationContext {
}
// Transition to appropriate next state based on current state
if (this.currentState === States.WAITING) {
await this.transitionTo(States.REACTING);
} else if (this.currentState === States.REACTING) {
await this.transitionTo(States.TYPING)
if (this.currentState === States.WAITING) {
await this.transitionTo(States.REACTING)
} else if (this.currentState === States.REACTING) {
await this.transitionTo(States.TYPING)
}
}
@ -192,8 +199,8 @@ export class OwenAnimationContext {
this.resetActivityTimer()
// Wake up if sleeping
if (this.currentState === States.SLEEPING) {
this.transitionTo(States.WAITING)
if (this.currentState === States.SLEEPING) {
this.transitionTo(States.WAITING)
}
}
@ -213,7 +220,7 @@ export class OwenAnimationContext {
*/
async handleInactivity () {
console.log('Inactivity detected, transitioning to sleep')
await this.transitionTo(States.SLEEPING)
await this.transitionTo(States.SLEEPING)
}
/**
@ -234,18 +241,38 @@ export class OwenAnimationContext {
// Update inactivity timer
this.inactivityTimer += deltaTime
if (this.inactivityTimer > this.inactivityTimeout && this.currentState !== States.SLEEPING) {
if (this.inactivityTimer > this.inactivityTimeout && this.currentState !== States.SLEEPING) {
this.handleInactivity()
}
}
/**
* Get an animation clip by name
* @param {string} name - The animation clip name
* Get an animation clip by name (supports all naming schemes)
* @param {string} name - The animation clip name in any supported scheme
* @returns {AnimationClip|undefined} The animation clip or undefined if not found
*/
getClip (name) {
return this.clips.get(name)
// First try direct lookup
let clip = this.clips.get(name)
if (clip) return clip
// Try to find clip using name mapper
try {
const allNames = this.nameMapper.getAllNames(name)
// Try each possible name variant
for (const variant of Object.values(allNames)) {
clip = this.clips.get(variant)
if (clip) return clip
}
} catch (error) {
// If name mapping fails, continue with pattern search
console.debug(`Name mapping failed for "${name}":`, error.message)
}
// Fall back to pattern matching for legacy compatibility
const exactMatches = this.getClipsByPattern(`^${name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`)
return exactMatches.length > 0 ? exactMatches[0] : undefined
}
/**
@ -266,6 +293,80 @@ export class OwenAnimationContext {
return matches
}
/**
* Get an animation clip by name in a specific naming scheme
* @param {string} name - The animation name
* @param {string} [targetScheme] - Target scheme: 'legacy', 'artist', 'hierarchical', 'semantic'
* @returns {AnimationClip|undefined} The animation clip or undefined if not found
*/
getClipByScheme (name, targetScheme) {
try {
if (targetScheme) {
const convertedName = this.nameMapper.convert(name, targetScheme)
return this.clips.get(convertedName)
} else {
return this.getClip(name)
}
} catch (error) {
console.debug(`Scheme conversion failed for "${name}" to "${targetScheme}":`, error.message)
return undefined
}
}
/**
* Get all naming scheme variants for an animation
* @param {string} name - The animation name in any scheme
* @returns {Object} Object with all scheme variants: {legacy, artist, hierarchical, semantic}
*/
getAnimationNames (name) {
try {
return this.nameMapper.getAllNames(name)
} catch (error) {
console.warn(`Could not get animation name variants for "${name}":`, error.message)
return {
legacy: name,
artist: name,
hierarchical: name,
semantic: name
}
}
}
/**
* Validate an animation name and get suggestions if invalid
* @param {string} name - The animation name to validate
* @returns {Object} Validation result with isValid, scheme, error, and suggestions
*/
validateAnimationName (name) {
try {
return this.nameMapper.validateAnimationName(name)
} catch (error) {
return {
isValid: false,
scheme: 'unknown',
error: error.message,
suggestions: []
}
}
}
/**
* Get available animations by state and emotion
* @param {string} state - The state name (wait, react, type, sleep)
* @param {string} [emotion] - The emotion name (angry, happy, sad, shocked, neutral)
* @param {string} [scheme='semantic'] - The naming scheme to return
* @returns {string[]} Array of animation names in the specified scheme
*/
getAnimationsByStateAndEmotion (state, emotion = '', scheme = 'semantic') {
try {
const animations = this.nameMapper.getAnimationsByFilter({ state, emotion })
return animations.map(anim => anim[scheme] || anim.semantic)
} catch (error) {
console.warn(`Could not filter animations by state "${state}" and emotion "${emotion}":`, error.message)
return []
}
}
/**
* Get the current state name
* @returns {string} The current state name

View File

@ -14,6 +14,20 @@ export { OwenAnimationContext } from './core/OwenAnimationContext.js'
// Animation system exports
export { AnimationClip, AnimationClipFactory } from './animation/AnimationClip.js'
// Multi-scheme animation naming exports
export { AnimationNameMapper } from './animation/AnimationNameMapper.js'
export {
LegacyAnimations,
ArtistAnimations,
HierarchicalAnimations,
SemanticAnimations,
NamingSchemes,
convertAnimationName,
getAllAnimationNames,
validateAnimationName,
getAnimationsByStateAndEmotion
} from './animation/AnimationConstants.js'
// Loader exports
export { AnimationLoader, GLTFAnimationLoader } from './loaders/AnimationLoader.js'

View File

@ -17,7 +17,7 @@ export class ReactStateHandler extends StateHandler {
* @param {OwenAnimationContext} context - The animation context
*/
constructor (context) {
super(States.REACTING, context)
super(States.REACTING, context)
/**
* Current emotional state
@ -33,7 +33,7 @@ export class ReactStateHandler extends StateHandler {
* @returns {Promise<void>}
*/
async enter (_fromState = null, emotion = Emotions.NEUTRAL) {
console.log(`Entering REACTING state with emotion: ${emotion}`)
console.log(`Entering REACTING state with emotion: ${emotion}`)
this.emotion = emotion
// Play appropriate reaction
@ -51,7 +51,7 @@ export class ReactStateHandler extends StateHandler {
* @returns {Promise<void>}
*/
async exit (toState = null, emotion = Emotions.NEUTRAL) {
console.log(`Exiting REACTING state to ${toState} with emotion: ${emotion}`)
console.log(`Exiting REACTING state to ${toState} with emotion: ${emotion}`)
if (this.currentClip) {
await this.stopCurrentClip()
@ -154,6 +154,6 @@ export class ReactStateHandler extends StateHandler {
* @returns {string[]} Array of available state transitions
*/
getAvailableTransitions () {
return [ States.TYPING, States.WAITING ]
return [States.TYPING, States.WAITING]
}
}

View File

@ -17,7 +17,7 @@ export class SleepStateHandler extends StateHandler {
* @param {OwenAnimationContext} context - The animation context
*/
constructor (context) {
super(States.SLEEPING, context)
super(States.SLEEPING, context)
/**
* Sleep animation clip
@ -39,7 +39,7 @@ export class SleepStateHandler extends StateHandler {
* @returns {Promise<void>}
*/
async enter (fromState = null, _emotion = Emotions.NEUTRAL) {
console.log(`Entering SLEEPING state from ${fromState}`)
console.log(`Entering SLEEPING state from ${fromState}`)
// Play sleep transition if available
const sleepTransition = this.context.getClip('wait_2sleep_T')
@ -65,7 +65,7 @@ export class SleepStateHandler extends StateHandler {
* @returns {Promise<void>}
*/
async exit (toState = null, _emotion = Emotions.NEUTRAL) {
console.log(`Exiting SLEEPING state to ${toState}`)
console.log(`Exiting SLEEPING state to ${toState}`)
this.isDeepSleep = false
if (this.currentClip) {
@ -107,8 +107,8 @@ export class SleepStateHandler extends StateHandler {
// Any message should wake up the character
if (this.isDeepSleep) {
console.log('Waking up due to user message')
// This will trigger a state transition to REACTING
await this.context.transitionTo(States.REACTING)
// This will trigger a state transition to REACTING
await this.context.transitionTo(States.REACTING)
}
}
@ -117,7 +117,7 @@ export class SleepStateHandler extends StateHandler {
* @returns {string[]} Array of available state transitions
*/
getAvailableTransitions () {
return [ States.WAITING, States.REACTING ]
return [States.WAITING, States.REACTING]
}
/**
@ -134,7 +134,7 @@ export class SleepStateHandler extends StateHandler {
*/
async wakeUp () {
if (this.isDeepSleep) {
await this.context.transitionTo(States.WAITING)
await this.context.transitionTo(States.WAITING)
}
}
}

View File

@ -26,10 +26,10 @@ export class StateFactory {
this.stateHandlers = new Map()
// Register default state handlers
this.registerStateHandler(States.WAITING, WaitStateHandler);
this.registerStateHandler(States.REACTING, ReactStateHandler);
this.registerStateHandler(States.TYPING, TypeStateHandler);
this.registerStateHandler(States.SLEEPING, SleepStateHandler)
this.registerStateHandler(States.WAITING, WaitStateHandler)
this.registerStateHandler(States.REACTING, ReactStateHandler)
this.registerStateHandler(States.TYPING, TypeStateHandler)
this.registerStateHandler(States.SLEEPING, SleepStateHandler)
}
/**

View File

@ -17,7 +17,7 @@ export class TypeStateHandler extends StateHandler {
* @param {OwenAnimationContext} context - The animation context
*/
constructor (context) {
super(States.TYPING, context)
super(States.TYPING, context)
/**
* Current emotional state
@ -39,7 +39,7 @@ export class TypeStateHandler extends StateHandler {
* @returns {Promise<void>}
*/
async enter (_fromState = null, emotion = Emotions.NEUTRAL) {
console.log(`Entering TYPING state with emotion: ${emotion}`)
console.log(`Entering TYPING state with emotion: ${emotion}`)
this.emotion = emotion
this.isTyping = true
@ -63,7 +63,7 @@ export class TypeStateHandler extends StateHandler {
* @returns {Promise<void>}
*/
async exit (toState = null, _emotion = Emotions.NEUTRAL) {
console.log(`Exiting TYPING state to ${toState}`)
console.log(`Exiting TYPING state to ${toState}`)
this.isTyping = false
if (this.currentClip) {
@ -106,7 +106,7 @@ export class TypeStateHandler extends StateHandler {
* @returns {string[]} Array of available state transitions
*/
getAvailableTransitions () {
return [ States.WAITING, States.REACTING ]
return [States.WAITING, States.REACTING]
}
/**

View File

@ -17,7 +17,7 @@ export class WaitStateHandler extends StateHandler {
* @param {OwenAnimationContext} context - The animation context
*/
constructor (context) {
super(States.WAITING, context)
super(States.WAITING, context)
/**
* The main idle animation clip
@ -51,7 +51,7 @@ export class WaitStateHandler extends StateHandler {
* @returns {Promise<void>}
*/
async enter (fromState = null, _emotion = Emotions.NEUTRAL) {
console.log(`Entering WAITING state from ${fromState}`)
console.log(`Entering WAITING state from ${fromState}`)
// Play idle loop
this.idleClip = this.context.getClip('wait_idle_L')
@ -72,7 +72,7 @@ export class WaitStateHandler extends StateHandler {
* @returns {Promise<void>}
*/
async exit (toState = null, _emotion = Emotions.NEUTRAL) {
console.log(`Exiting WAITING state to ${toState}`)
console.log(`Exiting WAITING state to ${toState}`)
if (this.currentClip) {
await this.stopCurrentClip()
@ -133,6 +133,6 @@ export class WaitStateHandler extends StateHandler {
* @returns {string[]} Array of available state transitions
*/
getAvailableTransitions () {
return [ States.REACTING, States.SLEEPING ]
return [States.REACTING, States.SLEEPING]
}
}