feat(sim): scrub-driven altitude transitions #28
@@ -125,6 +125,21 @@ test("wheel auto-scrubs one altitude and locks", async ({ page }) => {
|
||||
expect(await page.locator("#scale-name").textContent()).toBe(landed); // locked
|
||||
});
|
||||
|
||||
test("auto-scrub speed is constant per altitude (a 2-step jump takes ~2x a 1-step)", async ({ page }) => {
|
||||
await boot(page);
|
||||
async function timeJump(deltaY: number, target: string): Promise<number> {
|
||||
const t0 = await page.evaluate(() => performance.now());
|
||||
await page.locator("#stage").dispatchEvent("wheel", { deltaY });
|
||||
await page.waitForFunction((tgt) => !!document.querySelector("#scale-name")?.textContent?.includes(tgt), target, { timeout: 20000 });
|
||||
return (await page.evaluate(() => performance.now())) - t0;
|
||||
}
|
||||
const t1 = await timeJump(60, "orbit"); // cosmos -> orbit (1 altitude)
|
||||
const t2 = await timeJump(120, "reef"); // orbit -> reef (2 altitudes)
|
||||
const ratio = t2 / t1;
|
||||
expect(ratio).toBeGreaterThan(1.5); // proportional to distance, not a fixed total
|
||||
expect(ratio).toBeLessThan(2.7); // ~2x, not more
|
||||
});
|
||||
|
||||
test("a landed clip loops from the morph-tail offset (no jump-back to frame 0)", async ({ page }) => {
|
||||
await boot(page);
|
||||
await enableVideo(page); // base loop only loads when video is shown
|
||||
|
||||
+10
-3
@@ -639,14 +639,21 @@ function debounced() { clearTimeout(timer); timer = setTimeout(update, 80); }
|
||||
|
||||
// --- Scale ring (endless encoder, scrub-driven — see the scrub engine below) ---
|
||||
|
||||
// Animate `pos` to a target over `ms`, driving the same scrub path as a drag, then
|
||||
// land exactly on the integer target (frac 0 settles + locks in setPos).
|
||||
// Per-altitude duration of an AUTO transition (wheel / label-click). The total time
|
||||
// is PROPORTIONAL to the distance, so the speed between any two adjacent altitudes is
|
||||
// the same — a 2-altitude jump takes twice as long as a 1-altitude one. (A manual
|
||||
// drag is exempt: the operator drives its speed directly.)
|
||||
const PER_ALTITUDE_MS = 1200;
|
||||
|
||||
// Animate `pos` to a target at a constant per-altitude speed, driving the same scrub
|
||||
// path as a drag, then land exactly on the integer target (frac 0 settles + locks).
|
||||
let autoRaf = 0;
|
||||
function autoScrub(targetPos, ms = 600) {
|
||||
function autoScrub(targetPos, perAltMs = PER_ALTITUDE_MS) {
|
||||
if (!ring || ring.scales.length < 2) return;
|
||||
if (autoRaf) cancelAnimationFrame(autoRaf);
|
||||
const fromPos = pos, dist = targetPos - fromPos;
|
||||
if (!dist) { setPos(targetPos); return; }
|
||||
const ms = perAltMs * Math.abs(dist); // constant per-altitude speed → N steps take N× as long
|
||||
let startTs = null;
|
||||
const tick = (ts) => {
|
||||
if (startTs === null) startTs = ts;
|
||||
|
||||
Reference in New Issue
Block a user