compare: pad-to-match time mapping + fix stalled play at small du
- applyU now maps u to a shared global frame index uGlobal in [0, maxT-1]; each panel clamps to its own (T-1), so a shorter timeline pads its last frame while the longer one finishes — both advance at the same wall-clock tempo instead of rescaling their timelines. - tick() keeps u as a float closure variable; reading it back from the integer-step scrubber was quantizing du to 0 at slow tempo + high T (1600ms/frame, T=24: du ≈ 4e-4 → round to 0 on scrub), stalling playback after one frame.
This commit is contained in:
parent
89401e3aee
commit
d3f5088233
@ -459,27 +459,34 @@ async function main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ---- time mapping -----------------------------------------------------
|
// ---- time mapping -----------------------------------------------------
|
||||||
// Scrubber is 0..1000. Each panel picks round(u * (T-1)) as its frame idx.
|
// Scrubber is 0..1000, mapped to a shared global frame index uGlobal in
|
||||||
|
// [0, maxT-1]. Each panel clamps uGlobal to its own (T-1) — so a shorter
|
||||||
|
// timeline pads its last frame until the longer timeline finishes, and
|
||||||
|
// both panels advance at the same wall-clock tempo.
|
||||||
const SCRUB_MAX = 1000;
|
const SCRUB_MAX = 1000;
|
||||||
|
|
||||||
function framesOf(p) { return p ? p.data.frames.length : 0; }
|
function framesOf(p) { return p ? p.data.frames.length : 0; }
|
||||||
|
|
||||||
function timeLabelFor(p, u) {
|
function timeLabelForAtLocal(p, uLocal) {
|
||||||
if (!p) return '—';
|
if (!p) return '—';
|
||||||
const T = framesOf(p);
|
const T = framesOf(p);
|
||||||
if (T <= 0) return '—';
|
if (T <= 0) return '—';
|
||||||
const idx = Math.max(0, Math.min(T - 1, Math.round(u * (T - 1))));
|
const idx = Math.max(0, Math.min(T - 1, Math.round(uLocal)));
|
||||||
return p.data.times?.[idx] ?? String(idx);
|
return p.data.times?.[idx] ?? String(idx);
|
||||||
}
|
}
|
||||||
|
|
||||||
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';
|
const smooth = motionSel.value === 'smooth';
|
||||||
for (const p of Object.values(panels)) {
|
const tMax = maxT();
|
||||||
|
const uGlobal = u * (tMax - 1);
|
||||||
|
let uLocalA = 0, uLocalB = 0;
|
||||||
|
for (const [slot, p] of Object.entries(panels)) {
|
||||||
if (!p) continue;
|
if (!p) continue;
|
||||||
const T = framesOf(p);
|
const T = framesOf(p);
|
||||||
if (T <= 0) continue;
|
if (T <= 0) continue;
|
||||||
const uLocal = u * (T - 1);
|
const uLocal = Math.min(uGlobal, T - 1);
|
||||||
|
if (slot === 'a') uLocalA = uLocal; else uLocalB = uLocal;
|
||||||
if (smooth) {
|
if (smooth) {
|
||||||
p.setFrameInterpolated(uLocal);
|
p.setFrameInterpolated(uLocal);
|
||||||
} else {
|
} else {
|
||||||
@ -487,8 +494,8 @@ async function main() {
|
|||||||
p.setFrame(idx);
|
p.setFrame(idx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
timeAEl.textContent = timeLabelFor(panels.a, u);
|
timeAEl.textContent = timeLabelForAtLocal(panels.a, uLocalA);
|
||||||
timeBEl.textContent = timeLabelFor(panels.b, u);
|
timeBEl.textContent = timeLabelForAtLocal(panels.b, uLocalB);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- axes sync mode ---------------------------------------------------
|
// ---- axes sync mode ---------------------------------------------------
|
||||||
@ -523,22 +530,25 @@ async function main() {
|
|||||||
}
|
}
|
||||||
function baseMsPerFrame() { return 1600 / parseFloat(speedSel.value || '1'); }
|
function baseMsPerFrame() { return 1600 / parseFloat(speedSel.value || '1'); }
|
||||||
|
|
||||||
|
// u is the authoritative playhead (float in [0, 1]). The scrubber mirrors
|
||||||
|
// it for display; reading u back from the scrubber would quantize to the
|
||||||
|
// scrubber's integer step and stall the loop at small du.
|
||||||
|
let u = 0;
|
||||||
|
|
||||||
function tick(ts) {
|
function tick(ts) {
|
||||||
requestAnimationFrame(tick);
|
requestAnimationFrame(tick);
|
||||||
for (const p of Object.values(panels)) p?.render();
|
for (const p of Object.values(panels)) p?.render();
|
||||||
if (!playing) { lastTs = ts; return; }
|
if (!playing) { lastTs = ts; return; }
|
||||||
if (!lastTs) lastTs = ts;
|
if (!lastTs) { lastTs = ts; return; }
|
||||||
const dt = ts - lastTs;
|
const dt = ts - lastTs;
|
||||||
|
lastTs = ts;
|
||||||
|
if (dt <= 0) return;
|
||||||
const perFrame = baseMsPerFrame();
|
const perFrame = baseMsPerFrame();
|
||||||
const T = maxT();
|
const T = maxT();
|
||||||
const du = dt / (perFrame * (T - 1));
|
u += dt / (perFrame * (T - 1));
|
||||||
if (du > 0) {
|
|
||||||
let u = parseFloat(scrub.value) / SCRUB_MAX + du;
|
|
||||||
if (u >= 1) u -= Math.floor(u); // wrap 0..1
|
if (u >= 1) u -= Math.floor(u); // wrap 0..1
|
||||||
scrub.value = String(Math.round(u * SCRUB_MAX));
|
scrub.value = String(Math.round(u * SCRUB_MAX));
|
||||||
applyU(u);
|
applyU(u);
|
||||||
lastTs = ts;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
requestAnimationFrame(tick);
|
requestAnimationFrame(tick);
|
||||||
|
|
||||||
@ -551,7 +561,8 @@ async function main() {
|
|||||||
playBtn.addEventListener('click', () => setPlaying(!playing));
|
playBtn.addEventListener('click', () => setPlaying(!playing));
|
||||||
|
|
||||||
scrub.addEventListener('input', () => {
|
scrub.addEventListener('input', () => {
|
||||||
applyU(parseFloat(scrub.value) / SCRUB_MAX);
|
u = parseFloat(scrub.value) / SCRUB_MAX;
|
||||||
|
applyU(u);
|
||||||
});
|
});
|
||||||
|
|
||||||
speedSel.addEventListener('change', () => { lastTs = 0; });
|
speedSel.addEventListener('change', () => { lastTs = 0; });
|
||||||
|
|||||||
@ -111,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=5"></script>
|
<script type="module" src="/static/compare.js?v=7"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user