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.
252 lines
9.7 KiB
HTML
252 lines
9.7 KiB
HTML
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<title>px-icon demo</title>
|
|
<style>
|
|
:root {
|
|
--bg: #1b1712;
|
|
--panel-bg: #2a241c;
|
|
--ink: #e8dcc4;
|
|
--accent: #c9a15a;
|
|
--ok: #7fae6a;
|
|
}
|
|
* { box-sizing: border-box; }
|
|
body {
|
|
margin: 0;
|
|
padding: 24px;
|
|
background: var(--bg);
|
|
color: var(--ink);
|
|
font-family: ui-monospace, "SF Mono", Consolas, monospace;
|
|
font-size: 13px;
|
|
}
|
|
h1 { font-size: 15px; font-weight: 600; margin: 28px 0 4px; }
|
|
h1:first-child { margin-top: 0; }
|
|
.sub { color: #948a76; margin: 0 0 16px; line-height: 1.5; max-width: 640px; }
|
|
.controls {
|
|
display: flex; gap: 24px; flex-wrap: wrap; align-items: center;
|
|
margin-bottom: 16px; padding: 12px 16px; background: var(--panel-bg);
|
|
border: 1px solid #3a3327;
|
|
}
|
|
.control { display: flex; align-items: center; gap: 8px; }
|
|
.control label { color: #b8ac93; }
|
|
input[type="range"] { width: 120px; }
|
|
.control .val { color: var(--accent); min-width: 40px; display: inline-block; }
|
|
select {
|
|
background: var(--bg); color: var(--ink); border: 1px solid #3a3327;
|
|
padding: 3px 6px; font-family: inherit;
|
|
}
|
|
button {
|
|
background: var(--bg); color: var(--ink); border: 1px solid var(--accent);
|
|
padding: 6px 12px; font-family: inherit; cursor: pointer;
|
|
}
|
|
button:hover { background: #241f18; }
|
|
.icon-row {
|
|
display: flex; gap: 20px; align-items: flex-end; flex-wrap: wrap;
|
|
margin-bottom: 8px; padding: 16px; background: var(--panel-bg);
|
|
border: 1px solid #3a3327;
|
|
}
|
|
.icon-wrap { display: flex; flex-direction: column; align-items: center; gap: 6px; }
|
|
.icon-wrap px-icon { width: 64px; height: 64px; }
|
|
.icon-label { color: #948a76; font-size: 11px; }
|
|
.log {
|
|
background: #14110d; border: 1px solid #3a3327; padding: 10px 12px;
|
|
max-height: 180px; overflow-y: auto; line-height: 1.6; margin-top: 8px;
|
|
}
|
|
.log .ok { color: var(--ok); }
|
|
.log .t { color: #6b6152; }
|
|
code { color: var(--accent); }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<h1>px-icon</h1>
|
|
<p class="sub">
|
|
Standard <svg> markup, authored inline as normal. The library rasterizes each one onto a shared
|
|
art-pixel grid into its shadow DOM, while the original <svg> stays untouched in the light DOM.
|
|
</p>
|
|
|
|
<div class="controls">
|
|
<div class="control">
|
|
<label for="apSize">art-pixel size (px)</label>
|
|
<input type="range" id="apSize" min="2" max="20" value="6">
|
|
<span class="val" id="apSizeVal">6</span>
|
|
</div>
|
|
<div class="control">
|
|
<label for="mode">downsample</label>
|
|
<select id="mode">
|
|
<option value="average">area average</option>
|
|
<option value="point">hard point sample</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="icon-row" id="mainRow">
|
|
<div class="icon-wrap">
|
|
<px-icon><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200">
|
|
<polygon points="60,50 140,50 170,90 100,180 30,90" fill="#8e1f3a" stroke="#3a0d17" stroke-width="4"/>
|
|
<polygon points="60,50 140,50 100,80" fill="#c9385c"/>
|
|
<polygon points="60,50 30,90 100,80" fill="#a02648"/>
|
|
<polygon points="140,50 170,90 100,80" fill="#a02648"/>
|
|
<polygon points="30,90 100,80 100,180" fill="#6e1730"/>
|
|
<polygon points="170,90 100,80 100,180" fill="#5c1329"/>
|
|
</svg></px-icon>
|
|
<div class="icon-label">ruby (own set)</div>
|
|
</div>
|
|
<div class="icon-wrap">
|
|
<px-icon><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200">
|
|
<rect x="10" y="10" width="180" height="180" rx="14" fill="#7a5a34" stroke="#3f2c18" stroke-width="6"/>
|
|
<rect x="26" y="26" width="148" height="148" fill="none" stroke="#c9a15a" stroke-width="3" stroke-dasharray="10 8"/>
|
|
</svg></px-icon>
|
|
<div class="icon-label">box (own set)</div>
|
|
</div>
|
|
<div class="icon-wrap">
|
|
<px-icon><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200">
|
|
<defs>
|
|
<linearGradient id="pxg1" x1="0%" y1="0%" x2="100%" y2="100%">
|
|
<stop offset="0%" stop-color="#e8c887"/>
|
|
<stop offset="45%" stop-color="#c9a15a"/>
|
|
<stop offset="100%" stop-color="#6b4423"/>
|
|
</linearGradient>
|
|
</defs>
|
|
<circle cx="100" cy="100" r="90" fill="url(#pxg1)"/>
|
|
</svg></px-icon>
|
|
<div class="icon-label">gradient (own set)</div>
|
|
</div>
|
|
<div class="icon-wrap">
|
|
<px-icon><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
|
<path fill="#4a90d9" stroke="#1f5b96" stroke-width="1" d="M12 2 2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"/>
|
|
</svg></px-icon>
|
|
<div class="icon-label">layers (pasted-in external set)</div>
|
|
</div>
|
|
</div>
|
|
|
|
<h1>datastar morph safety</h1>
|
|
<p class="sub">
|
|
This icon lives in a container Idiomorph can morph, the same engine Datastar uses for server-driven
|
|
DOM patches. The buttons below simulate the server re-sending HTML for this fragment: once identical,
|
|
once with a different icon. The raster survives both without flashing back to raw SVG, because the
|
|
morph only ever touches the light-dom <svg>, never the shadow-dom canvas.
|
|
</p>
|
|
<div class="icon-row" id="morphRow">
|
|
<div class="icon-wrap">
|
|
<div id="morphTarget"><px-icon><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200">
|
|
<polygon points="60,50 140,50 170,90 100,180 30,90" fill="#8e1f3a" stroke="#3a0d17" stroke-width="4"/>
|
|
<polygon points="60,50 140,50 100,80" fill="#c9385c"/>
|
|
<polygon points="60,50 30,90 100,80" fill="#a02648"/>
|
|
<polygon points="140,50 170,90 100,80" fill="#a02648"/>
|
|
<polygon points="30,90 100,80 100,180" fill="#6e1730"/>
|
|
<polygon points="170,90 100,80 100,180" fill="#5c1329"/>
|
|
</svg></px-icon></div>
|
|
<div class="icon-label">morph target</div>
|
|
</div>
|
|
</div>
|
|
<div class="controls">
|
|
<button id="morphSame">simulate server morph (identical markup)</button>
|
|
<button id="morphSwap">simulate server morph (swap to different icon)</button>
|
|
</div>
|
|
<div class="log" id="morphLog"></div>
|
|
|
|
<h1>progressive enhancement</h1>
|
|
<p class="sub">
|
|
With a debug delay set on the render pipeline, the raw <svg> (via the shadow-root <slot>) stays
|
|
visible the whole time rasterization is genuinely in flight, then the canvas swaps in when it's ready.
|
|
No JS, or a slow one, degrades to a plain icon rather than nothing.
|
|
</p>
|
|
<div class="controls">
|
|
<div class="control">
|
|
<label for="peDelay">artificial render delay (ms)</label>
|
|
<input type="range" id="peDelay" min="0" max="3000" step="250" value="0">
|
|
<span class="val" id="peDelayVal">0</span>
|
|
</div>
|
|
<button id="peRerender">re-mount icon to see the delay</button>
|
|
</div>
|
|
<div class="icon-row" id="peRow">
|
|
<div class="icon-wrap" id="peWrap">
|
|
<px-icon><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
|
<path fill="#4a90d9" stroke="#1f5b96" stroke-width="1" d="M12 2 2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"/>
|
|
</svg></px-icon>
|
|
</div>
|
|
</div>
|
|
|
|
<script type="module">
|
|
import { refreshAll } from '../px-icon.js';
|
|
|
|
const apSizeInput = document.getElementById('apSize');
|
|
const apSizeVal = document.getElementById('apSizeVal');
|
|
const modeSelect = document.getElementById('mode');
|
|
|
|
function applyConfig() {
|
|
const ap = apSizeInput.value;
|
|
apSizeVal.textContent = ap;
|
|
document.documentElement.style.setProperty('--pxicon-ap-size', ap + 'px');
|
|
document.documentElement.style.setProperty('--pxicon-mode', modeSelect.value);
|
|
refreshAll();
|
|
}
|
|
apSizeInput.addEventListener('input', applyConfig);
|
|
modeSelect.addEventListener('change', applyConfig);
|
|
applyConfig();
|
|
|
|
// progressive enhancement delay control
|
|
const peDelay = document.getElementById('peDelay');
|
|
const peDelayVal = document.getElementById('peDelayVal');
|
|
const peWrap = document.getElementById('peWrap');
|
|
const peRerender = document.getElementById('peRerender');
|
|
const peIconMarkup = peWrap.innerHTML;
|
|
|
|
peDelay.addEventListener('input', () => {
|
|
peDelayVal.textContent = peDelay.value;
|
|
document.documentElement.style.setProperty('--pxicon-debug-delay', peDelay.value);
|
|
});
|
|
peRerender.addEventListener('click', () => {
|
|
// re-mount so connectedCallback runs again with the current delay
|
|
peWrap.innerHTML = '';
|
|
requestAnimationFrame(() => { peWrap.innerHTML = peIconMarkup; });
|
|
});
|
|
</script>
|
|
|
|
<script src="https://unpkg.com/idiomorph@0.7.4/dist/idiomorph.min.js"></script>
|
|
<script type="module">
|
|
const morphTarget = document.getElementById('morphTarget');
|
|
const log = document.getElementById('morphLog');
|
|
|
|
const originalMarkup = morphTarget.innerHTML;
|
|
const swappedMarkup = `<px-icon><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
|
<path fill="#4a90d9" stroke="#1f5b96" stroke-width="1" d="M12 2 2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"/>
|
|
</svg></px-icon>`;
|
|
|
|
function logLine(text, ok) {
|
|
const line = document.createElement('div');
|
|
const t = new Date().toLocaleTimeString();
|
|
line.innerHTML = `<span class="t">${t}</span> ${ok ? '<span class="ok">✓</span> ' : ''}${text}`;
|
|
log.prepend(line);
|
|
}
|
|
|
|
function currentCanvas() {
|
|
const el = morphTarget.querySelector('px-icon');
|
|
return el && el.shadowRoot ? el.shadowRoot.querySelector('canvas') : null;
|
|
}
|
|
|
|
document.getElementById('morphSame').addEventListener('click', () => {
|
|
const before = currentCanvas();
|
|
Idiomorph.morph(morphTarget, `<div id="morphTarget">${originalMarkup}</div>`);
|
|
const after = currentCanvas();
|
|
logLine(
|
|
before && after === before
|
|
? 'identical morph applied, same <canvas> node survived untouched'
|
|
: 'canvas node changed (unexpected, check console)',
|
|
before && after === before
|
|
);
|
|
});
|
|
|
|
document.getElementById('morphSwap').addEventListener('click', () => {
|
|
Idiomorph.morph(morphTarget, `<div id="morphTarget">${swappedMarkup}</div>`);
|
|
logLine('server sent a different icon, light-dom <svg> morphed, watch it re-rasterize above', true);
|
|
});
|
|
|
|
logLine('ready. click a button to simulate a server-driven morph.', false);
|
|
</script>
|
|
|
|
</body>
|
|
</html>
|