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:
@@ -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())!;
|
||||
|
||||
Reference in New Issue
Block a user