From 79105a3ef96b2994628297a3deaefd7e1461addd Mon Sep 17 00:00:00 2001 From: BenStullsBets Date: Tue, 30 Jun 2026 09:34:10 -0700 Subject: [PATCH] feat(a11y): wire flash-clamp into autoScrub; document 3-flash/sec audit Clamp each per-altitude step to the WCAG 2.3.1 floor (no-op at 1200ms, guards a future lowering). Audit: #black fade + audio crossfade can't exceed 3/sec, no clamp needed. boot() now seeds the gate-dismissed flag (returning visitor) so the run-sim test isn't blocked by the new interstitial. Co-Authored-By: Claude Opus 4.8 (1M context) --- simulator/e2e/tests/altitude-lock.spec.ts | 5 ++++- simulator/static/app.js | 9 ++++++++- simulator/static/index.html | 1 + 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/simulator/e2e/tests/altitude-lock.spec.ts b/simulator/e2e/tests/altitude-lock.spec.ts index b92b402..4cac281 100644 --- a/simulator/e2e/tests/altitude-lock.spec.ts +++ b/simulator/e2e/tests/altitude-lock.spec.ts @@ -10,7 +10,10 @@ import { test, expect, Page } from "@playwright/test"; // (" · (i/n)"). async function boot(page: Page) { - await page.addInitScript(() => localStorage.setItem("hef.devMode", "1")); + await page.addInitScript(() => { + localStorage.setItem("hef.devMode", "1"); + localStorage.setItem("hef.motionWarnDismissed", "1"); // returning visitor: skip the photosensitivity gate + }); await page.goto("/"); // Wait for the "Loading Universe" preload to finish (splash gets class "done"). await page.waitForFunction( diff --git a/simulator/static/app.js b/simulator/static/app.js index f28f487..9ebc064 100644 --- a/simulator/static/app.js +++ b/simulator/static/app.js @@ -705,7 +705,14 @@ function autoScrub(targetPos, perAltMs = PER_ALTITUDE_MS) { if (autoRaf) cancelAnimationFrame(autoRaf); const fromPos = pos, dist = targetPos - fromPos; if (!dist) { setPos(targetPos); return; } - const ms = perAltMs * Math.abs(dist); // constant per-altitude speed → N steps take N× as long + // Photosensitivity floor (WCAG 2.3.1): each altitude step is one luminance + // transition (a blended crossfade), so a single step must take >= 1/3 s to stay + // <= 3 flashes/sec. The clamp is a no-op at today's 1200ms but guards the floor + // if PER_ALTITUDE_MS is ever lowered. (Audit: the only other luminance sources — + // the #black cover fade (200ms, single, >=1.2s apart) and the audio-coupled + // crossfade — cannot repeat faster than 3/sec, so they need no clamp.) + const safeAltMs = HEFFlash.clampDurationMs(perAltMs, 1); + const ms = safeAltMs * Math.abs(dist); // constant per-altitude speed → N steps take N× as long let startTs = null; const tick = (ts) => { if (startTs === null) startTs = ts; diff --git a/simulator/static/index.html b/simulator/static/index.html index bfe4d8c..803e989 100644 --- a/simulator/static/index.html +++ b/simulator/static/index.html @@ -129,6 +129,7 @@ +