feat(learner): cross-fade deck slides
The deck viewer draws the next slide into a second canvas and fades it in over the current one, 4 px from the direction of travel; the stage never blanks while pdf.js draws. A newer draw into a canvas cancels the older one. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
a83ac02a90
commit
d5e836cc79
@@ -189,4 +189,39 @@ run("navigation motion (headless Chrome)", () => {
|
||||
expect(closing.some((h) => h > 1 && h < full - 1)).toBe(true);
|
||||
expect(closing.at(-1)).toBe(0);
|
||||
}, 30_000);
|
||||
|
||||
test("slides cross-fade in the direction of travel and the stage never blanks", async () => {
|
||||
await open("/c/sample/m1-video", ".stage canvas[data-front]");
|
||||
await until(() => chrome.eval<boolean>(`document.querySelector(".deckbar .count").textContent.trim() === "1 / 3"`), "deck loaded");
|
||||
// Next slide; then sample every frame: the animations running and the most opaque canvas.
|
||||
const frames = await chrome.eval<{ anims: string[]; maxOpacity: number }[]>(`(() => {
|
||||
document.querySelector('.deckbar [aria-label="Next slide"]').click();
|
||||
const out = [], t0 = performance.now();
|
||||
return new Promise((r) => { const f = () => {
|
||||
const cs = [...document.querySelectorAll(".stage canvas")];
|
||||
out.push({ anims: document.getAnimations().map((a) => a.animationName).filter(Boolean), maxOpacity: Math.max(...cs.map((c) => Number(getComputedStyle(c).opacity))) });
|
||||
if (performance.now() - t0 < 600) requestAnimationFrame(f); else r(out);
|
||||
}; f(); });
|
||||
})()`);
|
||||
const names = new Set(frames.flatMap((f) => f.anims));
|
||||
expect(names.has("stc-slide-next")).toBe(true);
|
||||
expect(names.has("stc-hold-out")).toBe(true);
|
||||
expect(frames.every((f) => f.maxOpacity === 1)).toBe(true);
|
||||
expect(await text(".deckbar .count")).toBe("2 / 3");
|
||||
expect(await chrome.eval<number>(`document.querySelectorAll('.stage canvas[role="img"]').length`)).toBe(1);
|
||||
expect(await chrome.eval<string>(`document.querySelector('.stage canvas[data-front]').getAttribute("aria-label")`)).toBe("Slide 2 of 3");
|
||||
|
||||
// The arrow keys still turn slides, backwards with the mirrored motion.
|
||||
await reset();
|
||||
await chrome.send("Input.dispatchKeyEvent", { type: "keyDown", key: "ArrowLeft", code: "ArrowLeft", windowsVirtualKeyCode: 37 });
|
||||
await chrome.send("Input.dispatchKeyEvent", { type: "keyUp", key: "ArrowLeft", code: "ArrowLeft", windowsVirtualKeyCode: 37 });
|
||||
await until(async () => (await text(".deckbar .count")) === "1 / 3", "previous slide");
|
||||
await Bun.sleep(300);
|
||||
expect((await m()).anims.some((a) => a.startsWith("stc-slide-prev@"))).toBe(true);
|
||||
|
||||
// Two quick clicks: the second supersedes the first draw and the stage ends on the last slide.
|
||||
await chrome.eval(`(() => { const b = document.querySelector('.deckbar [aria-label="Next slide"]'); b.click(); setTimeout(() => b.click(), 5); return true; })()`);
|
||||
await until(() => chrome.eval<boolean>(`document.querySelector(".stage canvas[data-front]").dataset.page === "3"`), "slide 3 drawn");
|
||||
expect(await text(".deckbar .count")).toBe("3 / 3");
|
||||
}, 30_000);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user