diff options
| author | skal <pascal.massimino@gmail.com> | 2026-02-05 21:54:38 +0100 |
|---|---|---|
| committer | skal <pascal.massimino@gmail.com> | 2026-02-05 21:54:38 +0100 |
| commit | 1b57c0d7bfbdddfb9060a54df220ee51d9246f05 (patch) | |
| tree | 3db2e69946fa3fe2a57f68564d93cd036568bf64 | |
| parent | 58ad5c16d6f1b2afc51ec5b36ed08643643e5fd0 (diff) | |
feat(timeline-editor): Add re-order button and 10% viewport slack
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
| -rw-r--r-- | tools/timeline_editor/index.html | 35 |
1 files changed, 32 insertions, 3 deletions
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 @@ <button id="saveBtn" disabled>💾 Save demo.seq</button> <button id="addSequenceBtn" disabled>➕ Add Sequence</button> <button id="deleteBtn" disabled>🗑️ Delete Selected</button> + <button id="reorderBtn" disabled>🔄 Re-order by Time</button> </div> <div class="zoom-controls"> @@ -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) { |
