@@ -248,6 +248,20 @@ export function TrajectoryOverlay({
248248
249249 let frameId = 0 ;
250250
251+ // Pre-compute max velocity magnitude per track once per metadata load.
252+ // Avoids an O(n) reduce across all samples on every rendered frame.
253+ const maxVelocityByTrack = new Map < string , number > ( ) ;
254+ if ( metadata ) {
255+ for ( const [ trackName , track ] of Object . entries ( metadata . tracks ) ) {
256+ let max = 0 ;
257+ for ( const sample of track . samples ) {
258+ const m = getVectorMagnitude ( sample . velocityVector2DPerSecond ) ;
259+ if ( m > max ) max = m ;
260+ }
261+ maxVelocityByTrack . set ( trackName , max ) ;
262+ }
263+ }
264+
251265 const resizeCanvas = ( ) => {
252266 const rect = container . getBoundingClientRect ( ) ;
253267 const devicePixelRatio = window . devicePixelRatio || 1 ;
@@ -308,9 +322,7 @@ export function TrajectoryOverlay({
308322 }
309323
310324 const latestSample = track . samples [ endIndex ] ;
311- const maxVelocityMagnitude = track . samples . reduce ( ( currentMax , sample ) => (
312- Math . max ( currentMax , getVectorMagnitude ( sample . velocityVector2DPerSecond ) )
313- ) , 0 ) ;
325+ const maxVelocityMagnitude = maxVelocityByTrack . get ( trackName ) ?? 0 ;
314326
315327 const startIndex = historyWindowSec == null
316328 ? 1
@@ -438,25 +450,85 @@ export function TrajectoryOverlay({
438450 } ) ;
439451 } ;
440452
441- const loop = ( ) => {
453+ // Type helper for the requestVideoFrameCallback API (not yet in lib.dom.d.ts everywhere)
454+ type VideoWithRVFC = HTMLVideoElement & {
455+ requestVideoFrameCallback : ( cb : ( ) => void ) => number ;
456+ cancelVideoFrameCallback : ( id : number ) => void ;
457+ } ;
458+
459+ const video = videoRef . current ;
460+ const supportsRVFC = video != null && 'requestVideoFrameCallback' in HTMLVideoElement . prototype ;
461+ let rvcId = 0 ;
462+
463+ const renderOnce = ( ) => {
442464 resizeCanvas ( ) ;
443465 renderFrame ( ) ;
444- frameId = window . requestAnimationFrame ( loop ) ;
445466 } ;
446467
447- const resizeObserver = new ResizeObserver ( ( ) => {
448- resizeCanvas ( ) ;
468+ // When rVFC is supported, render exactly once per decoded video frame (perfectly
469+ // GPU-synced, zero wasted work while paused or between frames).
470+ const scheduleRVFC = ( ) => {
471+ if ( ! video ) return ;
472+ rvcId = ( video as VideoWithRVFC ) . requestVideoFrameCallback ( ( ) => {
473+ renderOnce ( ) ;
474+ if ( ! video . paused ) scheduleRVFC ( ) ;
475+ } ) ;
476+ } ;
477+
478+ // Fallback: plain rAF loop (only runs while playing — see handlePlay/handlePause).
479+ const fallbackLoop = ( ) => {
449480 renderFrame ( ) ;
450- } ) ;
481+ frameId = window . requestAnimationFrame ( fallbackLoop ) ;
482+ } ;
483+
484+ const startLoop = ( ) => {
485+ if ( supportsRVFC ) {
486+ scheduleRVFC ( ) ;
487+ } else {
488+ window . cancelAnimationFrame ( frameId ) ;
489+ frameId = window . requestAnimationFrame ( fallbackLoop ) ;
490+ }
491+ } ;
492+
493+ const stopLoop = ( ) => {
494+ if ( supportsRVFC ) {
495+ ( video as VideoWithRVFC ) . cancelVideoFrameCallback ( rvcId ) ;
496+ } else {
497+ window . cancelAnimationFrame ( frameId ) ;
498+ }
499+ } ;
500+
501+ const handlePlay = ( ) => startLoop ( ) ;
502+ const handlePause = ( ) => { stopLoop ( ) ; renderOnce ( ) ; } ;
503+ // Render once per completed seek (covers scrubbing while paused).
504+ const handleSeeked = ( ) => { renderOnce ( ) ; } ;
505+
506+ const resizeObserver = new ResizeObserver ( ( ) => { renderOnce ( ) ; } ) ;
507+
508+ if ( video ) {
509+ video . addEventListener ( 'play' , handlePlay ) ;
510+ video . addEventListener ( 'pause' , handlePause ) ;
511+ video . addEventListener ( 'seeked' , handleSeeked ) ;
512+ }
451513
452514 resizeObserver . observe ( container ) ;
453515 resizeCanvas ( ) ;
454- renderFrame ( ) ;
455- frameId = window . requestAnimationFrame ( loop ) ;
516+ renderOnce ( ) ;
517+
518+ // If the video is already playing when this effect runs, kick off the loop.
519+ if ( video && ! video . paused ) {
520+ startLoop ( ) ;
521+ }
456522
457523 return ( ) => {
458- resizeObserver . disconnect ( ) ;
524+ if ( video ) {
525+ video . removeEventListener ( 'play' , handlePlay ) ;
526+ video . removeEventListener ( 'pause' , handlePause ) ;
527+ video . removeEventListener ( 'seeked' , handleSeeked ) ;
528+ stopLoop ( ) ;
529+ }
459530 window . cancelAnimationFrame ( frameId ) ;
531+ resizeObserver . disconnect ( ) ;
460532 } ;
461533 } , [ containerRef , enabled , historyWindowSec , metadata , poseColor . b , poseColor . g , poseColor . r , showBlackBackground , showPose , videoRef , visibleTrackNames ] ) ;
462534
0 commit comments