From d0b026734ada94cf2e1b1dbded7c4472a4955877 Mon Sep 17 00:00:00 2001 From: Michael Pilosov Date: Wed, 22 Apr 2026 14:29:24 -0600 Subject: [PATCH] compare: interpolate between frames for smooth point-trajectory motion Default is smooth (lerp each point between adjacent frames); a motion:step toggle preserves the snap-to-frame behaviour. Points missing in either adjacent frame are hidden during the transition to avoid pop-in. --- app/web/static/compare.js | 54 ++++++++++++++++++++++++++++++++-- app/web/static/style.css | 3 +- app/web/templates/compare.html | 12 ++++++-- app/web/templates/index.html | 2 +- 4 files changed, 64 insertions(+), 7 deletions(-) diff --git a/app/web/static/compare.js b/app/web/static/compare.js index b4f54f0..50f9d02 100644 --- a/app/web/static/compare.js +++ b/app/web/static/compare.js @@ -21,6 +21,7 @@ const playBtn = document.getElementById('cc-play'); const scrub = document.getElementById('cc-scrub'); const speedSel = document.getElementById('cc-speed'); const syncSel = document.getElementById('cc-sync'); +const motionSel = document.getElementById('cc-motion'); const timeAEl = document.getElementById('cc-time').querySelector('[data-role="time-a"]'); const timeBEl = document.getElementById('cc-time').querySelector('[data-role="time-b"]'); @@ -211,7 +212,43 @@ function createPanel({ slotId, panelEl, data }) { packedN = j; geo.attributes.position.needsUpdate = true; geo.setDrawRange(0, packedN); - // If there was a highlighted id, reapply it so the overlay follows. + applyHighlightForCurrentFrame(); + } + + // Pack an interpolated frame. uLocal is a continuous index in [0, T-1]. + // Points missing in either adjacent frame are skipped for the duration of + // that transition (no connect-back to the last-known position). + function setFrameInterpolated(uLocal) { + const T = data.frames.length; + if (T === 0) return; + if (uLocal <= 0) return setFrame(0); + if (uLocal >= T - 1) return setFrame(T - 1); + const f0 = Math.floor(uLocal); + const f1 = f0 + 1; + const t = uLocal - f0; + const fr0 = data.frames[f0], fr1 = data.frames[f1]; + const x0 = fr0.x, y0 = fr0.y, x1 = fr1.x, y1 = fr1.y; + const ptIds = data.point_ids; + let j = 0; + for (let i = 0; i < x0.length; i++) { + const a = x0[i], b = x1[i], c = y0[i], d = y1[i]; + if (a === null || a === undefined || Number.isNaN(a) + || b === null || b === undefined || Number.isNaN(b) + || c === null || c === undefined || Number.isNaN(c) + || d === null || d === undefined || Number.isNaN(d)) continue; + const xi = a + (b - a) * t; + const yi = c + (d - c) * t; + positions[j * 3 + 0] = xi; + positions[j * 3 + 1] = yi; + positions[j * 3 + 2] = 0; + packedX[j] = xi; + packedY[j] = yi; + packedId[j] = ptIds[i]; + j++; + } + packedN = j; + geo.attributes.position.needsUpdate = true; + geo.setDrawRange(0, packedN); applyHighlightForCurrentFrame(); } @@ -301,6 +338,7 @@ function createPanel({ slotId, panelEl, data }) { panelEl, data, setFrame, + setFrameInterpolated, setBounds, setHighlight, resize, @@ -400,12 +438,18 @@ async function main() { function applyU(u) { u = Math.max(0, Math.min(1, u)); + const smooth = motionSel.value === 'smooth'; for (const p of Object.values(panels)) { if (!p) continue; const T = framesOf(p); if (T <= 0) continue; - const idx = Math.max(0, Math.min(T - 1, Math.round(u * (T - 1)))); - p.setFrame(idx); + const uLocal = u * (T - 1); + if (smooth) { + p.setFrameInterpolated(uLocal); + } else { + const idx = Math.max(0, Math.min(T - 1, Math.round(uLocal))); + p.setFrame(idx); + } } timeAEl.textContent = timeLabelFor(panels.a, u); timeBEl.textContent = timeLabelFor(panels.b, u); @@ -476,6 +520,10 @@ async function main() { speedSel.addEventListener('change', () => { lastTs = 0; }); + motionSel.addEventListener('change', () => { + applyU(parseFloat(scrub.value) / SCRUB_MAX); + }); + // ---- linked hover ----------------------------------------------------- function wireHover(pA, pB) { if (!pA) return; diff --git a/app/web/static/style.css b/app/web/static/style.css index 265b35e..aadd309 100644 --- a/app/web/static/style.css +++ b/app/web/static/style.css @@ -1627,7 +1627,8 @@ button.submit:disabled { background: var(--faint); border-color: var(--faint); c .compare-controls .cc-time-b { color: var(--warm); } .compare-controls .cc-speed-wrap, -.compare-controls .cc-sync-wrap { +.compare-controls .cc-sync-wrap, +.compare-controls .cc-motion-wrap { display: inline-flex; align-items: center; gap: 0.35rem; diff --git a/app/web/templates/compare.html b/app/web/templates/compare.html index ab2bad5..c36c336 100644 --- a/app/web/templates/compare.html +++ b/app/web/templates/compare.html @@ -4,7 +4,7 @@ embedding notebook · compare - + - + diff --git a/app/web/templates/index.html b/app/web/templates/index.html index 6dfd542..1cefdf9 100644 --- a/app/web/templates/index.html +++ b/app/web/templates/index.html @@ -4,7 +4,7 @@ embedding notebook - +