Touch devices blur the live backdrop instead of the baked layer

Mobile engines repaint background-attachment: fixed late (iOS treats it
as scroll), so the baked glass showed a stale crop of the wallpaper
until a tap forced a repaint - "some of the UI looks bugged until I
click it". On touch devices (max-width 639px or pointer: coarse) the
bake is dropped for a real backdrop-filter over the live scene, so the
bubbles blur through the glass as they pass and the position is correct
by construction. Hand-rolled frost fills keep their own tints there;
desktop keeps the baked layer (measured cheaper and verified correct).

Pinned with a touch-context check in the mobile suite (124 checks).
Contrast 0 findings, axe clean, chat 101/101, regression 44/44, i18n
13/13, native 33/33. Version 0.3.8, SW -> freezmo-v67.
This commit is contained in:
Martin Dvořák 2026-10-13 14:07:24 +00:00
commit b315e2a1aa
6 changed files with 84 additions and 4 deletions

View file

@ -1411,6 +1411,15 @@ Things I chose while building, with the reasoning:
pre-blurred wallpaper — the scene is clearly visible through the glass, and the pre-blurred wallpaper — the scene is clearly visible through the glass, and the
pixel-contrast scan still passes. pixel-contrast scan still passes.
73. **Touch devices blur the live backdrop; desktop keeps the bake.** Mobile engines repaint
`background-attachment: fixed` late (and iOS treats it as scroll), so the baked glass showed a
stale crop of the wallpaper until a tap forced a repaint — "some of the UI looks bugged until I
click it". On touch devices (`max-width: 639px` or `pointer: coarse`) the bake is dropped for a
real `backdrop-filter` over the live scene (what the floating chrome always did) — the bubbles
now blur *through* the glass as they pass, and the position is correct by construction. The
hand-rolled frost fills keep their own tints there; desktop keeps the baked layer (measured
cheaper there and verified correct). Pinned in `tests/mobile-test.mjs` with a touch context.
72. **More, shinier bubbles.** Aero's rising bubbles went from ten to eighteen (the extra eight 72. **More, shinier bubbles.** Aero's rising bubbles went from ten to eighteen (the extra eight
interleaved between the others, 18px to 96px, 10-23s runs) with a tighter specular hotspot, a interleaved between the others, 18px to 96px, 10-23s runs) with a tighter specular hotspot, a
second reflected-light glint low on the sphere, a 1px glass edge and a brighter inner rim on second reflected-light glint low on the sphere, a 1px glass edge and a brighter inner rim on

View file

