Browse Source

Scrim now added to collapsible menu on left.

Fullscreen icon fixed.
Show overlay on radar for color mode.
Mouse scroll direction changed to correct way.
Persistent display update bug fixed.
refactor/modularize
RUSHIL AMBARISH KADU 9 months ago
parent
commit
611bf47a2a
  1. 110
      steps/context.md
  2. 20
      steps/index.html
  3. 25
      steps/src/dom.js
  4. 17
      steps/src/main.js

110
steps/context.md

@ -0,0 +1,110 @@
Context Document: Radar and Video Synchronizer Application
1. High-Level Overview
This document provides a detailed technical overview of the Radar and Video Synchronizer web application. Its purpose is to give an AI assistant a comprehensive understanding of the codebase to facilitate efficient and accurate development assistance.
Core Purpose: The application is a high-precision, browser-based tool for visualizing and synchronizing radar sensor data (from a JSON file) with a corresponding video file. It allows for detailed analysis of object tracks, point clouds, and vehicle dynamics.
Core Technologies:
Frontend: HTML5, Tailwind CSS
Logic: Modular JavaScript (ES6 Modules)
Visualization: p5.js library for rendering the radar plot and speed graph.
Data Handling: Web Workers and the Oboe.js library for non-blocking parsing of large JSON files.
Persistence: IndexedDB for caching session files (JSON, Video) and localStorage for user settings (UI layout, theme, etc.).
Key Features:
Synchronized playback of video and radar data.
Resizable two-panel layout (Radar and Video).
Collapsible sidebar for display settings.
Dynamic coloring of radar points (by SNR, Cluster, etc.).
Visualization of tracked object trajectories, speed, and Time-to-Collision (TTC).
A speed graph showing ego-velocity and CAN bus speed over time.
Robust keyboard shortcuts for power users.
Session management (Save/Load settings and file references).
2. Project Architecture & File Structure
The application follows a modern modular JavaScript architecture. Logic is separated into distinct files, each with a single responsibility. All source code is in the /src/ directory.
index.html: The main HTML file. It's a shell that contains the DOM structure and loads the main JavaScript module (/src/main.js).
/src/
main.js: The Orchestrator. This is the application's entry point. It initializes all other modules, wires up all event listeners (clicks, keydown, etc.), and manages the overall application lifecycle on DOMContentLoaded.
state.js: The Single Source of Truth. Exports a single global appState object that holds all dynamic data (e.g., vizData, isPlaying, currentFrame). All modules import and reference this object to get the current state.
dom.js: The UI Abstraction Layer. Exports constants for every key DOM element (videoPlayer, playPauseBtn, etc.). It also contains functions that directly manipulate the DOM, such as updateFrame() and updatePersistentOverlays(). For any changes to UI text or visibility, this is the primary file to inspect.
sync.js: The Heartbeat/Clock. Contains the animationLoop() function, which is the core of the synchronized playback. It uses performance.now() to create a high-precision clock, calculates the current media time, finds the corresponding radar frame, and handles resynchronization.
fileParsers.js: The Data Processor. Contains the logic for post-processing the raw data from files. parseVisualizationJson() takes the parsed JSON object and enriches it with calculated timestampMs values and determines global SNR ranges.
parser.worker.js: The Heavy Lifter. This Web Worker is responsible for parsing the potentially massive JSON file off the main thread to prevent the UI from freezing. It uses the Clarinet.js streaming parser for efficiency.
db.js: The Caching Layer. Manages all interactions with IndexedDB. It's used to save and load the JSON and video files so that subsequent sessions load almost instantly.
/p5/radarSketch.js: The p5.js sketch responsible for drawing the main radar visualization (point cloud, tracks, axes).
/p5/speedGraphSketch.js: The p5.js sketch for drawing the time-series speed graph.
drawUtils.js: The Artist's Toolkit. Contains pure drawing functions that are called by radarSketch.js. This is where the visual appearance (colors, shapes, lines, text) of the radar objects is defined. To change how tracks or points are drawn, modify this file.
utils.js: A collection of pure, reusable helper functions (e.g., findRadarFrameIndexForTime (binary search), timestamp parsers, throttle).
modal.js: Manages the logic for the pop-up modal dialogs (e.g., for notifications, confirmations, and progress bars).
theme.js: Handles the dark/light mode theme switching and persists the choice to localStorage.
constants.js: Stores shared, static values like VIDEO_FPS and radar plot boundaries.
3. Data Flow & State Management
Data Loading Sequence:
User Action: The user clicks a "Load" button in the UI (index.html).
Event Trigger: The click is handled by an event listener in main.js.
File Selection: The browser's file input is opened.
Caching: Upon file selection, the change event fires. In main.js, the file is immediately sent to db.js to be saved in IndexedDB via saveFileWithMetadata().
Parsing (JSON): The JSON file is passed to the parser.worker.js. The worker streams the file, constructs a complete JavaScript object, and sends it back to main.js.
Processing: main.js receives the parsed object and passes it to fileParsers.js's parseVisualizationJson() function. This function calculates the relative timestamps and other necessary metadata.
State Update: The processed data is stored in the central appState.vizData object in state.js.
UI Update: main.js calls functions in dom.js and creates new p5.js instances (radarSketch, speedGraphSketch) to render the data now available in appState.
State Management (appState):
The appState object in state.js is the central hub. All modules import it and read from it. It is mutated directly by the core logic in main.js and sync.js. Key properties include:
vizData: The large object containing all radar frames and track data.
isPlaying: A boolean that controls the animationLoop.
currentFrame: The integer index of the currently displayed radar frame.
videoStartDate, radarStartTimeMs: Date objects used to calculate the time offset.
p5_instance, speedGraphInstance: References to the active p5.js sketches.
4. Key Logic and Interaction Flows
Playback Synchronization (sync.js): The animationLoop is the core. It does not rely on the video's timeupdate event, which can be imprecise. Instead, it creates its own high-resolution timer with performance.now(). It calculates what the video's currentTime should be and then finds the corresponding radar frame using a binary search (findRadarFrameIndexForTime in utils.js). It periodically checks for drift between its calculated time and the actual video.currentTime and corrects the video if necessary.
UI Updates (dom.js): The updateFrame(frame, forceVideoSeek) function is the primary entry point for changing what's on screen. It updates the frame counter, seeks the video if forceVideoSeek is true, and calls the .redraw() methods on the p5 sketches. It is called both by the animationLoop (for smooth playback) and by UI event listeners like the timeline slider (for seeking).
Session Persistence (main.js & db.js): On DOMContentLoaded, the application first checks localStorage for saved filenames and UI settings. It then calls loadFreshFileFromDB from db.js, which attempts to load the files from IndexedDB. If successful, the application loads the cached data, bypassing the need for the user to re-select the files.
Keyboard Shortcuts (main.js): A single, comprehensive keydown event listener is attached to the document. It checks for various keys and programmatically triggers .click() events on the corresponding DOM elements (e.g., pressing Spacebar clicks the playPauseBtn). It includes a check to prevent shortcuts from firing when the user is typing in an input field.

