Glass theme, after Apple's Liquid Glass (#43)
Translucent panels with backdrop-filter blur and saturation over a soft coloured wash, light and dark, going solid under prefers-reduced-transparency and prefers-contrast: more. The sticky filter bar and column headings are frosted, since the list scrolls under them. Text on a see-through panel lands on whatever the wash is behind it, so the palette's hex values alone no longer say whether it clears AA. contrast.js now samples the wash as the browser composites it on three viewport shapes. It caught the first light palette at 3.7:1 for faint text, and a tinted selection that failed everywhere; both were changed. Left out: SVG displacement-map refraction, which Chromium alone applies to a backdrop and only on fixed-size shapes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -58,5 +58,39 @@ for (const [name, p] of Object.entries(themes)) {
|
||||
// A border the same colour as the ground it is drawn on does not show (Nordic, once).
|
||||
if (p.line === p.panel2) { bad++; console.log(`FAIL ${name}: --line is --panel2, so borders on it vanish`); }
|
||||
}
|
||||
// Glass draws its text on a coloured wash, or on a panel that lets the wash through, so its hex
|
||||
// grounds are not what the text lands on. Sample the wash the way the browser composites it, on a
|
||||
// laptop, a phone and a tablet, and hold the text to AA wherever it is darkest or lightest.
|
||||
const washes = [...css.matchAll(/radial-gradient\((\d+)% (\d+)% at (\d+)% (\d+)%,var\(--wash(\d)\)/g)]
|
||||
.map(m => ({ rx: +m[1], ry: +m[2], cx: +m[3], cy: +m[4], n: m[5] }));
|
||||
const rgba = (sel, n) => {
|
||||
const m = css.slice(css.indexOf(sel + ' {')).match(new RegExp(`--wash${n}:rgba\\((\\d+),(\\d+),(\\d+),([\\d.]+)\\)`));
|
||||
return [[+m[1], +m[2], +m[3]], +m[4]];
|
||||
};
|
||||
const rgb = h => [1, 3, 5].map(i => parseInt(h.slice(i, i + 2), 16));
|
||||
const hex = c => '#' + c.map(v => Math.round(v).toString(16).padStart(2, '0')).join('');
|
||||
const over = (c, a, g) => g.map((x, i) => c[i] * a + x * (1 - a));
|
||||
for (const [name, sel] of [['glass', ':root[data-theme="glass"]'], ['glass light', ':root[data-theme="glass"][data-mode="light"]']]) {
|
||||
const p = themes[name];
|
||||
const tint = washes.map(w => [w, rgba(sel, w.n)]).reverse(); // the first listed is drawn on top
|
||||
const worst = {};
|
||||
for (const [W, H] of [[1440, 900], [390, 844], [1024, 1366]])
|
||||
for (let x = 0; x <= W; x += W / 40) for (let y = 0; y <= H; y += H / 40) {
|
||||
let g = rgb(p.bg);
|
||||
for (const [w, [c, a]] of tint) {
|
||||
const d = Math.hypot((x - w.cx * W / 100) / (w.rx * W / 100), (y - w.cy * H / 100) / (w.ry * H / 100));
|
||||
g = over(c, a * Math.max(0, 1 - d), g);
|
||||
}
|
||||
// The list sits on the bare wash; the sidebar, detail pane and dialogs on 70% panel over it.
|
||||
for (const [gn, gc] of [['the wash', g], ['a panel', over(rgb(p.panel), .7, g)]])
|
||||
for (const fg of ['fg', 'dim', 'faint', 'accent', 'bad', 'warn']) {
|
||||
const r = ratio(p[fg], hex(gc)), k = `--${fg} on ${gn}`;
|
||||
if (!(k in worst) || r < worst[k]) worst[k] = r;
|
||||
}
|
||||
}
|
||||
if (!washes.length) { bad++; console.log('FAIL glass: no wash gradients found in app.css'); }
|
||||
for (const [k, r] of Object.entries(worst))
|
||||
if (r < 4.5) { bad++; console.log(`FAIL ${name}: ${k} is ${r.toFixed(2)}:1 at worst, needs 4.5`); }
|
||||
}
|
||||
if (bad) process.exit(1);
|
||||
console.log(`OK: ${Object.keys(themes).length} palettes clear AA for every pair the page draws`);
|
||||
|
||||
Reference in New Issue
Block a user