Ice scene: the glossy orbs become rising bubbles, ~38% more transparent

- Orbs rise from below the bottom edge to above the top edge, each with
  its own 36-68s pass (slower on phones), a negative delay (scene already
  populated on load), own horizontal position, a gentle sway and a slight
  scale drift; opacity fades in/out at the edges so the loop never pops.
- Only transform + opacity animate, each orb on its own composited layer
  (will-change: transform); no filter/backdrop-filter, nothing animates
  size/offsets/shadow. The glacier blobs are untouched.
- Opacity lowered ~38% in both themes: 0.55->0.34 light, 0.8->0.5 dark,
  phone overrides 0.32->0.2 / 0.42->0.26.
- prefers-reduced-motion: rise removed, orbs return to the old static
  placements (a paused-from-start CSS animation is not applied in Chrome
  and would park them below the fold); tab hidden: a visibility listener
  parks the whole scene (.ice-paused), blob drift included.
- Measured with a CDP perf trace on a 4x throttled phone profile: p95
  33.4ms, 0 frames over 34ms, no long tasks - identical to the pre-change
  baseline; ~0.6% extra main-thread CPU. Desktop stays compositor-bound
  from the pre-existing blob drift, as before. tests/bubble-perf.mjs
  keeps it repeatable.
- New bubbles suite (34 checks) in the repo harness; the axe contrast
  matrix is now 16 runs (360/390/1280 x dark/light x EN/CS) and passes.
- SW cache -> freezmo-v31; README decision 46.

Full suite: 11/11 suites, 424 checks.
This commit is contained in:
Martin Dvořák 2026-10-11 15:50:17 +00:00
commit 6b6a6ea158
9 changed files with 419 additions and 31 deletions

View file

@ -221,10 +221,10 @@ for (const locale of ["en", "cs"]) {
await context.close();
}
/* 5. axe-core contrast audit (dark + light, en + cs) ----------------- */
/* 5. axe-core contrast audit (360/390/desktop × dark/light × en/cs) -- */
const AXE = axeSource();
async function audit(label, { locale, theme, openSettingsNow }) {
const { context, page } = await makePage(browser);
async function audit(label, { width = 390, locale, theme, openSettingsNow }) {
const { context, page } = await makePage(browser, { width, height: width >= 1280 ? 900 : 800, deviceScaleFactor: width >= 1280 ? 1 : 2 });
await seed(page, { locale, theme, decks: [DECK] });
await page.goto(BASE, { waitUntil: "networkidle" });
await page.waitForTimeout(1200);
@ -246,11 +246,18 @@ async function audit(label, { locale, theme, openSettingsNow }) {
check(`axe ${label}: no contrast violations`, report.violations.length === 0, JSON.stringify(report.violations).slice(0, 300));
await context.close();
}
await audit("dark/en library", { locale: "en", theme: "dark" });
await audit("dark/cs library", { locale: "cs", theme: "dark" });
await audit("light/en library", { locale: "en", theme: "light" });
await audit("dark/en settings", { locale: "en", theme: "dark", openSettingsNow: true });
await audit("dark/cs settings", { locale: "cs", theme: "dark", openSettingsNow: true });
for (const width of [360, 390, 1280]) {
for (const theme of ["dark", "light"]) {
for (const locale of ["en", "cs"]) {
await audit(`${width} ${theme}/${locale} library`, { width, locale, theme });
}
}
}
for (const theme of ["dark", "light"]) {
for (const locale of ["en", "cs"]) {
await audit(`390 ${theme}/${locale} settings`, { width: 390, locale, theme, openSettingsNow: true });
}
}
await browser.close();
finish();