telepresence and animation menus
This commit is contained in:
7
package-lock.json
generated
7
package-lock.json
generated
@@ -10,6 +10,7 @@
|
||||
"dependencies": {
|
||||
"dotenv": "^17.4.2",
|
||||
"express": "^4.18.2",
|
||||
"golden-layout": "^2.6.0",
|
||||
"ws": "^8.14.2"
|
||||
}
|
||||
},
|
||||
@@ -371,6 +372,12 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/golden-layout": {
|
||||
"version": "2.6.0",
|
||||
"resolved": "https://registry.npmjs.org/golden-layout/-/golden-layout-2.6.0.tgz",
|
||||
"integrity": "sha512-sIVQCiRWOymHbVD1Aw/T9/ijbPYAVGBlgGYd1N9MRKfcyBNSpjr87Vg9nSHm+RCT8ELrvK8IJYJV0QRJuVUkCQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/gopd": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"dependencies": {
|
||||
"dotenv": "^17.4.2",
|
||||
"express": "^4.18.2",
|
||||
"golden-layout": "^2.6.0",
|
||||
"ws": "^8.14.2"
|
||||
}
|
||||
}
|
||||
|
||||
554
public/animator-backup.html
Normal file
554
public/animator-backup.html
Normal file
@@ -0,0 +1,554 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Re-Commander — Animator</title>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
<style>
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
/* ── Header ── */
|
||||
header {
|
||||
flex-shrink: 0;
|
||||
background: var(--surface);
|
||||
border-bottom: 1px solid var(--border);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.header-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8px 16px;
|
||||
gap: 12px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
.header-bar h1 {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.header-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-shrink: 0;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.header-status span {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* ── Toolbar ── */
|
||||
.toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 16px;
|
||||
flex-wrap: nowrap;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.toolbar-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.toolbar-group button {
|
||||
padding: 6px 12px;
|
||||
font-size: 11px;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
background: var(--surface2);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
color: var(--text);
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.toolbar-group button:hover {
|
||||
background: var(--accent);
|
||||
color: #000;
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
/* Top toolbar buttons - File, Edit, Playback, etc */
|
||||
.toolbar-group:nth-child(1) button,
|
||||
.toolbar-group:nth-child(2) button,
|
||||
.toolbar-group:nth-child(3) button,
|
||||
.toolbar-group:nth-child(5) button {
|
||||
padding: 10px 16px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
border: 2px solid var(--accent);
|
||||
background: linear-gradient(135deg, var(--accent)20, var(--accent)5);
|
||||
color: var(--accent);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.toolbar-group:nth-child(1) button:hover,
|
||||
.toolbar-group:nth-child(2) button:hover,
|
||||
.toolbar-group:nth-child(3) button:hover,
|
||||
.toolbar-group:nth-child(5) button:hover {
|
||||
background: var(--accent);
|
||||
color: #000;
|
||||
border-color: var(--accent);
|
||||
transform: scale(1.05);
|
||||
box-shadow: 0 2px 8px rgba(255,255,0,0.3);
|
||||
}
|
||||
|
||||
.toolbar-sep {
|
||||
width: 1px;
|
||||
height: 18px;
|
||||
background: var(--border);
|
||||
margin: 0 4px;
|
||||
}
|
||||
|
||||
.toolbar-spacer {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* ── Main Container ── */
|
||||
.main-container {
|
||||
flex: 1;
|
||||
display: grid;
|
||||
grid-template-columns: 250px 1fr 300px;
|
||||
gap: 1px;
|
||||
background: var(--border);
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.panel {
|
||||
background: var(--bg);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* ── Left Sidebar ── */
|
||||
.left-panel {
|
||||
background: var(--surface);
|
||||
border-right: 1px solid var(--border);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
padding: 12px;
|
||||
background: var(--surface2);
|
||||
border-bottom: 1px solid var(--border);
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.panel-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.library-item {
|
||||
padding: 10px;
|
||||
margin-bottom: 6px;
|
||||
background: var(--surface2);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.library-item:hover {
|
||||
background: var(--accent);
|
||||
color: #000;
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.library-item.selected {
|
||||
background: var(--accent);
|
||||
color: #000;
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
/* ── Center Area ── */
|
||||
.center-panel {
|
||||
background: #000;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-right: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.center-tabs {
|
||||
display: flex;
|
||||
background: var(--surface);
|
||||
border-bottom: 1px solid var(--border);
|
||||
flex-shrink: 0;
|
||||
overflow-x: auto;
|
||||
gap: 2px;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.center-tab {
|
||||
padding: 10px 16px;
|
||||
background: var(--surface2);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.center-tab.active {
|
||||
background: var(--accent);
|
||||
color: #000;
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.center-tab:hover {
|
||||
background: var(--accent);
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.center-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--muted);
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
/* ── Right Sidebar ── */
|
||||
.right-panel {
|
||||
background: var(--surface);
|
||||
border-left: 1px solid var(--border);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.right-tabs {
|
||||
display: flex;
|
||||
border-bottom: 1px solid var(--border);
|
||||
background: var(--surface2);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.right-tab {
|
||||
flex: 1;
|
||||
padding: 10px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-bottom: 2px solid transparent;
|
||||
color: var(--muted);
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.right-tab.active {
|
||||
color: var(--accent);
|
||||
border-bottom-color: var(--accent);
|
||||
}
|
||||
|
||||
.right-tab:hover {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.right-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 12px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.right-content.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.inspector-field {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.inspector-field label {
|
||||
display: block;
|
||||
font-size: 11px;
|
||||
color: var(--muted);
|
||||
margin-bottom: 4px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.inspector-field input,
|
||||
.inspector-field select,
|
||||
.inspector-field textarea {
|
||||
width: 100%;
|
||||
padding: 6px;
|
||||
background: var(--surface2);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
color: var(--text);
|
||||
font-size: 11px;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.inspector-field input:focus,
|
||||
.inspector-field select:focus,
|
||||
.inspector-field textarea:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.log-entry {
|
||||
padding: 6px;
|
||||
margin-bottom: 4px;
|
||||
background: var(--surface2);
|
||||
border-left: 2px solid var(--accent);
|
||||
font-size: 11px;
|
||||
font-family: monospace;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
/* ── Bottom Status Bar ── */
|
||||
.status-bar {
|
||||
flex-shrink: 0;
|
||||
background: var(--surface2);
|
||||
border-top: 1px solid var(--border);
|
||||
padding: 8px 16px;
|
||||
font-size: 11px;
|
||||
color: var(--muted);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 24px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.status-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.status-value {
|
||||
color: var(--text);
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- ── Header ── -->
|
||||
<header>
|
||||
<div class="header-bar">
|
||||
<div id="status-dot"></div>
|
||||
<h1>Re<span>-Commander</span> — Animator</h1>
|
||||
<div class="header-status">
|
||||
<span id="status-label">Connecting…</span>
|
||||
<span id="hotword-indicator"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="toolbar">
|
||||
<!-- File Operations -->
|
||||
<div class="toolbar-group">
|
||||
<button id="btn-new">📄 New</button>
|
||||
<button id="btn-open">📂 Open</button>
|
||||
<button id="btn-save">💾 Save</button>
|
||||
</div>
|
||||
|
||||
<div class="toolbar-sep"></div>
|
||||
|
||||
<!-- Edit -->
|
||||
<div class="toolbar-group">
|
||||
<button id="btn-undo">↶ Undo</button>
|
||||
<button id="btn-redo">↷ Redo</button>
|
||||
</div>
|
||||
|
||||
<div class="toolbar-sep"></div>
|
||||
|
||||
<!-- Animation Control -->
|
||||
<div class="toolbar-group">
|
||||
<button id="btn-play" style="background:var(--success);border-color:var(--success);">Play</button>
|
||||
<button id="btn-pause">Pause</button>
|
||||
<button id="btn-stop" class="danger">Stop</button>
|
||||
</div>
|
||||
|
||||
<div class="toolbar-sep"></div>
|
||||
|
||||
<!-- Timeline -->
|
||||
<div class="toolbar-group">
|
||||
<button id="btn-timeline">Timeline</button>
|
||||
<button id="btn-preview">Preview</button>
|
||||
</div>
|
||||
|
||||
<div class="toolbar-spacer"></div>
|
||||
|
||||
<!-- Right Side -->
|
||||
<div class="toolbar-group">
|
||||
<button id="btn-settings">Settings</button>
|
||||
<button id="btn-back-home">← Home</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- ── Main Content ── -->
|
||||
<div class="main-container">
|
||||
|
||||
<!-- Left Sidebar - Library -->
|
||||
<div class="left-panel">
|
||||
<div class="panel-header">Animation Library</div>
|
||||
<div class="panel-content">
|
||||
<div class="library-item">Eye_Blink_01</div>
|
||||
<div class="library-item">Eye_Blink_02</div>
|
||||
<div class="library-item">Eye_Double_Blink</div>
|
||||
<div class="library-item">eye_happy_01</div>
|
||||
<div class="library-item">eye_happy_02</div>
|
||||
<div class="library-item">eye_sad_01</div>
|
||||
<div class="library-item">eye_scared_00</div>
|
||||
<div class="library-item">Eye_Curious_01</div>
|
||||
<div class="library-item">Eye_Look_Center</div>
|
||||
<div class="library-item">Eye_Look_Left</div>
|
||||
<div class="library-item">Eye_Look_Right</div>
|
||||
<div class="library-item">Eye_Look_Up</div>
|
||||
<div class="library-item">Eye_Look_Down</div>
|
||||
<div class="library-item">Gesture_Wave</div>
|
||||
<div class="library-item">Gesture_Nod</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Center - Canvas/Preview Area -->
|
||||
<div class="center-panel">
|
||||
<div class="center-tabs">
|
||||
<div class="center-tab active" data-tab="canvas">Canvas</div>
|
||||
<div class="center-tab" data-tab="preview">🎬 Preview</div>
|
||||
</div>
|
||||
<div class="center-content">
|
||||
Imagine a 3d jibo model in a theater kinda thing
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right Sidebar - Properties & Log -->
|
||||
<div class="right-panel">
|
||||
<div class="right-tabs">
|
||||
<button class="right-tab active" data-panel="inspector">Inspector</button>
|
||||
<button class="right-tab" data-panel="properties">Properties</button>
|
||||
<button class="right-tab" data-panel="log">Log</button>
|
||||
</div>
|
||||
|
||||
<!-- Inspector Panel -->
|
||||
<div class="right-content active" id="inspector-panel">
|
||||
<div class="inspector-field">
|
||||
<label>Animation Name</label>
|
||||
<input type="text" placeholder="Select animation...">
|
||||
</div>
|
||||
<div class="inspector-field">
|
||||
<label>Duration (ms)</label>
|
||||
<input type="number" value="500" min="0">
|
||||
</div>
|
||||
<div class="inspector-field">
|
||||
<label>Repeat Count</label>
|
||||
<input type="number" value="1" min="1">
|
||||
</div>
|
||||
<div class="inspector-field">
|
||||
<label>Delay (ms)</label>
|
||||
<input type="number" value="0" min="0" step="100">
|
||||
</div>
|
||||
<div class="inspector-field">
|
||||
<label>Easing</label>
|
||||
<select>
|
||||
<option>Linear</option>
|
||||
<option>Ease In</option>
|
||||
<option>Ease Out</option>
|
||||
<option>Ease In Out</option>
|
||||
</select>
|
||||
</div>
|
||||
<button style="width:100%; padding:10px; margin-top:12px; background:var(--accent); color:#000; border:none; border-radius:4px; font-weight:600; cursor:pointer;">✨ Apply Animation</button>
|
||||
</div>
|
||||
|
||||
<!-- Properties Panel -->
|
||||
<div class="right-content" id="properties-panel">
|
||||
<div class="inspector-field">
|
||||
<label>Preview Mode</label>
|
||||
<select>
|
||||
<option>Normal</option>
|
||||
<option>Loop</option>
|
||||
<option>Once</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="inspector-field">
|
||||
<label>Resolution</label>
|
||||
<select>
|
||||
<option>High</option>
|
||||
<option>Medium</option>
|
||||
<option>Low</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="inspector-field">
|
||||
<label>Camera Angle</label>
|
||||
<input type="range" min="0" max="360" value="0">
|
||||
</div>
|
||||
<div class="inspector-field">
|
||||
<label>Playback Speed</label>
|
||||
<input type="range" min="0.5" max="2" step="0.1" value="1">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Log Panel -->
|
||||
<div class="right-content" id="log-panel">
|
||||
<div id="event-log" style="font-size:11px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- ── Status Bar ── -->
|
||||
<div class="status-bar">
|
||||
<div class="status-item">
|
||||
<span>Mode:</span>
|
||||
<span class="status-value">Virtual Robot</span>
|
||||
</div>
|
||||
<div class="status-item">
|
||||
<span>Animation:</span>
|
||||
<span class="status-value" id="status-animation">None</span>
|
||||
</div>
|
||||
<div class="status-item">
|
||||
<span>Status:</span>
|
||||
<span class="status-value" id="status-playback">Ready</span>
|
||||
</div>
|
||||
<div class="status-item" style="margin-left:auto;">
|
||||
<span id="status-info">Ready for animation</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="animator.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -3,143 +3,414 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Re-Commander — Animator Mode</title>
|
||||
<title>Re-Commander — Animator</title>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
<link rel="stylesheet" href="https://golden-layout.com/assets/css/goldenlayout-base.css">
|
||||
<link rel="stylesheet" href="https://golden-layout.com/assets/css/goldenlayout-dark-theme.css">
|
||||
<style>
|
||||
* { box-sizing: border-box; }
|
||||
|
||||
html, body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
font-family: system-ui, -apple-system, sans-serif;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
#app-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
/* ── Header ── */
|
||||
header {
|
||||
flex-shrink: 0;
|
||||
background: var(--surface);
|
||||
border-bottom: 1px solid var(--border);
|
||||
padding: 8px 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
#status-dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
background: var(--muted);
|
||||
transition: background 0.3s;
|
||||
}
|
||||
|
||||
#status-dot.ok {
|
||||
background: var(--success);
|
||||
box-shadow: 0 0 6px var(--success);
|
||||
}
|
||||
|
||||
header h1 {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
#status-label {
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
/* ── Toolbar ── */
|
||||
.toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
padding: 8px 16px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
background: var(--surface);
|
||||
}
|
||||
|
||||
.toolbar-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.toolbar-group button {
|
||||
padding: 6px 12px;
|
||||
font-size: 11px;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
background: var(--surface2);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
color: var(--text);
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.toolbar-group button:hover {
|
||||
background: var(--accent);
|
||||
color: #000;
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
/* Top toolbar buttons - File, Edit, Playback, etc */
|
||||
.toolbar-group:nth-child(1) button,
|
||||
.toolbar-group:nth-child(2) button,
|
||||
.toolbar-group:nth-child(3) button,
|
||||
.toolbar-group:nth-child(4) button,
|
||||
.toolbar-group:nth-child(6) button {
|
||||
padding: 10px 16px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
border: 2px solid var(--accent);
|
||||
background: linear-gradient(135deg, var(--accent)20, var(--accent)5);
|
||||
color: var(--accent);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.toolbar-group:nth-child(1) button:hover,
|
||||
.toolbar-group:nth-child(2) button:hover,
|
||||
.toolbar-group:nth-child(3) button:hover,
|
||||
.toolbar-group:nth-child(4) button:hover,
|
||||
.toolbar-group:nth-child(6) button:hover {
|
||||
background: var(--accent);
|
||||
color: #000;
|
||||
border-color: var(--accent);
|
||||
transform: scale(1.05);
|
||||
box-shadow: 0 2px 8px rgba(255,255,0,0.3);
|
||||
}
|
||||
|
||||
.toolbar-sep {
|
||||
width: 1px;
|
||||
height: 20px;
|
||||
background: var(--border);
|
||||
margin: 0 4px;
|
||||
}
|
||||
|
||||
.toolbar-spacer {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
/* ── GoldenLayout Customization ── */
|
||||
#layout-root {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.gl_container {
|
||||
background: var(--surface) !important;
|
||||
}
|
||||
|
||||
.gl_tab {
|
||||
background: var(--surface2) !important;
|
||||
color: var(--text) !important;
|
||||
border-bottom: 2px solid var(--border) !important;
|
||||
padding: 8px 12px !important;
|
||||
font-size: 12px !important;
|
||||
}
|
||||
|
||||
.gl_tab.gl_active {
|
||||
background: var(--accent) !important;
|
||||
color: #000 !important;
|
||||
border-bottom: 2px solid var(--accent) !important;
|
||||
}
|
||||
|
||||
.gl_tab:hover {
|
||||
background: var(--surface) !important;
|
||||
}
|
||||
|
||||
.gl_tabBar {
|
||||
background: var(--surface2) !important;
|
||||
border-bottom: 1px solid var(--border) !important;
|
||||
}
|
||||
|
||||
.gl_header {
|
||||
background: var(--surface) !important;
|
||||
border-bottom: 1px solid var(--border) !important;
|
||||
padding: 6px !important;
|
||||
}
|
||||
|
||||
.gl_splitter {
|
||||
background: var(--border) !important;
|
||||
}
|
||||
|
||||
/* ── Panel Content ── */
|
||||
.component-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.component-content > div:first-child {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.library-item {
|
||||
padding: 8px 12px;
|
||||
margin: 4px 0;
|
||||
background: var(--surface2);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.library-item:hover {
|
||||
background: var(--accent);
|
||||
color: #000;
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.library-item.selected {
|
||||
background: var(--accent);
|
||||
color: #000;
|
||||
font-weight: 600;
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.inspector-field {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.inspector-field label {
|
||||
display: block;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 4px;
|
||||
color: var(--muted);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.inspector-field input,
|
||||
.inspector-field select,
|
||||
.inspector-field textarea {
|
||||
width: 100%;
|
||||
padding: 6px 8px;
|
||||
background: var(--surface2);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 3px;
|
||||
color: var(--text);
|
||||
font-size: 11px;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.inspector-field input:focus,
|
||||
.inspector-field select:focus,
|
||||
.inspector-field textarea:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent);
|
||||
box-shadow: 0 0 4px rgba(255, 255, 0, 0.3);
|
||||
}
|
||||
|
||||
.inspector-field button {
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
margin-top: 8px;
|
||||
background: var(--accent);
|
||||
color: #000;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.inspector-field button:hover {
|
||||
transform: scale(1.02);
|
||||
box-shadow: 0 2px 6px rgba(255,255,0,0.4);
|
||||
}
|
||||
|
||||
.canvas-placeholder {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
font-size: 48px;
|
||||
color: var(--muted);
|
||||
background: #000;
|
||||
}
|
||||
|
||||
#event-log {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
font-family: monospace;
|
||||
font-size: 11px;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.log-entry {
|
||||
padding: 2px 0;
|
||||
border-bottom: 1px solid var(--border);
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.log-entry .ts { color: var(--muted); }
|
||||
.log-entry .evt { color: var(--accent); }
|
||||
.log-entry .evt-error { color: var(--danger); }
|
||||
.log-entry .evt-motion { color: var(--warn); }
|
||||
.log-entry .evt-entity { color: var(--success); }
|
||||
.log-entry .evt-success { color: var(--success); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app-container">
|
||||
<!-- Header -->
|
||||
<header>
|
||||
<div id="status-dot"></div>
|
||||
<h1>Re-Commander — Animator</h1>
|
||||
<span id="status-label">Connecting…</span>
|
||||
</header>
|
||||
|
||||
<header>
|
||||
<div id="status-dot"></div>
|
||||
<h1>Re<span>-Commander</span> — Animator</h1>
|
||||
<span id="status-label">Connecting…</span>
|
||||
<span id="hotword-indicator"></span>
|
||||
<button id="btn-back-home" style="margin-left: auto; padding: 5px 12px; font-size: 12px;">← Back to Home</button>
|
||||
</header>
|
||||
|
||||
<div class="main">
|
||||
|
||||
<!-- ── LEFT PANEL ── -->
|
||||
<div class="left-panel">
|
||||
|
||||
<!-- Animation Presets -->
|
||||
<div class="section">
|
||||
<div class="section-title">Animation Library</div>
|
||||
<div class="field">
|
||||
<label>Select Animation</label>
|
||||
<select id="anim-select">
|
||||
<optgroup label="── Blinks">
|
||||
<option>Eye_Blink_01</option>
|
||||
<option>Eye_Blink_02</option>
|
||||
<option>Eye_Double_Blink_01</option>
|
||||
</optgroup>
|
||||
<optgroup label="── Expressions">
|
||||
<option>eye_happy_00</option>
|
||||
<option>eye_happy_01</option>
|
||||
<option>eye_sad_01</option>
|
||||
<option>eye_scared_00</option>
|
||||
<option>Eye_Curious_01</option>
|
||||
</optgroup>
|
||||
<optgroup label="── Poses">
|
||||
<option>Eye_Look_Center_Middle_01</option>
|
||||
<option>Eye_Look_Left_Middle_01</option>
|
||||
<option>Eye_Look_Right_Middle_01</option>
|
||||
<option>Eye_Look_Center_Up_01</option>
|
||||
<option>Eye_Look_Center_Down_01</option>
|
||||
</optgroup>
|
||||
</select>
|
||||
</div>
|
||||
<button id="btn-play-anim" style="width:100%;">▶ Play Animation</button>
|
||||
<!-- Toolbar -->
|
||||
<div class="toolbar">
|
||||
<div class="toolbar-group">
|
||||
<button id="btn-new">📄 New</button>
|
||||
<button id="btn-open">📂 Open</button>
|
||||
<button id="btn-save">💾 Save</button>
|
||||
</div>
|
||||
|
||||
<!-- Animation Sequence -->
|
||||
<div class="section">
|
||||
<div class="section-title">Animation Sequence</div>
|
||||
<div class="field">
|
||||
<textarea id="sequence-list" rows="6" placeholder="Animation1 Animation2 Animation3" style="font-family: monospace; font-size: 12px;"></textarea>
|
||||
</div>
|
||||
<div class="controls-row">
|
||||
<button id="btn-play-sequence" style="flex:1;">▶ Play Sequence</button>
|
||||
<button id="btn-stop-sequence" class="danger" style="flex:1;">■ Stop</button>
|
||||
</div>
|
||||
<div class="toolbar-sep"></div>
|
||||
|
||||
<div class="toolbar-group">
|
||||
<button id="btn-undo">↶ Undo</button>
|
||||
<button id="btn-redo">↷ Redo</button>
|
||||
</div>
|
||||
|
||||
<!-- Timeline -->
|
||||
<div class="section">
|
||||
<div class="section-title">Timeline</div>
|
||||
<div class="row">
|
||||
<div class="toolbar-sep"></div>
|
||||
|
||||
<div class="toolbar-group">
|
||||
<button id="btn-play" style="background:var(--success);border-color:var(--success);color:#000;">▶ Play</button>
|
||||
<button id="btn-pause">⏸ Pause</button>
|
||||
<button id="btn-stop" class="danger">⏹ Stop</button>
|
||||
</div>
|
||||
|
||||
<div class="toolbar-sep"></div>
|
||||
|
||||
<div class="toolbar-group">
|
||||
<button id="btn-timeline">📊 Timeline</button>
|
||||
<button id="btn-preview">👁 Preview</button>
|
||||
</div>
|
||||
|
||||
<div class="toolbar-spacer"></div>
|
||||
|
||||
<div class="toolbar-group">
|
||||
<button id="btn-settings">⚙️ Settings</button>
|
||||
<button id="btn-back-home">← Home</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- GoldenLayout Container -->
|
||||
<div id="layout-root"></div>
|
||||
</div>
|
||||
|
||||
<!-- Library Component Template -->
|
||||
<template id="tpl-library">
|
||||
<div class="component-content">
|
||||
<div id="library-list"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Canvas Component Template -->
|
||||
<template id="tpl-canvas">
|
||||
<div class="component-content">
|
||||
<div class="canvas-placeholder">🤖</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Inspector Component Template -->
|
||||
<template id="tpl-inspector">
|
||||
<div class="component-content">
|
||||
<div>
|
||||
<div class="inspector-field">
|
||||
<label>Animation Name</label>
|
||||
<input type="text" placeholder="Select animation...">
|
||||
</div>
|
||||
<div class="inspector-field">
|
||||
<label>Duration (ms)</label>
|
||||
<input type="number" value="500" min="0">
|
||||
</div>
|
||||
<div class="inspector-field">
|
||||
<label>Repeat Count</label>
|
||||
<input type="number" id="repeat-count" value="1" min="1">
|
||||
<input type="number" value="1" min="1">
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="inspector-field">
|
||||
<label>Delay (ms)</label>
|
||||
<input type="number" id="anim-delay" value="500" min="0" step="100">
|
||||
<input type="number" value="0" min="0" step="100">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div><!-- /left-panel -->
|
||||
|
||||
<!-- ── CENTER PANEL ── -->
|
||||
<div class="center-panel">
|
||||
|
||||
<!-- Camera feed -->
|
||||
<div id="camera-wrap">
|
||||
<img id="camera-feed" alt="Jibo camera" style="display:none;">
|
||||
<div id="camera-no-feed">
|
||||
<div style="font-size:40px;">📹</div>
|
||||
<div>Animation Preview</div>
|
||||
<div style="font-size:11px;color:var(--muted);">Start video to see animations being played</div>
|
||||
</div>
|
||||
<div id="click-dot"></div>
|
||||
</div>
|
||||
|
||||
<!-- Playback Controls -->
|
||||
<div class="controls-row" style="justify-content:center; gap:10px; margin-top:20px;">
|
||||
<button id="btn-video-start" style="flex-basis: auto;">▶ Start Video</button>
|
||||
<button id="btn-video-stop" class="danger" style="flex-basis: auto;">■ Stop</button>
|
||||
</div>
|
||||
|
||||
</div><!-- /center-panel -->
|
||||
|
||||
<!-- ── RIGHT PANEL ── -->
|
||||
<div class="right-panel">
|
||||
<div class="tabs">
|
||||
<button class="tab-btn active" data-tab="tab-camera">Camera</button>
|
||||
<button class="tab-btn" data-tab="tab-log">Log</button>
|
||||
</div>
|
||||
|
||||
<!-- Camera tab -->
|
||||
<div class="tab-panel active" id="tab-camera">
|
||||
<div class="section-title">Video Stream</div>
|
||||
<div class="row">
|
||||
<label>Resolution</label>
|
||||
<select id="photo-res">
|
||||
<option value="highRes">HighRes</option>
|
||||
<option value="medRes">MedRes</option>
|
||||
<option value="lowRes">LowRes</option>
|
||||
<div class="inspector-field">
|
||||
<label>Easing</label>
|
||||
<select>
|
||||
<option>Linear</option>
|
||||
<option>Ease In</option>
|
||||
<option>Ease Out</option>
|
||||
<option>Ease In Out</option>
|
||||
</select>
|
||||
</div>
|
||||
<button id="btn-photo" style="width:100%;margin-bottom:10px;">📷 Take Photo</button>
|
||||
<div id="photo-strip"></div>
|
||||
</div>
|
||||
|
||||
<!-- Log tab -->
|
||||
<div class="tab-panel" id="tab-log" style="padding:0;display:flex;flex-direction:column;flex:1;">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;padding:8px 10px;border-bottom:1px solid var(--border);">
|
||||
<span class="section-title" style="margin:0;">Animation Log</span>
|
||||
<button id="btn-clear-log" style="font-size:11px;padding:3px 8px;">Clear</button>
|
||||
<div class="inspector-field">
|
||||
<button>✨ Apply Animation</button>
|
||||
</div>
|
||||
<div id="event-log"></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
</div><!-- /right-panel -->
|
||||
</div>
|
||||
<!-- Log Component Template -->
|
||||
<template id="tpl-log">
|
||||
<div class="component-content">
|
||||
<div id="event-log"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Photo modal -->
|
||||
<div id="photo-modal">
|
||||
<div id="photo-modal-close">✕</div>
|
||||
<img id="photo-modal-img" src="" alt="Photo">
|
||||
</div>
|
||||
|
||||
<script src="animator.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/golden-layout@2/dist/umd/index.js" defer></script>
|
||||
<script src="animator.js" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -48,6 +48,11 @@ function handleServerMessage(msg) {
|
||||
function handleJiboEvent(body, txId) {
|
||||
if (!body) return;
|
||||
const evt = body.Event || body.ResponseString || '?';
|
||||
|
||||
if (evt === 'onVideoReady' && body.URI) {
|
||||
startVideoFeed(body.URI);
|
||||
}
|
||||
|
||||
logEvent(evt, body, txId);
|
||||
}
|
||||
|
||||
@@ -72,149 +77,247 @@ const get = (path) => api('GET', path);
|
||||
function setStatus(ok, label) {
|
||||
const dot = document.getElementById('status-dot');
|
||||
const lbl = document.getElementById('status-label');
|
||||
dot.className = ok ? 'ok' : '';
|
||||
lbl.textContent = label;
|
||||
if (dot) dot.className = ok ? 'ok' : '';
|
||||
if (lbl) lbl.textContent = label;
|
||||
}
|
||||
|
||||
// ── Animation Control ────────────────────────────────────────────────────────
|
||||
// ── Event Log ────────────────────────────────────────────────────────────────
|
||||
|
||||
document.getElementById('btn-play-anim').addEventListener('click', () => {
|
||||
const name = document.getElementById('anim-select').value;
|
||||
if (name) {
|
||||
post('/api/display/anim', { name });
|
||||
logEvent('Animation Played', { animation: name }, null);
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('btn-play-sequence').addEventListener('click', () => {
|
||||
const sequence = document.getElementById('sequence-list').value
|
||||
.split('\n')
|
||||
.map(line => line.trim())
|
||||
.filter(line => line.length > 0);
|
||||
|
||||
if (sequence.length === 0) {
|
||||
alert('Please enter at least one animation');
|
||||
return;
|
||||
}
|
||||
|
||||
const repeatCount = parseInt(document.getElementById('repeat-count').value) || 1;
|
||||
const delay = parseInt(document.getElementById('anim-delay').value) || 500;
|
||||
|
||||
post('/api/display/anim-sequence', { sequence, repeatCount, delay });
|
||||
logEvent('Sequence Started', { count: sequence.length, repeats: repeatCount }, null);
|
||||
});
|
||||
|
||||
document.getElementById('btn-stop-sequence').addEventListener('click', () => {
|
||||
post('/api/cancel', {});
|
||||
logEvent('Sequence Stopped', {}, null);
|
||||
});
|
||||
|
||||
// ── Camera / Video ────────────────────────────────────────────────────────────
|
||||
|
||||
let videoTxId = null;
|
||||
|
||||
document.getElementById('btn-video-start').addEventListener('click', async () => {
|
||||
const r = await post('/api/video/start', { duration: 0 });
|
||||
if (r) videoTxId = r.txId;
|
||||
document.getElementById('video-status').textContent = 'Waiting for VideoReady…';
|
||||
});
|
||||
|
||||
document.getElementById('btn-video-stop').addEventListener('click', () => {
|
||||
post('/api/video/stop', {});
|
||||
stopVideoFeed();
|
||||
});
|
||||
|
||||
function startVideoFeed(uri) {
|
||||
const feed = document.getElementById('camera-feed');
|
||||
const noFeed = document.getElementById('camera-no-feed');
|
||||
feed.src = '/proxy/stream?uri=' + encodeURIComponent(uri);
|
||||
feed.style.display = 'block';
|
||||
noFeed.style.display = 'none';
|
||||
videoActive = true;
|
||||
}
|
||||
|
||||
function stopVideoFeed() {
|
||||
const feed = document.getElementById('camera-feed');
|
||||
feed.src = '';
|
||||
feed.style.display = 'none';
|
||||
document.getElementById('camera-no-feed').style.display = '';
|
||||
videoActive = false;
|
||||
}
|
||||
|
||||
// ── Take Photo ────────────────────────────────────────────────────────────────
|
||||
|
||||
document.getElementById('btn-photo').addEventListener('click', () => {
|
||||
post('/api/photo', {
|
||||
camera: 'right',
|
||||
resolution: document.getElementById('photo-res').value
|
||||
});
|
||||
});
|
||||
|
||||
function addPhoto(url) {
|
||||
const strip = document.getElementById('photo-strip');
|
||||
const img = document.createElement('img');
|
||||
img.src = url;
|
||||
img.title = url;
|
||||
img.addEventListener('click', () => openPhotoModal(img.src));
|
||||
strip.prepend(img);
|
||||
}
|
||||
|
||||
// ── Photo modal ───────────────────────────────────────────────────────────────
|
||||
|
||||
function openPhotoModal(src) {
|
||||
document.getElementById('photo-modal-img').src = src;
|
||||
document.getElementById('photo-modal').classList.add('open');
|
||||
}
|
||||
|
||||
document.getElementById('photo-modal').addEventListener('click', (e) => {
|
||||
if (e.target === e.currentTarget || e.target.id === 'photo-modal-close') {
|
||||
document.getElementById('photo-modal').classList.remove('open');
|
||||
}
|
||||
});
|
||||
|
||||
// ── Tabs ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
document.querySelectorAll('.tab-btn').forEach(btn => {
|
||||
btn.addEventListener('click', function () {
|
||||
document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
|
||||
document.querySelectorAll('.tab-panel').forEach(p => p.classList.remove('active'));
|
||||
this.classList.add('active');
|
||||
document.getElementById(this.dataset.tab).classList.add('active');
|
||||
});
|
||||
});
|
||||
|
||||
// ── Event log ─────────────────────────────────────────────────────────────────
|
||||
|
||||
const MAX_LOG = 200;
|
||||
const MAX_LOG = 100;
|
||||
|
||||
function logEvent(eventName, body, txId) {
|
||||
const log = document.getElementById('event-log');
|
||||
const now = new Date().toLocaleTimeString();
|
||||
|
||||
// Pick color class based on event type
|
||||
let cls = 'evt';
|
||||
if (eventName.toLowerCase().includes('error')) cls = 'evt-error';
|
||||
else if (eventName.toLowerCase().includes('play')) cls = 'evt-success';
|
||||
else if (eventName.toLowerCase().includes('stop')) cls = 'evt-error';
|
||||
else if (eventName.toLowerCase().includes('selected')) cls = 'evt-entity';
|
||||
|
||||
// Extract detail from body
|
||||
let detail = '';
|
||||
if (body.animation) detail = ' ' + body.animation;
|
||||
if (body && body.animation) detail = ' "' + body.animation + '"';
|
||||
else if (body && body.error) detail = ' — ' + body.error;
|
||||
else if (body && body.uri) detail = ' ' + body.uri;
|
||||
|
||||
const el = document.createElement('div');
|
||||
el.className = 'log-entry';
|
||||
el.innerHTML = `<span class="ts">${now}</span> <span class="${cls}">${eventName}</span>${detail}`;
|
||||
log.prepend(el);
|
||||
|
||||
while (log.children.length > MAX_LOG) log.removeChild(log.lastChild);
|
||||
if (log) {
|
||||
log.prepend(el);
|
||||
// Trim log
|
||||
while (log.children.length > MAX_LOG) log.removeChild(log.lastChild);
|
||||
}
|
||||
|
||||
console.log(`[${now}]`, eventName, body);
|
||||
}
|
||||
|
||||
document.getElementById('btn-clear-log').addEventListener('click', () => {
|
||||
document.getElementById('event-log').innerHTML = '';
|
||||
// ── Video Helpers ───────────────────────────────────────────────────────────
|
||||
|
||||
function startVideoFeed(uri) {
|
||||
logEvent('Video feed ready', { uri }, null);
|
||||
}
|
||||
|
||||
function stopVideoFeed() {
|
||||
logEvent('Video stopped', {}, null);
|
||||
}
|
||||
|
||||
// ── GoldenLayout Setup ───────────────────────────────────────────────────────
|
||||
|
||||
const layoutConfig = {
|
||||
root: {
|
||||
type: 'row',
|
||||
children: [
|
||||
{
|
||||
type: 'component',
|
||||
componentType: 'library',
|
||||
componentState: { title: 'Animation Library' },
|
||||
width: 20,
|
||||
title: 'Library'
|
||||
},
|
||||
{
|
||||
type: 'column',
|
||||
children: [
|
||||
{
|
||||
type: 'component',
|
||||
componentType: 'canvas',
|
||||
componentState: { title: 'Canvas' },
|
||||
title: 'Canvas',
|
||||
height: 70
|
||||
}
|
||||
],
|
||||
width: 60
|
||||
},
|
||||
{
|
||||
type: 'column',
|
||||
children: [
|
||||
{
|
||||
type: 'stack',
|
||||
activeItemIndex: 0,
|
||||
width: 20,
|
||||
children: [
|
||||
{
|
||||
type: 'component',
|
||||
componentType: 'inspector',
|
||||
componentState: { title: 'Inspector' },
|
||||
title: 'Inspector'
|
||||
},
|
||||
{
|
||||
type: 'component',
|
||||
componentType: 'log',
|
||||
componentState: { title: 'Log' },
|
||||
title: 'Log'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
// ── Initialization ──────────────────────────────────────────────────────────
|
||||
|
||||
function setupComponentRegistry() {
|
||||
const componentRegistry = new GoldenLayout.ComponentRegistry();
|
||||
|
||||
componentRegistry.registerComponent('library', (container, state) => {
|
||||
const content = document.querySelector('#tpl-library').content.cloneNode(true);
|
||||
container.element.appendChild(content);
|
||||
|
||||
const libraryList = container.element.querySelector('#library-list');
|
||||
const animations = [
|
||||
'Eye_Blink_01', 'Eye_Blink_02', 'Eye_Double_Blink', 'eye_happy_01', 'eye_happy_02',
|
||||
'eye_sad_01', 'eye_scared_00', 'Eye_Curious_01', 'Eye_Look_Center', 'Eye_Look_Left',
|
||||
'Eye_Look_Right', 'Eye_Look_Up', 'Eye_Look_Down', 'Gesture_Wave', 'Gesture_Nod'
|
||||
];
|
||||
|
||||
animations.forEach(anim => {
|
||||
const item = document.createElement('div');
|
||||
item.className = 'library-item';
|
||||
item.textContent = anim;
|
||||
item.addEventListener('click', () => {
|
||||
document.querySelectorAll('.library-item').forEach(i => i.classList.remove('selected'));
|
||||
item.classList.add('selected');
|
||||
|
||||
const inspectorInput = document.querySelector('.inspector-field input');
|
||||
if (inspectorInput) inspectorInput.value = anim;
|
||||
|
||||
logEvent('Animation selected', { animation: anim }, null);
|
||||
});
|
||||
libraryList.appendChild(item);
|
||||
});
|
||||
});
|
||||
|
||||
componentRegistry.registerComponent('canvas', (container, state) => {
|
||||
const content = document.querySelector('#tpl-canvas').content.cloneNode(true);
|
||||
container.element.appendChild(content);
|
||||
});
|
||||
|
||||
componentRegistry.registerComponent('inspector', (container, state) => {
|
||||
const content = document.querySelector('#tpl-inspector').content.cloneNode(true);
|
||||
container.element.appendChild(content);
|
||||
|
||||
const applyBtn = container.element.querySelector('.inspector-field button');
|
||||
if (applyBtn) {
|
||||
applyBtn.addEventListener('click', async () => {
|
||||
const animName = container.element.querySelector('.inspector-field input').value;
|
||||
if (!animName) {
|
||||
alert('Please select an animation');
|
||||
return;
|
||||
}
|
||||
|
||||
const inputs = container.element.querySelectorAll('.inspector-field input');
|
||||
const duration = parseInt(inputs[1]?.value) || 500;
|
||||
const repeat = parseInt(inputs[2]?.value) || 1;
|
||||
const delay = parseInt(inputs[3]?.value) || 0;
|
||||
|
||||
await post('/api/display/anim', { name: animName, duration, repeat, delay });
|
||||
logEvent('Applied animation', { animation: animName, duration, repeat, delay }, null);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
componentRegistry.registerComponent('log', (container, state) => {
|
||||
const content = document.querySelector('#tpl-log').content.cloneNode(true);
|
||||
container.element.appendChild(content);
|
||||
});
|
||||
|
||||
return componentRegistry;
|
||||
}
|
||||
|
||||
function setupToolbarButtons() {
|
||||
const btn = (id, fn) => {
|
||||
const el = document.getElementById(id);
|
||||
if (el) el.addEventListener('click', fn);
|
||||
};
|
||||
|
||||
btn('btn-new', () => alert('New animation - Coming soon'));
|
||||
btn('btn-open', () => alert('Open animation - Coming soon'));
|
||||
btn('btn-save', () => alert('Save animation - Coming soon'));
|
||||
btn('btn-undo', () => logEvent('Undo', {}, null));
|
||||
btn('btn-redo', () => logEvent('Redo', {}, null));
|
||||
|
||||
btn('btn-play', async () => {
|
||||
const inspectorInput = document.querySelector('.inspector-field input');
|
||||
const animName = inspectorInput ? inspectorInput.value : 'greeting';
|
||||
await post('/api/display/anim', { name: animName });
|
||||
logEvent('Playing animation', { animation: animName }, null);
|
||||
});
|
||||
|
||||
btn('btn-pause', () => {
|
||||
logEvent('Pause animation', {}, null);
|
||||
});
|
||||
|
||||
btn('btn-stop', () => {
|
||||
post('/api/cancel', {});
|
||||
logEvent('Stop animation', {}, null);
|
||||
});
|
||||
|
||||
btn('btn-timeline', () => alert('Timeline view - Coming soon'));
|
||||
|
||||
btn('btn-preview', async () => {
|
||||
await post('/api/video/stop', {});
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
await post('/api/video/start', { duration: 0 });
|
||||
logEvent('Preview video started', {}, null);
|
||||
});
|
||||
|
||||
btn('btn-settings', () => alert('Settings - Coming soon'));
|
||||
|
||||
btn('btn-back-home', () => {
|
||||
post('/api/video/stop', {});
|
||||
window.location.href = 'index.html';
|
||||
});
|
||||
}
|
||||
|
||||
// ── Initialization ──────────────────────────────────────────────────────────
|
||||
|
||||
function waitForGoldenLayout(callback) {
|
||||
if (typeof GoldenLayout === 'undefined') {
|
||||
setTimeout(() => waitForGoldenLayout(callback), 50);
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
waitForGoldenLayout(() => {
|
||||
// Setup components
|
||||
const componentRegistry = setupComponentRegistry();
|
||||
|
||||
// Initialize GoldenLayout
|
||||
const layout = new GoldenLayout(layoutConfig, componentRegistry, document.querySelector('#layout-root'));
|
||||
layout.init();
|
||||
|
||||
// Setup toolbar
|
||||
setupToolbarButtons();
|
||||
|
||||
// Connect to server
|
||||
connectWS();
|
||||
logEvent('Animator initialized with GoldenLayout', {}, null);
|
||||
});
|
||||
});
|
||||
|
||||
// ── Back to home ──────────────────────────────────────────────────────────────
|
||||
|
||||
document.getElementById('btn-back-home')?.addEventListener('click', () => {
|
||||
window.location.href = 'index.html';
|
||||
});
|
||||
|
||||
// ── Initialization ────────────────────────────────────────────────────────────
|
||||
|
||||
connectWS();
|
||||
|
||||
@@ -273,6 +273,7 @@ label { font-size: 12px; color: var(--muted); display: block; margin-bottom: 3px
|
||||
.log-entry .evt-error { color: var(--danger); }
|
||||
.log-entry .evt-motion { color: var(--warn); }
|
||||
.log-entry .evt-entity { color: var(--success); }
|
||||
.log-entry .evt-success { color: var(--success); }
|
||||
|
||||
/* ── Tabs ── */
|
||||
.tabs {
|
||||
|
||||
@@ -5,6 +5,137 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Re-Commander — Telepresence Mode</title>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
<style>
|
||||
body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
header {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.telecom-container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Full-screen video area */
|
||||
.video-fullscreen {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
background: #000;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#camera-wrap {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
background: #000;
|
||||
}
|
||||
|
||||
#camera-feed {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
|
||||
#camera-no-feed {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
/* Top Control Bar */
|
||||
.telecom-top-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 8px 16px;
|
||||
background: var(--surface);
|
||||
border-bottom: 1px solid var(--border);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
#btn-back-home {
|
||||
padding: 6px 12px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.telecom-title {
|
||||
flex: 1;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.telecom-status {
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* Bottom Control Bar */
|
||||
.telecom-bottom-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px 16px;
|
||||
background: var(--surface);
|
||||
border-top: 1px solid var(--border);
|
||||
flex-shrink: 0;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.bar-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.bar-label {
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.telecom-bottom-bar button {
|
||||
padding: 8px 14px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
#video-quality-select {
|
||||
padding: 6px 8px;
|
||||
background: var(--surface2);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
color: var(--text);
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#video-quality-select:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -13,131 +144,51 @@
|
||||
<h1>Re<span>-Commander</span> — Telepresence</h1>
|
||||
<span id="status-label">Connecting…</span>
|
||||
<span id="hotword-indicator"></span>
|
||||
<button id="btn-back-home" style="margin-left: auto; padding: 5px 12px; font-size: 12px;">← Back to Home</button>
|
||||
</header>
|
||||
|
||||
<div class="main">
|
||||
<div class="telecom-container">
|
||||
|
||||
<!-- ── LEFT PANEL ── -->
|
||||
<div class="left-panel">
|
||||
<!-- Top Control Bar -->
|
||||
<div class="telecom-top-bar">
|
||||
<button id="btn-back-home">← Back to Home</button>
|
||||
<div class="telecom-title">🎥 Telepresence Stream</div>
|
||||
<div class="telecom-status" id="telecom-status">Ready</div>
|
||||
</div>
|
||||
|
||||
<!-- Telepresence Controls -->
|
||||
<div class="section">
|
||||
<div class="section-title">Presence Controls</div>
|
||||
<div class="row">
|
||||
<label>User Name</label>
|
||||
<input type="text" id="user-name" placeholder="Your name">
|
||||
</div>
|
||||
<div class="row">
|
||||
<label>Status</label>
|
||||
<select id="presence-status">
|
||||
<option value="active">Active</option>
|
||||
<option value="away">Away</option>
|
||||
<option value="busy">Busy</option>
|
||||
<option value="offline">Offline</option>
|
||||
</select>
|
||||
</div>
|
||||
<button id="btn-start-telepresence" style="width:100%;">🔴 Start Broadcasting</button>
|
||||
</div>
|
||||
|
||||
<!-- Head Navigation -->
|
||||
<div class="section">
|
||||
<div class="section-title">Head Navigation</div>
|
||||
<div style="display:flex; gap:16px; align-items:center; margin-bottom:8px;">
|
||||
<div class="arrow-pad">
|
||||
<div></div>
|
||||
<button id="btn-up" title="Look up (↑)">UP</button>
|
||||
<div></div>
|
||||
<button id="btn-left" title="Look left (←)">←</button>
|
||||
<div></div>
|
||||
<button id="btn-right" title="Look right (→)">→</button>
|
||||
<div></div>
|
||||
<button id="btn-down" title="Look down (↓)">↓</button>
|
||||
<div></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Say -->
|
||||
<div class="section">
|
||||
<div class="section-title">Say</div>
|
||||
<div class="field">
|
||||
<textarea id="say-text" rows="2" placeholder="Hello! I am present via telepresence."></textarea>
|
||||
</div>
|
||||
<div class="controls-row">
|
||||
<button id="btn-say">🎤 Say</button>
|
||||
<button id="btn-say-cancel" class="danger">✕ Stop</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Audio Settings -->
|
||||
<div class="section">
|
||||
<div class="section-title">Audio</div>
|
||||
<div class="row">
|
||||
<label>Microphone Level</label>
|
||||
<input type="range" id="mic-level" min="0" max="1" step="0.05" value="0.75">
|
||||
</div>
|
||||
<div class="row">
|
||||
<label>Speaker Volume</label>
|
||||
<input type="range" id="speaker-volume" min="0" max="1" step="0.05" value="0.75">
|
||||
<span id="volume-label" style="min-width:32px;text-align:right;font-size:12px;">75%</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div><!-- /left-panel -->
|
||||
|
||||
<!-- ── CENTER PANEL ── -->
|
||||
<div class="center-panel">
|
||||
|
||||
<!-- Camera feed (main telepresence view) -->
|
||||
<!-- Full-screen Video -->
|
||||
<div class="video-fullscreen">
|
||||
<div id="camera-wrap">
|
||||
<img id="camera-feed" alt="Jibo camera" style="display:none;">
|
||||
<img id="camera-feed" alt="Telepresence feed" style="display:none;">
|
||||
<div id="camera-no-feed">
|
||||
<div style="font-size:40px;">📹</div>
|
||||
<div>Telepresence View</div>
|
||||
<div style="font-size:11px;color:var(--muted);">Start broadcasting to see live camera feed</div>
|
||||
</div>
|
||||
<div id="click-dot"></div>
|
||||
</div>
|
||||
|
||||
<!-- Remote View / User Info -->
|
||||
<div class="controls-row" style="justify-content:center; gap:10px; margin-top:20px; flex-direction: column; align-items: center;">
|
||||
<div id="remote-status" style="font-size:13px;color:var(--text);padding:10px;background:var(--surface2);border-radius:6px;border:1px solid var(--border);min-width:300px;text-align:center;">
|
||||
Waiting to start telepresence...
|
||||
</div>
|
||||
<button id="btn-video-start" style="flex-basis: auto;">▶ Start Video</button>
|
||||
<button id="btn-video-stop" class="danger" style="flex-basis: auto;">■ Stop</button>
|
||||
</div>
|
||||
|
||||
</div><!-- /center-panel -->
|
||||
|
||||
<!-- ── RIGHT PANEL ── -->
|
||||
<div class="right-panel">
|
||||
<div class="tabs">
|
||||
<button class="tab-btn active" data-tab="tab-participants">Participants</button>
|
||||
<button class="tab-btn" data-tab="tab-log">Log</button>
|
||||
</div>
|
||||
|
||||
<!-- Participants tab -->
|
||||
<div class="tab-panel active" id="tab-participants">
|
||||
<div class="section-title">Remote Users</div>
|
||||
<div id="participants-list" style="font-size:12px;">
|
||||
<div style="color:var(--muted);">No remote users connected</div>
|
||||
<div>No camera feed</div>
|
||||
<div style="font-size:11px;">Click restart video below to start broadcasting</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Log tab -->
|
||||
<div class="tab-panel" id="tab-log" style="padding:0;display:flex;flex-direction:column;flex:1;">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;padding:8px 10px;border-bottom:1px solid var(--border);">
|
||||
<span class="section-title" style="margin:0;">Session Log</span>
|
||||
<button id="btn-clear-log" style="font-size:11px;padding:3px 8px;">Clear</button>
|
||||
</div>
|
||||
<div id="event-log"></div>
|
||||
<!-- Bottom Control Bar -->
|
||||
<div class="telecom-bottom-bar">
|
||||
<div class="bar-group">
|
||||
<label class="bar-label">Quality:</label>
|
||||
<select id="video-quality-select">
|
||||
<option value="highRes">High</option>
|
||||
<option value="medRes" selected>Medium</option>
|
||||
<option value="lowRes">Low</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
</div><!-- /right-panel -->
|
||||
<div class="bar-group" style="margin-left:auto; gap: 8px;">
|
||||
<button id="btn-play-eye-anim">👁 Play Eye</button>
|
||||
<button id="btn-video-start" style="background:var(--success);border-color:var(--success);">▶ Start Video</button>
|
||||
<button id="btn-video-stop" class="danger">■ Stop</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script src="telepresence.js"></script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user