From 1b57c0d7bfbdddfb9060a54df220ee51d9246f05 Mon Sep 17 00:00:00 2001 From: skal Date: Thu, 5 Feb 2026 21:54:38 +0100 Subject: feat(timeline-editor): Add re-order button and 10% viewport slack MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Feature #1: Re-order Sequences by Time - Added "🔄 Re-order by Time" button to controls - Sorts sequences by startTime (ascending order) - Preserves focus on currently active sequence - Algorithm: 1. Store reference to current active sequence (lastActiveSeqIndex) 2. Sort sequences array: sequences.sort((a, b) => a.startTime - b.startTime) 3. Re-render timeline (recalculates _yPosition for all) 4. Find new index of previously active sequence 5. Scroll to its new Y position 6. Update lastActiveSeqIndex to new index - Button enabled when file is loaded - Shows success message after re-ordering - Useful when sequences are added/moved out of order Use Case: Before: Seq2(10s), Seq0(0s), Seq1(5s) ← Out of order After: Seq0(0s), Seq1(5s), Seq2(10s) ← Chronological order Feature #2: 10% Viewport Slack for Visual Comfort - Added headroom when calculating current time during scroll - Previously: currentTime = scrollLeft / pixelsPerSecond - Now: currentTime = (scrollLeft / pixelsPerSecond) + slack - Slack = 10% of viewport width in time units Benefits: - Sequences don't get targeted when right at left edge - More comfortable visual positioning - Sequences become active when ~10% into viewport - Prevents "edge hugging" during diagonal scroll Example: Viewport width: 1000px, pixelsPerSecond: 100 Slack = (1000 / 100) * 0.1 = 1.0 second scrollLeft = 500px → currentTime = 5.0s + 1.0s = 6.0s Sequence at 6.0s becomes active (not 5.0s) Technical Details: - viewportWidth = timelineContainer.clientWidth - slack = (viewportWidth / pixelsPerSecond) * 0.1 - Applied before finding target sequence - Works with any zoom level (pixelsPerSecond changes) - Dynamic calculation on every wheel event --- tools/timeline_editor/index.html | 35 ++++++++++++++++++++++++++++++++--- 1 file changed, 32 insertions(+), 3 deletions(-) (limited to 'tools/timeline_editor/index.html') diff --git a/tools/timeline_editor/index.html b/tools/timeline_editor/index.html index 94e88f6..d450522 100644 --- a/tools/timeline_editor/index.html +++ b/tools/timeline_editor/index.html @@ -372,6 +372,7 @@ +
@@ -423,6 +424,7 @@ const saveBtn = document.getElementById('saveBtn'); const addSequenceBtn = document.getElementById('addSequenceBtn'); const deleteBtn = document.getElementById('deleteBtn'); + const reorderBtn = document.getElementById('reorderBtn'); const propertiesPanel = document.getElementById('propertiesPanel'); const propertiesContent = document.getElementById('propertiesContent'); const messageArea = document.getElementById('messageArea'); @@ -868,6 +870,7 @@ renderTimeline(); saveBtn.disabled = false; addSequenceBtn.disabled = false; + reorderBtn.disabled = false; showMessage(`Loaded ${currentFile} - ${sequences.length} sequences`, 'success'); } catch (err) { showMessage(`Error parsing file: ${err.message}`, 'error'); @@ -916,6 +919,30 @@ showMessage('Item deleted', 'success'); }); + // Re-order sequences by time + reorderBtn.addEventListener('click', () => { + // Store current active sequence (if any) + const currentActiveSeq = lastActiveSeqIndex >= 0 ? sequences[lastActiveSeqIndex] : null; + + // Sort sequences by start time (ascending) + sequences.sort((a, b) => a.startTime - b.startTime); + + // Re-render timeline + renderTimeline(); + + // Restore focus on previously active sequence + if (currentActiveSeq) { + const newIndex = sequences.indexOf(currentActiveSeq); + if (newIndex >= 0 && sequences[newIndex]._yPosition !== undefined) { + // Scroll to keep it in view + timelineContainer.scrollTop = sequences[newIndex]._yPosition; + lastActiveSeqIndex = newIndex; + } + } + + showMessage('Sequences re-ordered by start time', 'success'); + }); + // Zoom zoomSlider.addEventListener('input', (e) => { const zoom = parseInt(e.target.value); @@ -963,12 +990,14 @@ // Horizontal scroll timelineContainer.scrollLeft += e.deltaY; - // Calculate current time position (left edge of viewport) + // Calculate current time position with 10% headroom for visual comfort const currentScrollLeft = timelineContainer.scrollLeft; - const currentTime = currentScrollLeft / pixelsPerSecond; + const viewportWidth = timelineContainer.clientWidth; + const slack = (viewportWidth / pixelsPerSecond) * 0.1; // 10% of viewport width in seconds + const currentTime = (currentScrollLeft / pixelsPerSecond) + slack; // Find the closest sequence that should be visible at current time - // (the last sequence that starts before or at current time) + // (the last sequence that starts before or at current time + slack) let targetSeqIndex = 0; for (let i = 0; i < sequences.length; i++) { if (sequences[i].startTime <= currentTime) { -- cgit v1.2.3