mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-28 19:01:34 +00:00
The shadow of a device marker is sized from the marker, and the marker is sized from the container: --device-shell-shadow is expressed in --dev-size, which resolves to 2.5cqw. With box-shadow in the transition list, every container-query re-evaluation — a tooltip, a scrollbar, a rotation — produced a new computed value and restarted a 150 ms non-composited transition on every marker at once. The owner's Firefox profile shows what that costs: 244 box-shadow transitions, all on span.device-shell-frame, all oncompositor:false, in bursts of exactly 61 (the markers on screen), four bursts in two seconds. During them the tab presented 103 frames in 11 seconds — 9.4 per second, CONTENT_FRAME_TIME median 149 ms and up to 320 — while the refresh driver waited for paint 381 times. Our JavaScript in the worst three seconds: 16 ms. Chromium starts the same transitions (measured: one pixel of container width starts two per marker in both engines) and merely pays less for them, which is why this hid there. The shadow itself is unchanged; it simply applies at once. The core keeps the same treatment, so the selection and focus rings appear without a fade — an instant ring is ordinary feedback, a faded one costs a full-frame repaint per marker. Hover still animates border-color. Issue: #524 User-Visible: yes
106 lines
5.2 KiB
JavaScript
106 lines
5.2 KiB
JavaScript
// #524: маркеры не анимируют тень, выраженную в контейнерных единицах.
|
|
//
|
|
// Тень оболочки и кольцо ядра заданы через `--dev-size`, а он восходит к
|
|
// `2.5cqw`. Пока `box-shadow` стоял в списке переходов, ЛЮБОЙ пересчёт
|
|
// контейнерных запросов — а он случается от подсказки, полосы прокрутки,
|
|
// поворота экрана — менял вычисленное значение у каждого маркера и запускал
|
|
// на нём некомпозируемый 150-миллисекундный переход. В профиле владельца
|
|
// (Firefox 155) 61 такой переход разом давал 9,4 к/с и кадры по 149 мс.
|
|
//
|
|
// Свидетель судит ПРИЧИНУ, а не время кадра: считает старты переходов, а не
|
|
// миллисекунды, и поэтому не флейкует и не зависит от движка. Проверять это
|
|
// в Chromium честно: переход запускается в обоих движках одинаково, разница
|
|
// только в цене перерисовки.
|
|
import { launch, checkAll, finish } from './serve.mjs';
|
|
|
|
const { page, browser } = await launch({ width: 1100, height: 820 });
|
|
|
|
// Слушатель ставится один раз и живёт до конца прогона.
|
|
await page.evaluate(() => {
|
|
const card = window.__card;
|
|
const root = card.shadowRoot || card.renderRoot;
|
|
window.__starts = [];
|
|
root.addEventListener('transitionrun', (event) => {
|
|
const target = event.target;
|
|
window.__starts.push({
|
|
property: event.propertyName,
|
|
cls: target?.getAttribute?.('class') || target?.tagName || '?',
|
|
onMarker: !!target?.closest?.('.dev'),
|
|
});
|
|
}, true);
|
|
});
|
|
|
|
const out = await page.evaluate(async () => {
|
|
const result = {};
|
|
const card = window.__card;
|
|
const root = card.shadowRoot || card.renderRoot;
|
|
const frame = () => new Promise((resolve) => requestAnimationFrame(resolve));
|
|
const settle = async () => { await card.updateComplete; await frame(); await frame(); };
|
|
card._setMode('devices');
|
|
await settle();
|
|
const markers = [...root.querySelectorAll('[data-hp="device"]')];
|
|
result.enoughMarkers = markers.length >= 8;
|
|
result.everyMarkerHasShell = markers.every((marker) => !!marker.querySelector('.device-shell-frame'));
|
|
|
|
// AC1: ширина контейнера меняется на один пиксель — переходов тени быть не должно.
|
|
const stage = root.querySelector('.stage');
|
|
const width = stage.getBoundingClientRect().width;
|
|
window.__starts.length = 0;
|
|
stage.style.width = `${Math.round(width) - 1}px`;
|
|
await frame(); await frame();
|
|
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
const afterResize = [...window.__starts];
|
|
stage.style.removeProperty('width');
|
|
await settle();
|
|
result.containerResizeStartsNoShadowTransition = afterResize
|
|
.every((entry) => entry.property !== 'box-shadow');
|
|
result.shadowTransitionsOnResize = afterResize.filter((e) => e.property === 'box-shadow').length;
|
|
result.resizeStartsNothingOnMarkers = afterResize.filter((e) => e.onMarker).length;
|
|
|
|
// AC3: кольцо выделения появляется сразу и без перехода.
|
|
const target = markers[0];
|
|
const core = target.querySelector('.device-core');
|
|
const ringWidth = () => {
|
|
const shadow = getComputedStyle(core).boxShadow;
|
|
const px = [...shadow.matchAll(/(-?\d+(?:\.\d+)?)px/g)].map((m) => Math.abs(Number(m[1])));
|
|
return Math.max(0, ...px);
|
|
};
|
|
const before = ringWidth();
|
|
card._selId = target.dataset.id;
|
|
card.requestUpdate();
|
|
await card.updateComplete;
|
|
await frame();
|
|
result.selectionRingAppearsInOneFrame = ringWidth() > before;
|
|
result.selectionRingDoesNotAnimate = (core.getAnimations?.() || []).length === 0;
|
|
card._selId = null;
|
|
card.requestUpdate();
|
|
await settle();
|
|
window.__starts.length = 0;
|
|
return result;
|
|
});
|
|
|
|
// AC2: наведение настоящей мышью — отклик остался, анимируется рамка.
|
|
const box = await page.evaluate(() => {
|
|
const card = window.__card;
|
|
const root = card.shadowRoot || card.renderRoot;
|
|
const marker = root.querySelector('[data-hp="device"]');
|
|
const rect = marker.getBoundingClientRect();
|
|
return { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 };
|
|
});
|
|
await page.mouse.move(box.x - 120, box.y - 120);
|
|
await page.waitForTimeout(60);
|
|
await page.mouse.move(box.x, box.y);
|
|
await page.waitForTimeout(250);
|
|
const hover = await page.evaluate(() => {
|
|
const starts = [...window.__starts];
|
|
return {
|
|
// Браузер сообщает длинные формы: border-top-color и так далее.
|
|
hoverAnimatesBorder: starts.some((entry) => /^border-\w+-color$/.test(entry.property) && entry.onMarker),
|
|
hoverStartsNoShadow: starts.every((entry) => entry.property !== 'box-shadow'),
|
|
};
|
|
});
|
|
|
|
checkAll({ ...out, ...hover }, { shadowTransitionsOnResize: 0, resizeStartsNothingOnMarkers: 0 });
|
|
await browser.close();
|
|
finish();
|