summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorskal <pascal.massimino@gmail.com>2026-02-06 22:45:34 +0100
committerskal <pascal.massimino@gmail.com>2026-02-06 22:45:34 +0100
commit64145080cddbc0fe9fec7159e9ffdedca48ae9be (patch)
tree58459b5cc81fd923c466a53e1665f3c87fd7ec3d
parent6906b9d75e21b904b3f54f4a4c5b591dd459dc69 (diff)
feat(timeline): Add Ctrl+wheel zoom centered on mouse cursor
FEATURE: Implemented zoom-with-mousewheel for timeline editor, centered on cursor position. IMPLEMENTATION: - Detect Ctrl/Cmd + wheel event - Calculate time position under cursor BEFORE zoom: time_under_cursor = (scrollLeft + mouseX) / oldPixelsPerSecond - Adjust pixelsPerSecond (±10 per wheel notch, clamped to 10-500) - Re-render waveform and timeline at new zoom level - Adjust scroll position AFTER zoom to keep same time under cursor: new_scrollLeft = time_under_cursor * newPixelsPerSecond - mouseX CONTROLS: - Ctrl/Cmd + wheel up: Zoom in (+10 px/sec) - Ctrl/Cmd + wheel down: Zoom out (-10 px/sec) - Wheel without Ctrl: Diagonal scroll (existing behavior) TRICKY PARTS: - Mouse position must be relative to timeline container (not page) - Scroll position adjustment ensures zoom feels "anchored" to cursor - Zoom range clamped to 10-500 px/sec to prevent extreme values TESTING: - Open tools/timeline_editor/index.html - Load a demo.seq file - Hold Ctrl/Cmd and scroll wheel to zoom - Verify that the timeline zooms in/out centered on cursor position This addresses the "tricky to get right" concern by properly handling the coordinate space transform between old and new zoom levels.
-rw-r--r--tools/timeline_editor/index.html41
1 files changed, 39 insertions, 2 deletions
diff --git a/tools/timeline_editor/index.html b/tools/timeline_editor/index.html
index f85f914..074b711 100644
--- a/tools/timeline_editor/index.html
+++ b/tools/timeline_editor/index.html
@@ -1279,11 +1279,48 @@
updateProperties();
});
- // Mouse wheel diagonal scroll (follows time-ordered sequence cascade)
+ // Mouse wheel: zoom (with Ctrl/Cmd) or diagonal scroll
timelineContainer.addEventListener('wheel', (e) => {
e.preventDefault();
- // Horizontal scroll
+ // Zoom mode: Ctrl/Cmd + wheel
+ if (e.ctrlKey || e.metaKey) {
+ // Get mouse position relative to timeline container
+ const rect = timelineContainer.getBoundingClientRect();
+ const mouseX = e.clientX - rect.left; // Mouse X in viewport coordinates
+
+ // Calculate time position under cursor BEFORE zoom
+ const scrollLeft = timelineContainer.scrollLeft;
+ const timeUnderCursor = (scrollLeft + mouseX) / pixelsPerSecond;
+
+ // Calculate new zoom level
+ const zoomDelta = e.deltaY > 0 ? -10 : 10; // Wheel down = zoom out, wheel up = zoom in
+ const oldPixelsPerSecond = pixelsPerSecond;
+ const newPixelsPerSecond = Math.max(10, Math.min(500, pixelsPerSecond + zoomDelta));
+
+ if (newPixelsPerSecond !== oldPixelsPerSecond) {
+ pixelsPerSecond = newPixelsPerSecond;
+
+ // Update zoom slider and labels
+ zoomSlider.value = pixelsPerSecond;
+ zoomLevel.textContent = `${pixelsPerSecond}%`;
+ pixelsPerSecLabel.textContent = pixelsPerSecond;
+
+ // Re-render waveform and timeline at new zoom
+ if (audioBuffer) {
+ renderWaveform();
+ }
+ renderTimeline();
+
+ // Adjust scroll position so time under cursor stays in same place
+ // After zoom: new_scrollLeft = time_under_cursor * newPixelsPerSecond - mouseX
+ const newScrollLeft = timeUnderCursor * newPixelsPerSecond - mouseX;
+ timelineContainer.scrollLeft = newScrollLeft;
+ }
+ return;
+ }
+
+ // Normal mode: diagonal scroll
timelineContainer.scrollLeft += e.deltaY;
// Calculate current time position with 10% headroom for visual comfort