refactor: rename states for clarity and consistency

- Updated state names in constants and related files from WAIT, REACT, TYPE, SLEEP to WAITING, REACTING, TYPING, SLEEPING.
- Adjusted all references to the renamed states across the codebase, including state handlers and transition logic.
- Ensured that logging messages reflect the new state names for better readability.
This commit is contained in:
2025-05-24 04:16:02 +02:00
parent 472de05e4b
commit d513e80c07
12 changed files with 694 additions and 314 deletions

View File

@ -103,7 +103,7 @@ customStates.set("custom", CustomStateHandler);
const owenSystem = await OwenSystemFactory.createCustomOwenSystem(gltfModel, scene, customStates); const owenSystem = await OwenSystemFactory.createCustomOwenSystem(gltfModel, scene, customStates);
// Manual state transitions // Manual state transitions
await owenSystem.transitionTo(States.REACT, Emotions.HAPPY); await owenSystem.transitionTo(States.REACTING, Emotions.HAPPY);
``` ```
## 🎮 Animation Naming Convention ## 🎮 Animation Naming Convention

View File

@ -177,16 +177,16 @@ class OwenDemo {
switch (event.key) { switch (event.key) {
case '1': case '1':
this.owenSystem.transitionTo(States.WAIT) this.owenSystem.transitionTo(States.WAITING)
break break
case '2': case '2':
this.owenSystem.transitionTo(States.REACT) this.owenSystem.transitionTo(States.REACTING)
break break
case '3': case '3':
this.owenSystem.transitionTo(States.TYPE) this.owenSystem.transitionTo(States.TYPING)
break break
case '4': case '4':
this.owenSystem.transitionTo(States.SLEEP) this.owenSystem.transitionTo(States.SLEEPING)
break break
case ' ': case ' ':
this.sendTestMessage() this.sendTestMessage()

View File

@ -92,7 +92,7 @@ class SimpleOwenExample {
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
async demonstrateStateTransitions () { async demonstrateStateTransitions () {
const states = [States.REACT, States.TYPE, States.WAIT, States.SLEEP] const states = [ States.REACTING, States.TYPING, States.WAITING, States.SLEEPING ]
for (const state of states) { for (const state of states) {
console.log(`🔄 Transitioning to ${state.toUpperCase()} state...`) console.log(`🔄 Transitioning to ${state.toUpperCase()} state...`)

View File

@ -1,10 +1,19 @@
<!DOCTYPE html> <!DOCTYPING html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Owen Animation System Test</title> <title>Owen Animation System Demo - Implementation Test</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<!--
This demo has been refactored to mock the actual Owen Animation System implementation.
It follows the same architectural patterns:
- OwenAnimationContext as the main controller
- State handlers for each state (Wait, React, Type, Sleep)
- Animation clips with proper naming convention
- Constants matching the real implementation
- Dependency injection and factory patterns
-->
<style> <style>
body { body {
margin: 0; margin: 0;
@ -138,7 +147,7 @@
width: 120px; width: 120px;
height: 120px; height: 120px;
border-radius: 50%; border-radius: 50%;
background: linear-gradient(45deg, #667eea, #764ba2); background: transparent;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
@ -146,6 +155,13 @@
color: white; color: white;
margin-bottom: 20px; margin-bottom: 20px;
transition: transform 0.3s ease; transition: transform 0.3s ease;
overflow: hidden;
}
.owen-avatar svg {
width: 100%;
height: 100%;
display: block;
} }
.owen-state { .owen-state {
@ -200,7 +216,7 @@
<body> <body>
<div id="container"> <div id="container">
<div id="controls"> <div id="controls">
<h2>🤖 Owen Animation Control Panel</h2> <h2>🤖 Owen Animation System Demo</h2>
<div class="control-group"> <div class="control-group">
<label>Send Message to Owen:</label> <label>Send Message to Owen:</label>
@ -260,157 +276,458 @@
</div> </div>
<div id="owen-character"> <div id="owen-character">
<div class="owen-avatar" id="owenAvatar">🤖</div> <div class="owen-avatar" id="owenAvatar">
<!-- SVG will be inserted here by JavaScript -->
<svg width="100" height="100" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<circle cx="50" cy="50" r="35" fill="#4CAF50" stroke="#333" stroke-width="2"/>
<ellipse cx="45" cy="40" rx="15" ry="10" fill="white" opacity="0.3"/>
<circle cx="35" cy="45" r="5" fill="#333"/>
<circle cx="65" cy="45" r="5" fill="#333"/>
<path d="M 30 65 Q 50 75 70 65" stroke="#333" stroke-width="3" fill="none" stroke-linecap="round"/>
<line x1="50" y1="15" x2="50" y2="5" stroke="#333" stroke-width="2"/>
<circle cx="50" cy="5" r="2" fill="#ff4444"/>
</svg>
</div>
<div class="owen-state" id="owenState">Initializing...</div> <div class="owen-state" id="owenState">Initializing...</div>
<div class="owen-emotion" id="owenEmotion">Neutral</div> <div class="owen-emotion" id="owenEmotion">Neutral</div>
</div> </div>
</div> </div>
<script> <script>
// Import the Owen system (in a real scenario, this would be imported) // Mock Owen Animation System based on actual implementation structure
// For this demo, we'll include a simplified version inline // This simulates the real Owen system for testing purposes
// Mock Owen Animation System for Testing // Constants matching the real implementation
class MockOwenSystem { const States = {
WAITING: 'wait',
REACTING: 'react',
TYPING: 'type',
SLEEPING: 'sleep'
};
const Emotions = {
NEUTRAL: '',
ANGRY: 'an',
SHOCKED: 'sh',
HAPPY: 'ha',
SAD: 'sa'
};
const Config = {
DEFAULT_FADE_IN: 0.3,
DEFAULT_FADE_OUT: 0.3,
QUIRK_INTERVAL: 5000,
INACTIVITY_TIMEOUT: 15000,
QUIRK_PROBABILITY: 0.3
};
// Mock AnimationClip class
class MockAnimationClip {
constructor(name, type, state, emotion = '') {
this.name = name;
this.type = type;
this.state = state;
this.emotion = emotion;
this.action = null;
this.isPlaying = false;
}
play() {
this.isPlaying = true;
console.log(`Playing animation: ${this.name}`);
}
stop() {
this.isPlaying = false;
console.log(`Stopping animation: ${this.name}`);
}
}
// Mock StateHandler base class
class MockStateHandler {
constructor(stateName, context) {
this.stateName = stateName;
this.context = context;
this.currentClip = null;
}
async enter(fromState = null, emotion = Emotions.NEUTRAL) {
console.log(`Entering ${this.stateName} state from ${fromState} with emotion: ${emotion}`);
// Play appropriate idle animation
const clipName = this.getIdleClipName(emotion);
await this.playClip(clipName);
}
async exit(toState, emotion = Emotions.NEUTRAL) {
console.log(`Exiting ${this.stateName} state to ${toState} with emotion: ${emotion}`);
if (this.currentClip) {
this.currentClip.stop();
this.currentClip = null;
}
}
getIdleClipName(emotion) {
return `${this.stateName}_idle_L`;
}
async playClip(clipName) {
const clip = this.context.getClip(clipName);
if (clip) {
if (this.currentClip) {
this.currentClip.stop();
}
this.currentClip = clip;
clip.play();
this.context.activeAnimations.add(clipName);
// Simulate animation duration
setTimeout(() => {
this.context.activeAnimations.delete(clipName);
this.context.updateAnimationList();
}, clipName.includes('_T') ? 500 : 2000);
}
}
update(deltaTime) {
// Base update implementation
}
async handleMessage(message) {
// Base message handling
console.log(`${this.stateName} state handling message: "${message}"`);
}
}
// Wait State Handler
class MockWaitStateHandler extends MockStateHandler {
constructor(context) {
super(States.WAITING, context);
this.quirkTimer = 0;
this.quirkInterval = Config.QUIRK_INTERVAL;
}
update(deltaTime) {
super.update(deltaTime);
// Handle quirk animations
this.quirkTimer += deltaTime;
if (this.quirkTimer > this.quirkInterval && Math.random() < Config.QUIRK_PROBABILITY) {
this.playRandomQuirk();
this.quirkTimer = 0;
}
}
playRandomQuirk() {
const quirkClips = this.context.getClipsByPattern('wait_*_Q');
if (quirkClips.length > 0) {
const randomQuirk = quirkClips[Math.floor(Math.random() * quirkClips.length)];
this.playClip(randomQuirk.name);
}
}
}
// React State Handler
class MockReactStateHandler extends MockStateHandler {
constructor(context) {
super(States.REACTING, context);
}
getIdleClipName(emotion) {
if (emotion && emotion !== Emotions.NEUTRAL) {
const emotionalClip = `${this.stateName}_${emotion}_L`;
if (this.context.getClip(emotionalClip)) {
return emotionalClip;
}
}
return super.getIdleClipName(emotion);
}
}
// Type State Handler
class MockTypeStateHandler extends MockStateHandler {
constructor(context) {
super(States.TYPING, context);
}
async enter(fromState = null, emotion = Emotions.NEUTRAL) {
await super.enter(fromState, emotion);
this.context.isTyping = true;
}
async exit(toState, emotion = Emotions.NEUTRAL) {
this.context.isTyping = false;
await super.exit(toState, emotion);
}
getIdleClipName(emotion) {
if (emotion && emotion !== Emotions.NEUTRAL) {
const emotionalClip = `${this.stateName}_${emotion}_L`;
if (this.context.getClip(emotionalClip)) {
return emotionalClip;
}
}
return super.getIdleClipName(emotion);
}
}
// Sleep State Handler
class MockSleepStateHandler extends MockStateHandler {
constructor(context) {
super(States.SLEEPING, context);
}
}
// Mock Owen Animation Context (main system)
class MockOwenAnimationContext {
constructor() { constructor() {
this.currentState = 'wait'; this.model = null;
this.currentEmotion = 'neutral'; this.mixer = null;
this.lastActivity = Date.now(); this.clips = new Map();
this.states = new Map();
this.currentState = States.WAITING;
this.currentStateHandler = null;
this.inactivityTimer = 0;
this.inactivityTimeout = Config.INACTIVITY_TIMEOUT;
this.initialized = false;
this.activeAnimations = new Set(); this.activeAnimations = new Set();
this.isTyping = false; this.isTyping = false;
this.messageQueue = [];
// Mock animation clips based on the naming convention
this.availableClips = [
'wait_idle_L',
'wait_pickNose_Q',
'wait_wave_Q',
'wait_2react_T',
'wait_2sleep_T',
'react_idle_L',
'react_2type_T',
'react_angry2type_an_T',
'react_shock2type_sh_T',
'type_idle_L',
'type_angry_L',
'type_shocked_L',
'type_2wait_T',
'type_angry2wait_T',
'type_shocked2wait_T',
'sleep_idle_L',
'sleep_2wait_T'
];
this.stateColors = { this.stateColors = {
wait: '#4CAF50', [States.WAITING]: '#4CAF50',
react: '#ff9800', [States.REACTING]: '#ff9800',
type: '#2196f3', [States.TYPING]: '#2196f3',
sleep: '#9c27b0' [States.SLEEPING]: '#9c27b0'
}; };
this.emotionEmojis = { // SVG graphics for each state/emotion combination
neutral: '😊', this.svgGraphics = this.createSVGGraphics();
angry: '😠',
shocked: '😲',
happy: '😄',
sad: '😢'
};
this.inactivityTimer = null; this.lastActivity = Date.now();
this.quirkTimer = null;
} }
async initialize() { async initialize() {
console.log('Owen system initializing...'); if (this.initialized) return;
await this.transitionTo('wait');
this.startQuirkTimer(); // Create mock animation clips following the naming convention
this.updateUI(); this.createMockClips();
console.log('Owen system ready!');
// Initialize state handlers
this.initializeStates();
// Start in wait state
await this.transitionTo(States.WAITING);
this.initialized = true;
console.log('Owen Animation System initialized');
} }
async transitionTo(newState, emotion = 'neutral') { createSVGGraphics() {
const svgs = {};
// Base robot head SVG template
const createRobotSVG = (state, emotion) => {
const baseColor = this.stateColors[state] || '#4CAF50';
// Define facial expressions for emotions
const expressions = {
[Emotions.NEUTRAL]: {
eyes: '<circle cx="35" cy="45" r="5" fill="#333"/><circle cx="65" cy="45" r="5" fill="#333"/>',
mouth: '<path d="M 30 65 Q 50 75 70 65" stroke="#333" stroke-width="3" fill="none" stroke-linecap="round"/>'
},
[Emotions.ANGRY]: {
eyes: '<path d="M 30 40 L 40 50 M 60 50 L 70 40" stroke="#333" stroke-width="4" stroke-linecap="round"/>',
mouth: '<path d="M 30 70 Q 50 60 70 70" stroke="#333" stroke-width="3" fill="none" stroke-linecap="round"/>'
},
[Emotions.SHOCKED]: {
eyes: '<circle cx="35" cy="45" r="8" fill="#333"/><circle cx="65" cy="45" r="8" fill="#333"/>',
mouth: '<ellipse cx="50" cy="68" rx="8" ry="12" fill="#333"/>'
},
[Emotions.HAPPY]: {
eyes: '<path d="M 30 40 Q 35 50 40 40 M 60 40 Q 65 50 70 40" stroke="#333" stroke-width="3" fill="none" stroke-linecap="round"/>',
mouth: '<path d="M 25 60 Q 50 80 75 60" stroke="#333" stroke-width="3" fill="none" stroke-linecap="round"/>'
},
[Emotions.SAD]: {
eyes: '<circle cx="35" cy="45" r="4" fill="#333"/><circle cx="65" cy="45" r="4" fill="#333"/><path d="M 33 50 Q 35 55 37 50 M 63 50 Q 65 55 67 50" stroke="#4A90E2" stroke-width="2" fill="none"/>',
mouth: '<path d="M 30 70 Q 50 60 70 70" stroke="#333" stroke-width="3" fill="none" stroke-linecap="round"/>'
}
};
// Add state-specific indicators
const stateIndicators = {
[States.WAITING]: '',
[States.REACTING]: '<circle cx="85" cy="25" r="3" fill="#ff9800" opacity="0.8"><animate attributeName="opacity" values="0.8;0.3;0.8" dur="1s" repeatCount="indefinite"/></circle>',
[States.TYPING]: '<rect x="20" y="85" width="6" height="4" fill="#2196f3" rx="1"><animate attributeName="opacity" values="1;0.3;1" dur="0.5s" repeatCount="indefinite"/></rect><rect x="30" y="85" width="6" height="4" fill="#2196f3" rx="1"><animate attributeName="opacity" values="1;0.3;1" dur="0.5s" begin="0.2s" repeatCount="indefinite"/></rect><rect x="40" y="85" width="6" height="4" fill="#2196f3" rx="1"><animate attributeName="opacity" values="1;0.3;1" dur="0.5s" begin="0.4s" repeatCount="indefinite"/></rect>',
[States.SLEEPING]: '<path d="M 20 20 Q 25 15 30 20 Q 35 25 40 20" stroke="#9c27b0" stroke-width="2" fill="none" opacity="0.7"><animate attributeName="opacity" values="0.7;0.2;0.7" dur="2s" repeatCount="indefinite"/></path><text x="45" y="18" font-family="Arial" font-size="8" fill="#9c27b0" opacity="0.7">Z<animate attributeName="opacity" values="0.7;0.2;0.7" dur="2s" begin="0.5s" repeatCount="indefinite"/></text><text x="55" y="15" font-family="Arial" font-size="6" fill="#9c27b0" opacity="0.7">z<animate attributeName="opacity" values="0.7;0.2;0.7" dur="2s" begin="1s" repeatCount="indefinite"/></text>'
};
const expr = expressions[emotion] || expressions[Emotions.NEUTRAL];
return `<svg width="100" height="100" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<!-- Robot head -->
<circle cx="50" cy="50" r="35" fill="${baseColor}" stroke="#333" stroke-width="2"/>
<!-- Head highlight -->
<ellipse cx="45" cy="40" rx="15" ry="10" fill="white" opacity="0.3"/>
<!-- Eyes -->
${expr.eyes}
<!-- Mouth -->
${expr.mouth}
<!-- Antenna -->
<line x1="50" y1="15" x2="50" y2="5" stroke="#333" stroke-width="2"/>
<circle cx="50" cy="5" r="2" fill="#ff4444"/>
<!-- State indicator -->
${stateIndicators[state] || ''}
</svg>`;
};
// Generate all combinations
const states = [States.WAITING, States.REACTING, States.TYPING, States.SLEEPING];
const emotions = [Emotions.NEUTRAL, Emotions.ANGRY, Emotions.SHOCKED, Emotions.HAPPY, Emotions.SAD];
states.forEach(state => {
emotions.forEach(emotion => {
const key = `${state}_${emotion || 'neutral'}`;
svgs[key] = createRobotSVG(state, emotion);
});
});
return svgs;
}
createMockClips() {
const clipDefinitions = [
// Wait state clips
{ name: 'wait_idle_L', state: 'wait', type: 'L' },
{ name: 'wait_pickNose_Q', state: 'wait', type: 'Q' },
{ name: 'wait_wave_Q', state: 'wait', type: 'Q' },
{ name: 'wait_2react_T', state: 'wait', type: 'T' },
{ name: 'wait_2sleep_T', state: 'wait', type: 'T' },
// React state clips
{ name: 'react_idle_L', state: 'react', type: 'L' },
{ name: 'react_an_L', state: 'react', type: 'L', emotion: 'an' },
{ name: 'react_sh_L', state: 'react', type: 'L', emotion: 'sh' },
{ name: 'react_2type_T', state: 'react', type: 'T' },
{ name: 'react_an2type_T', state: 'react', type: 'T', emotion: 'an' },
{ name: 'react_sh2type_T', state: 'react', type: 'T', emotion: 'sh' },
// Type state clips
{ name: 'type_idle_L', state: 'type', type: 'L' },
{ name: 'type_an_L', state: 'type', type: 'L', emotion: 'an' },
{ name: 'type_sh_L', state: 'type', type: 'L', emotion: 'sh' },
{ name: 'type_2wait_T', state: 'type', type: 'T' },
{ name: 'type_an2wait_T', state: 'type', type: 'T', emotion: 'an' },
{ name: 'type_sh2wait_T', state: 'type', type: 'T', emotion: 'sh' },
// Sleep state clips
{ name: 'sleep_idle_L', state: 'sleep', type: 'L' },
{ name: 'sleep_2wait_T', state: 'sleep', type: 'T' }
];
for (const def of clipDefinitions) {
const clip = new MockAnimationClip(def.name, def.type, def.state, def.emotion || '');
this.clips.set(def.name, clip);
}
}
initializeStates() {
this.states.set(States.WAITING, new MockWaitStateHandler(this));
this.states.set(States.REACTING, new MockReactStateHandler(this));
this.states.set(States.TYPING, new MockTypeStateHandler(this));
this.states.set(States.SLEEPING, new MockSleepStateHandler(this));
}
async transitionTo(newState, emotion = Emotions.NEUTRAL) {
if (newState === this.currentState) return;
const fromState = this.currentState; const fromState = this.currentState;
console.log(`Transitioning from ${fromState} to ${newState} with emotion: ${emotion}`); console.log(`Transitioning from ${fromState} to ${newState} with emotion: ${emotion}`);
// Stop current animations // Exit current state
this.stopAllAnimations(); if (this.currentStateHandler) {
await this.currentStateHandler.exit(newState, emotion);
}
// Play transition animation if available // Play transition animation if available
const transitionClip = `${fromState}_2${newState}_T`; const transitionClip = `${fromState}_2${newState}_T`;
if (this.availableClips.includes(transitionClip)) { const emotionalTransition = emotion && emotion !== Emotions.NEUTRAL ?
await this.playAnimation(transitionClip, 500); `${fromState}_${emotion}2${newState}_T` : null;
const clipToPlay = (emotionalTransition && this.getClip(emotionalTransition)) ?
emotionalTransition : transitionClip;
if (this.getClip(clipToPlay)) {
await this.playTransitionAnimation(clipToPlay);
} }
// Update state // Update state
this.currentState = newState; this.currentState = newState;
this.currentEmotion = emotion; this.currentStateHandler = this.states.get(newState);
this.lastActivity = Date.now(); this.lastActivity = Date.now();
// Play state animation // Enter new state
await this.enterState(newState, emotion); if (this.currentStateHandler) {
await this.currentStateHandler.enter(fromState, emotion);
}
this.updateUI(); this.updateUI();
this.resetInactivityTimer(); this.resetActivityTimer();
} }
async enterState(state, emotion = 'neutral') { async playTransitionAnimation(clipName) {
let clipName = `${state}_idle_L`; const clip = this.getClip(clipName);
if (clip) {
clip.play();
this.activeAnimations.add(clipName);
this.updateAnimationList();
// Handle emotional states // Wait for transition to complete
if (emotion !== 'neutral' && (state === 'type' || state === 'react')) { await new Promise(resolve => {
const emotionCode = this.getEmotionCode(emotion);
const emotionalClip = `${state}_${emotionCode}_L`;
if (this.availableClips.includes(emotionalClip)) {
clipName = emotionalClip;
}
}
if (this.availableClips.includes(clipName)) {
this.playAnimation(clipName);
}
// Handle state-specific logic
switch (state) {
case 'wait':
this.startQuirkTimer();
break;
case 'type':
this.isTyping = true;
document.getElementById('typingIndicator').classList.add('active');
break;
case 'sleep':
this.stopQuirkTimer();
setTimeout(() => { setTimeout(() => {
if (this.currentState === 'sleep') { clip.stop();
this.transitionTo('wait'); this.activeAnimations.delete(clipName);
} this.updateAnimationList();
}, 10000); // Wake up after 10 seconds in demo resolve();
break; }, 500);
});
} }
} }
async handleUserMessage(message) { async handleUserMessage(message) {
console.log(`Handling user message: "${message}"`);
this.onUserActivity(); this.onUserActivity();
// Analyze message for emotion // Analyze message for emotion
const emotion = this.analyzeMessageEmotion(message); const emotion = this.analyzeMessageEmotion(message);
// Transition to react state // If sleeping, wake up first
if (this.currentState !== 'react') { if (this.currentState === States.SLEEPING) {
await this.transitionTo('react', emotion); await this.transitionTo(States.WAITING);
}
// Transition to react state if not already there
if (this.currentState === States.WAITING) {
await this.transitionTo(States.REACTING, emotion);
}
// Let current state handle the message
if (this.currentStateHandler) {
await this.currentStateHandler.handleMessage(message);
} }
// Brief pause to show reaction // Brief pause to show reaction
setTimeout(async () => { setTimeout(async () => {
// Transition to type state // Transition to type state
await this.transitionTo('type', emotion); await this.transitionTo(States.TYPING, emotion);
// Show typing indicator
document.getElementById('typingIndicator').classList.add('active');
// Simulate typing duration // Simulate typing duration
const typingDuration = Math.min(message.length * 100, 3000); const typingDuration = Math.min(message.length * 100, 3000);
setTimeout(async () => { setTimeout(async () => {
this.isTyping = false;
document.getElementById('typingIndicator').classList.remove('active'); document.getElementById('typingIndicator').classList.remove('active');
await this.transitionTo('wait'); await this.transitionTo(States.WAITING);
}, typingDuration); }, typingDuration);
}, 1000); }, 1000);
} }
@ -419,103 +736,105 @@
const text = message.toLowerCase(); const text = message.toLowerCase();
if (text.includes('!') || text.includes('urgent') || text.includes('asap') || text.includes('now')) { if (text.includes('!') || text.includes('urgent') || text.includes('asap') || text.includes('now')) {
return 'shocked'; return Emotions.SHOCKED;
} }
if (text.includes('error') || text.includes('problem') || text.includes('issue') || text.includes('wrong')) { if (text.includes('error') || text.includes('problem') || text.includes('issue') || text.includes('wrong')) {
return 'angry'; return Emotions.ANGRY;
} }
if (text.includes('great') || text.includes('awesome') || text.includes('good') || text.includes('excellent')) { if (text.includes('great') || text.includes('awesome') || text.includes('good') || text.includes('excellent')) {
return 'happy'; return Emotions.HAPPY;
} }
if (text.includes('sad') || text.includes('sorry') || text.includes('disappointed')) { if (text.includes('sad') || text.includes('sorry') || text.includes('disappointed')) {
return 'sad'; return Emotions.SAD;
} }
return 'neutral'; return Emotions.NEUTRAL;
} }
onUserActivity() { onUserActivity() {
this.lastActivity = Date.now(); this.lastActivity = Date.now();
this.resetInactivityTimer(); this.resetActivityTimer();
// Wake up if sleeping // Wake up if sleeping
if (this.currentState === 'sleep') { if (this.currentState === States.SLEEPING) {
this.transitionTo('wait'); this.transitionTo(States.WAITING);
} }
} }
resetInactivityTimer() { resetActivityTimer() {
if (this.inactivityTimer) { this.inactivityTimer = 0;
clearTimeout(this.inactivityTimer); }
async handleInactivity() {
console.log('Inactivity detected, transitioning to sleep');
await this.transitionTo(States.SLEEPING);
}
update(deltaTime) {
if (!this.initialized) return;
// Update current state
if (this.currentStateHandler) {
this.currentStateHandler.update(deltaTime);
} }
this.inactivityTimer = setTimeout(() => { // Update inactivity timer
if (this.currentState === 'wait') { this.inactivityTimer += deltaTime;
this.transitionTo('sleep'); if (this.inactivityTimer > this.inactivityTimeout && this.currentState !== States.SLEEPING) {
this.handleInactivity();
}
}
// Utility methods
getClip(name) {
return this.clips.get(name);
}
getClipsByPattern(pattern) {
const regex = new RegExp(pattern.replace(/\*/g, '.*'));
const matches = [];
for (const [name, clip] of this.clips) {
if (regex.test(name)) {
matches.push(clip);
} }
}, 15000); // 15 seconds for demo
}
startQuirkTimer() {
this.stopQuirkTimer();
this.quirkTimer = setInterval(() => {
if (this.currentState === 'wait' && Math.random() < 0.3) {
this.playRandomQuirk();
}
}, 5000);
}
stopQuirkTimer() {
if (this.quirkTimer) {
clearInterval(this.quirkTimer);
this.quirkTimer = null;
} }
return matches;
} }
playRandomQuirk() { getCurrentState() {
const quirks = this.availableClips.filter(clip => return this.currentState;
clip.startsWith('wait_') && clip.endsWith('_Q')
);
if (quirks.length > 0) {
const randomQuirk = quirks[Math.floor(Math.random() * quirks.length)];
this.playAnimation(randomQuirk, 2000);
}
} }
async playAnimation(clipName, duration = null) { getCurrentStateHandler() {
console.log(`Playing animation: ${clipName}`); return this.currentStateHandler;
this.activeAnimations.add(clipName);
this.updateAnimationList();
// Simulate animation duration
const animDuration = duration || (clipName.includes('_T') ? 500 : 2000);
setTimeout(() => {
this.activeAnimations.delete(clipName);
this.updateAnimationList();
}, animDuration);
} }
stopAllAnimations() { getAvailableClips() {
this.activeAnimations.clear(); return Array.from(this.clips.keys());
this.updateAnimationList();
} }
getEmotionCode(emotion) { getAvailableStates() {
const emotionCodes = { return Array.from(this.states.keys());
angry: 'an',
shocked: 'sh',
happy: 'ha',
sad: 'sa'
};
return emotionCodes[emotion] || '';
} }
updateUI() { updateUI() {
// Convert emotion code back to readable form
const emotionMap = {
[Emotions.NEUTRAL]: 'neutral',
[Emotions.ANGRY]: 'angry',
[Emotions.SHOCKED]: 'shocked',
[Emotions.HAPPY]: 'happy',
[Emotions.SAD]: 'sad'
};
const currentEmotion = this.getCurrentEmotion();
const emotionName = emotionMap[currentEmotion] || 'neutral';
// Update status panel // Update status panel
document.getElementById('currentState').textContent = this.currentState.toUpperCase(); document.getElementById('currentState').textContent = this.currentState.toUpperCase();
document.getElementById('currentEmotion').textContent = this.currentEmotion; document.getElementById('currentEmotion').textContent = emotionName;
document.getElementById('lastActivity').textContent = new Date(this.lastActivity).toLocaleTimeString(); document.getElementById('lastActivity').textContent = new Date(this.lastActivity).toLocaleTimeString();
document.getElementById('activeClips').textContent = this.activeAnimations.size; document.getElementById('activeClips').textContent = this.activeAnimations.size;
@ -524,10 +843,14 @@
const stateEl = document.getElementById('owenState'); const stateEl = document.getElementById('owenState');
const emotionEl = document.getElementById('owenEmotion'); const emotionEl = document.getElementById('owenEmotion');
avatar.style.background = `linear-gradient(45deg, ${this.stateColors[this.currentState]}, #764ba2)`; // Get the appropriate SVG for current state and emotion
avatar.textContent = this.emotionEmojis[this.currentEmotion]; const svgKey = `${this.currentState}_${currentEmotion || 'neutral'}`;
const svgContent = this.svgGraphics[svgKey] || this.svgGraphics[`${this.currentState}_neutral`];
avatar.innerHTML = svgContent;
avatar.style.background = 'transparent'; // Remove background since SVG handles coloring
stateEl.textContent = this.currentState.toUpperCase(); stateEl.textContent = this.currentState.toUpperCase();
emotionEl.textContent = this.currentEmotion; emotionEl.textContent = emotionName;
// Add animation effect // Add animation effect
avatar.style.transform = 'scale(1.1)'; avatar.style.transform = 'scale(1.1)';
@ -536,6 +859,14 @@
}, 200); }, 200);
} }
getCurrentEmotion() {
// Extract emotion from current state handler or default to neutral
if (this.currentStateHandler && this.currentStateHandler.currentClip) {
return this.currentStateHandler.currentClip.emotion || Emotions.NEUTRAL;
}
return Emotions.NEUTRAL;
}
updateAnimationList() { updateAnimationList() {
const listEl = document.getElementById('animationList'); const listEl = document.getElementById('animationList');
listEl.innerHTML = ''; listEl.innerHTML = '';
@ -551,6 +882,23 @@
}); });
} }
} }
dispose() {
// Stop all animations
for (const [, clip] of this.clips) {
if (clip.isPlaying) {
clip.stop();
}
}
// Clear caches
this.clips.clear();
this.states.clear();
this.activeAnimations.clear();
this.initialized = false;
console.log('Owen Animation System disposed');
}
} }
// Global Owen system instance // Global Owen system instance
@ -558,7 +906,7 @@
// Initialize the system // Initialize the system
async function initializeOwen() { async function initializeOwen() {
owenSystem = new MockOwenSystem(); owenSystem = new MockOwenAnimationContext();
await owenSystem.initialize(); await owenSystem.initialize();
// Add mouse activity listener // Add mouse activity listener
@ -575,6 +923,28 @@
sendMessage(); sendMessage();
} }
}); });
// Start update loop
startUpdateLoop();
}
// Update loop for the system
function startUpdateLoop() {
let lastTime = performance.now();
function update() {
const currentTime = performance.now();
const deltaTime = currentTime - lastTime;
lastTime = currentTime;
if (owenSystem) {
owenSystem.update(deltaTime);
}
requestAnimationFrame(update);
}
update();
} }
// UI Control Functions // UI Control Functions
@ -592,7 +962,17 @@
owenSystem.transitionTo(state); owenSystem.transitionTo(state);
} }
function testEmotion(emotion) { function testEmotion(emotionName) {
// Map UI emotion names to emotion codes
const emotionMap = {
'neutral': Emotions.NEUTRAL,
'angry': Emotions.ANGRY,
'shocked': Emotions.SHOCKED,
'happy': Emotions.HAPPY,
'sad': Emotions.SAD
};
const emotion = emotionMap[emotionName] || Emotions.NEUTRAL;
owenSystem.transitionTo(owenSystem.currentState, emotion); owenSystem.transitionTo(owenSystem.currentState, emotion);
} }
@ -606,11 +986,11 @@
} }
function simulateInactivity() { function simulateInactivity() {
owenSystem.transitionTo('sleep'); owenSystem.transitionTo(States.SLEEPING);
} }
function resetSystem() { function resetSystem() {
owenSystem.transitionTo('wait', 'neutral'); owenSystem.transitionTo(States.WAITING, Emotions.NEUTRAL);
} }
// Start the demo when page loads // Start the demo when page loads
@ -619,8 +999,8 @@
// Update UI every second // Update UI every second
setInterval(() => { setInterval(() => {
if (owenSystem) { if (owenSystem) {
document.getElementById('lastActivity').textContent = const timeSinceActivity = Math.floor((Date.now() - owenSystem.lastActivity) / 1000);
Math.floor((Date.now() - owenSystem.lastActivity) / 1000) + 's ago'; document.getElementById('lastActivity').textContent = timeSinceActivity + 's ago';
} }
}, 1000); }, 1000);
</script> </script>

