Browse Source

feat(sync): Rearchitect sync logic and add advanced navigation

This commit introduces a major refactoring of the synchronization architecture and adds several new user-facing navigation features to improve usability, performance, and accuracy.

### Synchronization Architecture Rearchitect

- **Pre-computed Timestamps:** Implemented a "baking" process (`precomputeRadarVideoSync`) that calculates a `videoSyncedTime` for each radar frame upon data load or offset change. This eliminates redundant on-the-fly calculations during playback.

- **Simplified Sync Loop:** The main `videoFrameCallback` now performs a simple binary search against the pre-computed timestamps, making the core loop faster and more reliable.

- **Centralized Offset Logic:** Manual offset changes are now handled by a single function (`forceResyncWithOffset`) that re-bakes the timing data, ensuring UI consistency and removing duplicated code.

- **Accurate Drift Calculation:** Fixed the debug overlay's drift calculation to use the new `videoSyncedTime`, providing accurate diagnostics with both automatic and manual offsets.

### New Navigation & UI Features

- **Scroll-to-Seek:**
    - Implemented scroll-to-seek on both the main radar plot and the video player.
    - Features dynamic acceleration for fast scrubbing and single-frame precision for slow scrolling.
    - Uses a stateful approach and debouncing to provide a smooth, responsive UI while preventing video stutter.
    - All overlays now update instantly during scroll gestures for a seamless experience.

- **Frame-by-Frame Seeking:**
    - Added keyboard shortcuts (`ArrowUp`/`ArrowDown`) for single-frame video seeking.
    - Fixed a bug where radar seeking (`ArrowLeft`/`ArrowRight`) did not update the UI when paused.

- **God Mode (Zoom) Integration:**
    - Disabled timeline seeking via scroll wheel when zoom mode is active to prevent conflicting user actions.

### Bug Fixes

- Fixed a "sticky acceleration" bug where scroll speed was not reset after a seeking gesture.
- Corrected an inverted scroll direction on the timeline slider.
- Fixed a rendering error in the advanced debug overlay caused by a stale variable reference.
- Ensured persistent overlays are correctly hidden when debug overlays are active.
refactor/sync-centralize
RUSHIL AMBARISH KADU 6 months ago
parent
commit
02601fd52c
  1. 103
      steps/src/sync.js

103
steps/src/sync.js

