telepresence and animation menus

This commit is contained in:
Kevin
2026-04-19 22:55:40 +03:00
parent 07f26e3906
commit 9b4cff9af1
7 changed files with 1349 additions and 361 deletions

View File

@@ -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);
if (log) {
log.prepend(el);
// Trim log
while (log.children.length > MAX_LOG) log.removeChild(log.lastChild);
}
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();