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:
grokbot_ty
2026-09-07 21:26:50 +00:00
parent 4cebdc4b70
commit a1d4a96a0a
2 changed files with 86 additions and 23 deletions
+53 -21
View File
@@ -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>