20
steps/index.html

@ -116,14 +116,14 @@
<!-- Checkboxes -->
<div class="flex flex-col items-start gap-3 w-full">
<label class="flex items-center gap-2 text-sm cursor-pointer"><input type="checkbox" id="toggle-snr-color"
class="form-checkbox h-4 w-4 text-blue-600 rounded focus:ring-blue-500" /> Color by SNR (S)</label>
class="form-checkbox h-4 w-4 text-blue-600 rounded focus:ring-blue-500" /> Color by SNR (S) (1)</label>
<label class="flex items-center gap-2 text-sm cursor-pointer"><input type="checkbox" id="toggle-cluster-color"
class="form-checkbox h-4 w-4 text-blue-600 rounded focus:ring-blue-500" /> Color by Cluster</label>
class="form-checkbox h-4 w-4 text-blue-600 rounded focus:ring-blue-500" /> Color by Cluster (2) </label>
<label class="flex items-center gap-2 text-sm cursor-pointer"><input type="checkbox" id="toggle-inlier-color"
class="form-checkbox h-4 w-4 text-blue-600 rounded focus:ring-blue-500" /> Color by Inlier</label>
class="form-checkbox h-4 w-4 text-blue-600 rounded focus:ring-blue-500" /> Color by Inlier (3)</label>
<label class="flex items-center gap-2 text-sm cursor-pointer"><input type="checkbox"
id="toggle-stationary-color" class="form-checkbox h-4 w-4 text-blue-600 rounded focus:ring-blue-500" />
Color by Stationary</label>
Color by Stationary (4) </label>
<label class="flex items-center gap-2 text-sm cursor-pointer"><input type="checkbox" id="toggle-velocity"
class="form-checkbox h-4 w-4 text-blue-600 rounded focus:ring-blue-500" checked /> Show Object Details
(D)</label>
@ -204,6 +204,10 @@
</svg>
</button>
<!-- START: ADD THIS NEW SCRIM ELEMENT -->
<div id="menu-scrim" class="fixed inset-0 bg-black/50 z-20 hidden"></div>
<!-- END: ADD THIS NEW SCRIM ELEMENT -->
<!-- Main Content -->
<main class="flex-grow container mx-auto p-4 pt-8 flex flex-col lg:flex-row gap-6">
<div class="lg:w-1/2 flex flex-col gap-4">
@ -235,8 +239,8 @@
class="absolute top-0 left-0 bg-black bg-opacity-60 text-white p-2 font-mono text-xs hidden w-full"></div>
</div>
<div id="speed-graph-container"
class="w-full h-[27vh] bg-white dark:bg-gray-700 border border-gray-200 dark:border-gray-600 rounded-lg shadow-inner flex items-center justify-center">
<p id="speed-graph-placeholder" class="text-gray-500 dark:text-gray-400 text-lg">Load JSON to see speed graph
class="w-full h-[25vh] bg-white dark:bg-gray-700 border border-gray-200 dark:border-gray-600 rounded-lg shadow-inner flex items-center justify-center">
<p id="speed-graph-placeholder" class="text-gray-500 dark:text-gray-400 text-lg">Load JSON & Video to see speed graph
</p>
</div>
</div>
@ -277,9 +281,9 @@
</div>
<div class="flex items-center justify-center gap-4">
<button id="play-pause-btn"
class="px-5 py-2 rounded-lg bg-gray-200 dark:bg-gray-600 dark:hover:bg-gray-500 hover:bg-gray-300 font-semibold w-20">Play</button>
class="px-4 py-2 rounded-lg bg-gray-200 dark:bg-gray-600 dark:hover:bg-gray-500 hover:bg-gray-300 font-semibold w-20">Play</button>
<button id="stop-btn"
class="px-5 py-2 rounded-lg bg-gray-200 dark:bg-gray-600 dark:hover:bg-gray-500 hover:bg-gray-300 font-semibold">Stop</button>
class="px-5 py-2 rounded-lg bg-gray-200 dark:bg-gray-600 dark:hover:bg-gray-500 hover:bg-gray-300 font-semibold">STOP</button>
<div class="text-center">
<span id="frame-counter" class="font-mono text-lg">Frame: 0 / 0</span>
</div>

