Files
Owen/examples/mock-demo/owen_test_demo.html
Kaj Kowalski ad8dbb95dd
Some checks failed
CI/CD Pipeline / Test & Lint (16.x) (push) Has been cancelled
CI/CD Pipeline / Test & Lint (18.x) (push) Has been cancelled
CI/CD Pipeline / Test & Lint (20.x) (push) Has been cancelled
CI/CD Pipeline / Security Audit (push) Has been cancelled
CI/CD Pipeline / Release (push) Has been cancelled
Release / Validate Version (push) Has been cancelled
Release / Build and Test (push) Has been cancelled
Release / Create Release (push) Has been cancelled
Release / Publish to NPM (push) Has been cancelled
Release / Deploy Demo (push) Has been cancelled
Animation Processing Pipeline / Validate Animation Names (push) Has been cancelled
Animation Processing Pipeline / Process Blender Animation Assets (push) Has been cancelled
Animation Processing Pipeline / Update Animation Documentation (push) Has been cancelled
Animation Processing Pipeline / Deploy Animation Demo (push) Has been cancelled
Implement multi-scheme animation name mapper for Owen Animation System
- 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.
2025-05-24 05:40:03 +02:00

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>