Browse Source

Might be the solution an architectue change.

refactor/sync-centralize
RUSHIL AMBARISH KADU 6 months ago
parent
commit
98c818365a
  1. 15
      steps/src/debug.js
  2. 139
      steps/src/main.js
  3. 3
      steps/src/p5/radarSketch.js
  4. 2
      steps/src/state.js
  5. 252
      steps/src/sync.js
  6. 5
      steps/src/utils.js

15
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,
};

139
steps/src/main.js

@ -32,11 +32,9 @@ import {
startPlayback, startPlayback,
pausePlayback, pausePlayback,
stopPlayback, stopPlayback,
forceResyncWithOffset,
initSyncUIHandlers, initSyncUIHandlers,
updateFrame, updateFrame,
resetVisualization, resetVisualization,
handleTimelineInput,
} from "./sync.js"; } from "./sync.js";
import { radarSketch } from "./p5/radarSketch.js"; import { radarSketch } from "./p5/radarSketch.js";
import { speedGraphSketch } from "./p5/speedGraphSketch.js"; import { speedGraphSketch } from "./p5/speedGraphSketch.js";
@ -57,7 +55,9 @@ import {
formatTime, formatTime,
} from "./utils.js"; } from "./utils.js";
import { appState } from "./state.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.appState = appState; // exposing the appState to console
window.debugFlags = debugFlags; // Expose debug flags to the console for runtime toggling
import { import {
themeToggleBtn, themeToggleBtn,
canvasContainer, canvasContainer,
@ -317,20 +317,17 @@ function finalizeSetup(_parsedJsonData) {
canvasPlaceholder.style.display = "none"; canvasPlaceholder.style.display = "none";
featureToggles.classList.remove("hidden"); 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) => { 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 // Create the p5 instances
if (!appState.p5_instance) { if (!appState.p5_instance) {
appState.p5_instance = new p5(radarSketch); appState.p5_instance = new p5(radarSketch);
@ -345,8 +342,6 @@ function finalizeSetup(_parsedJsonData) {
if (!appState.speedGraphInstance) { if (!appState.speedGraphInstance) {
appState.speedGraphInstance = new p5(speedGraphSketch); 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(); resetVisualization();
appState.speedGraphInstance.setData(appState.vizData, videoPlayer.duration); appState.speedGraphInstance.setData(appState.vizData, videoPlayer.duration);
appState.speedGraphInstance.redraw(); appState.speedGraphInstance.redraw();
@ -358,6 +353,7 @@ function finalizeSetup(_parsedJsonData) {
snrMaxInput.value = appState.globalMaxSnr.toFixed(1); snrMaxInput.value = appState.globalMaxSnr.toFixed(1);
} }
} }
// Sets up the video player with the given file URL. // Sets up the video player with the given file URL.
function setupVideoPlayer(fileURL) { function setupVideoPlayer(fileURL) {
videoPlayer.src = fileURL; videoPlayer.src = fileURL;
@ -366,89 +362,6 @@ function setupVideoPlayer(fileURL) {
videoPlayer.playbackRate = parseFloat(speedSlider.value); 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. // Event listener for loading JSON file.
loadJsonBtn.addEventListener("click", () => jsonFileInput.click()); loadJsonBtn.addEventListener("click", () => jsonFileInput.click());
loadVideoBtn.addEventListener("click", () => videoFileInput.click()); loadVideoBtn.addEventListener("click", () => videoFileInput.click());
@ -647,7 +560,7 @@ fullscreenBtn.addEventListener("click", () => {
// Event listener for offset input change. // Event listener for offset input change.
offsetInput.addEventListener("input", () => { offsetInput.addEventListener("input", () => {
autoOffsetIndicator.classList.add("hidden"); 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. // Event listener for apply SNR button click.
@ -675,11 +588,12 @@ playPauseBtn.addEventListener("click", () => {
if (!appState.vizData && !videoPlayer.src) return; if (!appState.vizData && !videoPlayer.src) return;
appState.isPlaying = !appState.isPlaying; appState.isPlaying = !appState.isPlaying;
playPauseBtn.textContent = appState.isPlaying ? "Pause" : "Play";
if (appState.isPlaying) { if (appState.isPlaying) {
playPauseBtn.textContent = "Pause";
startPlayback(); startPlayback();
} else { } else {
playPauseBtn.textContent = "Play";
pausePlayback(); pausePlayback();
} }
}); });
@ -727,7 +641,7 @@ timelineSlider.addEventListener("mousemove", (event) => {
if (!frameData) return; if (!frameData) return;
// 3. Update the tooltip's content // 3. Update the tooltip's content
const formattedTime = formatTime(frameData.timestampMs);
const formattedTime = formatTime(frameData.relativeTimeSec * 1000);
timelineTooltip.innerHTML = `Frame: ${ timelineTooltip.innerHTML = `Frame: ${
frameIndex + 1 frameIndex + 1
}<br>Time: ${formattedTime}`; }<br>Time: ${formattedTime}`;
@ -954,12 +868,12 @@ function calculateAndSetOffset() {
appState.radarStartTimeMs = jsonDate.getTime(); appState.radarStartTimeMs = jsonDate.getTime();
console.log(`Radar start date set to: ${jsonDate.toISOString()}`); console.log(`Radar start date set to: ${jsonDate.toISOString()}`);
if (appState.videoStartDate) { if (appState.videoStartDate) {
const offset =
appState.offset =
appState.radarStartTimeMs - appState.videoStartDate.getTime(); 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"); 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") { if (event.key === "Enter") {
// Prevent the default browser action for the Enter key (like submitting a form) // Prevent the default browser action for the Enter key (like submitting a form)
event.preventDefault(); 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 () => { initDB(async () => {
console.log("Database initialized. Checking for cached session..."); console.log("Database initialized. Checking for cached session...");
// Load filenames and the last known offset from localStorage
appState.jsonFilename = localStorage.getItem("jsonFilename"); appState.jsonFilename = localStorage.getItem("jsonFilename");
appState.videoFilename = localStorage.getItem("videoFilename"); appState.videoFilename = localStorage.getItem("videoFilename");
appState.offset = parseFloat(localStorage.getItem("visualizerOffset")) || 0;
if (appState.jsonFilename) { if (appState.jsonFilename) {
// --- START: FIX FOR AUTO-RELOAD --- // --- START: FIX FOR AUTO-RELOAD ---

3
steps/src/p5/radarSketch.js

@ -1,4 +1,5 @@
import { appState } from "../state.js"; import { appState } from "../state.js";
import { debugFlags } from "../debug.js";
import { import {
RADAR_X_MAX, RADAR_X_MAX,
// Define radar plot boundaries // Define radar plot boundaries
@ -124,6 +125,8 @@ export const radarSketch = function (p) {
}; };
p.draw = function () { p.draw = function () {
if (debugFlags.drawing) console.log("draw_DEBUG: radarSketch.draw() called.");
// --- START: FPS Calculation & Display --- // --- START: FPS Calculation & Display ---
const currentTime = p.millis(); const currentTime = p.millis();
if (lastFrameTime > 0) { if (lastFrameTime > 0) {

2
steps/src/state.js

@ -9,6 +9,8 @@ export const appState = {
vizData: null, vizData: null,
zoomSketchInstance: null, // Add this line zoomSketchInstance: null, // Add this line
// Stores the processed CAN bus data (speed, time) // Stores the processed CAN bus data (speed, time)
offset: 0, // The calculated or manually set offset in milliseconds.
videoStartDate: null, videoStartDate: null,
// The timestamp (in milliseconds) of the first radar frame, extracted from the JSON filename // The timestamp (in milliseconds) of the first radar frame, extracted from the JSON filename
radarStartTimeMs: 0, radarStartTimeMs: 0,

252
steps/src/sync.js

@ -1,7 +1,6 @@
import { appState } from "./state.js"; import { appState } from "./state.js";
import { import {
timelineSlider, timelineSlider,
speedSlider,
offsetInput, offsetInput,
stopBtn, stopBtn,
playPauseBtn, playPauseBtn,
@ -15,6 +14,7 @@ import {
} from "./dom.js"; } from "./dom.js";
import { findRadarFrameIndexForTime } from "./utils.js"; import { findRadarFrameIndexForTime } from "./utils.js";
import { throttledUpdateExplorer } from "./dataExplorer.js"; import { throttledUpdateExplorer } from "./dataExplorer.js";
import { debugFlags } from "./debug.js";
// --- [START] MOVED FROM DOM.JS --- // --- [START] MOVED FROM DOM.JS ---
@ -30,14 +30,8 @@ export function resetVisualization() {
// --- NEW Playback Control Functions --- // --- NEW Playback Control Functions ---
let seekDebounceTimer = null;
let lastScrollTime = 0;
let scrollSpeed = 0;
export function startPlayback() { export function startPlayback() {
if (videoPlayer.src && videoPlayer.readyState > 1) { if (videoPlayer.src && videoPlayer.readyState > 1) {
appState.masterClockStart = performance.now();
appState.mediaTimeStart = videoPlayer.currentTime;
appState.lastSyncTime = appState.masterClockStart;
videoPlayer.play(); videoPlayer.play();
videoPlayer.requestVideoFrameCallback(videoFrameCallback); // Start the high-precision loop videoPlayer.requestVideoFrameCallback(videoFrameCallback); // Start the high-precision loop
} }
@ -54,10 +48,12 @@ export function forceResyncWithOffset() {
// Make sure visualization data is loaded before proceeding // Make sure visualization data is loaded before proceeding
if (!appState.vizData) return; 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 the video is playing, pause it to allow for precise frame tuning.
if (appState.isPlaying) { if (appState.isPlaying) {
// Directly pause playback and update state, avoiding a synthetic click. // Directly pause playback and update state, avoiding a synthetic click.
@ -73,7 +69,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) {
export function updateFrame(frame, forceVideoSeek = false) {
const startTime = performance.now(); //start emasuring timer of performance. const startTime = performance.now(); //start emasuring timer of performance.
if ( if (
!appState.vizData || !appState.vizData ||
@ -111,19 +107,18 @@ export function updateFrame(frame, forceVideoSeek) {
canSpeedDisplay.classList.add("hidden"); canSpeedDisplay.classList.add("hidden");
} }
// --- END OF NEW BLOCK --- // --- END OF NEW BLOCK ---
let timeForUpdates = videoPlayer.currentTime; // NEW: Default to the video's current time
if ( if (
forceVideoSeek && forceVideoSeek &&
videoPlayer.src && videoPlayer.src &&
videoPlayer.readyState > 1 && videoPlayer.readyState > 1 &&
appState.videoStartDate &&
frameData 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) { if (targetVideoTimeSec >= 0 && targetVideoTimeSec <= videoPlayer.duration) {
// Ensure target time is within video duration // Ensure target time is within video duration
if (Math.abs(videoPlayer.currentTime - targetVideoTimeSec) > 0.05) { 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 videoPlayer.currentTime = targetVideoTimeSec; // Seek video if drift is significant
} }
// MODIFIED: Use the calculated target time for our updates, not the stale videoPlayer.currentTime // 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 } // 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 --- // --- NEW: Centralized Explorer Update ---
throttledUpdateExplorer(); throttledUpdateExplorer();
// --- END: Centralized Explorer Update --- // --- 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) { 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; 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 // Re-register the callback for the next frame to create a loop
videoPlayer.requestVideoFrameCallback(videoFrameCallback); 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() { 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 // Update debug overlay information
updatePersistentOverlays(currentMediaTime);
updateDebugOverlay(currentMediaTime);
updatePersistentOverlays(videoPlayer.currentTime);
updateDebugOverlay(videoPlayer.currentTime);
// --- START: Centralized Redraw Logic --- // --- START: Centralized Redraw Logic ---
// Explicitly redraw all active sketches in sync with the animation frame. // Explicitly redraw all active sketches in sync with the animation frame.
@ -278,92 +197,27 @@ export function animationLoop() {
// --- END: Centralized Redraw Logic --- // --- END: Centralized Redraw Logic ---
// Request the next frame // 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; 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) { 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() { export function initSyncUIHandlers() {
timelineSlider.addEventListener("input", handleTimelineInput); timelineSlider.addEventListener("input", handleTimelineInput);
timelineSlider.addEventListener("wheel", handleTimelineWheel);
} }

5
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; if (!vizData || vizData.radarFrames.length === 0) return -1;
// Initialize low, high, and answer variables for binary search // Initialize low, high, and answer variables for binary search
// 'ans' will store the index of the closest frame found so far // '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); let mid = Math.floor((low + high) / 2);
// If the current frame's timestamp is less than or equal to the target time, // 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. // 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; ans = mid;
low = mid + 1; low = mid + 1;
} else { } else {
@ -144,4 +144,3 @@ export function formatUTCTime(date) {
const milliseconds = String(date.getUTCMilliseconds()).padStart(3, '0'); const milliseconds = String(date.getUTCMilliseconds()).padStart(3, '0');
return `${hours}:${minutes}:${seconds}.${milliseconds}`; return `${hours}:${minutes}:${seconds}.${milliseconds}`;
} }
Loading…
Cancel
Save