View File

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

View File

@ -59,7 +59,7 @@ export class OwenAnimationContext {
* Current active state * Current active state
* @type {string} * @type {string}
*/ */
this.currentState = States.WAIT this.currentState = States.WAITING
/** /**
* Current active state handler * Current active state handler
@ -105,7 +105,7 @@ export class OwenAnimationContext {
this.initializeStates() this.initializeStates()
// Start in wait state // Start in wait state
await this.transitionTo(States.WAIT) await this.transitionTo(States.WAITING)
this.initialized = true this.initialized = true
console.log('Owen Animation System initialized') console.log('Owen Animation System initialized')
@ -167,8 +167,8 @@ export class OwenAnimationContext {
this.onUserActivity() this.onUserActivity()
// If sleeping, wake up first // If sleeping, wake up first
if (this.currentState === States.SLEEP) { if (this.currentState === States.SLEEPING) {
await this.transitionTo(States.REACT) await this.transitionTo(States.REACTING)
} }
// Let current state handle the message // Let current state handle the message
@ -177,10 +177,10 @@ export class OwenAnimationContext {
} }
// Transition to appropriate next state based on current state // Transition to appropriate next state based on current state
if (this.currentState === States.WAIT) { if (this.currentState === States.WAITING) {
await this.transitionTo(States.REACT) await this.transitionTo(States.REACTING);
} else if (this.currentState === States.REACT) { } else if (this.currentState === States.REACTING) {
await this.transitionTo(States.TYPE) await this.transitionTo(States.TYPING)
} }
} }
@ -192,8 +192,8 @@ export class OwenAnimationContext {
this.resetActivityTimer() this.resetActivityTimer()
// Wake up if sleeping // Wake up if sleeping
if (this.currentState === States.SLEEP) { if (this.currentState === States.SLEEPING) {
this.transitionTo(States.WAIT) this.transitionTo(States.WAITING)
} }
} }
@ -213,7 +213,7 @@ export class OwenAnimationContext {
*/ */
async handleInactivity () { async handleInactivity () {
console.log('Inactivity detected, transitioning to sleep') console.log('Inactivity detected, transitioning to sleep')
await this.transitionTo(States.SLEEP) await this.transitionTo(States.SLEEPING)
} }
/** /**
@ -234,7 +234,7 @@ export class OwenAnimationContext {
// Update inactivity timer // Update inactivity timer
this.inactivityTimer += deltaTime this.inactivityTimer += deltaTime
if (this.inactivityTimer > this.inactivityTimeout && this.currentState !== States.SLEEP) { if (this.inactivityTimer > this.inactivityTimeout && this.currentState !== States.SLEEPING) {
this.handleInactivity() this.handleInactivity()
} }
} }

8
src/index.d.ts vendored
View File

@ -16,10 +16,10 @@ export const ClipTypes: {
}; };
export const States: { export const States: {
readonly WAIT: 'wait'; readonly WAITING: 'wait';
readonly REACT: 'react'; readonly REACTING: 'react';
readonly TYPE: 'type'; readonly TYPING: 'type';
readonly SLEEP: 'sleep'; readonly SLEEPING: 'sleep';
}; };
export const Emotions: { export const Emotions: {

View File

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

View File

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

View File

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

View File

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

View File

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