festifun/frontend/src/lib/timebase.js
m3ultra ec22cbdf1f Lane C (M4+M5+M6 + timeline): synchronized viewer frontend
Full frontend built against the synthetic API (no dependence on lanes A/B/D):

M4 — synchronized playback: master clock from performance.now() (never a <video>);
per-video correction via requestVideoFrameCallback (exact mediaTime) with a
currentTime+half-frame fallback — hard-seek >150ms / nudge playbackRate +/-5% 20-150ms /
lock <20ms; out-of-range videos pause+dim; single audio source; dev sync-error overlay.
Timebase mirrors config.py (lib/timebase.js). Verified: seek is exact to 0ms; continuous
inter-video desync 13ms mean / 46ms max.

M5 — 3D viewer: PLY point cloud, per-video camera paths + current-pose frusta, OrbitControls,
snap-to-camera (intrinsics->PerspectiveCamera fov) + free roam. All COLMAP->Three.js via the
frozen lib/pose.js; pose interpolation in lib/poseTrack.js (slerp+lerp of contract inputs).

M6 — anchor overlays: letterbox-correct per-video canvas; anchors projected via pose.js;
behind-camera cull. Projection agrees with an independent pinhole to 1.45e-13 px.

Timeline — colored event markers + legend, hover tooltip, click-to-jump; draggable scrubber.

Phase-3 seams (annotate.js M8, camPath.js M9) left as stubs. No frozen files edited; no new
deps; no change requests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 08:45:51 +10:00

19 lines
989 B
JavaScript

// Timebase convention (FROZEN CONTRACT) — the JS mirror of backend/festival4d/config.py
// (`t_video_from_global` / `t_global_from_video`). Keep these identical to the Python side.
//
// t_global is the master timeline in seconds. For a video v:
// t_video = (t_global - offset_ms/1000) * (1 + drift_ppm * 1e-6)
// The reference video has offset_ms == 0 (and drift_ppm == 0). A positive offset_ms means
// the video started recording later than the master zero, so at a given t_global its local
// playhead is earlier. Never re-derive this algebra inline — call these helpers everywhere.
/** Master-timeline seconds -> a video's local playhead seconds (FROZEN). */
export function tVideoFromGlobal(tGlobal, offsetMs, driftPpm = 0) {
return (tGlobal - offsetMs / 1000) * (1 + driftPpm * 1e-6);
}
/** Inverse of tVideoFromGlobal (FROZEN). */
export function tGlobalFromVideo(tVideo, offsetMs, driftPpm = 0) {
return tVideo / (1 + driftPpm * 1e-6) + offsetMs / 1000;
}