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.
 
 
 

333 lines
12 KiB

import { appState } from "./state.js";
import {
timelineSlider,
speedSlider,
offsetInput,
stopBtn,
playPauseBtn,
updateDebugOverlay,
updatePersistentOverlays,
videoPlayer,
frameCounter,
toggleEgoSpeed,
egoSpeedDisplay,
canSpeedDisplay,
} from "./dom.js";
import { findRadarFrameIndexForTime } from "./utils.js";
import { throttledUpdateExplorer } from "./dataExplorer.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 ---
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
}
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;
console.log(
`Forcing resync with new offset: ${offsetInput.value}`
);
// 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) {
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 ---
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;
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
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();
// --- 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;
}
}
export function videoFrameCallback(now, metadata) {
// If the video is no longer playing, stop the callback loop.
if (!appState.isPlaying || videoPlayer.paused) {
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);
updateFrame(frameIndex, false); // Update radar, but don't seek video.
// Re-register the callback for the next frame to create a loop
videoPlayer.requestVideoFrameCallback(videoFrameCallback);
}
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 > 150ms
if (drift > 0.15) {
// Resync if drift is > 150ms
console.warn(`Resyncing video. Drift was: ${drift.toFixed(3)}s`);
videoPlayer.currentTime = currentMediaTime;
}
appState.lastSyncTime = now;
}
// Stop playback at the end of the video
if (currentMediaTime >= videoPlayer.duration) {
stopBtn.click();
return;
}
// Update debug overlay information
updatePersistentOverlays(currentMediaTime);
updateDebugOverlay(currentMediaTime);
// --- 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
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;
}
// --- 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) {
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);
updateDebugOverlay(videoPlayer.currentTime);
}, 300); // Wait 300ms after the last scroll event
throttledUpdateExplorer();
}
export function initSyncUIHandlers() {
timelineSlider.addEventListener("input", handleTimelineInput);
timelineSlider.addEventListener("wheel", handleTimelineWheel);
}