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.
This commit is contained in:
parent
fc6aad5516
commit
d0b026734a
@ -21,6 +21,7 @@ const playBtn = document.getElementById('cc-play');
|
|||||||
const scrub = document.getElementById('cc-scrub');
|
const scrub = document.getElementById('cc-scrub');
|
||||||
const speedSel = document.getElementById('cc-speed');
|
const speedSel = document.getElementById('cc-speed');
|
||||||
const syncSel = document.getElementById('cc-sync');
|
const syncSel = document.getElementById('cc-sync');
|
||||||
|
const motionSel = document.getElementById('cc-motion');
|
||||||
const timeAEl = document.getElementById('cc-time').querySelector('[data-role="time-a"]');
|
const timeAEl = document.getElementById('cc-time').querySelector('[data-role="time-a"]');
|
||||||
const timeBEl = document.getElementById('cc-time').querySelector('[data-role="time-b"]');
|
const timeBEl = document.getElementById('cc-time').querySelector('[data-role="time-b"]');
|
||||||
|
|
||||||
@ -211,7 +212,43 @@ function createPanel({ slotId, panelEl, data }) {
|
|||||||
packedN = j;
|
packedN = j;
|
||||||
geo.attributes.position.needsUpdate = true;
|
geo.attributes.position.needsUpdate = true;
|
||||||
geo.setDrawRange(0, packedN);
|
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();
|
applyHighlightForCurrentFrame();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -301,6 +338,7 @@ function createPanel({ slotId, panelEl, data }) {
|
|||||||
panelEl,
|
panelEl,
|
||||||
data,
|
data,
|
||||||
setFrame,
|
setFrame,
|
||||||
|
setFrameInterpolated,
|
||||||
setBounds,
|
setBounds,
|
||||||
setHighlight,
|
setHighlight,
|
||||||
resize,
|
resize,
|
||||||
@ -400,13 +438,19 @@ async function main() {
|
|||||||
|
|
||||||
function applyU(u) {
|
function applyU(u) {
|
||||||
u = Math.max(0, Math.min(1, u));
|
u = Math.max(0, Math.min(1, u));
|
||||||
|
const smooth = motionSel.value === 'smooth';
|
||||||
for (const p of Object.values(panels)) {
|
for (const p of Object.values(panels)) {
|
||||||
if (!p) continue;
|
if (!p) continue;
|
||||||
const T = framesOf(p);
|
const T = framesOf(p);
|
||||||
if (T <= 0) continue;
|
if (T <= 0) continue;
|
||||||
const idx = Math.max(0, Math.min(T - 1, Math.round(u * (T - 1))));
|
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);
|
p.setFrame(idx);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
timeAEl.textContent = timeLabelFor(panels.a, u);
|
timeAEl.textContent = timeLabelFor(panels.a, u);
|
||||||
timeBEl.textContent = timeLabelFor(panels.b, u);
|
timeBEl.textContent = timeLabelFor(panels.b, u);
|
||||||
}
|
}
|
||||||
@ -476,6 +520,10 @@ async function main() {
|
|||||||
|
|
||||||
speedSel.addEventListener('change', () => { lastTs = 0; });
|
speedSel.addEventListener('change', () => { lastTs = 0; });
|
||||||
|
|
||||||
|
motionSel.addEventListener('change', () => {
|
||||||
|
applyU(parseFloat(scrub.value) / SCRUB_MAX);
|
||||||
|
});
|
||||||
|
|
||||||
// ---- linked hover -----------------------------------------------------
|
// ---- linked hover -----------------------------------------------------
|
||||||
function wireHover(pA, pB) {
|
function wireHover(pA, pB) {
|
||||||
if (!pA) return;
|
if (!pA) return;
|
||||||
|
|||||||
@ -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-time-b { color: var(--warm); }
|
||||||
|
|
||||||
.compare-controls .cc-speed-wrap,
|
.compare-controls .cc-speed-wrap,
|
||||||
.compare-controls .cc-sync-wrap {
|
.compare-controls .cc-sync-wrap,
|
||||||
|
.compare-controls .cc-motion-wrap {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.35rem;
|
gap: 0.35rem;
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||||
<title>embedding notebook · compare</title>
|
<title>embedding notebook · compare</title>
|
||||||
<link rel="stylesheet" href="/static/style.css?v=21" />
|
<link rel="stylesheet" href="/static/style.css?v=22" />
|
||||||
<script type="importmap">
|
<script type="importmap">
|
||||||
{
|
{
|
||||||
"imports": {
|
"imports": {
|
||||||
@ -91,6 +91,14 @@
|
|||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
|
<label class="cc-motion-wrap">
|
||||||
|
<span class="cc-lbl">motion</span>
|
||||||
|
<select class="cc-motion" id="cc-motion">
|
||||||
|
<option value="smooth" selected>smooth</option>
|
||||||
|
<option value="step">step</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
|
||||||
<label class="cc-sync-wrap">
|
<label class="cc-sync-wrap">
|
||||||
<span class="cc-lbl">axes</span>
|
<span class="cc-lbl">axes</span>
|
||||||
<select class="cc-sync" id="cc-sync">
|
<select class="cc-sync" id="cc-sync">
|
||||||
@ -103,6 +111,6 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<script src="/static/theme.js?v=11"></script>
|
<script src="/static/theme.js?v=11"></script>
|
||||||
<script type="module" src="/static/compare.js?v=1"></script>
|
<script type="module" src="/static/compare.js?v=2"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||||
<title>embedding notebook</title>
|
<title>embedding notebook</title>
|
||||||
<link rel="stylesheet" href="/static/style.css?v=21" />
|
<link rel="stylesheet" href="/static/style.css?v=22" />
|
||||||
<script src="https://unpkg.com/htmx.org@2.0.4"></script>
|
<script src="https://unpkg.com/htmx.org@2.0.4"></script>
|
||||||
<script type="importmap">
|
<script type="importmap">
|
||||||
{
|
{
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user