@ -597,6 +597,57 @@
background-repeat: no-repeat, no-repeat; background-repeat: no-repeat, no-repeat;
} }
/* Touch devices: the baked backdrop is `background-attachment: fixed`, which
mobile engines repaint late (and iOS treats as scroll) — panes showed a
stale crop of the wallpaper until a tap forced a repaint ("some UI looks
bugged until I click it"). Touch hardware has the GPU budget for the honest
version instead: a real backdrop blur over the live scene, so the bubbles
blur through the glass as they pass. Desktop keeps the bake (measured
cheaper there, and verified correct). */
@media (max-width: 639px), (pointer: coarse) {
.aero .card,
.aero .glass-solid,
.aero .sidebar-surface,
.aero .gate-card,
.aero .list-group,
.aero .nav-action,
.aero .nav-row[aria-current="page"],
.aero .nav-row[data-active="true"],
.aero .glass-chip,
.aero .glass-pill,
.aero .btn-secondary,
.aero .btn-primary,
.aero .grad-ice,
.aero .segmented,
.aero .segmented > button[aria-pressed="true"],
.aero .segmented > a[aria-current="page"],
.aero .chat-bubble,
.aero .chat-answer {
background-image: none;
@apply backdrop-blur-md backdrop-saturate-[1.4];
}
.aero .chat-bubble,
.aero .chat-answer {
background-color: rgb(255 255 255 / 0.55);
}
.aero .tabbar {
background-image: none;
}
/* The hand-rolled frost fills drop the bake too and keep their own tints. */
.aero [class*="bg-white/35"],
.aero [class*="bg-white/40"],
.aero [class*="bg-white/45"],
.aero [class*="bg-white/50"],
.aero [class*="bg-white/55"],
.aero [class*="bg-white/60"],
.aero [class*="bg-white/70"],
.aero [class*="bg-[var(--surface)]"],
.aero [class*="bg-[var(--surface-strong)]"],
.aero [class*="bg-frost-50"] {
background-image: none;
}
}
/* Inside a sheet or dialog the backdrop is not the raw scene — a scrim and a /* Inside a sheet or dialog the backdrop is not the raw scene — a scrim and a
more opaque sheet glass sit in between — so the viewport-aligned frosted more opaque sheet glass sit in between — so the viewport-aligned frosted
layer would not match what is actually behind the element (it showed the layer would not match what is actually behind the element (it showed the

View file

@ -1,6 +1,6 @@
{ {
"name": "freezmo", "name": "freezmo",
"version": "0.3.7", "version": "0.3.8",
"private": true, "private": true,
"license": "GPL-3.0-only", "license": "GPL-3.0-only",
"scripts": { "scripts": {

View file

@ -9,7 +9,7 @@
* *
* Bumping CACHE_VERSION is how a new deploy invalidates old assets. * Bumping CACHE_VERSION is how a new deploy invalidates old assets.
*/ */
const CACHE_VERSION = "freezmo-v66"; const CACHE_VERSION = "freezmo-v67";
const SHELL_CACHE = `${CACHE_VERSION}-shell`; const SHELL_CACHE = `${CACHE_VERSION}-shell`;
const ASSET_CACHE = `${CACHE_VERSION}-assets`; const ASSET_CACHE = `${CACHE_VERSION}-assets`;

View file

@ -121,8 +121,8 @@ export async function seed(
); );
} }
export async function makePage(browser, { width = 390, height = 844, deviceScaleFactor = 2, reducedMotion, colorScheme } = {}) { export async function makePage(browser, { width = 390, height = 844, deviceScaleFactor = 2, reducedMotion, colorScheme, isMobile, hasTouch } = {}) {
const context = await browser.newContext({ viewport: { width, height }, deviceScaleFactor, reducedMotion, colorScheme }); const context = await browser.newContext({ viewport: { width, height }, deviceScaleFactor, reducedMotion, colorScheme, isMobile, hasTouch });
const page = await context.newPage(); const page = await context.newPage();
return { context, page }; return { context, page };
} }

View file

@ -287,6 +287,26 @@ const AXE = axeSource();
await context.close(); await context.close();
} }
/* 6d. touch devices blur the real backdrop (the baked layer mispaints) - */
{
const { context, page } = await makePage(browser, { width: 390, height: 844, isMobile: true, hasTouch: true });
await seed(page, { decks: [DECK], theme: "aero" });
await page.goto(BASE, { waitUntil: "networkidle" });
await page.waitForTimeout(1000);
const glass = await page.evaluate(() => {
const coarsish = matchMedia("(pointer: coarse)").matches || matchMedia("(max-width: 639px)").matches;
const el = document.querySelector(".segmented");
const cs = getComputedStyle(el);
return { coarsish, backdrop: cs.backdropFilter, image: cs.backgroundImage };
});
check(
"theme: touch glass blurs the live backdrop (no baked layer)",
glass.coarsish && glass.backdrop.includes("blur") && glass.image === "none",
JSON.stringify(glass),
);
await context.close();
}
/* 6c. a dark OS preference must not leak the dark palette into Aero ------ */ /* 6c. a dark OS preference must not leak the dark palette into Aero ------ */
{ {
const { context, page } = await makePage(browser, { width: 390, height: 844, colorScheme: "dark" }); const { context, page } = await makePage(browser, { width: 390, height: 844, colorScheme: "dark" });