gate-b: fix mask overlay opacity, auto-sheet fallback, and knob debounce
- drawMaskOverlay: reduce green overlay max alpha 140→80 (~31% opacity), lighten green from (0,200,0) to (60,220,60) so photo stays visible - resolveSheet: when auto-detect is ambiguous, try both green and black sheet modes, pick whichever produces non-trivial slab area (1%-99%) - Knob sliders: debounce full remask to 150ms; oninput redraws with previous mask (cheap), onchange triggers immediate full remask - Add onDestroy cleanup for debounce timeout Fixes: issue #55 — iPad mask preview nearly solid green, knobs laggy
This commit is contained in:
@@ -20,6 +20,38 @@
|
||||
import { SKU_REGEX } from '$lib/validation/constants.js';
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
|
||||
// ---- Debounce helpers for knob remask ----
|
||||
let remaskTimeout: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
function scheduleRemask() {
|
||||
if (remaskTimeout) clearTimeout(remaskTimeout);
|
||||
remaskTimeout = setTimeout(() => {
|
||||
const photo = currentPhoto();
|
||||
if (!photo || !photo.knobSettings) return;
|
||||
applyMaskKnobs(photo.knobSettings);
|
||||
if (maskCanvas) drawMaskOverlay(maskCanvas, photo);
|
||||
remaskTimeout = null;
|
||||
}, 150);
|
||||
}
|
||||
|
||||
function handleKnobChange(knobs: KnobSettings) {
|
||||
if (remaskTimeout) clearTimeout(remaskTimeout);
|
||||
remaskTimeout = null;
|
||||
applyMaskKnobs(knobs);
|
||||
const photo = currentPhoto();
|
||||
if (photo && maskCanvas) drawMaskOverlay(maskCanvas, photo);
|
||||
}
|
||||
|
||||
function handleKnobInput(knobs: KnobSettings) {
|
||||
const photo = currentPhoto();
|
||||
if (!photo) return;
|
||||
photo.knobSettings = { ...knobs };
|
||||
// Immediately redraw with the current mask — cheap, no remask.
|
||||
if (maskCanvas) drawMaskOverlay(maskCanvas, photo);
|
||||
// Schedule full remask after 150ms of no further input.
|
||||
scheduleRemask();
|
||||
}
|
||||
|
||||
// ---- Reactive state (Svelte 5 runes) ----
|
||||
let state = $state<SlabState>(createSlabState());
|
||||
|
||||
@@ -309,12 +341,14 @@
|
||||
|
||||
const imgData = ctx.createImageData(canvas.width, canvas.height);
|
||||
const mask = photo.maskResult!.mask;
|
||||
// Translucent light green overlay — photo remains clearly visible.
|
||||
// Max alpha 80 (~31% opacity) on full-slab pixels; feathered edge blends smoothly.
|
||||
for (let i = 0; i < mask.length; i++) {
|
||||
const alpha = Math.round((mask[i]! / 255) * 140);
|
||||
const alpha = Math.round((mask[i]! / 255) * 80);
|
||||
const pixelIdx = i * 4;
|
||||
imgData.data[pixelIdx] = 0;
|
||||
imgData.data[pixelIdx + 1] = 200;
|
||||
imgData.data[pixelIdx + 2] = 0;
|
||||
imgData.data[pixelIdx] = 60;
|
||||
imgData.data[pixelIdx + 1] = 220;
|
||||
imgData.data[pixelIdx + 2] = 60;
|
||||
imgData.data[pixelIdx + 3] = alpha;
|
||||
}
|
||||
ctx.putImageData(imgData, 0, 0);
|
||||
@@ -415,6 +449,14 @@
|
||||
ctx.putImageData(imgData, 0, 0);
|
||||
}
|
||||
|
||||
// ---- Lifecycle ----
|
||||
onDestroy(() => {
|
||||
if (remaskTimeout) {
|
||||
clearTimeout(remaskTimeout);
|
||||
remaskTimeout = null;
|
||||
}
|
||||
});
|
||||
|
||||
// ---- Effects ----
|
||||
$effect(() => {
|
||||
if (state.step === 'mask' && maskCanvas) {
|
||||
@@ -486,8 +528,7 @@
|
||||
<label>Sheet Mode</label>
|
||||
<select value={getMaskKnobs().sheet} onchange={(e) => {
|
||||
const knobs = getMaskKnobs();
|
||||
applyMaskKnobs({ ...knobs, sheet: e.target.value as KnobSettings['sheet'] });
|
||||
maskCanvas && drawMaskOverlay(maskCanvas, currentPhoto()!);
|
||||
handleKnobChange({ ...knobs, sheet: e.target.value as KnobSettings['sheet'] });
|
||||
}}>
|
||||
<option value="auto">Auto</option>
|
||||
<option value="green">Green</option>
|
||||
@@ -497,29 +538,20 @@
|
||||
<div class="knob-group">
|
||||
<label>Sensitivity: {getMaskKnobs().sensitivity.toFixed(2)}</label>
|
||||
<input type="range" min="0" max="1" step="0.01" value={getMaskKnobs().sensitivity}
|
||||
oninput={(e) => {
|
||||
const knobs = getMaskKnobs();
|
||||
applyMaskKnobs({ ...knobs, sensitivity: parseFloat(e.target.value) });
|
||||
maskCanvas && drawMaskOverlay(maskCanvas, currentPhoto()!);
|
||||
}} />
|
||||
oninput={(e) => handleKnobInput({ ...getMaskKnobs(), sensitivity: parseFloat(e.target.value) })}
|
||||
onchange={(e) => handleKnobChange({ ...getMaskKnobs(), sensitivity: parseFloat(e.target.value) })} />
|
||||
</div>
|
||||
<div class="knob-group">
|
||||
<label>Edge Offset: {getMaskKnobs().edgeOffset}</label>
|
||||
<input type="range" min="-20" max="20" step="1" value={getMaskKnobs().edgeOffset}
|
||||
oninput={(e) => {
|
||||
const knobs = getMaskKnobs();
|
||||
applyMaskKnobs({ ...knobs, edgeOffset: parseInt(e.target.value) });
|
||||
maskCanvas && drawMaskOverlay(maskCanvas, currentPhoto()!);
|
||||
}} />
|
||||
oninput={(e) => handleKnobInput({ ...getMaskKnobs(), edgeOffset: parseInt(e.target.value) })}
|
||||
onchange={(e) => handleKnobChange({ ...getMaskKnobs(), edgeOffset: parseInt(e.target.value) })} />
|
||||
</div>
|
||||
<div class="knob-group">
|
||||
<label>Feather: {getMaskKnobs().feather}</label>
|
||||
<input type="range" min="0" max="10" step="1" value={getMaskKnobs().feather}
|
||||
oninput={(e) => {
|
||||
const knobs = getMaskKnobs();
|
||||
applyMaskKnobs({ ...knobs, feather: parseInt(e.target.value) });
|
||||
maskCanvas && drawMaskOverlay(maskCanvas, currentPhoto()!);
|
||||
}} />
|
||||
oninput={(e) => handleKnobInput({ ...getMaskKnobs(), feather: parseInt(e.target.value) })}
|
||||
onchange={(e) => handleKnobChange({ ...getMaskKnobs(), feather: parseInt(e.target.value) })} />
|
||||
</div>
|
||||
<div class="photo-selector">
|
||||
<label>Photo:</label>
|
||||
|
||||
Reference in New Issue
Block a user