|
|
|
@ -137,6 +137,12 @@ export function updateFrame(frame, forceVideoSeek = false) { |
|
|
|
// --- END: Centralized Explorer Update ---
|
|
|
|
const endTime = performance.now(); |
|
|
|
appState.lastFrameRenderTime = endTime - startTime; // <-- End timer and update state
|
|
|
|
|
|
|
|
// --- START: FIX for Overlay Visibility During Scrubbing ---
|
|
|
|
// Update overlays here to ensure they refresh when scrubbing while paused.
|
|
|
|
updatePersistentOverlays(videoPlayer.currentTime); |
|
|
|
updateDebugOverlay(videoPlayer.currentTime); |
|
|
|
// --- END: FIX for Overlay Visibility During Scrubbing ---
|
|
|
|
} |
|
|
|
// --- [END] MOVED FROM DOM.JS ---
|
|
|
|
|
|
|
|
@ -154,7 +160,9 @@ export function stopPlayback() { |
|
|
|
* 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 (debugFlags.sync) { |
|
|
|
console.log(`[${performance.now().toFixed(3)}] vfc_DEBUG: videoFrameCallback running.`); |
|
|
|
} |
|
|
|
|
|
|
|
if (!appState.isPlaying || videoPlayer.paused || !appState.vizData) { |
|
|
|
return; |
|
|
|
@ -170,7 +178,7 @@ export function videoFrameCallback(now, metadata) { |
|
|
|
// 3. Update the application state. This is the ONLY state this function changes.
|
|
|
|
if (frameIndex !== appState.currentFrame) { |
|
|
|
appState.currentFrame = frameIndex; |
|
|
|
updateFrame(appState.currentFrame); // <-- MOVE UI UPDATE CALL HERE
|
|
|
|
// This is the ONLY state this function should change. All UI updates are in animationLoop.
|
|
|
|
} |
|
|
|
|
|
|
|
// Re-register the callback for the next frame to create a loop
|
|
|
|
@ -182,12 +190,12 @@ export function videoFrameCallback(now, metadata) { |
|
|
|
* 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."); |
|
|
|
if (debugFlags.sync) { |
|
|
|
console.log(`[${performance.now().toFixed(3)}] anim_DEBUG: animationLoop running.`); |
|
|
|
} |
|
|
|
|
|
|
|
// Update debug overlay information
|
|
|
|
updatePersistentOverlays(videoPlayer.currentTime); |
|
|
|
// updatePersistentOverlays(); // This is a duplicate call and can be removed
|
|
|
|
updateDebugOverlay(videoPlayer.currentTime); |
|
|
|
// The render loop is responsible for ALL UI updates, ensuring perfect sync.
|
|
|
|
updateFrame(appState.currentFrame); |
|
|
|
|
|
|
|
// --- START: Centralized Redraw Logic ---
|
|
|
|
// Explicitly redraw all active sketches in sync with the animation frame.
|
|
|
|
@ -201,22 +209,86 @@ export function animationLoop() { |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
let timelineDebounceTimer; |
|
|
|
export function handleTimelineInput(event) { |
|
|
|
if (!appState.vizData) return; |
|
|
|
|
|
|
|
// 1. If playing, pause playback to allow scrubbing.
|
|
|
|
if (appState.isPlaying) { |
|
|
|
pausePlayback(); |
|
|
|
appState.isPlaying = false; |
|
|
|
playPauseBtn.textContent = "Play"; |
|
|
|
} |
|
|
|
|
|
|
|
// 2. Get the target frame from the slider.
|
|
|
|
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
|
|
|
|
|
|
|
|
// 3. Update UI immediately for responsiveness, but WITHOUT forcing a video seek.
|
|
|
|
updateFrame(frame, false); |
|
|
|
if (appState.p5_instance) appState.p5_instance.redraw(); |
|
|
|
if (appState.speedGraphInstance) appState.speedGraphInstance.redraw(); |
|
|
|
|
|
|
|
// 4. Use a debouncer to perform the expensive video seek after the user stops dragging.
|
|
|
|
clearTimeout(timelineDebounceTimer); |
|
|
|
timelineDebounceTimer = setTimeout(() => { |
|
|
|
updateFrame(appState.currentFrame, true); // Perform final, precise video seek.
|
|
|
|
}, 300); // 300ms delay after last input event.
|
|
|
|
} |
|
|
|
|
|
|
|
let lastScrollTime = 0; |
|
|
|
let scrollSpeed = 0; |
|
|
|
let seekDebounceTimer; |
|
|
|
|
|
|
|
function handleTimelineWheel(event) { |
|
|
|
if (!appState.vizData) return; |
|
|
|
event.preventDefault(); // Prevent default page scroll
|
|
|
|
|
|
|
|
// 1. Pause playback if the user starts scrubbing.
|
|
|
|
if (appState.isPlaying) { |
|
|
|
pausePlayback(); |
|
|
|
appState.isPlaying = false; |
|
|
|
playPauseBtn.textContent = "Play"; |
|
|
|
} |
|
|
|
|
|
|
|
// 2. Calculate scroll speed to create a dynamic seek amount.
|
|
|
|
const now = performance.now(); |
|
|
|
const timeDelta = now - (lastScrollTime || now); |
|
|
|
lastScrollTime = now; |
|
|
|
scrollSpeed = timeDelta > 0 ? 1000 / timeDelta : scrollSpeed; |
|
|
|
|
|
|
|
// 3. Map scroll speed to an acceleration curve.
|
|
|
|
// The sensitivity value (e.g., 4) can be adjusted for more/less acceleration.
|
|
|
|
const speedMultiplier = 1 + Math.floor(scrollSpeed / 4); |
|
|
|
const seekAmount = Math.max(1, speedMultiplier); // Ensure we always move at least 1 frame.
|
|
|
|
|
|
|
|
// 4. Calculate the new frame index.
|
|
|
|
const direction = Math.sign(event.deltaY); |
|
|
|
// FIX: Invert the direction. Scrolling down (positive deltaY) should advance the frame.
|
|
|
|
let newFrame = appState.currentFrame + direction * seekAmount; |
|
|
|
|
|
|
|
// 5. Clamp the new frame to the valid range.
|
|
|
|
const totalFrames = appState.vizData.radarFrames.length - 1; |
|
|
|
newFrame = Math.max(0, Math.min(newFrame, totalFrames)); |
|
|
|
|
|
|
|
// 6. Update the UI immediately for responsive feedback, but WITHOUT forcing a video seek.
|
|
|
|
// This makes the slider feel fast without causing video stutter.
|
|
|
|
updateFrame(newFrame, false); |
|
|
|
// --- START: Immediate Redraw for Responsiveness ---
|
|
|
|
// Manually trigger redraws here so the radar visualization updates as the user scrolls.
|
|
|
|
if (appState.p5_instance) appState.p5_instance.redraw(); |
|
|
|
if (appState.speedGraphInstance) appState.speedGraphInstance.redraw(); |
|
|
|
// --- END: Immediate Redraw for Responsiveness ---
|
|
|
|
|
|
|
|
// 7. Use a debouncer for the expensive video seek. This will only run once
|
|
|
|
// after the user has finished scrolling, ensuring a final, precise sync.
|
|
|
|
clearTimeout(seekDebounceTimer); |
|
|
|
seekDebounceTimer = setTimeout(() => { |
|
|
|
// Perform the final, expensive video seek.
|
|
|
|
updateFrame(appState.currentFrame, true); |
|
|
|
}, 300); // 300ms delay after the last scroll event.
|
|
|
|
} |
|
|
|
|
|
|
|
export function initSyncUIHandlers() { |
|
|
|
timelineSlider.addEventListener("input", handleTimelineInput); |
|
|
|
timelineSlider.addEventListener("wheel", handleTimelineWheel, { passive: false }); |
|
|
|
} |