@ -8,16 +8,19 @@ import {
updatePersistentOverlays, updatePersistentOverlays,
videoPlayer, videoPlayer,
frameCounter, frameCounter,
canvasContainer,
toggleEgoSpeed, toggleEgoSpeed,
egoSpeedDisplay, egoSpeedDisplay,
canSpeedDisplay, canSpeedDisplay,
} from "./dom.js"; } from "./dom.js";
import { VIDEO_FPS } from "./constants.js";
import { findRadarFrameIndexForTime, precomputeRadarVideoSync } from "./utils.js"; import { findRadarFrameIndexForTime, precomputeRadarVideoSync } from "./utils.js";
import { throttledUpdateExplorer } from "./dataExplorer.js"; import { throttledUpdateExplorer } from "./dataExplorer.js";
import { debugFlags } from "./debug.js"; import { debugFlags } from "./debug.js";
// --- [START] MOVED FROM DOM.JS --- // --- [START] MOVED FROM DOM.JS ---
//----------------------RESET VISUALIZATION Function----------------------// //----------------------RESET VISUALIZATION Function----------------------//
// Resets the visualization to its initial state. // Resets the visualization to its initial state.
export function resetVisualization() { export function resetVisualization() {
@ -72,7 +75,7 @@ export function forceResyncWithOffset() {
//----------------------UPDATE FRAME Function----------------------// //----------------------UPDATE FRAME Function----------------------//
// Updates the UI to reflect the current radar frame and synchronizes video playback. // Updates the UI to reflect the current radar frame and synchronizes video playback.
export function updateFrame(frame, forceVideoSeek = false) {
export function updateFrame(frame, forceVideoSeek = false, overrideTime = null) {
const startTime = performance.now(); //start emasuring timer of performance. const startTime = performance.now(); //start emasuring timer of performance.
if ( if (
!appState.vizData || !appState.vizData ||
@ -141,8 +144,11 @@ export function updateFrame(frame, forceVideoSeek = false) {
// --- START: FIX for Overlay Visibility During Scrubbing --- // --- START: FIX for Overlay Visibility During Scrubbing ---
// Update overlays here to ensure they refresh when scrubbing while paused. // Update overlays here to ensure they refresh when scrubbing while paused.
updatePersistentOverlays(videoPlayer.currentTime);
updateDebugOverlay(videoPlayer.currentTime);
// If an overrideTime is provided (e.g., from a scroll-seek), use it.
// Otherwise, use the video player's current time.
const displayTime = overrideTime !== null ? overrideTime : videoPlayer.currentTime;
updatePersistentOverlays(displayTime);
updateDebugOverlay(displayTime);
// --- END: FIX for Overlay Visibility During Scrubbing --- // --- END: FIX for Overlay Visibility During Scrubbing ---
} }
// --- [END] MOVED FROM DOM.JS --- // --- [END] MOVED FROM DOM.JS ---
@ -239,8 +245,19 @@ let lastScrollTime = 0;
let scrollSpeed = 0; let scrollSpeed = 0;
let seekDebounceTimer; let seekDebounceTimer;
let lastVideoScrollTime = 0;
let videoScrollSpeed = 0;
let videoSeekDebounceTimer;
let targetVideoTime = null; // NEW: State variable to track target time during scroll
function handleTimelineWheel(event) { function handleTimelineWheel(event) {
if (!appState.vizData) return;
// If no data, or if close-up mode is active, do not seek.
// The wheel event is used for zooming in close-up mode.
if (!appState.vizData || appState.isCloseUpMode) {
return;
}
event.preventDefault(); // Prevent default page scroll event.preventDefault(); // Prevent default page scroll
// 1. Pause playback if the user starts scrubbing. // 1. Pause playback if the user starts scrubbing.
@ -263,8 +280,8 @@ function handleTimelineWheel(event) {
// 4. Calculate the new frame index. // 4. Calculate the new frame index.
const direction = Math.sign(event.deltaY); const direction = Math.sign(event.deltaY);
// FIX: Invert the direction. Scrolling down (positive deltaY) should advance the frame.
let newFrame = appState.currentFrame - direction * seekAmount;
// Scrolling down (positive deltaY) should advance the frame (increase index).
let newFrame = appState.currentFrame + direction * seekAmount;
// 5. Clamp the new frame to the valid range. // 5. Clamp the new frame to the valid range.
const totalFrames = appState.vizData.radarFrames.length - 1; const totalFrames = appState.vizData.radarFrames.length - 1;
@ -288,7 +305,79 @@ function handleTimelineWheel(event) {
}, 300); // 300ms delay after the last scroll event. }, 300); // 300ms delay after the last scroll event.
} }
function handleVideoPanelWheel(event) {
if (!appState.vizData || !videoPlayer.src || videoPlayer.duration <= 0) return;
event.preventDefault(); // Prevent default page scroll
// 1. On the first scroll event, pause playback and initialize our target time.
if (appState.isPlaying) {
pausePlayback();
appState.isPlaying = false;
playPauseBtn.textContent = "Play";
}
if (targetVideoTime === null) {
targetVideoTime = videoPlayer.currentTime;
}
// 2. Calculate scroll speed for acceleration.
const now = performance.now();
const timeDelta = now - (lastVideoScrollTime || now);
lastVideoScrollTime = now;
videoScrollSpeed = timeDelta > 0 ? 1000 / timeDelta : videoScrollSpeed;
// 3. Map scroll speed to an acceleration curve.
const speedMultiplier = Math.floor(videoScrollSpeed / 8);
const seekAmount = Math.max(1, speedMultiplier); // Always move at least 1 frame.
// 4. Calculate the new target time based on our stateful variable.
const direction = Math.sign(event.deltaY);
const timeIncrement = (direction * seekAmount) / VIDEO_FPS;
targetVideoTime += timeIncrement;
// 5. Clamp the new time to the video's bounds.
targetVideoTime = Math.max(0, Math.min(targetVideoTime, videoPlayer.duration));
// 6. Find the corresponding radar frame for the new target time.
const newRadarFrame = findRadarFrameIndexForTime(targetVideoTime, appState.vizData);
console.log('--- Video Wheel Debug ---');
console.log(`Scroll Speed: ${videoScrollSpeed.toFixed(2)}`);
console.log(`Seek Amount (frames): ${seekAmount}`);
console.log(`Time Increment (s): ${timeIncrement.toFixed(4)}`);
console.log(`New Target Time (s): ${targetVideoTime.toFixed(4)}`);
console.log(`New Radar Frame: ${newRadarFrame}`);
// 7. Update the UI immediately for responsive feedback, but WITHOUT forcing a video seek.
updateFrame(newRadarFrame, false, targetVideoTime);
if (appState.p5_instance) appState.p5_instance.redraw();
if (appState.speedGraphInstance) appState.speedGraphInstance.redraw();
// 8. Use a debouncer for the expensive video seek.
clearTimeout(videoSeekDebounceTimer);
videoSeekDebounceTimer = setTimeout(() => {
console.log(`--- Debounced Seek Fired ---`);
console.log(`Final Seek Time (s): ${targetVideoTime.toFixed(4)}`);
// Perform the final, expensive video seek.
videoPlayer.currentTime = targetVideoTime;
// Reset the state variable, so the next scroll interaction starts fresh.
targetVideoTime = null;
lastVideoScrollTime = 0; // Also reset scroll time to prevent huge initial jump
videoScrollSpeed = 0; // FIX: Reset scroll speed to prevent "sticky" acceleration.
}, 150);
}
export function initSyncUIHandlers() { export function initSyncUIHandlers() {
timelineSlider.addEventListener("input", handleTimelineInput); timelineSlider.addEventListener("input", handleTimelineInput);
timelineSlider.addEventListener("wheel", handleTimelineWheel, { passive: false });
timelineSlider.addEventListener("wheel", handleTimelineWheel, {
passive: false,
});
// Use the canvas container for radar frame seeking
canvasContainer.addEventListener("wheel", handleTimelineWheel, {
passive: false,
});
// Use the video player for video frame seeking
videoPlayer.addEventListener("wheel", handleVideoPanelWheel, {
passive: false,
});
} }
Loading…
Cancel
Save