From 673593c9b6682a4b673c5fd69ae2f5a2cfebdf21 Mon Sep 17 00:00:00 2001 From: Ty Balascio Date: Fri, 25 Sep 2026 08:26:20 -0700 Subject: [PATCH] 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. --- frontend/src/capture/SessionCapture.svelte | 61 +++++++++++++++++++++- 1 file changed, 60 insertions(+), 1 deletion(-) diff --git a/frontend/src/capture/SessionCapture.svelte b/frontend/src/capture/SessionCapture.svelte index 35686da..7d29b65 100644 --- a/frontend/src/capture/SessionCapture.svelte +++ b/frontend/src/capture/SessionCapture.svelte @@ -18,6 +18,56 @@ $: measure = kind === 'measure' ? cap : 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; function pickFiles(e: Event) { @@ -93,7 +143,13 @@ {:else if kind === 'mask'}

3. Mask

-

Adjust sheet and sensitivity, then approve the mask.

+

Adjust sheet and sensitivity, then approve the mask. Removed background shows as a gray checkerboard.

+ {#if cap.source && maskObjUrl} +
+ + calibration photo (hidden, drives mask preview) +
+ {/if} {#if control}