diff --git a/package-lock.json b/package-lock.json index 1825c1a..bcb55ae 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 4e15789..caf37ba 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "dependencies": { "dotenv": "^17.4.2", "express": "^4.18.2", + "golden-layout": "^2.6.0", "ws": "^8.14.2" } } diff --git a/public/animator-backup.html b/public/animator-backup.html new file mode 100644 index 0000000..68de77c --- /dev/null +++ b/public/animator-backup.html @@ -0,0 +1,554 @@ + + + + + + Re-Commander — Animator + + + + + + +
+
+
+

Re-Commander — Animator

+
+ Connecting… + +
+
+ +
+ +
+ + + +
+ +
+ + +
+ + +
+ +
+ + +
+ + + +
+ +
+ + +
+ + +
+ +
+ + +
+ + +
+
+
+ + +
+ + +
+
Animation Library
+
+
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
+
+
+ + +
+
+
Canvas
+
🎬 Preview
+
+
+ Imagine a 3d jibo model in a theater kinda thing +
+
+ + +
+
+ + + +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+
+ +
+ + +
+
+ Mode: + Virtual Robot +
+
+ Animation: + None +
+
+ Status: + Ready +
+
+ Ready for animation +
+
+ + + + diff --git a/public/animator.html b/public/animator.html index cf3242d..1ded7c4 100644 --- a/public/animator.html +++ b/public/animator.html @@ -3,143 +3,414 @@ - Re-Commander — Animator Mode + Re-Commander — Animator + + + +
+ +
+
+

Re-Commander — Animator

+ Connecting… +
-
-
-

Re-Commander — Animator

- Connecting… - - -
- -
- - -
- - -
-
Animation Library
-
- - -
- + +
+
+ + +
- -
-
Animation Sequence
-
- -
-
- - -
+
+ +
+ +
- -
-
Timeline
-
+
+ +
+ + + +
+ +
+ +
+ + +
+ +
+ +
+ + +
+
+ + +
+
+ + + + + + + + + -
-
+ + - -
-
- Photo -
- - + + diff --git a/public/animator.js b/public/animator.js index b21bfbe..e6392f3 100644 --- a/public/animator.js +++ b/public/animator.js @@ -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 = `${now} ${eventName}${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(); diff --git a/public/style.css b/public/style.css index c3c8aa9..c9b2ae0 100644 --- a/public/style.css +++ b/public/style.css @@ -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 { diff --git a/public/telepresence.html b/public/telepresence.html index 64e6c62..960ec16 100644 --- a/public/telepresence.html +++ b/public/telepresence.html @@ -5,6 +5,137 @@ Re-Commander — Telepresence Mode + @@ -13,131 +144,51 @@

Re-Commander — Telepresence

Connecting… - -
- - -
- - -
-
Presence Controls
-
- - -
-
- - -
- -
- - -
-
Head Navigation
-
-
-
- -
- -
- -
- -
-
-
-
- - -
-
Say
-
- -
-
- - -
-
- - -
-
Audio
-
- - -
-
- - - 75% -
-
- -
- - -
- - +
+ + +
+ +
🎥 Telepresence Stream
+
Ready
+
+ + +
- +
📹
-
Telepresence View
-
Start broadcasting to see live camera feed
-
-
-
- - -
-
- Waiting to start telepresence... -
- - -
- -
- - -
-
- - -
- - -
-
Remote Users
-
-
No remote users connected
+
No camera feed
+
Click restart video below to start broadcasting
- - -
-
- Session Log - -
-
+
+ + +
+
+ +
- -
+ +
+ + + +
+
+
+ +