feat(welcome): reduce-motion toggle for morph transitions; decouple loop from it

Per operator: Speed only controls the resting loop, so reduce-motion is still
needed to turn off the morph/transition videos. Re-add a visible 'Reduce motion'
toggle on BOTH the welcome screen and the panel (mirrored, default from OS
prefers-reduced-motion, remembers the choice). When ON, altitude changes are
instant cuts instead of an animated morph tween (autoScrub's reduced branch);
the resting loop is unaffected — it follows the Speed slider (0x freezes it).

Decouples the loop from reduce-motion (was: main froze both videos + disabled
the slider under OS-reduced). playLoop()/landing/applyPlaySpeed no longer check
isReduced(); isReduced() now only gates the morph tween. Welcome controls are
now Language / Speed / Audio / Reduce motion. i18n: restore rm.label (4 langs),
reword warn.body to point at the on-screen control.

Tests: a11y Task 5 reworked (speed independent of reduce-motion; new instant-cut
behavior test); welcome defaults assert reduce-motion off. node 35, e2e 32,
static 2, pytest 301 (3 pre-existing ffmpeg/cv2 env fails).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
BenStullsBets
2026-06-30 13:27:18 -07:00
parent c176ae7231
commit 5f9ae6b212
5 changed files with 77 additions and 47 deletions
+21 -8
View File
@@ -48,12 +48,11 @@ test.describe("Task 4 — Photosensitivity notice", () => {
});
});
test.describe("Task 5 — Playback speed slider (replaces Reduce motion)", () => {
test("the Reduce motion toggle is gone, replaced by a 04x speed slider", async ({ page }) => {
test.describe("Task 5 — Playback speed slider + Reduce motion", () => {
test("Speed is a 04x forward slider; the loop follows it (default 1x)", async ({ page }) => {
await page.emulateMedia({ reducedMotion: "no-preference" });
await page.goto("/");
await enter(page);
await expect(page.locator("#reduce-motion")).toHaveCount(0);
const slider = page.locator("#play-speed");
await expect(slider).toBeVisible();
await expect(slider).toHaveValue("1"); // default = normal speed
@@ -81,14 +80,28 @@ test.describe("Task 5 — Playback speed slider (replaces Reduce motion)", () =>
.toBe(true);
});
test("OS reduced-motion disables the slider and freezes the loop", async ({ page }) => {
test("Reduce motion defaults from the OS setting and does NOT disable Speed", async ({ page }) => {
await page.emulateMedia({ reducedMotion: "reduce" });
await page.goto("/");
// The welcome toggle reflects the OS preference before launch.
await expect(page.locator("#welcome-reduce-motion")).toBeChecked();
await enter(page);
await expect(page.locator("#play-speed")).toBeDisabled();
await expect
.poll(() => page.locator("#vid-loop").evaluate((v: HTMLVideoElement) => v.paused))
.toBe(true); // OS prefers-reduced-motion → frozen regardless of slider
await expect(page.locator("#reduce-motion")).toBeChecked(); // panel mirrors it
await expect(page.locator("#play-speed")).toBeEnabled(); // Speed is independent of reduce-motion
});
test("Reduce motion turns altitude changes into instant cuts (no morph tween)", async ({ page }) => {
await page.emulateMedia({ reducedMotion: "no-preference" });
await page.goto("/");
await enter(page);
await page.locator('label[for="reduce-motion"]').click(); // hidden checkbox → toggle via label
await expect(page.locator("#reduce-motion")).toBeChecked();
const start = (await page.locator("#scale-name").textContent())!;
const t0 = await page.evaluate(() => performance.now());
await page.locator("#stage").dispatchEvent("wheel", { deltaY: 60 }); // descend one altitude
await page.waitForFunction((s) => document.querySelector("#scale-name")?.textContent !== s, start, { timeout: 5000 });
const dt = (await page.evaluate(() => performance.now())) - t0;
expect(dt).toBeLessThan(600); // instant cut, not the ~1200ms animated morph tween
});
});
+2 -1
View File
@@ -25,12 +25,13 @@ test("welcome screen shows on load with the expected defaults; the panel is hidd
await expect(page.locator("#welcome")).toBeVisible();
await expect(page.locator("#welcome")).toHaveClass(/welcoming/);
// Defaults: Language English, Speed 1, Audio 2.
// Defaults: Language English, Speed 1, Audio 2, Reduce motion off (no OS pref).
await expect(page.locator("#welcome-lang")).toHaveValue("en");
await expect(page.locator("#welcome-play-speed")).toHaveValue("1");
await expect(page.locator("#welcome-play-speed-val")).toHaveText("1.00×");
await expect(page.locator("#welcome-audio")).toHaveValue("2");
await expect(page.locator("#welcome-audio-val")).toHaveText("2");
await expect(page.locator("#welcome-reduce-motion")).not.toBeChecked();
// Heads-up + Launch button present; "Loading Universe" progress present.
await expect(page.locator("#welcome-title")).toBeVisible();