fix(fe): render the photo + mask overlay on the Mask step

The mask screen showed only controls and a pixel-count line - no photo, so
the owner had nothing to adjust the sheet/sensitivity against. Draw the
calibration photo to a canvas and paint removed (non-slab) pixels as a
light-gray checkerboard (CAPTURE-45/50), redrawing when the overlay settles.
Verified in Chromium: mask step now shows the photo with the checkerboard.
This commit is contained in:
2026-09-25 08:26:20 -07:00
parent b03939cf84
commit 673593c9b6
+60 -1
View File
@@ -18,6 +18,56 @@
$: measure = kind === 'measure' ? cap : null; $: measure = kind === 'measure' ? cap : null;
$: results = measure?.results ?? null; $: results = measure?.results ?? null;
// Mask preview: the calibration photo with removed (non-slab) pixels shown
// as a light-gray checkerboard (CAPTURE-45/50). Redrawn when the overlay
// settles. The photo element is bound so its natural size is available.
let maskCanvas: HTMLCanvasElement;
let maskImg: HTMLImageElement;
let maskObjUrl = '';
$: if (kind === 'mask' && cap.source) {
if (maskObjUrl) URL.revokeObjectURL(maskObjUrl);
maskObjUrl = URL.createObjectURL(cap.source.blob);
}
$: if (kind === 'mask' && maskImg?.complete && maskImg.naturalWidth > 0 && overlay) {
drawMaskPreview();
}
function drawMaskPreview() {
const canvas = maskCanvas;
if (!canvas || !maskImg || !overlay) return;
const w = maskImg.naturalWidth;
const h = maskImg.naturalHeight;
// Cap the canvas at a sensible size for performance; keep aspect.
const maxDim = 900;
const scale = Math.min(1, maxDim / Math.max(w, h));
canvas.width = Math.round(w * scale);
canvas.height = Math.round(h * scale);
const ctx = canvas.getContext('2d');
if (!ctx) return;
ctx.drawImage(maskImg, 0, 0, canvas.width, canvas.height);
// Downsample the full-res bitset to the canvas grid and checker removed px.
const bitset = overlay.bitset;
const cw = canvas.width;
const ch = canvas.height;
const cell = 6; // checker cell px
const imgData = ctx.getImageData(0, 0, cw, ch);
const px = imgData.data;
for (let y = 0; y < ch; y++) {
for (let x = 0; x < cw; x++) {
const sx = Math.min(w - 1, Math.round((x / cw) * w));
const sy = Math.min(h - 1, Math.round((y / ch) * h));
if (bitset[sy * w + sx]) continue; // slab pixel: keep photo
const checker = ((Math.floor(x / cell) + Math.floor(y / cell)) % 2 === 0);
const i = (y * cw + x) * 4;
const g = checker ? 210 : 235;
px[i] = g; px[i + 1] = g; px[i + 2] = g; px[i + 3] = 255;
}
}
ctx.putImageData(imgData, 0, 0);
}
let fileInput: HTMLInputElement; let fileInput: HTMLInputElement;
function pickFiles(e: Event) { function pickFiles(e: Event) {
@@ -93,7 +143,13 @@
{:else if kind === 'mask'} {:else if kind === 'mask'}
<h2>3. Mask</h2> <h2>3. Mask</h2>
<p>Adjust sheet and sensitivity, then approve the mask.</p> <p>Adjust sheet and sensitivity, then approve the mask. Removed background shows as a gray checkerboard.</p>
{#if cap.source && maskObjUrl}
<div class="mask-preview">
<canvas bind:this={maskCanvas} aria-label="mask preview"></canvas>
<img bind:this={maskImg} src={maskObjUrl} alt="calibration photo (hidden, drives mask preview)" class="mask-src" />
</div>
{/if}
{#if control} {#if control}
<div class="controls"> <div class="controls">
<label>Sheet <label>Sheet
@@ -154,6 +210,9 @@
</section> </section>
<style> <style>
.mask-preview { margin: 1rem 0; }
.mask-preview canvas { width: 100%; height: auto; max-height: 70vh; object-fit: contain; display: block; border: 1px solid #444; background: #000; }
.mask-src { position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none; }
.picker-row { display: flex; align-items: center; gap: 0.75rem; flex-wrap: wrap; margin: 1rem 0; color: #aaa; } .picker-row { display: flex; align-items: center; gap: 0.75rem; flex-wrap: wrap; margin: 1rem 0; color: #aaa; }
.picker { min-height: 44px; padding: 0.65rem 1rem; background: #00e5ff; color: #002; border: 0; font-weight: 700; border-radius: 4px; } .picker { min-height: 44px; padding: 0.65rem 1rem; background: #00e5ff; color: #002; border: 0; font-weight: 700; border-radius: 4px; }
.file-input { position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none; } .file-input { position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none; }