mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-29 03:09:36 +00:00
187 lines
9.1 KiB
JavaScript
187 lines
9.1 KiB
JavaScript
// Issue #213: dense fractional geometry plus painted-centroid proof at common DPRs.
|
|
import { launch, checkAll, finish } from './serve.mjs';
|
|
|
|
const dprs = [1, 1.25, 1.5, 2];
|
|
const paintedSizes = [24, 24.25, 31.75, 32, 47.25, 50.4, 55.75, 63.25, 79.75, 95.5, 111.75, 112];
|
|
const evidence = [];
|
|
|
|
for (const dpr of dprs) {
|
|
const { page, browser } = await launch(
|
|
{ width: 980, height: 760 }, dpr, [], { colorScheme: 'light' },
|
|
);
|
|
await page.evaluate(async () => {
|
|
const c = window.__card;
|
|
c.hass = { ...c.hass, themes: { ...(c.hass.themes || {}), darkMode: false } };
|
|
c._regSignature = '';
|
|
c._maybeRebuildDevices();
|
|
c._setMode('view');
|
|
c.requestUpdate();
|
|
await c.updateComplete;
|
|
await new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)));
|
|
const root = c.shadowRoot || c.renderRoot;
|
|
const marker = root.querySelector('.dev[data-id="d_light1"]');
|
|
const layer = marker.closest('.devlayer');
|
|
for (const node of layer.children) {
|
|
if (node !== marker) node.style.display = 'none';
|
|
}
|
|
for (const node of layer.parentElement.children) {
|
|
if (node !== layer) node.style.visibility = 'hidden';
|
|
}
|
|
marker.className = 'dev theme-light';
|
|
marker.style.left = '50%';
|
|
marker.style.top = '50%';
|
|
marker.style.setProperty('--dev-scale', '1');
|
|
marker.style.setProperty('--device-face-bg', 'rgb(0 255 0)');
|
|
marker.style.setProperty('--device-face-fg', 'rgb(0 255 0)');
|
|
marker.style.setProperty('--device-shell-stroke', 'rgb(255 0 0)');
|
|
for (const extra of marker.querySelectorAll(
|
|
'.device-pulse, .activity-dot, .newdot, .habadge, .lqi, .device-sections',
|
|
)) extra.style.display = 'none';
|
|
marker.querySelector('ha-icon').style.visibility = 'hidden';
|
|
marker.querySelector('.device-core').style.transition = 'none';
|
|
marker.querySelector('.device-core').style.boxShadow = 'none';
|
|
const frame = marker.querySelector('.device-shell-frame');
|
|
frame.style.transition = 'none';
|
|
frame.style.boxShadow = 'none';
|
|
frame.style.border = '0';
|
|
// A solid diagnostic shell makes the alpha-weighted centroid stable even
|
|
// when the production stroke is only one sparse physical pixel wide.
|
|
frame.style.background = 'rgb(255 0 0)';
|
|
root.querySelector('.stage').style.background = 'rgb(0 0 255)';
|
|
});
|
|
|
|
const dense = await page.evaluate(async () => {
|
|
const root = window.__card.shadowRoot || window.__card.renderRoot;
|
|
const marker = root.querySelector('.dev[data-id="d_light1"]');
|
|
const core = marker.querySelector('.device-core');
|
|
const frame = marker.querySelector('.device-shell-frame');
|
|
let maxDx = 0;
|
|
let maxDy = 0;
|
|
let maxSizeError = 0;
|
|
let samples = 0;
|
|
for (let quarter = 24 * 4; quarter <= 112 * 4; quarter++) {
|
|
const size = quarter / 4;
|
|
marker.style.setProperty('--device-base-size', `${size}px`);
|
|
// A style/layout read is intentional: this smoke enumerates browser
|
|
// quantisation, production has no per-marker measurement loop.
|
|
const a = core.getBoundingClientRect();
|
|
const b = frame.getBoundingClientRect();
|
|
maxDx = Math.max(maxDx, Math.abs((a.left + a.width / 2) - (b.left + b.width / 2)));
|
|
maxDy = Math.max(maxDy, Math.abs((a.top + a.height / 2) - (b.top + b.height / 2)));
|
|
maxSizeError = Math.max(maxSizeError, Math.abs(a.width - size), Math.abs(a.height - size));
|
|
samples++;
|
|
}
|
|
return { samples, maxDx, maxDy, maxSizeError };
|
|
});
|
|
|
|
const painted = [];
|
|
for (const size of paintedSizes) {
|
|
const clip = await page.evaluate(async (nextSize) => {
|
|
const root = window.__card.shadowRoot || window.__card.renderRoot;
|
|
const marker = root.querySelector('.dev[data-id="d_light1"]');
|
|
marker.style.setProperty('--device-base-size', `${nextSize}px`);
|
|
await new Promise((resolve) => requestAnimationFrame(resolve));
|
|
const r = marker.querySelector('.device-core').getBoundingClientRect();
|
|
const cx = r.left + r.width / 2;
|
|
const cy = r.top + r.height / 2;
|
|
// Fixed integer clip bounds remove screenshot-crop quantisation from the
|
|
// centroid comparison; all tested shells fit with a generous margin.
|
|
return { x: Math.floor(cx - 85), y: Math.floor(cy - 85), width: 170, height: 170 };
|
|
}, size);
|
|
await page.evaluate(() => {
|
|
const marker = (window.__card.shadowRoot || window.__card.renderRoot)
|
|
.querySelector('.dev[data-id="d_light1"]');
|
|
marker.querySelector('.device-core').style.visibility = 'hidden';
|
|
marker.querySelector('.device-shell-frame').style.visibility = 'visible';
|
|
});
|
|
const shellPng = await page.screenshot({ type: 'png', clip });
|
|
await page.evaluate(() => {
|
|
const marker = (window.__card.shadowRoot || window.__card.renderRoot)
|
|
.querySelector('.dev[data-id="d_light1"]');
|
|
marker.querySelector('.device-core').style.visibility = 'visible';
|
|
marker.querySelector('.device-shell-frame').style.visibility = 'hidden';
|
|
});
|
|
const corePng = await page.screenshot({ type: 'png', clip });
|
|
await page.evaluate(() => {
|
|
const marker = (window.__card.shadowRoot || window.__card.renderRoot)
|
|
.querySelector('.dev[data-id="d_light1"]');
|
|
marker.querySelector('.device-shell-frame').style.visibility = 'visible';
|
|
});
|
|
const centroid = await page.evaluate(async ({ shellBase64, coreBase64 }) => {
|
|
const point = async (base64, channel) => {
|
|
const raw = atob(base64);
|
|
const bytes = Uint8Array.from(raw, (char) => char.charCodeAt(0));
|
|
const bitmap = await createImageBitmap(new Blob([bytes], { type: 'image/png' }));
|
|
const canvas = new OffscreenCanvas(bitmap.width, bitmap.height);
|
|
const context = canvas.getContext('2d', { willReadFrequently: true });
|
|
context.drawImage(bitmap, 0, 0);
|
|
const pixels = context.getImageData(0, 0, bitmap.width, bitmap.height).data;
|
|
let weight = 0, weightedX = 0, weightedY = 0;
|
|
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
for (let y = 0; y < bitmap.height; y++) for (let x = 0; x < bitmap.width; x++) {
|
|
const i = (y * bitmap.width + x) * 4;
|
|
const [r, g, b, a] = [pixels[i], pixels[i + 1], pixels[i + 2], pixels[i + 3]];
|
|
const primary = channel === 'red' ? r : g;
|
|
const other = channel === 'red' ? Math.max(g, b) : Math.max(r, b);
|
|
const sample = Math.max(0, primary - other) * a / 255;
|
|
weight += sample; weightedX += x * sample; weightedY += y * sample;
|
|
if (sample > 0) {
|
|
minX = Math.min(minX, x); minY = Math.min(minY, y);
|
|
maxX = Math.max(maxX, x); maxY = Math.max(maxY, y);
|
|
}
|
|
}
|
|
return {
|
|
x: weightedX / weight, y: weightedY / weight, weight,
|
|
boundsX: (minX + maxX) / 2, boundsY: (minY + maxY) / 2,
|
|
};
|
|
};
|
|
// Capture each paint layer in isolation. Subtracting the large core from
|
|
// a one-pixel annulus amplifies harmless edge AA into a false centroid.
|
|
const red = await point(shellBase64, 'red');
|
|
const green = await point(coreBase64, 'green');
|
|
return {
|
|
dx: Math.abs(red.x - green.x),
|
|
dy: Math.abs(red.y - green.y),
|
|
boundsDx: Math.abs(red.boundsX - green.boundsX),
|
|
boundsDy: Math.abs(red.boundsY - green.boundsY),
|
|
redWeight: red.weight,
|
|
greenWeight: green.weight,
|
|
};
|
|
}, { shellBase64: shellPng.toString('base64'), coreBase64: corePng.toString('base64') });
|
|
painted.push({ size, ...centroid });
|
|
}
|
|
|
|
const mutant = await page.evaluate(async () => {
|
|
const root = window.__card.shadowRoot || window.__card.renderRoot;
|
|
const marker = root.querySelector('.dev[data-id="d_light1"]');
|
|
marker.style.setProperty('--device-base-size', '50.4px');
|
|
const frame = marker.querySelector('.device-shell-frame');
|
|
frame.style.transform = 'translateX(1px)';
|
|
await new Promise((resolve) => requestAnimationFrame(resolve));
|
|
const core = marker.querySelector('.device-core').getBoundingClientRect();
|
|
const shell = frame.getBoundingClientRect();
|
|
return Math.abs((core.left + core.width / 2) - (shell.left + shell.width / 2));
|
|
});
|
|
|
|
evidence.push({ dpr, dense, painted, mutant });
|
|
await browser.close();
|
|
}
|
|
|
|
const result = {
|
|
denseQuarterPixelMatrixCovers24To112: evidence.every((row) => row.dense.samples === 353),
|
|
domCentresShareOneLayoutQuantum: evidence.every((row) =>
|
|
row.dense.maxDx <= 0.02 && row.dense.maxDy <= 0.02 && row.dense.maxSizeError <= 0.02),
|
|
paintedCentroidsStayWithinCalibratedHalfCssPixel: evidence.every((row) => row.painted.every((sample) =>
|
|
sample.redWeight > 0 && sample.greenWeight > 0
|
|
&& sample.dx <= row.dpr / 2 + 0.1 && sample.dy <= row.dpr / 2 + 0.1)),
|
|
paintedSupportStaysWithinOneRasterPixel: evidence.every((row) => row.painted.every((sample) =>
|
|
sample.boundsDx <= 1.01 && sample.boundsDy <= 1.01)),
|
|
onePixelNudgeMutantIsDetected: evidence.every((row) => row.mutant >= 0.98),
|
|
};
|
|
|
|
if (Object.values(result).some((value) => value !== true)) {
|
|
console.error(`pixel alignment evidence: ${JSON.stringify(evidence, null, 2)}`);
|
|
}
|
|
checkAll(result);
|
|
await finish(null, result);
|