diff --git a/steps/index.html b/steps/index.html
index fa94dce..b26b973 100644
--- a/steps/index.html
+++ b/steps/index.html
@@ -221,7 +221,7 @@
- Load CAN log to see speed graph
+ Load JSON to see speed graph
diff --git a/steps/src/dom.js b/steps/src/dom.js
index 377b97d..d395581 100644
--- a/steps/src/dom.js
+++ b/steps/src/dom.js
@@ -1,5 +1,7 @@
import { appState } from "./state.js";
-import { VIDEO_FPS } from "./constants.js"; // Import VIDEO_FPS for debug overlay calculations
+import { formatUTCTime } from "./utils.js";
+// Also import VIDEO_FPS from constants
+import { VIDEO_FPS } from "./constants.js";
// --- DOM Element References --- //
@@ -54,6 +56,8 @@ export const modalProgressContainer = document.getElementById("modal-progress-co
export const modalProgressBar = document.getElementById("modal-progress-bar");
export const modalProgressText = document.getElementById("modal-progress-text");
export const timelineTooltip = document.getElementById("timeline-tooltip");
+export const radarInfoOverlay = document.getElementById("radar-info-overlay");
+export const videoInfoOverlay = document.getElementById("video-info-overlay");
//----------------------UPDATE FRAME Function----------------------//
// Updates the UI to reflect the current radar frame and synchronizes video playback.
@@ -120,7 +124,7 @@ export function updateFrame(frame, forceVideoSeek) {
if (!appState.isPlaying) {
// MODIFIED: Use our new synchronized time variable
- updateDebugOverlay(timeForUpdates);
+ updatePersistentOverlays(timeForUpdates);
}
// --- End of fix ---
@@ -234,3 +238,42 @@ export function updateDebugOverlay(currentMediaTime) {
debugOverlay.innerHTML = content.join("
"); // Update debug overlay content.
}
+
+
+export function updatePersistentOverlays(currentMediaTime) {
+ // If we don't have the necessary data, hide the overlays and exit.
+ if (!appState.vizData || !appState.videoStartDate) {
+ radarInfoOverlay.classList.add('hidden');
+ videoInfoOverlay.classList.add('hidden');
+ return;
+ }
+
+ // Otherwise, make sure they are visible.
+ radarInfoOverlay.classList.remove('hidden');
+ videoInfoOverlay.classList.remove('hidden');
+
+ // --- Update Radar Overlay ---
+ const currentRadarFrame = appState.vizData.radarFrames[appState.currentFrame];
+ if (currentRadarFrame) {
+ const absRadarTime = new Date(appState.videoStartDate.getTime() + currentRadarFrame.timestampMs);
+ const targetRadarTimeMs = currentRadarFrame.timestampMs;
+ const offsetMs = parseFloat(offsetInput.value) || 0;
+ const driftMs = (currentMediaTime * 1000 + offsetMs) - targetRadarTimeMs;
+ const driftColor = Math.abs(driftMs) > 50 ? "#FF6347" : "#98FB98"; // Tomato red or Pale green
+
+ radarInfoOverlay.innerHTML = `
+ Frame: ${appState.currentFrame + 1}
+ Abs Time: ${formatUTCTime(absRadarTime)}
+ Drift: ${driftMs.toFixed(0)}ms
+ `;
+ }
+
+ // --- Update Video Overlay ---
+ const absVideoTime = new Date(appState.videoStartDate.getTime() + (currentMediaTime * 1000));
+ const videoFrame = Math.floor(currentMediaTime * VIDEO_FPS);
+
+ videoInfoOverlay.innerHTML = `
+ Frame: ${videoFrame}
+ Abs Time: ${formatUTCTime(absVideoTime)}
+ `;
+}
\ No newline at end of file
diff --git a/steps/src/main.js b/steps/src/main.js
index 06d9945..dd9547d 100644
--- a/steps/src/main.js
+++ b/steps/src/main.js
@@ -84,7 +84,9 @@ import { initializeTheme } from "./theme.js";
import { initDB, saveFileWithMetadata, loadFreshFileFromDB } from "./db.js";
-let seekDebounceTimer = null; // Add this line
+let seekDebounceTimer = null; //timeline slider variables.
+let lastScrollTime = 0; //timeline slider variables.
+let scrollSpeed = 0; //timeline slider variables.
// Sets up the video player with the given file URL.
function setupVideoPlayer(fileURL) {
@@ -191,7 +193,8 @@ clearCacheBtn.addEventListener("click", async () => {
}
});
-// In src/main.js, REPLACE the jsonFileInput event listener with this:
+// In main.js, REPLACE your existing jsonFileInput event listener with this entire block:
+
jsonFileInput.addEventListener("change", (event) => {
const file = event.target.files[0];
if (!file) return;
@@ -213,24 +216,22 @@ jsonFileInput.addEventListener("change", (event) => {
const { type, data, message, percent } = e.data;
if (type === "progress") {
- // Update the progress bar whenever the worker reports progress
updateModalProgress(percent);
} else if (type === "complete") {
- // Worker is done! Process the data it sent back.
updateModalProgress(100);
const result = await parseVisualizationJson(
- data, // Use the data object directly from the worker
+ data,
appState.radarStartTimeMs,
appState.videoStartDate
);
if (result.error) {
showModal(result.error);
+ worker.terminate(); // Terminate worker on error
return;
}
- // --- START: New Cleanup Logic ---
- // If p5.js instances already exist, remove them completely
+
if (appState.p5_instance) {
appState.p5_instance.remove();
appState.p5_instance = null;
@@ -238,10 +239,9 @@ jsonFileInput.addEventListener("change", (event) => {
if (appState.speedGraphInstance) {
appState.speedGraphInstance.remove();
appState.speedGraphInstance = null;
- // Also reset the placeholder text
speedGraphPlaceholder.classList.remove("hidden");
}
- // --- END: New Cleanup Logic ---
+
appState.vizData = result.data;
appState.globalMinSnr = result.minSnr;
appState.globalMaxSnr = result.maxSnr;
@@ -255,21 +255,23 @@ jsonFileInput.addEventListener("change", (event) => {
if (!appState.p5_instance) {
appState.p5_instance = new p5(radarSketch);
}
- if (appState.vizData && videoPlayer.duration) {
- if (!appState.speedGraphInstance) {
- appState.speedGraphInstance = new p5(speedGraphSketch);
- }
- appState.speedGraphInstance.setData(
- appState.vizData,
- videoPlayer.duration
- );
+
+ // --- START: This is the new, corrected logic ---
+ // After processing the new JSON, check if a video is already loaded and ready.
+ // If it is, this is the trigger to create or 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: This is the new, corrected logic ---
- // Close the modal and terminate the worker
document.getElementById("modal-ok-btn").click();
worker.terminate();
+
} else if (type === "error") {
- // The worker ran into an error
showModal(message);
worker.terminate();
}
@@ -390,8 +392,53 @@ timelineSlider.addEventListener("input", (event) => {
updateDebugOverlay(videoPlayer.currentTime);
}, 250); // Wait for 250ms of inactivity before firing
});
-// In src/main.js, add this new block of event listeners
+// --- Timeline Scroll-to-Seek Logic ---
+
+timelineSlider.addEventListener("wheel", (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
+ }
+ 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);
+ }, 300); // Wait 300ms after the last scroll event
+});
+
+// In src/main.js, add this new block of event listeners
// --- Timeline Scrub-to-Seek Preview Logic ---
timelineSlider.addEventListener("mouseover", () => {
@@ -412,10 +459,11 @@ timelineSlider.addEventListener("mousemove", (event) => {
const hoverFraction = (event.clientX - rect.left) / rect.width;
// 2. Calculate the corresponding frame index
- const sliderMax = parseInt(timelineSlider.max, 10) || (appState.vizData.radarFrames.length - 1);
-let frameIndex = Math.round(hoverFraction * sliderMax);
-// The value is already clamped by this calculation, but an extra check is safe
-frameIndex = Math.max(0, Math.min(frameIndex, sliderMax));
+ const sliderMax =
+ parseInt(timelineSlider.max, 10) || appState.vizData.radarFrames.length - 1;
+ let frameIndex = Math.round(hoverFraction * sliderMax);
+ // The value is already clamped by this calculation, but an extra check is safe
+ frameIndex = Math.max(0, Math.min(frameIndex, sliderMax));
const frameData = appState.vizData.radarFrames[frameIndex];
if (!frameData) return;
diff --git a/steps/src/sync.js b/steps/src/sync.js
index 4c9a568..298516c 100644
--- a/steps/src/sync.js
+++ b/steps/src/sync.js
@@ -6,6 +6,7 @@ import {
stopBtn,
updateFrame,
updateDebugOverlay,
+ updatePersistentOverlays,
} from "./dom.js";
import { findRadarFrameIndexForTime } from "./utils.js";
@@ -67,7 +68,7 @@ export function animationLoop() {
}
// Update debug overlay information
- updateDebugOverlay(currentMediaTime);
+ updatePersistentOverlays(currentMediaTime);
// Redraw the speed graph if an instance exists
if (appState.speedGraphInstance) appState.speedGraphInstance.redraw();
diff --git a/steps/src/utils.js b/steps/src/utils.js
index e59e2c3..ca8d497 100644
--- a/steps/src/utils.js
+++ b/steps/src/utils.js
@@ -145,4 +145,21 @@ export function formatTime(milliseconds) {
const ms = Math.round(milliseconds % 1000);
return `${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}.${String(ms).padStart(3, '0')}`;
-}
\ No newline at end of file
+}
+
+/**
+ * Formats a Date object into a HH:MM:SS.ms UTC string.
+ * @param {Date} date The date object to format.
+ * @returns {string} The formatted time string.
+ */
+export function formatUTCTime(date) {
+ if (!date || isNaN(date.getTime())) {
+ return "00:00:00.000";
+ }
+ const hours = String(date.getUTCHours()).padStart(2, '0');
+ const minutes = String(date.getUTCMinutes()).padStart(2, '0');
+ const seconds = String(date.getUTCSeconds()).padStart(2, '0');
+ const milliseconds = String(date.getUTCMilliseconds()).padStart(3, '0');
+ return `${hours}:${minutes}:${seconds}.${milliseconds}`;
+}
+