Skip to content

Commit 5197fd0

Browse files
committed
Update animation UI
1 parent 5b6c639 commit 5197fd0

File tree

3 files changed

+73
-17
lines changed

3 files changed

+73
-17
lines changed

examples/led-matrix-painter/assets/app.js

Lines changed: 56 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ const gridEl = document.getElementById('grid');
77
const vectorEl = document.getElementById('vector');
88
const exportBtn = document.getElementById('export');
99
const playAnimationBtn = document.getElementById('play-animation');
10+
const stopAnimationBtn = document.getElementById('stop-animation');
1011
const clearBtn = document.getElementById('clear');
1112
const invertBtn = document.getElementById('invert');
1213
const rotate180Btn = document.getElementById('rotate180');
@@ -257,8 +258,6 @@ async function initEditor(){
257258

258259
if (data && data.ok && data.frame) {
259260
const frame = data.frame;
260-
loadedFrame = frame;
261-
loadedFrameId = frame.id;
262261

263262
// Populate grid
264263
setGridFromRows(frame.rows || []);
@@ -314,14 +313,36 @@ async function exportH(){
314313
makeGrid();
315314
if (exportBtn) exportBtn.addEventListener('click', exportH); else console.warn('[ui] export button not found');
316315

316+
let animationTimeout = null;
317+
318+
function displayFrame(frame) {
319+
if (!frame) return;
320+
321+
// Populate grid
322+
setGridFromRows(frame.rows || []);
323+
324+
// Populate name input
325+
if (frameTitle) frameTitle.textContent = frame.name || `Frame ${frame.id}`;
326+
327+
// Mark as loaded in sidebar
328+
markLoaded(frame);
329+
}
330+
317331
async function playAnimation() {
318332
if (!playAnimationBtn) return;
319333

334+
// Stop any previous animation loop
335+
if (animationTimeout) {
336+
clearTimeout(animationTimeout);
337+
animationTimeout = null;
338+
}
339+
320340
try {
321341
playAnimationBtn.disabled = true;
322342
const frameIds = sessionFrames.map(f => f.id);
323343
if (frameIds.length === 0) {
324344
showError('No frames to play');
345+
playAnimationBtn.disabled = false; // re-enable button
325346
return;
326347
}
327348

@@ -340,20 +361,51 @@ async function playAnimation() {
340361

341362
if (data.error) {
342363
showError('Error: ' + data.error);
364+
playAnimationBtn.disabled = false;
343365
} else {
344366
console.debug('[ui] Animation played successfully, frames=', data.frames_played);
345367
showVectorText('Animation played: ' + data.frames_played + ' frames');
368+
369+
// Start frontend animation simulation
370+
let currentFrameIndex = 0;
371+
const animateNextFrame = () => {
372+
if (currentFrameIndex >= sessionFrames.length) {
373+
// Animation finished
374+
playAnimationBtn.disabled = false;
375+
animationTimeout = null;
376+
return;
377+
}
378+
379+
const frame = sessionFrames[currentFrameIndex];
380+
displayFrame(frame);
381+
382+
const duration = frame.duration_ms || 1000;
383+
currentFrameIndex++;
384+
385+
animationTimeout = setTimeout(animateNextFrame, duration);
386+
};
387+
animateNextFrame();
346388
}
347389

348390
} catch (err) {
349391
console.error('[ui] playAnimation failed', err);
350-
} finally {
351-
playAnimationBtn.disabled = false;
392+
playAnimationBtn.disabled = false; // re-enable on error
352393
}
353394
}
354395

355396
if (playAnimationBtn) playAnimationBtn.addEventListener('click', playAnimation); else console.warn('[ui] play animation button not found');
356397

398+
if (stopAnimationBtn) {
399+
stopAnimationBtn.addEventListener('click', () => {
400+
if (animationTimeout) {
401+
clearTimeout(animationTimeout);
402+
animationTimeout = null;
403+
playAnimationBtn.disabled = false;
404+
showVectorText('Animation stopped');
405+
}
406+
});
407+
}
408+
357409
// Save frame button removed - auto-persist replaces it
358410
const animControls = document.getElementById('anim-controls');
359411
const animNameInput = document.getElementById('anim-name');
@@ -589,8 +641,6 @@ async function loadFrameIntoEditor(id){
589641

590642
if(data && data.ok && data.frame){
591643
const f = data.frame;
592-
loadedFrame = f;
593-
loadedFrameId = f.id;
594644

595645
// Populate grid
596646
setGridFromRows(f.rows || []);
@@ -661,8 +711,6 @@ async function handleNewFrameClick() {
661711
}, 'json', 'create new frame');
662712

663713
if (data && data.ok && data.frame) {
664-
loadedFrame = data.frame;
665-
loadedFrameId = data.frame.id;
666714
// Set name to the backend-assigned name (Frame {id})
667715
if(frameTitle) frameTitle.textContent = data.frame.name || `Frame ${data.frame.id}`;
668716

examples/led-matrix-painter/assets/style.css

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -464,6 +464,10 @@ input:checked + .slider:before {
464464
background: #008184;
465465
}
466466

467+
#play-animation:disabled {
468+
background: #006567; /* Darken background when animation is running */
469+
}
470+
467471
#stop-animation {
468472
background: rgba(0, 129, 132, 0.2); /* #008184 with 20% alpha */
469473
}

examples/led-matrix-painter/python/main.py

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from app_frame import AppFrame # user module defining AppFrame
88
import store # user module for DB operations
99
import logging
10+
import threading
1011

1112
BRIGHTNESS_LEVELS = 8 # must match the frontend slider range (0..BRIGHTNESS_LEVELS-1)
1213

@@ -272,6 +273,13 @@ def export_frames(payload: dict = None):
272273
return {'header': header}
273274

274275

276+
def play_animation_thread(animation_bytes):
277+
try:
278+
Bridge.call("play_animation", bytes(animation_bytes))
279+
logger.info("Animation sent to board successfully")
280+
except Exception as e:
281+
logger.warning(f"Failed to send animation to board: {e}")
282+
275283
def play_animation(payload: dict):
276284
"""Play animation sequence on the board.
277285
@@ -314,15 +322,11 @@ def play_animation(payload: dict):
314322

315323
logger.debug(f"Animation data prepared: {len(animation_bytes)} bytes ({len(animation_bytes)//20} frames)")
316324

317-
# Send to board via Bridge as bytes (not list)
318-
# Bridge expects bytes object for std::vector<uint8_t>
319-
try:
320-
Bridge.call("play_animation", bytes(animation_bytes))
321-
logger.info("Animation sent to board successfully")
322-
return {'ok': True, 'frames_played': len(frames)}
323-
except Exception as e:
324-
logger.warning(f"Failed to send animation to board: {e}")
325-
return {'error': str(e)}
325+
# Run Bridge.call in a separate thread
326+
thread = threading.Thread(target=play_animation_thread, args=(animation_bytes,))
327+
thread.start()
328+
329+
return {'ok': True, 'frames_played': len(frames)} # Return immediately
326330

327331

328332
ui.expose_api('POST', '/update_board', update_board)

0 commit comments

Comments
 (0)