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:
@ -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
|
||||||
|
|||||||
@ -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()
|
||||||
|
|||||||
@ -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...`)
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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'
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -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
8
src/index.d.ts
vendored
@ -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: {
|
||||||
|
|||||||
@ -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 ]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -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 ]
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -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 ]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user