Visualizer work
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

223 lines
7.4 KiB

import { appState } from "./state.js";
import {
timelineSlider,
offsetInput,
stopBtn,
playPauseBtn,
updateDebugOverlay,
updatePersistentOverlays,
videoPlayer,
frameCounter,
toggleEgoSpeed,
egoSpeedDisplay,
canSpeedDisplay,
} from "./dom.js";
import { findRadarFrameIndexForTime } from "./utils.js";
import { throttledUpdateExplorer } from "./dataExplorer.js";
import { debugFlags } from "./debug.js";
// --- [START] MOVED FROM DOM.JS ---
//----------------------RESET VISUALIZATION Function----------------------//
// Resets the visualization to its initial state.
export function resetVisualization() {
appState.isPlaying = false;
playPauseBtn.textContent = "Play";
const numFrames = appState.vizData.radarFrames.length;
timelineSlider.max = numFrames > 0 ? numFrames - 1 : 0;
updateFrame(0, true); // Update to the first frame and force video seek
}
// --- NEW Playback Control Functions ---
export function startPlayback() {
if (videoPlayer.src && videoPlayer.readyState > 1) {
videoPlayer.play();
videoPlayer.requestVideoFrameCallback(videoFrameCallback); // Start the high-precision loop
}
requestAnimationFrame(animationLoop); // Keep rAF for non-video sync (e.g. scrubbing)
}
export function pausePlayback() {
if (videoPlayer.src) {
videoPlayer.pause();
}
}
export function forceResyncWithOffset() {
// Make sure visualization data is loaded before proceeding
if (!appState.vizData) return;
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.
pausePlayback();
appState.isPlaying = false;
playPauseBtn.textContent = "Play";
}
// Call updateFrame, forcing it to resync the video to the current radar frame
// using the new offset value from the input box.
updateFrame(appState.currentFrame, true);
}
//----------------------UPDATE FRAME Function----------------------//
// Updates the UI to reflect the current radar frame and synchronizes video playback.
export function updateFrame(frame, forceVideoSeek = false) {
const startTime = performance.now(); //start emasuring timer of performance.
if (
!appState.vizData ||
frame < 0 ||
frame >= appState.vizData.radarFrames.length
)
// Exit if no visualization data or invalid frame.
return; // Exit if no visualization data or invalid frame
appState.currentFrame = frame;
timelineSlider.value = appState.currentFrame;
frameCounter.textContent = `Frame: ${appState.currentFrame + 1} / ${
appState.vizData.radarFrames.length
}`;
const frameData = appState.vizData.radarFrames[appState.currentFrame];
if (toggleEgoSpeed.checked && frameData) {
// Update ego speed display if enabled.
const egoVy_kmh = (frameData.egoVelocity[1] * 3.6).toFixed(1); // Convert m/s to km/h and format
egoSpeedDisplay.textContent = `Ego: ${egoVy_kmh} km/h`;
egoSpeedDisplay.classList.remove("hidden");
} else {
egoSpeedDisplay.classList.add("hidden"); // Hide ego speed display.
}
// --- ADD THIS NEW BLOCK ---
if (
frameData &&
frameData.canVehSpeed_kmph !== null &&
!isNaN(frameData.canVehSpeed_kmph)
) {
canSpeedDisplay.textContent = `CAN: ${frameData.canVehSpeed_kmph.toFixed(
1
)} km/h`;
canSpeedDisplay.classList.remove("hidden");
} else {
canSpeedDisplay.classList.add("hidden");
}
// --- END OF NEW BLOCK ---
if (
forceVideoSeek &&
videoPlayer.src &&
videoPlayer.readyState > 1 &&
frameData
) {
// 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) {
// Check for significant drift
videoPlayer.currentTime = targetVideoTimeSec; // Seek video if drift is significant
}
// MODIFIED: Use the calculated target time for our updates, not the stale videoPlayer.currentTime
}
} // End of forceVideoSeek block
// The animationLoop is now responsible for all redraws.
// We no longer call redraw() from here.
// --- NEW: Centralized Explorer Update ---
throttledUpdateExplorer();
// --- END: Centralized Explorer Update ---
const endTime = performance.now();
appState.lastFrameRenderTime = endTime - startTime; // <-- End timer and update state
}
// --- [END] MOVED FROM DOM.JS ---
export function stopPlayback() {
videoPlayer.pause();
if (appState.vizData) {
updateFrame(0, true);
} else if (videoPlayer.src) {
videoPlayer.currentTime = 0;
}
}
/**
* 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 (debugFlags.sync) console.log("vfc_DEBUG: videoFrameCallback running.");
if (!appState.isPlaying || videoPlayer.paused || !appState.vizData) {
return;
}
// 1. Get video time and calculate the target time on the radar's timeline.
const videoNowSec = metadata.mediaTime;
const targetRadarTimeSec = videoNowSec - (appState.offset / 1000);
// 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 (debugFlags.sync) console.log("anim_DEBUG: animationLoop running.");
// 1. Update all UI elements based on the current frame.
updateFrame(appState.currentFrame);
// Update debug overlay information
updatePersistentOverlays(videoPlayer.currentTime);
updateDebugOverlay(videoPlayer.currentTime);
// --- START: Centralized Redraw Logic ---
// Explicitly redraw all active sketches in sync with the animation frame.
if (appState.p5_instance) appState.p5_instance.redraw();
if (appState.speedGraphInstance) appState.speedGraphInstance.redraw();
// --- END: Centralized Redraw Logic ---
// Request the next frame
if (appState.isPlaying) {
requestAnimationFrame(animationLoop);
}
}
export function handleTimelineInput(event) {
if (!appState.vizData) return;
if (appState.isPlaying) {
pausePlayback();
appState.isPlaying = false;
playPauseBtn.textContent = "Play";
}
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);
}