diff --git a/steps/src/debug.js b/steps/src/debug.js new file mode 100644 index 0000000..4d9aa3c --- /dev/null +++ b/steps/src/debug.js @@ -0,0 +1,15 @@ +// This file centralizes all debug logging flags for the application. +// To enable a specific set of logs, set the corresponding flag to `true`. +// These flags can also be modified at runtime via the browser console +// by accessing the global `debugFlags` object (e.g., `debugFlags.sync = true`). + +export const debugFlags = { + // Logs from videoFrameCallback and animationLoop in sync.js + sync: false, + + // Logs from the main p5.js draw() functions (e.g., radarSketch.js) + drawing: false, + + // Logs related to file loading, parsing, and caching + fileLoading: false, +}; \ No newline at end of file diff --git a/steps/src/main.js b/steps/src/main.js index eb767d4..bc46e58 100644 --- a/steps/src/main.js +++ b/steps/src/main.js @@ -32,11 +32,9 @@ import { startPlayback, pausePlayback, stopPlayback, - forceResyncWithOffset, initSyncUIHandlers, updateFrame, resetVisualization, - handleTimelineInput, } from "./sync.js"; import { radarSketch } from "./p5/radarSketch.js"; import { speedGraphSketch } from "./p5/speedGraphSketch.js"; @@ -57,7 +55,9 @@ import { formatTime, } from "./utils.js"; import { appState } from "./state.js"; +import { debugFlags } from "./debug.js"; // Import the new debug flags window.appState = appState; // exposing the appState to console +window.debugFlags = debugFlags; // Expose debug flags to the console for runtime toggling import { themeToggleBtn, canvasContainer, @@ -317,20 +317,17 @@ function finalizeSetup(_parsedJsonData) { canvasPlaceholder.style.display = "none"; featureToggles.classList.remove("hidden"); - // --- START OF THE FIX --- - // This is the critical step. Before we do anything else, we loop through the - // radar data and recalculate the relative timestamp for every single frame. - // This ensures the data is perfectly synced to the video's confirmed timeline. - if (appState.vizData && appState.videoStartDate) { + // This is the critical step. We loop through the radar data ONCE to create + // a relative timestamp in seconds for every frame. This simplifies all + // future synchronization math. + if (appState.vizData) { appState.vizData.radarFrames.forEach((frame) => { - frame.timestampMs = - appState.radarStartTimeMs + - frame.timestamp - - appState.videoStartDate.getTime(); + // frame.timestamp is the relative time in ms from the radar's start. + // We convert it to seconds for easier comparison with video.mediaTime. + frame.relativeTimeSec = frame.timestamp / 1000; }); } - // --- END OF THE FIX --- - + // Create the p5 instances if (!appState.p5_instance) { appState.p5_instance = new p5(radarSketch); @@ -345,8 +342,6 @@ function finalizeSetup(_parsedJsonData) { if (!appState.speedGraphInstance) { appState.speedGraphInstance = new p5(speedGraphSketch); } - // The previous logic for setting the frame and redrawing was correct. - // It failed because the underlying timestamp data was wrong. resetVisualization(); appState.speedGraphInstance.setData(appState.vizData, videoPlayer.duration); appState.speedGraphInstance.redraw(); @@ -358,6 +353,7 @@ function finalizeSetup(_parsedJsonData) { snrMaxInput.value = appState.globalMaxSnr.toFixed(1); } } + // Sets up the video player with the given file URL. function setupVideoPlayer(fileURL) { videoPlayer.src = fileURL; @@ -366,89 +362,6 @@ function setupVideoPlayer(fileURL) { videoPlayer.playbackRate = parseFloat(speedSlider.value); } -// In src/main.js, add this new function -function loadVideoWithProgress(videoObject) { - if (!videoObject) return; - - showModal("Loading video...", false, true); - updateModalProgress(0); - - // Define event handlers so we can add and remove them correctly - const onProgress = () => { - if (videoPlayer.duration > 0) { - // Find the end of the buffered content - const bufferedEnd = - videoPlayer.buffered.length > 0 ? videoPlayer.buffered.end(0) : 0; - const percent = (bufferedEnd / videoPlayer.duration) * 100; - updateModalProgress(percent); - } - }; - - const onCanPlayThrough = () => { - updateModalProgress(100); - // Give the user a moment to see 100% before closing the modal - setTimeout(() => { - document.getElementById("modal-ok-btn").click(); - }, 400); - - // Clean up the event listeners we added - videoPlayer.removeEventListener("progress", onProgress); - videoPlayer.removeEventListener("canplaythrough", onCanPlayThrough); - }; - - const onError = () => { - showModal("Error: Could not load the video file."); - // Clean up event listeners on error - videoPlayer.removeEventListener("progress", onProgress); - videoPlayer.removeEventListener("canplaythrough", onCanPlayThrough); - videoPlayer.removeEventListener("error", onError); - }; - - // This one-time event is for re-syncing data once the video's metadata is ready - - videoPlayer.addEventListener( - "loadedmetadata", - () => { - // This is the perfect time to re-sync data if needed - if (appState.vizData) { - appState.vizData.radarFrames.forEach((frame) => { - frame.timestampMs = - appState.radarStartTimeMs + - frame.timestamp - - appState.videoStartDate.getTime(); - }); - resetVisualization(); - } - - // --- START: New Speed Graph Logic --- - // If we have data and the video is ready, create/update the speed graph - if (appState.vizData && videoPlayer.duration > 0) { - speedGraphPlaceholder.classList.add("hidden"); - if (!appState.speedGraphInstance) { - appState.speedGraphInstance = new p5(speedGraphSketch); - } - appState.speedGraphInstance.setData( - appState.vizData, - videoPlayer.duration - ); - } - // --- END: New Speed Graph Logic --- - }, - { once: true } - ); // { once: true } makes sure this runs only once per load - - // { once: true } //makes sure this runs only once per load - - // Add the listeners for progress tracking - videoPlayer.addEventListener("progress", onProgress); - videoPlayer.addEventListener("canplaythrough", onCanPlayThrough); - videoPlayer.addEventListener("error", onError); - - // Create the object URL and set the video source to trigger loading - const fileURL = URL.createObjectURL(videoObject); - setupVideoPlayer(fileURL); -} - // Event listener for loading JSON file. loadJsonBtn.addEventListener("click", () => jsonFileInput.click()); loadVideoBtn.addEventListener("click", () => videoFileInput.click()); @@ -647,7 +560,7 @@ fullscreenBtn.addEventListener("click", () => { // Event listener for offset input change. offsetInput.addEventListener("input", () => { autoOffsetIndicator.classList.add("hidden"); - localStorage.setItem("visualizerOffset", offsetInput.value); + // The value is now saved to localStorage only when 'Enter' is pressed. }); // Event listener for apply SNR button click. @@ -675,11 +588,12 @@ playPauseBtn.addEventListener("click", () => { if (!appState.vizData && !videoPlayer.src) return; appState.isPlaying = !appState.isPlaying; - playPauseBtn.textContent = appState.isPlaying ? "Pause" : "Play"; if (appState.isPlaying) { + playPauseBtn.textContent = "Pause"; startPlayback(); } else { + playPauseBtn.textContent = "Play"; pausePlayback(); } }); @@ -727,7 +641,7 @@ timelineSlider.addEventListener("mousemove", (event) => { if (!frameData) return; // 3. Update the tooltip's content - const formattedTime = formatTime(frameData.timestampMs); + const formattedTime = formatTime(frameData.relativeTimeSec * 1000); timelineTooltip.innerHTML = `Frame: ${ frameIndex + 1 }
Time: ${formattedTime}`; @@ -954,12 +868,12 @@ function calculateAndSetOffset() { appState.radarStartTimeMs = jsonDate.getTime(); console.log(`Radar start date set to: ${jsonDate.toISOString()}`); if (appState.videoStartDate) { - const offset = + appState.offset = appState.radarStartTimeMs - appState.videoStartDate.getTime(); - offsetInput.value = offset; - localStorage.setItem("visualizerOffset", offset); + offsetInput.value = appState.offset; + localStorage.setItem("visualizerOffset", appState.offset); autoOffsetIndicator.classList.remove("hidden"); - console.log(`Auto-calculated offset: ${offset} ms`); + console.log(`Auto-calculated offset: ${appState.offset} ms`); } } } @@ -969,8 +883,17 @@ offsetInput.addEventListener("keydown", (event) => { if (event.key === "Enter") { // Prevent the default browser action for the Enter key (like submitting a form) event.preventDefault(); - // Call the new centralized function from sync.js - forceResyncWithOffset(); + + // Update state and persist + const newOffset = parseFloat(offsetInput.value) || 0; + appState.offset = newOffset; + localStorage.setItem("visualizerOffset", newOffset); + console.log(`Manual offset entered: ${appState.offset}ms`); + + // Force a resync of the video to the current frame + if (appState.vizData) { + updateFrame(appState.currentFrame, true); + } } }); @@ -982,8 +905,10 @@ document.addEventListener("DOMContentLoaded", () => { initDB(async () => { console.log("Database initialized. Checking for cached session..."); + // Load filenames and the last known offset from localStorage appState.jsonFilename = localStorage.getItem("jsonFilename"); appState.videoFilename = localStorage.getItem("videoFilename"); + appState.offset = parseFloat(localStorage.getItem("visualizerOffset")) || 0; if (appState.jsonFilename) { // --- START: FIX FOR AUTO-RELOAD --- diff --git a/steps/src/p5/radarSketch.js b/steps/src/p5/radarSketch.js index 759e417..891ac8a 100644 --- a/steps/src/p5/radarSketch.js +++ b/steps/src/p5/radarSketch.js @@ -1,4 +1,5 @@ import { appState } from "../state.js"; +import { debugFlags } from "../debug.js"; import { RADAR_X_MAX, // Define radar plot boundaries @@ -124,6 +125,8 @@ export const radarSketch = function (p) { }; p.draw = function () { + if (debugFlags.drawing) console.log("draw_DEBUG: radarSketch.draw() called."); + // --- START: FPS Calculation & Display --- const currentTime = p.millis(); if (lastFrameTime > 0) { diff --git a/steps/src/state.js b/steps/src/state.js index e9aeec0..d4464bc 100644 --- a/steps/src/state.js +++ b/steps/src/state.js @@ -9,6 +9,8 @@ export const appState = { vizData: null, zoomSketchInstance: null, // Add this line // Stores the processed CAN bus data (speed, time) + offset: 0, // The calculated or manually set offset in milliseconds. + videoStartDate: null, // The timestamp (in milliseconds) of the first radar frame, extracted from the JSON filename radarStartTimeMs: 0, diff --git a/steps/src/sync.js b/steps/src/sync.js index 70ba11a..c3567cb 100644 --- a/steps/src/sync.js +++ b/steps/src/sync.js @@ -1,7 +1,6 @@ import { appState } from "./state.js"; import { timelineSlider, - speedSlider, offsetInput, stopBtn, playPauseBtn, @@ -15,6 +14,7 @@ import { } from "./dom.js"; import { findRadarFrameIndexForTime } from "./utils.js"; import { throttledUpdateExplorer } from "./dataExplorer.js"; +import { debugFlags } from "./debug.js"; // --- [START] MOVED FROM DOM.JS --- @@ -30,14 +30,8 @@ export function resetVisualization() { // --- NEW Playback Control Functions --- -let seekDebounceTimer = null; -let lastScrollTime = 0; -let scrollSpeed = 0; export function startPlayback() { if (videoPlayer.src && videoPlayer.readyState > 1) { - appState.masterClockStart = performance.now(); - appState.mediaTimeStart = videoPlayer.currentTime; - appState.lastSyncTime = appState.masterClockStart; videoPlayer.play(); videoPlayer.requestVideoFrameCallback(videoFrameCallback); // Start the high-precision loop } @@ -54,10 +48,12 @@ export function forceResyncWithOffset() { // Make sure visualization data is loaded before proceeding if (!appState.vizData) return; - console.log( - `Forcing resync with new offset: ${offsetInput.value}` - ); + const newOffset = parseFloat(offsetInput.value) || 0; + appState.offset = newOffset; // Update the central state + localStorage.setItem("visualizerOffset", newOffset); // Persist it + console.log(`Forcing resync with new offset: ${appState.offset}ms`); + // If the video is playing, pause it to allow for precise frame tuning. if (appState.isPlaying) { // Directly pause playback and update state, avoiding a synthetic click. @@ -73,7 +69,7 @@ export function forceResyncWithOffset() { //----------------------UPDATE FRAME Function----------------------// // Updates the UI to reflect the current radar frame and synchronizes video playback. -export function updateFrame(frame, forceVideoSeek) { +export function updateFrame(frame, forceVideoSeek = false) { const startTime = performance.now(); //start emasuring timer of performance. if ( !appState.vizData || @@ -111,19 +107,18 @@ export function updateFrame(frame, forceVideoSeek) { canSpeedDisplay.classList.add("hidden"); } // --- END OF NEW BLOCK --- - - let timeForUpdates = videoPlayer.currentTime; // NEW: Default to the video's current time - + if ( forceVideoSeek && videoPlayer.src && videoPlayer.readyState > 1 && - appState.videoStartDate && frameData ) { - const offsetMs = parseFloat(offsetInput.value) || 0; - const targetRadarTimeMs = frameData.timestampMs; - const targetVideoTimeSec = (targetRadarTimeMs - offsetMs) / 1000; + // Convert frame's relative time to the video's timeline + const targetRadarTimeSec = frameData.relativeTimeSec; + const targetVideoTimeSec = targetRadarTimeSec + (appState.offset / 1000); + + if (targetVideoTimeSec >= 0 && targetVideoTimeSec <= videoPlayer.duration) { // Ensure target time is within video duration if (Math.abs(videoPlayer.currentTime - targetVideoTimeSec) > 0.05) { @@ -131,20 +126,12 @@ export function updateFrame(frame, forceVideoSeek) { videoPlayer.currentTime = targetVideoTimeSec; // Seek video if drift is significant } // MODIFIED: Use the calculated target time for our updates, not the stale videoPlayer.currentTime - timeForUpdates = targetVideoTimeSec; // Update time for subsequent UI updates } } // End of forceVideoSeek block - if (!appState.isPlaying) { - // MODIFIED: Use our new synchronized time variable - updatePersistentOverlays(timeForUpdates); - } - // --- End of fix --- - // --- START: Conditional Redraw Logic --- - // Only force a redraw from here if the animation loop is NOT running. - // When playing, the animationLoop is responsible for redrawing. - if (!appState.isPlaying && appState.p5_instance) appState.p5_instance.redraw(); - if (!appState.isPlaying && appState.speedGraphInstance) appState.speedGraphInstance.redraw(); + // The animationLoop is now responsible for all redraws. + // We no longer call redraw() from here. + // --- NEW: Centralized Explorer Update --- throttledUpdateExplorer(); // --- END: Centralized Explorer Update --- @@ -162,114 +149,46 @@ export function stopPlayback() { } } +/** + * DATA LOOP: Runs on the video's clock (~30 FPS). + * Its ONLY job is to update appState.currentFrame. It does NO drawing. + */ export function videoFrameCallback(now, metadata) { - // If the video is no longer playing, stop the callback loop. - if (!appState.isPlaying || videoPlayer.paused) { + if (debugFlags.sync) console.log("vfc_DEBUG: videoFrameCallback running."); + + if (!appState.isPlaying || videoPlayer.paused || !appState.vizData) { return; } - // This is now the main animation driver during playback. - // It's perfectly synced with the video's frame presentation. - const currentTime = metadata.mediaTime; - const frameIndex = findRadarFrameIndexForTime(currentTime * 1000); + // 1. Get video time and calculate the target time on the radar's timeline. + const videoNowSec = metadata.mediaTime; + const targetRadarTimeSec = videoNowSec - (appState.offset / 1000); - updateFrame(frameIndex, false); // Update radar, but don't seek video. + // 2. Find the corresponding radar frame index. + const frameIndex = findRadarFrameIndexForTime(targetRadarTimeSec, appState.vizData); + + // 3. Update the application state. This is the ONLY state this function changes. + if (frameIndex !== appState.currentFrame) { + appState.currentFrame = frameIndex; + } // Re-register the callback for the next frame to create a loop videoPlayer.requestVideoFrameCallback(videoFrameCallback); } +/** + * RENDER LOOP: Runs on the monitor's refresh rate (~60+ FPS). + * Its ONLY job is to draw the current state. It does NO data calculation. + */ export function animationLoop() { - if (!appState.isPlaying) return; - - // Get the current playback speed from the slider - const playbackSpeed = parseFloat(speedSlider.value); - // Calculate the elapsed real time since the master clock started - const elapsedRealTime = performance.now() - appState.masterClockStart; - // Calculate the current media time based on the master clock, initial media time, elapsed real time, and playback speed - const currentMediaTime = - appState.mediaTimeStart + (elapsedRealTime / 1000) * playbackSpeed; - - // Update radar frame based on the master clock - // Check if visualization data and video start date are available - if (appState.vizData && appState.videoStartDate) { - // Get the offset from the input field, default to 0 if not a valid number - // --- START: Corrected Logic --- - const offsetMs = parseFloat(offsetInput.value) || 0; - // The master clock represents the VIDEO's timeline. - // To find the corresponding RADAR time, we must add the offset. - const targetRadarTimeMs = currentMediaTime * 1000 + offsetMs; - - const targetFrame = findRadarFrameIndexForTime( - targetRadarTimeMs, - appState.vizData - ); - // --- END: Corrected Logic --- - if (targetFrame !== appState.currentFrame) { - // Update the displayed frame if it's different from the current one - updateFrame(targetFrame, false); - } - } - - // Periodically check for drift between master clock and video element - const now = performance.now(); - if (now - appState.lastSyncTime > 150) { - const videoTime = videoPlayer.currentTime; - const drift = Math.abs(currentMediaTime - videoTime); - // Resync if drift is > 200ms - if (drift > 0.2) { // The drift threshold is 200ms. - console.warn(`Resyncing video. Drift was: ${drift.toFixed(3)}s`); - - // --- START: Resync Storm "Circuit Breaker" --- - if (appState.isResyncLockdownEnabled) { - const now = performance.now(); - // If the last resync was recent (within 2s), increment the counter. Otherwise, reset it. - if (appState.lastResyncTimestamp && (now - appState.lastResyncTimestamp < 2000)) { - appState.consecutiveResyncs = (appState.consecutiveResyncs || 0) + 1; - } else { - appState.consecutiveResyncs = 1; - } - appState.lastResyncTimestamp = now; - - // If more than 2 consecutive resyncs have occurred, trigger the lockdown. - if (appState.consecutiveResyncs > 2) { - // --- START: FIX for Lockdown Loop --- - if (!appState.isInLockdown) { // Only trigger if not already in lockdown - console.warn("Resync storm detected! Pausing playback to recover..."); - appState.isInLockdown = true; // Enter lockdown state - pausePlayback(); // Pause the video. - - // After a 1-second pause, resume playback. - // startPlayback() will handle the clock reset automatically. - setTimeout(() => { - console.log("Resuming playback after lockdown."); - appState.isInLockdown = false; // Exit lockdown state - startPlayback(); // Resume playback, which now correctly resets the clock. - }, 1000); // 1-second pause. - - appState.consecutiveResyncs = 0; // Reset the counter. - return; // Exit the animation loop for this frame to allow the pause. - } - // --- END: FIX for Lockdown Loop --- - } - } - // --- END: Resync Storm "Circuit Breaker" --- + if (debugFlags.sync) console.log("anim_DEBUG: animationLoop running."); - videoPlayer.currentTime = currentMediaTime; // Perform the standard resync. - } - - appState.lastSyncTime = now; - } - - // Stop playback at the end of the video - if (currentMediaTime >= videoPlayer.duration) { - stopBtn.click(); - return; - } + // 1. Update all UI elements based on the current frame. + updateFrame(appState.currentFrame); // Update debug overlay information - updatePersistentOverlays(currentMediaTime); - updateDebugOverlay(currentMediaTime); + updatePersistentOverlays(videoPlayer.currentTime); + updateDebugOverlay(videoPlayer.currentTime); // --- START: Centralized Redraw Logic --- // Explicitly redraw all active sketches in sync with the animation frame. @@ -278,92 +197,27 @@ export function animationLoop() { // --- END: Centralized Redraw Logic --- // Request the next frame - requestAnimationFrame(animationLoop); -} - -export function handleTimelineInput(event) { - if (!appState.vizData) return; - updateDebugOverlay(videoPlayer.currentTime); - updatePersistentOverlays(videoPlayer.currentTime); - // --- 1. Live Seeking (Throttled for performance) --- - // This part gives you the immediate visual feedback as you drag the slider. - // We use a simple timestamp check to prevent it from running too often. - const now = performance.now(); - if ( - !timelineSlider.lastInputTime || - now - timelineSlider.lastInputTime > 32 - ) { - // ~30fps throttle - if (appState.isPlaying) { - videoPlayer.pause(); - appState.isPlaying = false; - playPauseBtn.textContent = "Play"; - } - const frame = parseInt(event.target.value, 10); - updateFrame(frame, true); - appState.mediaTimeStart = videoPlayer.currentTime; - appState.masterClockStart = now; + if (appState.isPlaying) { + requestAnimationFrame(animationLoop); } - - // --- 2. Final, Precise Sync (Debounced for reliability) --- - // This part ensures a perfect sync only AFTER you stop moving the slider. - clearTimeout(seekDebounceTimer); // Always cancel the previously scheduled sync - - seekDebounceTimer = setTimeout(() => { - console.log("Slider movement stopped. Performing final, debounced resync."); - const finalFrame = parseInt(event.target.value, 10); - updateFrame(finalFrame, true); // Perform the final, precise seek - - // Also update the debug overlay with the final, settled time - updateDebugOverlay(videoPlayer.currentTime); - }, 250); // Wait for 250ms of inactivity before firing } -export function handleTimelineWheel(event) { +export function handleTimelineInput(event) { if (!appState.vizData) return; - // 1. Prevent the page from scrolling up and down - event.preventDefault(); - - // 2. Calculate scroll speed - const now = performance.now(); - const timeDelta = now - (lastScrollTime || now); // Handle first scroll - lastScrollTime = now; - // Calculate speed as "events per second", giving more weight to recent, fast scrolls - scrollSpeed = timeDelta > 0 ? 1000 / timeDelta : scrollSpeed; - - // 3. Map scroll speed to a dynamic seek multiplier - // This creates a nice acceleration curve. The '50' is a sensitivity value you can adjust. - const speedMultiplier = 1 + Math.floor(scrollSpeed / 4); - const baseSeekAmount = 1; // Base frames to move on a slow scroll - let seekAmount = Math.max(baseSeekAmount, speedMultiplier); - - // 4. Calculate the new frame index - const direction = Math.sign(event.deltaY); // +1 for down/right, -1 for up/left - const currentFrame = parseInt(timelineSlider.value, 10); - let newFrame = currentFrame - seekAmount * direction; - - // Clamp the new frame to the valid range - const totalFrames = appState.vizData.radarFrames.length - 1; - newFrame = Math.max(0, Math.min(newFrame, totalFrames)); - // 5. Update the UI if (appState.isPlaying) { - playPauseBtn.click(); // Pause if playing + pausePlayback(); + appState.isPlaying = false; + playPauseBtn.textContent = "Play"; } - updateFrame(newFrame, true); - // 6. Reuse the debouncer for a final, precise sync after scrolling stops - clearTimeout(seekDebounceTimer); - seekDebounceTimer = setTimeout(() => { - console.log("Scrolling stopped. Performing final, debounced resync."); - updateFrame(newFrame, true); - updatePersistentOverlays(videoPlayer.currentTime); - updateDebugOverlay(videoPlayer.currentTime); - }, 300); // Wait 300ms after the last scroll event - throttledUpdateExplorer(); + const frame = parseInt(event.target.value, 10); + updateFrame(frame, true); // Seek the video to the new frame + // Manually trigger redraws since the animation loop is not running + if (appState.p5_instance) appState.p5_instance.redraw(); + if (appState.speedGraphInstance) appState.speedGraphInstance.redraw(); } export function initSyncUIHandlers() { timelineSlider.addEventListener("input", handleTimelineInput); - timelineSlider.addEventListener("wheel", handleTimelineWheel); } diff --git a/steps/src/utils.js b/steps/src/utils.js index 22939d0..b12f7be 100644 --- a/steps/src/utils.js +++ b/steps/src/utils.js @@ -1,4 +1,4 @@ -export function findRadarFrameIndexForTime(targetTimeMs, vizData) { +export function findRadarFrameIndexForTime(targetTimeSec, vizData) { if (!vizData || vizData.radarFrames.length === 0) return -1; // Initialize low, high, and answer variables for binary search // 'ans' will store the index of the closest frame found so far @@ -11,7 +11,7 @@ export function findRadarFrameIndexForTime(targetTimeMs, vizData) { let mid = Math.floor((low + high) / 2); // If the current frame's timestamp is less than or equal to the target time, // it's a potential answer, and we try to find a more recent one in the right half. - if (vizData.radarFrames[mid].timestampMs <= targetTimeMs) { + if (vizData.radarFrames[mid].relativeTimeSec <= targetTimeSec) { ans = mid; low = mid + 1; } else { @@ -144,4 +144,3 @@ export function formatUTCTime(date) { const milliseconds = String(date.getUTCMilliseconds()).padStart(3, '0'); return `${hours}:${minutes}:${seconds}.${milliseconds}`; } -