feat(sim): scrub engine — drag drives pos, seeks morph, commits+locks+re-rolls
Per plan Task 3. A continuous float pos is the knob: setPos() seeks the segment's morph currentTime by frac (throttled one seek/rAF), crossfades audio, sweeps the needle live, and commits+locks+re-rolls on each integer crossing. Drag live-scrubs (no more commit-on-release); release holds the blend (no auto-complete). Client-side pool pick replaces the server round-trip for drag. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -29,12 +29,43 @@ async function wheelOnStage(page: Page, deltaY: number) {
|
||||
await page.locator("#stage").dispatchEvent("wheel", { deltaY });
|
||||
}
|
||||
|
||||
// The Audio toggle is a visually-hidden checkbox styled as a switch; set it + fire
|
||||
// `change` directly (headless Chromium relaxes the autoplay gesture requirement).
|
||||
async function enableAudio(page: Page) {
|
||||
await page.evaluate(() => {
|
||||
const c = document.querySelector("#audio") 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);
|
||||
expect(n).toBe(2);
|
||||
});
|
||||
|
||||
test("dragging the dial scrubs morph currentTime and audio gains", async ({ page }) => {
|
||||
await boot(page);
|
||||
await enableAudio(page); // hidden custom toggle → set + dispatch change
|
||||
const box = (await page.locator("#dial").boundingBox())!;
|
||||
const cx = box.x + box.width / 2, cy = box.y + box.height / 2;
|
||||
await page.mouse.move(cx, cy - 30);
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(cx + 18, cy - 24, { steps: 8 }); // partial clockwise turn (~0.5 detent)
|
||||
// The currentTime seek is throttled to one rAF; wait for it to land.
|
||||
await page.waitForFunction(() => (document.querySelector("video") as HTMLVideoElement).currentTime > 0, null, { timeout: 5000 });
|
||||
const state = await page.evaluate(() => ({
|
||||
t: (document.querySelector("video") as HTMLVideoElement).currentTime,
|
||||
ga: (document.querySelector("#aud") as HTMLAudioElement).volume,
|
||||
gb: (document.querySelector("#aud-b") as HTMLAudioElement).volume,
|
||||
}));
|
||||
expect(state.t).toBeGreaterThan(0); // morph scrubbed off frame 0
|
||||
expect(state.gb).toBeGreaterThan(0); // next scale fading in
|
||||
expect(state.ga).toBeLessThan(1); // current scale fading out
|
||||
await page.mouse.up();
|
||||
});
|
||||
|
||||
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