25
steps/src/dom.js

@ -94,12 +94,10 @@ export const toggleMenuBtn = document.getElementById("toggle-menu-btn");
export const fullscreenBtn = document.getElementById("fullscreen-btn");
export const mainContent = document.querySelector("main");
export const closeMenuBtn = document.getElementById("close-menu-btn");
export const fullscreenEnterIcon = document.getElementById(
"fullscreen-enter-icon"
);
export const fullscreenExitIcon = document.getElementById(
"fullscreen-exit-icon"
);
export const fullscreenEnterIcon = document.getElementById("fullscreen-enter-icon");
export const fullscreenExitIcon = document.getElementById("fullscreen-exit-icon");
export const menuScrim = document.getElementById("menu-scrim");
//----------------------UPDATE FRAME Function----------------------//
// Updates the UI to reflect the current radar frame and synchronizes video playback.
@ -281,6 +279,15 @@ export function updateDebugOverlay(currentMediaTime) {
debugOverlay.innerHTML = content.join("<br>"); // Update debug overlay content.
}
// This function checks the state of the color toggles and returns the active mode.
function getCurrentColorMode() {
if (toggleSnrColor.checked) return "Color by SNR (1)";
if (toggleClusterColor.checked) return "Color by Cluster (2)";
if (toggleInlierColor.checked) return "Color by Inlier (3)";
if (toggleStationaryColor.checked) return "Color by Stationary (4)";
return "Default"; // The default mode when no specific color toggle is checked
}
export function updatePersistentOverlays(currentMediaTime) {
// If we don't have the necessary data, hide the overlays and exit.
const isDebug1Visible = toggleDebugOverlay.checked;
@ -303,18 +310,18 @@ export function updatePersistentOverlays(currentMediaTime) {
// --- Update Radar Overlay ---
const currentRadarFrame = appState.vizData.radarFrames[appState.currentFrame];
if (currentRadarFrame) {
const absRadarTime = new Date(
appState.videoStartDate.getTime() + currentRadarFrame.timestampMs
);
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
const colorMode = getCurrentColorMode();
radarInfoOverlay.innerHTML = `
Frame: ${appState.currentFrame + 1}
Abs Time: ${formatUTCTime(absRadarTime)}
Drift: <b style="color: ${driftColor};">${driftMs.toFixed(0)}ms</b>
Mode: <b>${colorMode}</b>
`;
}

17
steps/src/main.js

@ -91,6 +91,7 @@ import {
closeMenuBtn,
fullscreenEnterIcon,
fullscreenExitIcon,
menuScrim,
} from "./dom.js";
import { initializeTheme } from "./theme.js";
@ -350,23 +351,27 @@ sessionFileInput.addEventListener("change", (event) => {
// --- END: Add Session Management Logic ---
// --- Collapsible Menu Logic ---
// --- Collapsible Menu Logic (Overlay Version) ---
function toggleMenu(show) {
if (show) {
collapsibleMenu.classList.remove("-translate-x-full");
mainContent.classList.add("lg:ml-96");
menuScrim.classList.remove("hidden"); // Show the scrim
// The line that pushed the content has been REMOVED.
} else {
collapsibleMenu.classList.add("-translate-x-full");
mainContent.classList.remove("lg:ml-96");
menuScrim.classList.add("hidden"); // Hide the scrim
}
}
// Open the menu
toggleMenuBtn.addEventListener("click", () => toggleMenu(true));
// Close the menu
// Close the menu with the 'X' button
closeMenuBtn.addEventListener("click", () => toggleMenu(false));
// NEW: Close the menu by clicking on the scrim
menuScrim.addEventListener("click", () => toggleMenu(false));
// --- Fullscreen Logic ---
fullscreenBtn.addEventListener("click", () => {
if (!document.fullscreenElement) {
@ -614,7 +619,7 @@ timelineSlider.addEventListener("wheel", (event) => {
// 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;
let newFrame = currentFrame - seekAmount * direction;
// Clamp the new frame to the valid range
const totalFrames = appState.vizData.radarFrames.length - 1;
@ -698,6 +703,8 @@ colorToggles.forEach((t) => {
});
}
if (appState.p5_instance) appState.p5_instance.redraw();
updatePersistentOverlays(videoPlayer.currentTime);
});
});

Loading…
Cancel
Save