diff --git a/frontend/index.html b/frontend/index.html index 9de1c73..6df6c84 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -5,39 +5,206 @@ Festival 4D -

Festival 4D

-
Foundation hello page — proves the API contract, CORS, and the frozen pose helper.
-
Loading…
+
+
+

Festival 4D

+ synchronized multi-cam replay · 3D viewer · x-ray overlays +
+ +
+
+ +
+ +
+
drag to orbit · click a camera or press 1–9 to snap · Esc to detach
+
+
+
+
+
+ +
+
+ + + + 0:00.0 / 0:00.0 + + speed + +
+
+
+
+ +
+
sync error
+
+
+ +
Loading project…
+ diff --git a/frontend/src/lib/poseTrack.js b/frontend/src/lib/poseTrack.js new file mode 100644 index 0000000..56fc260 --- /dev/null +++ b/frontend/src/lib/poseTrack.js @@ -0,0 +1,57 @@ +// Pose lookup + interpolation over a video's `camera_poses` track (from GET /api/videos/{id}/poses). +// +// Poses arrive keyed by frame_idx / t_video_s (every ~0.5 s on the synthetic fixture, all +// registered). To render a smooth frustum / follow-cam at an arbitrary t_video we interpolate +// BETWEEN stored poses in COLMAP space — slerp the world->camera quaternion, lerp the +// translation — then hand the interpolated (q, t) to the FROZEN colmapToThreejs() helper. +// We interpolate the CONTRACT INPUTS; we never reimplement the COLMAP->Three.js conversion. + +import * as THREE from "three"; + +const _qa = new THREE.Quaternion(); +const _qb = new THREE.Quaternion(); + +/** + * Interpolated pose at local video time `tv` (seconds). Returns a COLMAP-convention pose + * `{ q:[w,x,y,z], t:[x,y,z], intrinsics, registered, t_video_s }` ready for colmapToThreejs, + * or null if the track is empty. Clamps to the endpoints (no extrapolation). + * @param {Array} poses ascending-by-t_video_s pose list + * @param {number} tv local video time (seconds) + */ +export function poseAt(poses, tv) { + if (!poses || poses.length === 0) return null; + const n = poses.length; + if (tv <= poses[0].t_video_s) return poses[0]; + if (tv >= poses[n - 1].t_video_s) return poses[n - 1]; + + // binary search for the bracketing pair [lo, hi] with poses[lo].t <= tv < poses[hi].t + let lo = 0; + let hi = n - 1; + while (hi - lo > 1) { + const mid = (lo + hi) >> 1; + if (poses[mid].t_video_s <= tv) lo = mid; + else hi = mid; + } + const a = poses[lo]; + const b = poses[hi]; + const span = b.t_video_s - a.t_video_s; + const f = span > 1e-9 ? (tv - a.t_video_s) / span : 0; + + // COLMAP quaternion order is [w, x, y, z]; THREE.Quaternion is (x, y, z, w). + _qa.set(a.q[1], a.q[2], a.q[3], a.q[0]); + _qb.set(b.q[1], b.q[2], b.q[3], b.q[0]); + _qa.slerp(_qb, f); + const q = [_qa.w, _qa.x, _qa.y, _qa.z]; + const t = [ + a.t[0] + (b.t[0] - a.t[0]) * f, + a.t[1] + (b.t[1] - a.t[1]) * f, + a.t[2] + (b.t[2] - a.t[2]) * f, + ]; + return { + q, + t, + intrinsics: a.intrinsics, + registered: a.registered && b.registered, + t_video_s: tv, + }; +} diff --git a/frontend/src/lib/timebase.js b/frontend/src/lib/timebase.js new file mode 100644 index 0000000..57c7bc6 --- /dev/null +++ b/frontend/src/lib/timebase.js @@ -0,0 +1,18 @@ +// 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; +} diff --git a/frontend/src/main.js b/frontend/src/main.js index 1bc0574..d4e8f8e 100644 --- a/frontend/src/main.js +++ b/frontend/src/main.js @@ -1,91 +1,205 @@ -// Foundation hello page. Fetches /api/manifest (proving the API + CORS work cross-origin) -// and runs the frozen pose.js self-test in the browser. Lane C (M4–M6) replaces this with -// the real app; the modules alongside it are stubs to fill in. +// Festival 4D viewer — composition root (spec M4/M5/M6 + timeline markers). +// +// Boots by loading the frozen synthetic API (manifest, poses, anchors, events), builds the +// video grid + 3D scene + timeline, wires transport controls + keyboard shortcuts, and runs a +// single master animation loop that: advances the master clock & corrects every video +// (transport), redraws each overlay for its displayed frame, updates the 3D frusta / follow-cam, +// moves the timeline playhead, and refreshes the dev sync overlay. The master clock lives in +// transport.js and is derived from performance.now() — never from a