diff --git a/frontend/src/capture/MeasurePanel.svelte b/frontend/src/capture/MeasurePanel.svelte
index 1ad1bc4..dc52f7b 100644
--- a/frontend/src/capture/MeasurePanel.svelte
+++ b/frontend/src/capture/MeasurePanel.svelte
@@ -228,19 +228,21 @@
{/if}
- {#if maskedPreview}
-
- {:else}
-
- {previewError || 'Preparing background-removed preview…'}
-
+
+
+ {#if previewError}
+ {previewError}
{/if}
@@ -449,7 +451,7 @@
.mode-btn.active { background: #00e5ff; color: #002; }
.mode-hint { margin: 0 0 10px; }
.nudge { color: #e8a; font-size: 0.74rem; margin: 6px 0 0; line-height: 1.45; }
- .preview-loading { min-height: 420px; border: 1px solid #333; border-radius: 8px; display: flex; align-items: center; justify-content: center; color: #999; text-align: center; padding: 16px; box-sizing: border-box; }
+ .preview-err { color: #ffb4a2; font-size: 0.85rem; margin: 8px 0 0; }
.controls { margin-top: 16px; display: flex; flex-direction: column; gap: 14px; }
.ctl-label { font-size: 0.78rem; color: #aaa; margin: 0 0 6px; }
.ctl-label .sub { font-size: 0.7rem; color: #777; }
diff --git a/frontend/src/capture/PhotoStage.svelte b/frontend/src/capture/PhotoStage.svelte
index 1fc3d85..d40bf2c 100644
--- a/frontend/src/capture/PhotoStage.svelte
+++ b/frontend/src/capture/PhotoStage.svelte
@@ -69,7 +69,9 @@
};
const paintOverlays = (ctx: CanvasRenderingContext2D) => {
- if (!canvas || mode === 'tape') return;
+ if (!canvas) return;
+ // Mask checkerboard is valid in tape mode too (source-grid, moves with the photo).
+ // Bbox/axis arrow stay bbox-mode only — tape uses the fixed SVG fixture instead.
if (maskBitset && maskW > 0 && maskH > 0) {
const off = document.createElement('canvas');
off.width = maskW;
@@ -91,6 +93,7 @@
octx.putImageData(d, 0, 0);
ctx.drawImage(off, 0, 0, canvas.width, canvas.height);
}
+ if (mode === 'tape') return;
if (bbox && bbox.corners.length === 4) {
const c = bbox.corners;
ctx.strokeStyle = '#00e5ff';
@@ -392,7 +395,16 @@
.tape-stage { width: 100%; outline: none; }
.tw {
position: relative; width: 100%; aspect-ratio: 3 / 4;
- background: #111; border: 1px solid #333; border-radius: 8px; overflow: hidden;
+ /* Checkerboard so bg-removed / transparent pixels read as cutout (same SoT as Mask). */
+ background-color: #eee;
+ background-image:
+ linear-gradient(45deg, #ccc 25%, transparent 25%),
+ linear-gradient(-45deg, #ccc 25%, transparent 25%),
+ linear-gradient(45deg, transparent 75%, #ccc 75%),
+ linear-gradient(-45deg, transparent 75%, #ccc 75%);
+ background-size: 16px 16px;
+ background-position: 0 0, 0 8px, 8px -8px, -8px 0;
+ border: 1px solid #333; border-radius: 8px; overflow: hidden;
touch-action: none; user-select: none; cursor: grab;
}
.tw.dragging { cursor: grabbing; }
diff --git a/frontend/src/png/maskedPreview.ts b/frontend/src/png/maskedPreview.ts
index 07b872a..dd6a96d 100644
--- a/frontend/src/png/maskedPreview.ts
+++ b/frontend/src/png/maskedPreview.ts
@@ -30,22 +30,30 @@ export async function createMaskedPreviewBlob(
try {
const width = bitmap.width;
const height = bitmap.height;
- const hasOffscreenCanvas = typeof OffscreenCanvas !== 'undefined';
- const canvas: OffscreenCanvas | HTMLCanvasElement = hasOffscreenCanvas
- ? new OffscreenCanvas(width, height)
- : Object.assign(document.createElement('canvas'), { width, height });
+ if (bitset.length < width * height) {
+ throw new Error(`Mask bitset length ${bitset.length} < ${width * height}`);
+ }
+ // Prefer HTMLCanvasElement on the window thread — OffscreenCanvas.convertToBlob
+ // has hung / failed on some WebKit builds; worker code can still use Offscreen.
+ const useHtml = typeof document !== 'undefined';
+ const canvas: OffscreenCanvas | HTMLCanvasElement = useHtml
+ ? Object.assign(document.createElement('canvas'), { width, height })
+ : new OffscreenCanvas(width, height);
const ctx = canvas.getContext('2d') as CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D | null;
if (!ctx) throw new Error('Could not create preview canvas');
ctx.drawImage(bitmap, 0, 0);
const image = ctx.getImageData(0, 0, width, height);
image.data.set(applyMaskAlpha(image.data, bitset));
ctx.putImageData(image, 0, 0);
- if (hasOffscreenCanvas) {
- return await (canvas as OffscreenCanvas).convertToBlob({ type: 'image/png' });
+ if (useHtml) {
+ return await new Promise((resolve, reject) => {
+ (canvas as HTMLCanvasElement).toBlob(
+ (blob: Blob | null) => (blob ? resolve(blob) : reject(new Error('Could not encode preview'))),
+ 'image/png',
+ );
+ });
}
- return await new Promise((resolve, reject) => {
- (canvas as HTMLCanvasElement).toBlob((blob: Blob | null) => (blob ? resolve(blob) : reject(new Error('Could not encode preview'))), 'image/png');
- });
+ return await (canvas as OffscreenCanvas).convertToBlob({ type: 'image/png' });
} finally {
bitmap.close();
}