fix(web): size the live preview from the PNG's real dimensions, not config

The initial SSE render sized the preview image (and both overlay canvases)
from the server-reported config dimensions (cols x chain_length,
rows x parallel), while the scale slider's re-render path sized from
img.naturalWidth/naturalHeight. Whenever the snapshot PNG's actual size
disagrees with the config (stale config, display service not restarted
after a hardware change), the initial render stretched the image at a
fractional ratio - blurry despite image-rendering: pixelated - and
touching the scale slider "fixed" it. Reported live on the devpi test rig.

Both paths now size from the loaded image's natural dimensions inside
img.onload (which also removes a transient wrong-size flash between
src assignment and load). The meta label now reports the true snapshot
size. The preview card also gets overflow-x-auto so on narrow screens a
wide preview scrolls at its exact pixel-perfect size instead of being
squeezed into the viewport (fractional downscaling of pixel art also
reads as blur).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KEZK1P1Q1fu5pcuVrkrCFZ
This commit is contained in:
ChuckBuilds
2026-07-16 11:10:46 -04:00
co-authored by Claude Sonnet 5
parent 84c41dfbf0
commit b6a8a88665
2 changed files with 28 additions and 19 deletions
+24 -18
View File
@@ -1870,29 +1870,35 @@
// Current scale from slider
const scale = parseInt(document.getElementById('scaleRange')?.value || '8');
// Update image and meta label
// Update image and meta label. Size everything from the PNG's
// OWN dimensions (naturalWidth/Height) once it has loaded —
// not from the server-reported config dimensions. If the
// config disagrees with what the display service actually
// renders (stale config, service not yet restarted), sizing
// from config stretches the image at a fractional ratio and
// the preview looks blurry until the scale slider forces a
// re-render. The slider path always used natural dimensions;
// now both paths do.
img.style.imageRendering = 'pixelated';
img.onload = () => {
const nw = img.naturalWidth || data.width || 128;
const nh = img.naturalHeight || data.height || 64;
const width = nw * scale;
const height = nh * scale;
img.style.width = width + 'px';
img.style.height = height + 'px';
ledCanvas.width = width;
ledCanvas.height = height;
canvas.width = width;
canvas.height = height;
const meta = document.getElementById('previewMeta');
if (meta) {
meta.textContent = `${nw} x ${nh} @ ${scale}x`;
}
drawGrid(canvas, nw, nh, scale);
renderLedDots();
};
img.src = `data:image/png;base64,${data.image}`;
const meta = document.getElementById('previewMeta');
if (meta) {
meta.textContent = `${data.width || 128} x ${data.height || 64} @ ${scale}x`;
}
// Size the canvases to match
const width = (data.width || 128) * scale;
const height = (data.height || 64) * scale;
img.style.width = width + 'px';
img.style.height = height + 'px';
ledCanvas.width = width;
ledCanvas.height = height;
canvas.width = width;
canvas.height = height;
drawGrid(canvas, data.width || 128, data.height || 64, scale);
renderLedDots();
} else {
stage.style.display = 'none';
placeholder.style.display = 'block';