fix(sim): loop base from morph-tail offset so landed morphs don't jitter

Operator eyeball feedback: landing on an altitude jump-cut the loop back to frame 0.
Each morph reproduces the destination clip's first 3s (pipeline trim=0:3) and ends on
dst@~3s, so the steady-state loop now starts at LOOP_TAIL_S=3.0 and loops within
[3.0, duration] — the morph's last frame hands straight off to the loop, and the
morph-covered intro never re-shows at rest (removes the overlapping 'altitude video
that is part of the morph'). E2E asserts a landed clip loops from ~3s, not 0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
BenStullsBets
2026-06-27 16:29:22 -07:00
parent 70fc9a46ab
commit 614411b5e4
2 changed files with 59 additions and 1 deletions
+32
View File
@@ -39,6 +39,16 @@ async function enableAudio(page: Page) {
});
}
// The Video toggle gates the base-clip loop (update() only loads media when video is
// shown). Same hidden-checkbox pattern.
async function enableVideo(page: Page) {
await page.evaluate(() => {
const c = document.querySelector("#visual") as HTMLInputElement;
c.checked = true;
c.dispatchEvent(new Event("change", { bubbles: true }));
});
}
test("two audio elements exist for crossfade", async ({ page }) => {
await boot(page);
const n = await page.evaluate(() => document.querySelectorAll("audio#aud, audio#aud-b").length);
@@ -77,6 +87,28 @@ test("wheel auto-scrubs one altitude and locks", async ({ page }) => {
expect(await page.locator("#scale-name").textContent()).toBe(landed); // locked
});
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
const start = (await page.locator("#scale-name").textContent())!;
await wheelOnStage(page, 60);
await page.waitForFunction((s) => document.querySelector("#scale-name")?.textContent !== s, start, { timeout: 20000 });
// After landing, the base loop is armed (loopTail="1") and seeked to the tail
// (~3s) so it continues the morph's last frame instead of restarting at 0.
await page.waitForFunction(
() => { const v = document.querySelector("video") as HTMLVideoElement; return v.dataset.loopTail === "1" && v.currentTime >= 2.5; },
null,
{ timeout: 10000 },
);
const v = await page.evaluate(() => {
const el = document.querySelector("video") as HTMLVideoElement;
return { t: el.currentTime, loopTail: el.dataset.loopTail, morph: el.dataset.morph || "" };
});
expect(v.morph).toBe(""); // on the base loop, not a morph
expect(v.loopTail).toBe("1"); // tail-loop armed
expect(v.t).toBeGreaterThanOrEqual(2.5); // looping from ~3s, not 0
});
test("zoom in plays the matching morph, lands locked, and stays put while parked", async ({ page }) => {
await boot(page);
const start = (await page.locator("#scale-name").textContent())!;