mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-29 03:09:36 +00:00
With the day/night background the plan went blurry after zooming from 100 %
(sharp when the page was opened at 800 %) and navigating a strongly zoomed
plan flashed the page white. Both came from #582's composition, not from the
wall hatch that #685 replaced:
- `.stage.daycycle.hp-safe-daycycle-outline .plan-svg` promoted the scene
with `will-change: transform`; Chromium freezes the raster scale of such a
layer, so the 100 % raster was shown stretched. The explicit layer #582
needs is now `will-change: opacity` (re-rasters at the current scale).
- The filtered outline had `overflow: visible` and a gesture exposed every
scene (#544) without bound, so the promoted layers grew with zoom squared
(CDP LayerTree, ~460 %: plan-svg 15.9x, outline 13.2x the stage; 39x after
navigating at 800 %). The full card clips its outline to its box and marks
it data-hp-live-overflow="clip" (never exposed); the live viewport bounds
every other exposure with an inline clip-path: inset(-25%) that leaves
with it, so idle DOM stays byte-identical (#531).
Owner-verified in Chrome 152 (built-in browser, DPR 2): sharp after 100 ->
800 %, no white flashes after reloading at 800 %.
Owner decision: #685's analytic gradient is reverted (13af1d5e), the single
<pattern> is back at every zoom; its close-up golden scenes stay and check
the pattern, its terminal-frame smoke checks the pattern.
Witnesses: demo/smoke_daycycle_zoom_layers.mjs (800 % x DPR 2: layers vs
stage, the hint, reload path, button/wheel/pinch); #582/#532 smokes now pin
the opacity hint; test/live-viewport.test.mjs (bounded exposure, clipped
scene); test/daycycle-layers.test.mjs (cascade). Four Node-guarded mutants.
Issue: #689
User-Visible: yes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
65 lines
2.7 KiB
JavaScript
65 lines
2.7 KiB
JavaScript
// #689: day/night composition must not freeze the plan raster nor grow with
|
|
// zoom². The browser proof lives in demo/smoke_static_zoom_sharpness.mjs and
|
|
// demo/smoke_daycycle_layer_budget.mjs; this suite pins the cascade without
|
|
// Chromium so the mutants that guard it stay off the capped browser
|
|
// inventory (#659).
|
|
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { planStyles } from '../test-build/styles.js';
|
|
|
|
/** Top-level and @media rules of one stylesheet as [selectors[], decls{}]. */
|
|
const rulesOf = (cssText) => {
|
|
const text = cssText.replace(/\/\*[\s\S]*?\*\//g, '');
|
|
const out = [];
|
|
const walk = (chunk) => {
|
|
let i = 0;
|
|
while (i < chunk.length) {
|
|
const open = chunk.indexOf('{', i);
|
|
if (open === -1) break;
|
|
const header = chunk.slice(i, open).replace(/\s+/g, ' ').trim();
|
|
let depth = 1, j = open + 1;
|
|
while (j < chunk.length && depth > 0) {
|
|
if (chunk[j] === '{') depth++;
|
|
else if (chunk[j] === '}') depth--;
|
|
j++;
|
|
}
|
|
const body = chunk.slice(open + 1, j - 1);
|
|
if (header.startsWith('@media') || header.startsWith('@supports')) walk(body);
|
|
else if (!header.startsWith('@')) {
|
|
const decls = {};
|
|
for (const part of body.split(';')) {
|
|
const colon = part.indexOf(':');
|
|
if (colon > 0) decls[part.slice(0, colon).trim()] = part.slice(colon + 1).trim();
|
|
}
|
|
out.push([header.split(',').map((s) => s.trim()), decls]);
|
|
}
|
|
i = j;
|
|
}
|
|
};
|
|
walk(text);
|
|
return out;
|
|
};
|
|
|
|
const RULES = rulesOf(planStyles.cssText);
|
|
const touching = (predicate) => RULES.filter(([sels]) => sels.some(predicate));
|
|
|
|
test('#689 K1: no plan-svg rule carries a will-change: transform hint', () => {
|
|
const hinted = touching((s) => /\.plan-svg\b/.test(s))
|
|
.filter(([, d]) => /\btransform\b/.test(d['will-change'] || ''));
|
|
assert.deepEqual(hinted.map(([sels]) => sels.join(', ')), []);
|
|
});
|
|
|
|
test('#689 K1: the safe day/night scene keeps an explicit layer via an opacity hint', () => {
|
|
const decls = RULES.filter(([sels]) => sels.includes('.stage.daycycle.hp-safe-daycycle-outline .plan-svg'))
|
|
.map(([, d]) => d);
|
|
assert.ok(decls.some((d) => d['will-change'] === 'opacity'), 'the #582 explicit layer must stay');
|
|
});
|
|
|
|
test('#689 K3: the full card clips its filtered outline to its own box', () => {
|
|
const clip = RULES.filter(([sels]) => sels.includes('.stage .hp-paper-outline-svg')).map(([, d]) => d);
|
|
assert.ok(clip.some((d) => d.overflow === 'hidden'), '.stage .hp-paper-outline-svg must be overflow: hidden');
|
|
const opened = touching((s) => /^\.stage\b/.test(s) && /\.hp-paper-outline-svg\b/.test(s))
|
|
.filter(([, d]) => d.overflow === 'visible');
|
|
assert.deepEqual(opened.map(([sels]) => sels.join(', ')), []);
|
|
});
|