Some checks failed
CI/CD Pipeline / Test & Lint (16.x) (push) Has been cancelled
CI/CD Pipeline / Test & Lint (18.x) (push) Has been cancelled
CI/CD Pipeline / Test & Lint (20.x) (push) Has been cancelled
CI/CD Pipeline / Security Audit (push) Has been cancelled
CI/CD Pipeline / Release (push) Has been cancelled
Demo Deployment / Build Demo (push) Has been cancelled
Demo Deployment / Test Demo (push) Has been cancelled
Demo Deployment / Performance Audit (push) Has been cancelled
Demo Deployment / Deploy to Staging (push) Has been cancelled
Demo Deployment / Deploy to Production (push) Has been cancelled
Animation Processing Pipeline / Validate Animation Names (push) Has been cancelled
Animation Processing Pipeline / Process Blender Animation Assets (push) Has been cancelled
Multi-Scheme Testing / Validate Naming Schemes (artist) (push) Has been cancelled
Multi-Scheme Testing / Validate Naming Schemes (hierarchical) (push) Has been cancelled
Multi-Scheme Testing / Validate Naming Schemes (legacy) (push) Has been cancelled
Multi-Scheme Testing / Validate Naming Schemes (semantic) (push) Has been cancelled
Multi-Scheme Testing / Test Scheme Conversions (push) Has been cancelled
Multi-Scheme Testing / Validate Demo Functionality (push) Has been cancelled
Multi-Scheme Testing / Performance Benchmarks (push) Has been cancelled
Performance Testing / Animation Conversion Performance (100, artist) (push) Has been cancelled
Performance Testing / Animation Conversion Performance (100, hierarchical) (push) Has been cancelled
Performance Testing / Animation Conversion Performance (100, legacy) (push) Has been cancelled
Performance Testing / Animation Conversion Performance (100, semantic) (push) Has been cancelled
Performance Testing / Animation Conversion Performance (1000, artist) (push) Has been cancelled
Performance Testing / Animation Conversion Performance (1000, hierarchical) (push) Has been cancelled
Performance Testing / Animation Conversion Performance (1000, legacy) (push) Has been cancelled
Performance Testing / Animation Conversion Performance (1000, semantic) (push) Has been cancelled
Performance Testing / Animation Conversion Performance (5000, artist) (push) Has been cancelled
Performance Testing / Animation Conversion Performance (5000, hierarchical) (push) Has been cancelled
Performance Testing / Animation Conversion Performance (5000, legacy) (push) Has been cancelled
Performance Testing / Animation Conversion Performance (5000, semantic) (push) Has been cancelled
Performance Testing / Memory Usage Analysis (push) Has been cancelled
Performance Testing / Demo Performance Audit (push) Has been cancelled
Animation Processing Pipeline / Update Animation Documentation (push) Has been cancelled
Animation Processing Pipeline / Deploy Animation Demo (push) Has been cancelled
Performance Testing / Generate Performance Report (push) Has been cancelled
- Added AnimationNameMapper class to handle conversion between different animation naming schemes (legacy, artist, hierarchical, semantic). - Included methods for initialization, pattern matching, conversion, and validation of animation names. - Developed comprehensive unit tests for the animation name converter and demo pages using Playwright. - Created a Vite configuration for the demo application, including asset handling and optimization settings. - Enhanced the demo with features for batch conversion, performance metrics, and responsive design.
1533 lines
47 KiB
HTML
1533 lines
47 KiB
HTML
<!DOCTYPING html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
<title>Owen Animation System Demo - Implementation Test</title>
|
|
<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>
|
|
body {
|
|
margin: 0;
|
|
padding: 0;
|
|
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
overflow: hidden;
|
|
}
|
|
|
|
#container {
|
|
position: relative;
|
|
width: 100vw;
|
|
height: 100vh;
|
|
}
|
|
|
|
#controls {
|
|
position: absolute;
|
|
top: 20px;
|
|
left: 20px;
|
|
background: rgba(255, 255, 255, 0.95);
|
|
padding: 20px;
|
|
border-radius: 15px;
|
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
|
backdrop-filter: blur(10px);
|
|
z-index: 100;
|
|
min-width: 300px;
|
|
}
|
|
|
|
h2 {
|
|
margin: 0 0 15px 0;
|
|
color: #333;
|
|
font-size: 18px;
|
|
}
|
|
|
|
.control-group {
|
|
margin-bottom: 15px;
|
|
}
|
|
|
|
label {
|
|
display: block;
|
|
margin-bottom: 5px;
|
|
color: #555;
|
|
font-weight: 500;
|
|
}
|
|
|
|
button {
|
|
background: linear-gradient(45deg, #667eea, #764ba2);
|
|
color: white;
|
|
border: none;
|
|
padding: 10px 15px;
|
|
border-radius: 8px;
|
|
cursor: pointer;
|
|
margin: 5px 5px 5px 0;
|
|
font-size: 14px;
|
|
transition:
|
|
transform 0.2s,
|
|
box-shadow 0.2s;
|
|
}
|
|
|
|
button:hover {
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
|
|
}
|
|
|
|
button:active {
|
|
transform: translateY(0);
|
|
}
|
|
|
|
input[type="text"] {
|
|
width: 100%;
|
|
padding: 8px 12px;
|
|
border: 2px solid #ddd;
|
|
border-radius: 8px;
|
|
font-size: 14px;
|
|
margin-bottom: 10px;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
input[type="text"]:focus {
|
|
outline: none;
|
|
border-color: #667eea;
|
|
}
|
|
|
|
#status {
|
|
position: absolute;
|
|
bottom: 20px;
|
|
left: 20px;
|
|
background: rgba(0, 0, 0, 0.8);
|
|
color: white;
|
|
padding: 15px 20px;
|
|
border-radius: 10px;
|
|
font-family: "Courier New", monospace;
|
|
font-size: 14px;
|
|
max-width: 400px;
|
|
}
|
|
|
|
.status-line {
|
|
margin-bottom: 5px;
|
|
}
|
|
|
|
.status-line:last-child {
|
|
margin-bottom: 0;
|
|
}
|
|
|
|
.emotion-buttons {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 5px;
|
|
}
|
|
|
|
.emotion-neutral {
|
|
background: linear-gradient(45deg, #4caf50, #45a049);
|
|
}
|
|
.emotion-angry {
|
|
background: linear-gradient(45deg, #f44336, #d32f2f);
|
|
}
|
|
.emotion-shocked {
|
|
background: linear-gradient(45deg, #ff9800, #f57c00);
|
|
}
|
|
.emotion-happy {
|
|
background: linear-gradient(45deg, #ffeb3b, #fbc02d);
|
|
color: #333;
|
|
}
|
|
.emotion-sad {
|
|
background: linear-gradient(45deg, #2196f3, #1976d2);
|
|
}
|
|
|
|
/* Multi-scheme UI styling */
|
|
select {
|
|
padding: 8px 12px;
|
|
border: 2px solid #ddd;
|
|
border-radius: 8px;
|
|
font-size: 14px;
|
|
margin: 5px 5px 5px 0;
|
|
background: white;
|
|
cursor: pointer;
|
|
}
|
|
|
|
select:focus {
|
|
outline: none;
|
|
border-color: #667eea;
|
|
}
|
|
|
|
#nameConversionResults {
|
|
line-height: 1.4;
|
|
white-space: pre-wrap;
|
|
}
|
|
|
|
#nameConversionResults div {
|
|
margin-bottom: 3px;
|
|
}
|
|
|
|
#nameConversionResults strong {
|
|
color: #333;
|
|
}
|
|
|
|
#owen-character {
|
|
position: absolute;
|
|
right: 50px;
|
|
bottom: 50px;
|
|
width: 300px;
|
|
height: 400px;
|
|
background: rgba(255, 255, 255, 0.9);
|
|
border-radius: 20px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
|
|
}
|
|
|
|
.owen-avatar {
|
|
width: 120px;
|
|
height: 120px;
|
|
border-radius: 50%;
|
|
background: transparent;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 48px;
|
|
color: white;
|
|
margin-bottom: 20px;
|
|
transition: transform 0.3s ease;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.owen-avatar svg {
|
|
width: 100%;
|
|
height: 100%;
|
|
display: block;
|
|
}
|
|
|
|
.owen-state {
|
|
font-size: 18px;
|
|
font-weight: bold;
|
|
color: #333;
|
|
margin-bottom: 10px;
|
|
}
|
|
|
|
.owen-emotion {
|
|
font-size: 14px;
|
|
color: #666;
|
|
background: #f0f0f0;
|
|
padding: 5px 10px;
|
|
border-radius: 15px;
|
|
}
|
|
|
|
.animation-list {
|
|
max-height: 150px;
|
|
overflow-y: auto;
|
|
border: 1px solid #ddd;
|
|
border-radius: 5px;
|
|
padding: 10px;
|
|
background: #f9f9f9;
|
|
}
|
|
|
|
.animation-item {
|
|
padding: 5px;
|
|
margin: 2px 0;
|
|
border-radius: 3px;
|
|
font-size: 12px;
|
|
font-family: monospace;
|
|
}
|
|
|
|
.animation-playing {
|
|
background: #e8f5e8;
|
|
color: #2e7d32;
|
|
font-weight: bold;
|
|
}
|
|
|
|
.typing-indicator {
|
|
display: none;
|
|
color: #667eea;
|
|
font-style: italic;
|
|
}
|
|
|
|
.typing-indicator.active {
|
|
display: block;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div id="container">
|
|
<div id="controls">
|
|
<h2>🤖 Owen Animation System Demo</h2>
|
|
|
|
<div class="control-group">
|
|
<label>Send Message to Owen:</label>
|
|
<input
|
|
type="text"
|
|
id="messageInput"
|
|
placeholder="Type a message..."
|
|
/>
|
|
<button onclick="sendMessage()">Send Message</button>
|
|
<div class="typing-indicator" id="typingIndicator">
|
|
Owen is typing...
|
|
</div>
|
|
</div>
|
|
|
|
<div class="control-group">
|
|
<label>Manual State Transitions:</label>
|
|
<button onclick="transitionTo('wait')">Wait</button>
|
|
<button onclick="transitionTo('react')">React</button>
|
|
<button onclick="transitionTo('type')">Type</button>
|
|
<button onclick="transitionTo('sleep')">Sleep</button>
|
|
</div>
|
|
|
|
<div class="control-group">
|
|
<label>Test Emotions:</label>
|
|
<div class="emotion-buttons">
|
|
<button class="emotion-neutral" onclick="testEmotion('neutral')">
|
|
😊 Neutral
|
|
</button>
|
|
<button class="emotion-angry" onclick="testEmotion('angry')">
|
|
😠 Angry
|
|
</button>
|
|
<button class="emotion-shocked" onclick="testEmotion('shocked')">
|
|
😲 Shocked
|
|
</button>
|
|
<button class="emotion-happy" onclick="testEmotion('happy')">
|
|
😄 Happy
|
|
</button>
|
|
<button class="emotion-sad" onclick="testEmotion('sad')">
|
|
😢 Sad
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="control-group">
|
|
<label>Quick Test Messages:</label>
|
|
<button onclick="quickMessage('Hello Owen! How are you?')">
|
|
Friendly
|
|
</button>
|
|
<button onclick="quickMessage('URGENT: Fix this now!')">
|
|
Urgent
|
|
</button>
|
|
<button onclick="quickMessage('Great job on the project!')">
|
|
Praise
|
|
</button>
|
|
<button onclick="quickMessage('We have a problem...')">
|
|
Problem
|
|
</button>
|
|
</div>
|
|
|
|
<div class="control-group">
|
|
<label>System Controls:</label>
|
|
<button onclick="simulateActivity()">Simulate Activity</button>
|
|
<button onclick="simulateInactivity()">Force Sleep</button>
|
|
<button onclick="resetSystem()">Reset</button>
|
|
</div>
|
|
|
|
<div class="control-group">
|
|
<label>Multi-Scheme Animation Testing:</label>
|
|
<input
|
|
type="text"
|
|
id="animationNameInput"
|
|
placeholder="Enter animation name..."
|
|
/>
|
|
<select id="targetSchemeSelect">
|
|
<option value="legacy">Legacy (wait_idle_L)</option>
|
|
<option value="artist">Artist (Owen_WaitIdle)</option>
|
|
<option value="hierarchical">
|
|
Hierarchical (owen.state.wait.idle.loop)
|
|
</option>
|
|
<option value="semantic" selected>
|
|
Semantic (OwenWaitIdleLoop)
|
|
</option>
|
|
</select>
|
|
<button onclick="testAnimationConversion()">Convert Name</button>
|
|
<button onclick="validateAnimationName()">Validate</button>
|
|
</div>
|
|
|
|
<div class="control-group">
|
|
<label>Animation Name Results:</label>
|
|
<div
|
|
id="nameConversionResults"
|
|
style="
|
|
background: #f8f9fa;
|
|
padding: 10px;
|
|
border-radius: 5px;
|
|
font-family: monospace;
|
|
font-size: 12px;
|
|
max-height: 150px;
|
|
overflow-y: auto;
|
|
"
|
|
>
|
|
<div>Enter an animation name above to see conversions...</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="control-group">
|
|
<label>Quick Animation Tests:</label>
|
|
<button onclick="testQuickAnimation('wait_idle_L')">
|
|
Test Legacy
|
|
</button>
|
|
<button onclick="testQuickAnimation('Owen_ReactAngry')">
|
|
Test Artist
|
|
</button>
|
|
<button onclick="testQuickAnimation('owen.state.type.idle.loop')">
|
|
Test Hierarchical
|
|
</button>
|
|
<button onclick="testQuickAnimation('OwenSleepToWaitTransition')">
|
|
Test Semantic
|
|
</button>
|
|
</div>
|
|
|
|
<div class="control-group">
|
|
<label>Active Animations:</label>
|
|
<div class="animation-list" id="animationList">
|
|
<div class="animation-item">System initializing...</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="status">
|
|
<div class="status-line"><strong>Owen System Status</strong></div>
|
|
<div class="status-line">
|
|
State: <span id="currentState">Initializing</span>
|
|
</div>
|
|
<div class="status-line">
|
|
Emotion: <span id="currentEmotion">Neutral</span>
|
|
</div>
|
|
<div class="status-line">
|
|
Last Activity: <span id="lastActivity">Now</span>
|
|
</div>
|
|
<div class="status-line">
|
|
Active Clips: <span id="activeClips">0</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="owen-character">
|
|
<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-emotion" id="owenEmotion">Neutral</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
// Mock Owen Animation System based on actual implementation structure
|
|
// This simulates the real Owen system for testing purposes
|
|
|
|
// Constants matching the real implementation
|
|
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 Animation Name Mapper for multi-scheme naming support
|
|
class MockAnimationNameMapper {
|
|
constructor() {
|
|
// Sample animations with all naming schemes
|
|
this.animationMappings = [
|
|
{
|
|
legacy: "wait_idle_L",
|
|
artist: "Owen_WaitIdle",
|
|
hierarchical: "owen.state.wait.idle.loop",
|
|
semantic: "OwenWaitIdleLoop",
|
|
state: "wait",
|
|
emotion: "",
|
|
type: "loop",
|
|
},
|
|
{
|
|
legacy: "react_an2type_T",
|
|
artist: "Owen_ReactAngryToType",
|
|
hierarchical: "owen.state.react.angry.totype.transition",
|
|
semantic: "OwenReactAngryToTypeTransition",
|
|
state: "react",
|
|
emotion: "angry",
|
|
type: "transition",
|
|
},
|
|
{
|
|
legacy: "type_idle_L",
|
|
artist: "Owen_TypeIdle",
|
|
hierarchical: "owen.state.type.idle.loop",
|
|
semantic: "OwenTypeIdleLoop",
|
|
state: "type",
|
|
emotion: "",
|
|
type: "loop",
|
|
},
|
|
{
|
|
legacy: "sleep_2wait_T",
|
|
artist: "Owen_SleepToWait",
|
|
hierarchical: "owen.state.sleep.towait.transition",
|
|
semantic: "OwenSleepToWaitTransition",
|
|
state: "sleep",
|
|
emotion: "",
|
|
type: "transition",
|
|
},
|
|
];
|
|
}
|
|
|
|
convert(name, targetScheme) {
|
|
// Find the animation by checking all schemes
|
|
const animation = this.animationMappings.find((anim) =>
|
|
Object.values(anim).includes(name),
|
|
);
|
|
|
|
if (!animation) {
|
|
throw new Error(`Animation "${name}" not found`);
|
|
}
|
|
|
|
return animation[targetScheme] || name;
|
|
}
|
|
|
|
getAllNames(name) {
|
|
const animation = this.animationMappings.find((anim) =>
|
|
Object.values(anim).includes(name),
|
|
);
|
|
|
|
if (!animation) {
|
|
return {
|
|
legacy: name,
|
|
artist: name,
|
|
hierarchical: name,
|
|
semantic: name,
|
|
};
|
|
}
|
|
|
|
return {
|
|
legacy: animation.legacy,
|
|
artist: animation.artist,
|
|
hierarchical: animation.hierarchical,
|
|
semantic: animation.semantic,
|
|
};
|
|
}
|
|
|
|
validateAnimationName(name) {
|
|
const animation = this.animationMappings.find((anim) =>
|
|
Object.values(anim).includes(name),
|
|
);
|
|
|
|
if (!animation) {
|
|
return {
|
|
isValid: false,
|
|
scheme: "unknown",
|
|
error: `Animation "${name}" not found`,
|
|
suggestions: this.animationMappings
|
|
.slice(0, 3)
|
|
.map((a) => a.semantic),
|
|
};
|
|
}
|
|
|
|
// Determine which scheme this name belongs to
|
|
let scheme = "unknown";
|
|
if (animation.legacy === name) scheme = "legacy";
|
|
else if (animation.artist === name) scheme = "artist";
|
|
else if (animation.hierarchical === name) scheme = "hierarchical";
|
|
else if (animation.semantic === name) scheme = "semantic";
|
|
|
|
return {
|
|
isValid: true,
|
|
scheme: scheme,
|
|
error: null,
|
|
suggestions: [],
|
|
};
|
|
}
|
|
}
|
|
|
|
// Mock Owen Animation Context (main system)
|
|
class MockOwenAnimationContext {
|
|
constructor() {
|
|
this.model = null;
|
|
this.mixer = null;
|
|
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.isTyping = false;
|
|
|
|
// Multi-scheme animation name mapper
|
|
this.nameMapper = new MockAnimationNameMapper();
|
|
|
|
this.stateColors = {
|
|
[States.WAITING]: "#4CAF50",
|
|
[States.REACTING]: "#ff9800",
|
|
[States.TYPING]: "#2196f3",
|
|
[States.SLEEPING]: "#9c27b0",
|
|
};
|
|
|
|
// SVG graphics for each state/emotion combination
|
|
this.svgGraphics = this.createSVGGraphics();
|
|
|
|
this.lastActivity = Date.now();
|
|
}
|
|
|
|
async initialize() {
|
|
if (this.initialized) return;
|
|
|
|
// Create mock animation clips following the naming convention
|
|
this.createMockClips();
|
|
|
|
// Initialize state handlers
|
|
this.initializeStates();
|
|
|
|
// Start in wait state
|
|
await this.transitionTo(States.WAITING);
|
|
|
|
this.initialized = true;
|
|
console.log("Owen Animation System initialized");
|
|
}
|
|
|
|
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;
|
|
console.log(
|
|
`Transitioning from ${fromState} to ${newState} with emotion: ${emotion}`,
|
|
);
|
|
|
|
// Exit current state
|
|
if (this.currentStateHandler) {
|
|
await this.currentStateHandler.exit(newState, emotion);
|
|
}
|
|
|
|
// Play transition animation if available
|
|
const transitionClip = `${fromState}_2${newState}_T`;
|
|
const emotionalTransition =
|
|
emotion && emotion !== Emotions.NEUTRAL
|
|
? `${fromState}_${emotion}2${newState}_T`
|
|
: null;
|
|
|
|
const clipToPlay =
|
|
emotionalTransition && this.getClip(emotionalTransition)
|
|
? emotionalTransition
|
|
: transitionClip;
|
|
|
|
if (this.getClip(clipToPlay)) {
|
|
await this.playTransitionAnimation(clipToPlay);
|
|
}
|
|
|
|
// Update state
|
|
this.currentState = newState;
|
|
this.currentStateHandler = this.states.get(newState);
|
|
this.lastActivity = Date.now();
|
|
|
|
// Enter new state
|
|
if (this.currentStateHandler) {
|
|
await this.currentStateHandler.enter(fromState, emotion);
|
|
}
|
|
|
|
this.updateUI();
|
|
this.resetActivityTimer();
|
|
}
|
|
|
|
async playTransitionAnimation(clipName) {
|
|
const clip = this.getClip(clipName);
|
|
if (clip) {
|
|
clip.play();
|
|
this.activeAnimations.add(clipName);
|
|
this.updateAnimationList();
|
|
|
|
// Wait for transition to complete
|
|
await new Promise((resolve) => {
|
|
setTimeout(() => {
|
|
clip.stop();
|
|
this.activeAnimations.delete(clipName);
|
|
this.updateAnimationList();
|
|
resolve();
|
|
}, 500);
|
|
});
|
|
}
|
|
}
|
|
|
|
async handleUserMessage(message) {
|
|
console.log(`Handling user message: "${message}"`);
|
|
|
|
this.onUserActivity();
|
|
|
|
// Analyze message for emotion
|
|
const emotion = this.analyzeMessageEmotion(message);
|
|
|
|
// If sleeping, wake up first
|
|
if (this.currentState === States.SLEEPING) {
|
|
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
|
|
setTimeout(async () => {
|
|
// Transition to type state
|
|
await this.transitionTo(States.TYPING, emotion);
|
|
|
|
// Show typing indicator
|
|
document.getElementById("typingIndicator").classList.add("active");
|
|
|
|
// Simulate typing duration
|
|
const typingDuration = Math.min(message.length * 100, 3000);
|
|
setTimeout(async () => {
|
|
document
|
|
.getElementById("typingIndicator")
|
|
.classList.remove("active");
|
|
await this.transitionTo(States.WAITING);
|
|
}, typingDuration);
|
|
}, 1000);
|
|
}
|
|
|
|
analyzeMessageEmotion(message) {
|
|
const text = message.toLowerCase();
|
|
|
|
if (
|
|
text.includes("!") ||
|
|
text.includes("urgent") ||
|
|
text.includes("asap") ||
|
|
text.includes("now")
|
|
) {
|
|
return Emotions.SHOCKED;
|
|
}
|
|
if (
|
|
text.includes("error") ||
|
|
text.includes("problem") ||
|
|
text.includes("issue") ||
|
|
text.includes("wrong")
|
|
) {
|
|
return Emotions.ANGRY;
|
|
}
|
|
if (
|
|
text.includes("great") ||
|
|
text.includes("awesome") ||
|
|
text.includes("good") ||
|
|
text.includes("excellent")
|
|
) {
|
|
return Emotions.HAPPY;
|
|
}
|
|
if (
|
|
text.includes("sad") ||
|
|
text.includes("sorry") ||
|
|
text.includes("disappointed")
|
|
) {
|
|
return Emotions.SAD;
|
|
}
|
|
|
|
return Emotions.NEUTRAL;
|
|
}
|
|
|
|
onUserActivity() {
|
|
this.lastActivity = Date.now();
|
|
this.resetActivityTimer();
|
|
|
|
// Wake up if sleeping
|
|
if (this.currentState === States.SLEEPING) {
|
|
this.transitionTo(States.WAITING);
|
|
}
|
|
}
|
|
|
|
resetActivityTimer() {
|
|
this.inactivityTimer = 0;
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
// Update inactivity timer
|
|
this.inactivityTimer += deltaTime;
|
|
if (
|
|
this.inactivityTimer > this.inactivityTimeout &&
|
|
this.currentState !== States.SLEEPING
|
|
) {
|
|
this.handleInactivity();
|
|
}
|
|
}
|
|
|
|
// Utility methods
|
|
getClip(name) {
|
|
// First try direct lookup
|
|
let clip = this.clips.get(name);
|
|
if (clip) return clip;
|
|
|
|
// Try to find clip using name mapper
|
|
try {
|
|
const allNames = this.nameMapper.getAllNames(name);
|
|
|
|
// Try each possible name variant
|
|
for (const variant of Object.values(allNames)) {
|
|
clip = this.clips.get(variant);
|
|
if (clip) return clip;
|
|
}
|
|
} catch (error) {
|
|
// If name mapping fails, continue with original lookup
|
|
console.debug(`Name mapping failed for "${name}":`, error.message);
|
|
}
|
|
|
|
return undefined;
|
|
}
|
|
|
|
// New multi-scheme methods
|
|
getClipByScheme(name, targetScheme) {
|
|
try {
|
|
if (targetScheme) {
|
|
const convertedName = this.nameMapper.convert(name, targetScheme);
|
|
return this.clips.get(convertedName);
|
|
} else {
|
|
return this.getClip(name);
|
|
}
|
|
} catch (error) {
|
|
console.debug(
|
|
`Scheme conversion failed for "${name}" to "${targetScheme}":`,
|
|
error.message,
|
|
);
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
getAnimationNames(name) {
|
|
try {
|
|
return this.nameMapper.getAllNames(name);
|
|
} catch (error) {
|
|
console.warn(
|
|
`Could not get animation name variants for "${name}":`,
|
|
error.message,
|
|
);
|
|
return {
|
|
legacy: name,
|
|
artist: name,
|
|
hierarchical: name,
|
|
semantic: name,
|
|
};
|
|
}
|
|
}
|
|
|
|
validateAnimationName(name) {
|
|
try {
|
|
return this.nameMapper.validateAnimationName(name);
|
|
} catch (error) {
|
|
return {
|
|
isValid: false,
|
|
scheme: "unknown",
|
|
error: error.message,
|
|
suggestions: [],
|
|
};
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
return matches;
|
|
}
|
|
|
|
getCurrentState() {
|
|
return this.currentState;
|
|
}
|
|
|
|
getCurrentStateHandler() {
|
|
return this.currentStateHandler;
|
|
}
|
|
|
|
getAvailableClips() {
|
|
return Array.from(this.clips.keys());
|
|
}
|
|
|
|
getAvailableStates() {
|
|
return Array.from(this.states.keys());
|
|
}
|
|
|
|
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
|
|
document.getElementById("currentState").textContent =
|
|
this.currentState.toUpperCase();
|
|
document.getElementById("currentEmotion").textContent = emotionName;
|
|
document.getElementById("lastActivity").textContent = new Date(
|
|
this.lastActivity,
|
|
).toLocaleTimeString();
|
|
document.getElementById("activeClips").textContent =
|
|
this.activeAnimations.size;
|
|
|
|
// Update Owen character
|
|
const avatar = document.getElementById("owenAvatar");
|
|
const stateEl = document.getElementById("owenState");
|
|
const emotionEl = document.getElementById("owenEmotion");
|
|
|
|
// Get the appropriate SVG for current state and emotion
|
|
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();
|
|
emotionEl.textContent = emotionName;
|
|
|
|
// Add animation effect
|
|
avatar.style.transform = "scale(1.1)";
|
|
setTimeout(() => {
|
|
avatar.style.transform = "scale(1)";
|
|
}, 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() {
|
|
const listEl = document.getElementById("animationList");
|
|
listEl.innerHTML = "";
|
|
|
|
if (this.activeAnimations.size === 0) {
|
|
listEl.innerHTML =
|
|
'<div class="animation-item">No active animations</div>';
|
|
} else {
|
|
this.activeAnimations.forEach((clipName) => {
|
|
const item = document.createElement("div");
|
|
item.className = "animation-item animation-playing";
|
|
item.textContent = clipName;
|
|
listEl.appendChild(item);
|
|
});
|
|
}
|
|
}
|
|
|
|
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
|
|
let owenSystem;
|
|
|
|
// Initialize the system
|
|
async function initializeOwen() {
|
|
owenSystem = new MockOwenAnimationContext();
|
|
await owenSystem.initialize();
|
|
|
|
// Add mouse activity listener
|
|
document.addEventListener("mousemove", () => {
|
|
owenSystem.onUserActivity();
|
|
});
|
|
|
|
// Add keyboard activity listener
|
|
document.addEventListener("keydown", (e) => {
|
|
owenSystem.onUserActivity();
|
|
|
|
// Send message on Enter
|
|
if (
|
|
e.key === "Enter" &&
|
|
document.activeElement.id === "messageInput"
|
|
) {
|
|
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
|
|
function sendMessage() {
|
|
const input = document.getElementById("messageInput");
|
|
const message = input.value.trim();
|
|
|
|
if (message) {
|
|
owenSystem.handleUserMessage(message);
|
|
input.value = "";
|
|
}
|
|
}
|
|
|
|
function transitionTo(state) {
|
|
owenSystem.transitionTo(state);
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
function quickMessage(message) {
|
|
document.getElementById("messageInput").value = message;
|
|
sendMessage();
|
|
}
|
|
|
|
function simulateActivity() {
|
|
owenSystem.onUserActivity();
|
|
}
|
|
|
|
function simulateInactivity() {
|
|
owenSystem.transitionTo(States.SLEEPING);
|
|
}
|
|
|
|
function resetSystem() {
|
|
owenSystem.transitionTo(States.WAITING, Emotions.NEUTRAL);
|
|
}
|
|
|
|
// Multi-scheme animation testing functions
|
|
function testAnimationConversion() {
|
|
const input = document.getElementById("animationNameInput");
|
|
const select = document.getElementById("targetSchemeSelect");
|
|
const resultsDiv = document.getElementById("nameConversionResults");
|
|
|
|
const animationName = input.value.trim();
|
|
const targetScheme = select.value;
|
|
|
|
if (!animationName) {
|
|
resultsDiv.innerHTML =
|
|
'<div style="color: #dc3545;">Please enter an animation name</div>';
|
|
return;
|
|
}
|
|
|
|
try {
|
|
// Get all name variants
|
|
const allNames = owenSystem.nameMapper.getAllNames(animationName);
|
|
|
|
// Try conversion to target scheme
|
|
const convertedName = owenSystem.nameMapper.convert(
|
|
animationName,
|
|
targetScheme,
|
|
);
|
|
|
|
// Display results
|
|
resultsDiv.innerHTML = `
|
|
<div><strong>Input:</strong> "${animationName}"</div>
|
|
<div><strong>Converted to ${targetScheme}:</strong> "${convertedName}"</div>
|
|
<hr style="margin: 10px 0; border: 1px solid #dee2e6;">
|
|
<div><strong>All Scheme Variants:</strong></div>
|
|
<div><strong>Legacy:</strong> ${allNames.legacy}</div>
|
|
<div><strong>Artist:</strong> ${allNames.artist}</div>
|
|
<div><strong>Hierarchical:</strong> ${allNames.hierarchical}</div>
|
|
<div><strong>Semantic:</strong> ${allNames.semantic}</div>
|
|
`;
|
|
} catch (error) {
|
|
resultsDiv.innerHTML = `<div style="color: #dc3545;"><strong>Error:</strong> ${error.message}</div>`;
|
|
}
|
|
}
|
|
|
|
function validateAnimationName() {
|
|
const input = document.getElementById("animationNameInput");
|
|
const resultsDiv = document.getElementById("nameConversionResults");
|
|
|
|
const animationName = input.value.trim();
|
|
|
|
if (!animationName) {
|
|
resultsDiv.innerHTML =
|
|
'<div style="color: #dc3545;">Please enter an animation name</div>';
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const validation =
|
|
owenSystem.nameMapper.validateAnimationName(animationName);
|
|
|
|
let html = `<div><strong>Validation for:</strong> "${animationName}"</div>`;
|
|
|
|
if (validation.isValid) {
|
|
html += `
|
|
<div style="color: #28a745;"><strong>✓ Valid</strong></div>
|
|
<div><strong>Detected Scheme:</strong> ${validation.scheme}</div>
|
|
`;
|
|
} else {
|
|
html += `
|
|
<div style="color: #dc3545;"><strong>✗ Invalid</strong></div>
|
|
<div><strong>Error:</strong> ${validation.error}</div>
|
|
`;
|
|
|
|
if (validation.suggestions && validation.suggestions.length > 0) {
|
|
html += `
|
|
<div><strong>Suggestions:</strong></div>
|
|
<ul style="margin: 5px 0; padding-left: 20px;">
|
|
${validation.suggestions.map((s) => `<li>${s}</li>`).join("")}
|
|
</ul>
|
|
`;
|
|
}
|
|
}
|
|
|
|
resultsDiv.innerHTML = html;
|
|
} catch (error) {
|
|
resultsDiv.innerHTML = `<div style="color: #dc3545;"><strong>Error:</strong> ${error.message}</div>`;
|
|
}
|
|
}
|
|
|
|
function testQuickAnimation(animationName) {
|
|
document.getElementById("animationNameInput").value = animationName;
|
|
testAnimationConversion();
|
|
}
|
|
|
|
// Start the demo when page loads
|
|
window.addEventListener("load", initializeOwen);
|
|
|
|
// Update UI every second
|
|
setInterval(() => {
|
|
if (owenSystem) {
|
|
const timeSinceActivity = Math.floor(
|
|
(Date.now() - owenSystem.lastActivity) / 1000,
|
|
);
|
|
document.getElementById("lastActivity").textContent =
|
|
timeSinceActivity + "s ago";
|
|
}
|
|
}, 1000);
|
|
</script>
|
|
</body>
|
|
</html>
|