fix(capture): show bg-removed cutout on Measure Zoom/Pan immediately
Tape-mode PhotoStage skipped mask checkerboard painting, so Zoom/Pan depended on async createMaskedPreviewBlob — a hang/fail left the stage without a cutout. Paint mask overlay in tape mode, mount PhotoStage on source+bitset right away (swap to alpha PNG when ready), checkerboard stage backdrop, and prefer HTML canvas for the preview encode.
This commit is contained in:
@@ -228,19 +228,21 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{#if maskedPreview}
|
<!-- Cutout first: source + maskBitset paints checkerboard immediately (tape mode).
|
||||||
<PhotoStage
|
When createMaskedPreviewBlob finishes, swap to alpha PNG (same footprint). -->
|
||||||
mode="tape"
|
<PhotoStage
|
||||||
blob={maskedPreview}
|
mode="tape"
|
||||||
{tape}
|
blob={maskedPreview ?? state.source.blob}
|
||||||
{cropFill}
|
maskBitset={maskedPreview ? null : state.approvedMask.bitset}
|
||||||
{gestureMode}
|
{maskW}
|
||||||
onTapeChange={onTapeChange}
|
{maskH}
|
||||||
/>
|
{tape}
|
||||||
{:else}
|
{cropFill}
|
||||||
<div class="preview-loading" role="status">
|
{gestureMode}
|
||||||
{previewError || 'Preparing background-removed preview…'}
|
onTapeChange={onTapeChange}
|
||||||
</div>
|
/>
|
||||||
|
{#if previewError}
|
||||||
|
<p class="preview-err" role="status">{previewError}</p>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class="controls">
|
<div class="controls">
|
||||||
@@ -449,7 +451,7 @@
|
|||||||
.mode-btn.active { background: #00e5ff; color: #002; }
|
.mode-btn.active { background: #00e5ff; color: #002; }
|
||||||
.mode-hint { margin: 0 0 10px; }
|
.mode-hint { margin: 0 0 10px; }
|
||||||
.nudge { color: #e8a; font-size: 0.74rem; margin: 6px 0 0; line-height: 1.45; }
|
.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; }
|
.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 { font-size: 0.78rem; color: #aaa; margin: 0 0 6px; }
|
||||||
.ctl-label .sub { font-size: 0.7rem; color: #777; }
|
.ctl-label .sub { font-size: 0.7rem; color: #777; }
|
||||||
|
|||||||
@@ -69,7 +69,9 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
const paintOverlays = (ctx: CanvasRenderingContext2D) => {
|
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) {
|
if (maskBitset && maskW > 0 && maskH > 0) {
|
||||||
const off = document.createElement('canvas');
|
const off = document.createElement('canvas');
|
||||||
off.width = maskW;
|
off.width = maskW;
|
||||||
@@ -91,6 +93,7 @@
|
|||||||
octx.putImageData(d, 0, 0);
|
octx.putImageData(d, 0, 0);
|
||||||
ctx.drawImage(off, 0, 0, canvas.width, canvas.height);
|
ctx.drawImage(off, 0, 0, canvas.width, canvas.height);
|
||||||
}
|
}
|
||||||
|
if (mode === 'tape') return;
|
||||||
if (bbox && bbox.corners.length === 4) {
|
if (bbox && bbox.corners.length === 4) {
|
||||||
const c = bbox.corners;
|
const c = bbox.corners;
|
||||||
ctx.strokeStyle = '#00e5ff';
|
ctx.strokeStyle = '#00e5ff';
|
||||||
@@ -392,7 +395,16 @@
|
|||||||
.tape-stage { width: 100%; outline: none; }
|
.tape-stage { width: 100%; outline: none; }
|
||||||
.tw {
|
.tw {
|
||||||
position: relative; width: 100%; aspect-ratio: 3 / 4;
|
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;
|
touch-action: none; user-select: none; cursor: grab;
|
||||||
}
|
}
|
||||||
.tw.dragging { cursor: grabbing; }
|
.tw.dragging { cursor: grabbing; }
|
||||||
|
|||||||
@@ -30,22 +30,30 @@ export async function createMaskedPreviewBlob(
|
|||||||
try {
|
try {
|
||||||
const width = bitmap.width;
|
const width = bitmap.width;
|
||||||
const height = bitmap.height;
|
const height = bitmap.height;
|
||||||
const hasOffscreenCanvas = typeof OffscreenCanvas !== 'undefined';
|
if (bitset.length < width * height) {
|
||||||
const canvas: OffscreenCanvas | HTMLCanvasElement = hasOffscreenCanvas
|
throw new Error(`Mask bitset length ${bitset.length} < ${width * height}`);
|
||||||
? new OffscreenCanvas(width, height)
|
}
|
||||||
: Object.assign(document.createElement('canvas'), { 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;
|
const ctx = canvas.getContext('2d') as CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D | null;
|
||||||
if (!ctx) throw new Error('Could not create preview canvas');
|
if (!ctx) throw new Error('Could not create preview canvas');
|
||||||
ctx.drawImage(bitmap, 0, 0);
|
ctx.drawImage(bitmap, 0, 0);
|
||||||
const image = ctx.getImageData(0, 0, width, height);
|
const image = ctx.getImageData(0, 0, width, height);
|
||||||
image.data.set(applyMaskAlpha(image.data, bitset));
|
image.data.set(applyMaskAlpha(image.data, bitset));
|
||||||
ctx.putImageData(image, 0, 0);
|
ctx.putImageData(image, 0, 0);
|
||||||
if (hasOffscreenCanvas) {
|
if (useHtml) {
|
||||||
return await (canvas as OffscreenCanvas).convertToBlob({ type: 'image/png' });
|
return await new Promise<Blob>((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<Blob>((resolve, reject) => {
|
return await (canvas as OffscreenCanvas).convertToBlob({ type: 'image/png' });
|
||||||
(canvas as HTMLCanvasElement).toBlob((blob: Blob | null) => (blob ? resolve(blob) : reject(new Error('Could not encode preview'))), 'image/png');
|
|
||||||
});
|
|
||||||
} finally {
|
} finally {
|
||||||
bitmap.close();
|
bitmap.close();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user