diff --git a/app/demo/index.html b/app/demo/index.html index f3da360..2178436 100644 --- a/app/demo/index.html +++ b/app/demo/index.html @@ -490,8 +490,12 @@ function createScene(container, dataset) { trajectories: null, numFrames: 0, snapshotMs: 1000 / 12, - // random phase so the three scenes don't loop in lockstep - cycleStartMs: performance.now() - Math.random() * 2000, + // holdMs pads the end of each cycle at rest, so the pulse actually + // lands (otherwise frame-0-as-rest is zero-duration and never visible). + holdMs: 200, + // shared reference so all scenes stay in lockstep; applyF() sets this + // to the same value across scenes on every n-frames toggle. + cycleStartMs: 0, }; } @@ -515,7 +519,6 @@ function buildTrajectories(s, numFrames) { } s.trajectories = buf; s.numFrames = numFrames; - s.cycleStartMs = performance.now() - Math.random() * s.snapshotMs * numFrames; } function sizeScene(s) { @@ -630,9 +633,11 @@ async function main() { const fInputs = document.querySelectorAll('input[name="f"]'); function applyF(n) { numFrames = n; + // Shared start time keeps all three scenes in lockstep. + const start = performance.now(); for (const s of scenes) { s.numFrames = n; - s.cycleStartMs = performance.now() - Math.random() * s.snapshotMs * n; + s.cycleStartMs = start; } } fInputs.forEach(input => { @@ -691,14 +696,9 @@ async function main() { for (const s of scenes) { const N = s.numFrames; const n = s.basePositions.length; - const cycleMs = N * s.snapshotMs; + const walkMs = N * s.snapshotMs; + const cycleMs = walkMs + s.holdMs; const elapsed = ((now - s.cycleStartMs) % cycleMs + cycleMs) % cycleMs; - const frameF = elapsed / s.snapshotMs; - const frameIdx = Math.floor(frameF); - const interpT = frameF - frameIdx; - const nextIdx = (frameIdx + 1) % N; - const aOff = frameIdx * n; - const bOff = nextIdx * n; const total = n / 3; const drawCount = s.geometry.drawRange.count; @@ -706,13 +706,25 @@ async function main() { const limit = visibleN * 3; const pos = s.geometry.attributes.position.array; const base = s.basePositions; - const tr = s.trajectories; - // Jitter is applied in normalized space so σ means the same thing - // across datasets — independent of the raw feature magnitudes. - const scale = jitterScale; - const u = 1 - interpT; - for (let i = 0; i < limit; i++) { - pos[i] = base[i] + (tr[aOff + i] * u + tr[bOff + i] * interpT) * scale; + + if (elapsed >= walkMs) { + // Hold zone: snap-back has landed, sit at rest until next cycle. + for (let i = 0; i < limit; i++) pos[i] = base[i]; + } else { + const frameF = elapsed / s.snapshotMs; + const frameIdx = Math.floor(frameF); + const interpT = frameF - frameIdx; + const nextIdx = (frameIdx + 1) % N; + const aOff = frameIdx * n; + const bOff = nextIdx * n; + const tr = s.trajectories; + // Jitter is applied in normalized space so σ means the same thing + // across datasets — independent of the raw feature magnitudes. + const scale = jitterScale; + const u = 1 - interpT; + for (let i = 0; i < limit; i++) { + pos[i] = base[i] + (tr[aOff + i] * u + tr[bOff + i] * interpT) * scale; + } } s.geometry.attributes.position.needsUpdate = true;