2 Commits
Author SHA1 Message Date
Markk116 5a7dfcc019 take aspect ratio into account when rasterizing 2026-08-08 21:19:25 +02:00
mark 0c2785ca3b fix: sliders in demo require reload to rerender
CSS custom property changes (--pxicon-ap-size, --pxicon-mode) aren't
observed by MutationObserver/ResizeObserver, so a live-mounted <px-icon>
never rechecked its config. Add a pxicon:refresh document event + exported
refreshAll() helper; demo calls it from its control handlers.
2026-08-08 15:56:58 +02:00
2 changed files with 34 additions and 11 deletions
+2 -1
View File
@@ -170,7 +170,7 @@
</div> </div>
<script type="module"> <script type="module">
import '../px-icon.js'; import { refreshAll } from '../px-icon.js';
const apSizeInput = document.getElementById('apSize'); const apSizeInput = document.getElementById('apSize');
const apSizeVal = document.getElementById('apSizeVal'); const apSizeVal = document.getElementById('apSizeVal');
@@ -181,6 +181,7 @@ function applyConfig() {
apSizeVal.textContent = ap; apSizeVal.textContent = ap;
document.documentElement.style.setProperty('--pxicon-ap-size', ap + 'px'); document.documentElement.style.setProperty('--pxicon-ap-size', ap + 'px');
document.documentElement.style.setProperty('--pxicon-mode', modeSelect.value); document.documentElement.style.setProperty('--pxicon-mode', modeSelect.value);
refreshAll();
} }
apSizeInput.addEventListener('input', applyConfig); apSizeInput.addEventListener('input', applyConfig);
modeSelect.addEventListener('change', applyConfig); modeSelect.addEventListener('change', applyConfig);
+32 -10
View File
@@ -15,6 +15,14 @@ const DEFAULT_AP_SIZE = 8;
const DEFAULT_MODE = 'average'; const DEFAULT_MODE = 'average';
const SUPERSAMPLE = 8; // per grid cell, for point-sample mode const SUPERSAMPLE = 8; // per grid cell, for point-sample mode
// CSS custom properties (--pxicon-ap-size, --pxicon-mode, ...) aren't
// observable the way DOM mutations/resizes are: MutationObserver and
// ResizeObserver don't fire when a cascaded custom property changes.
// Instances listen for this document-level event so config changes (e.g.
// a page-level theme/control update) can trigger a re-check without every
// caller needing to touch each <px-icon>'s internals.
const REFRESH_EVENT = 'pxicon:refresh';
// key -> Promise<HTMLCanvasElement> (the small pre-upscale grid raster) // key -> Promise<HTMLCanvasElement> (the small pre-upscale grid raster)
const rasterCache = new Map(); const rasterCache = new Map();
@@ -131,12 +139,15 @@ class PxIcon extends HTMLElement {
// e.g. a morph swapping in a different icon. // e.g. a morph swapping in a different icon.
this._observer ??= new MutationObserver(() => this._scheduleRender()); this._observer ??= new MutationObserver(() => this._scheduleRender());
this._observer.observe(this, { childList: true, subtree: true, attributes: true, characterData: true }); this._observer.observe(this, { childList: true, subtree: true, attributes: true, characterData: true });
this._refreshHandler ??= () => this._scheduleRender();
document.addEventListener(REFRESH_EVENT, this._refreshHandler);
this._scheduleRender(); this._scheduleRender();
} }
disconnectedCallback() { disconnectedCallback() {
this._observer?.disconnect(); this._observer?.disconnect();
this._resizeObserver?.disconnect(); this._resizeObserver?.disconnect();
document.removeEventListener(REFRESH_EVENT, this._refreshHandler);
} }
_upgrade() { _upgrade() {
@@ -181,10 +192,12 @@ class PxIcon extends HTMLElement {
const apSize = parseFloat(cs.getPropertyValue('--pxicon-ap-size')) || DEFAULT_AP_SIZE; const apSize = parseFloat(cs.getPropertyValue('--pxicon-ap-size')) || DEFAULT_AP_SIZE;
const mode = (cs.getPropertyValue('--pxicon-mode') || DEFAULT_MODE).trim(); const mode = (cs.getPropertyValue('--pxicon-mode') || DEFAULT_MODE).trim();
const displaySize = Math.max(this.clientWidth, this.clientHeight) || 24; const displayW = this.clientWidth || 24;
const snapped = snapToGrid(displaySize, apSize); const displayH = this.clientHeight || 24;
const gridW = Math.max(1, Math.round(snapped / apSize)); const snappedW = snapToGrid(displayW, apSize);
const gridH = gridW; const snappedH = snapToGrid(displayH, apSize);
const gridW = Math.max(1, Math.round(snappedW / apSize));
const gridH = Math.max(1, Math.round(snappedH / apSize));
const svgMarkup = svgEl.outerHTML; const svgMarkup = svgEl.outerHTML;
const key = `${svgMarkup}|${mode}|${gridW}x${gridH}`; const key = `${svgMarkup}|${mode}|${gridW}x${gridH}`;
@@ -200,13 +213,14 @@ class PxIcon extends HTMLElement {
const gridCanvas = await getGridRaster(svgMarkup, mode, gridW, gridH); const gridCanvas = await getGridRaster(svgMarkup, mode, gridW, gridH);
const dpr = window.devicePixelRatio || 1; const dpr = window.devicePixelRatio || 1;
const backingSize = Math.round(snapped * dpr); const backingW = Math.round(snappedW * dpr);
this._canvas.width = backingSize; const backingH = Math.round(snappedH * dpr);
this._canvas.height = backingSize; this._canvas.width = backingW;
this._canvas.height = backingH;
const ctx = this._canvas.getContext('2d'); const ctx = this._canvas.getContext('2d');
ctx.imageSmoothingEnabled = false; ctx.imageSmoothingEnabled = false;
ctx.clearRect(0, 0, backingSize, backingSize); ctx.clearRect(0, 0, backingW, backingH);
ctx.drawImage(gridCanvas, 0, 0, backingSize, backingSize); ctx.drawImage(gridCanvas, 0, 0, backingW, backingH);
if (this._slot.isConnected) { if (this._slot.isConnected) {
// first successful raster: swap the placeholder slot for the // first successful raster: swap the placeholder slot for the
@@ -219,4 +233,12 @@ class PxIcon extends HTMLElement {
customElements.define('px-icon', PxIcon); customElements.define('px-icon', PxIcon);
export { PxIcon, rasterCache }; // Call after changing a --pxicon-* custom property (e.g. on :root, or any
// ancestor of a group of icons) to make every connected <px-icon> re-check
// its computed config and re-render if it actually changed. Cheap to call
// broadly: each instance's own key check skips the work if nothing did.
function refreshAll() {
document.dispatchEvent(new CustomEvent(REFRESH_EVENT));
}
export { PxIcon, rasterCache, refreshAll };