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:
@@ -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; }
|
||||||
|
|||||||
Reference in New Issue
Block a user