1/** 2 * Background Remove (rembg) + Sharpen wiring + the live edge-cleanup 3 * tuner that runs on the most-recent rembg cutout. 4 * 5 * rembg-run button: flatten + POST to /api/image/remove-bg with an 6 * optional hint_mask if the user has a wand/lasso selection 7 * active. After the new layer lands, hides every previously- 8 * visible layer so the cutout reads cleanly, and binds the 9 * live-tuner to the new layer. 10 * 11 * Live edge-cleanup tuner: snapshots the pristine cutout the 12 * moment it lands; subsequent feather/grow slider tweaks 13 * rebuild the layer's alpha from that snapshot WITHOUT 14 * re-running the model. 15 * - Grow > 0 â blur snap alpha, threshold low (32) â grow. 16 * - Grow < 0 â blur snap alpha, threshold high (200) â shrink. 17 * - Feather > 0 â blur the whole layer (alpha + RGB) so the 18 * edge softens AND the residual color fringe from the 19 * original background gets blurred away. 20 * 21 * Sharpen: small slider + button; just calls _applyImageTool 22 * against /api/image/sharpen. 23 * 24 * buildSelectionHintMask: pure-ish utility â returns a base64 PNG 25 * (no data: prefix) of the active wand or lasso selection, or 26 * null. Returned so other wand-rembg call sites can use it. 27 * 28 * @param {{ 29 * applyImageTool: (endpoint, payload, layerName, btn, opts?) => Promise<void>, 30 * openCookbookForDependency: (pkg: string) => void, 31 * composite: () => void, 32 * renderLayerPanel: () => void, 33 * uiModule: object, 34 * }} deps 35 * 36 * @returns {{ buildSelectionHintMask: () => string | null }} 37 */ 38import { state } from './state.js'; 39 40export function wireRembgAndSharpen({ 41 applyImageTool, openCookbookForDependency, 42 composite, renderLayerPanel, uiModule, 43}) { 44 // ââ Sharpen ââ 45 const sharpenPrev = document.getElementById('ge-sharpen-preview'); 46 if (sharpenPrev) sharpenPrev.style.opacity = '0.5'; 47 document.getElementById('ge-sharpen-amount')?.addEventListener('input', (e) => { 48 document.getElementById('ge-sharpen-label').textContent = e.target.value + '%'; 49 if (sharpenPrev) sharpenPrev.style.opacity = (parseInt(e.target.value, 10) / 100).toFixed(2); 50 }); 51 document.getElementById('ge-sharpen-run')?.addEventListener('click', () => { 52 const amount = parseInt(document.getElementById('ge-sharpen-amount')?.value || '50'); 53 applyImageTool('/api/image/sharpen', { amount }, 'Sharpened', document.getElementById('ge-sharpen-run')); 54 }); 55 56 // ââ Bg Remove ââ 57 document.getElementById('ge-rembg-install-link')?.addEventListener('click', () => { 58 openCookbookForDependency('rembg'); 59 }); 60 document.getElementById('ge-rembg-run')?.addEventListener('click', async () => { 61 const payload = {}; 62 const hint = buildSelectionHintMask(); 63 if (hint) payload.hint_mask = hint; 64 // NB: edge_feather / edge_grow are applied CLIENT-side so the 65 // sliders can re-tune the cutout without re-running the model. 66 const btn = document.getElementById('ge-rembg-run'); 67 const before = state.layers.length; 68 // Snapshot which layers were visible BEFORE the run so we know 69 // which to hide after a successful cutout. 70 const prevVisible = state.layers.filter(l => l.visible).map(l => l.id); 71 await applyImageTool('/api/image/remove-bg', payload, 'BG Removed', btn); 72 // applyImageTool finishes after fetch but the new layer is added 73 // inside img.onload (one tick later). Poll for up to 60 frames 74 // (~1s) for the new layer to appear before we auto-hide. 75 let frames = 0; 76 while (state.layers.length <= before && frames < 60) { 77 await new Promise(r => requestAnimationFrame(r)); 78 frames++; 79 } 80 if (state.layers.length > before) { 81 const newLayer = state.layers[state.layers.length - 1]; 82 bindRembgLiveTuner(newLayer); 83 // Auto-hide underlying layers so the user sees just the 84 // cutout â the eye toggles back on if they re-enable manually. 85 for (const layer of state.layers) { 86 if (prevVisible.includes(layer.id) && layer.id !== newLayer.id) { 87 layer.visible = false;
88 } 89 } 90 composite(); 91 renderLayerPanel(); 92 } 93 // Reset sliders so the new cutout starts clean. 94 const f = document.getElementById('ge-rembg-feather'); 95 const g = document.getElementById('ge-rembg-grow'); 96 if (f) { f.value = 0; document.getElementById('ge-rembg-feather-label').textContent = '0px'; syncRembgFeather(0); } 97 if (g) { g.value = 0; document.getElementById('ge-rembg-grow-label').textContent = '0px'; syncRembgGrow(0); } 98 }); 99 100 // ââ Live edge-cleanup tuner ââ 101 // Snapshots the pristine cutout the moment it lands; slider tweaks 102 // rebuild alpha from that snapshot. 103 function bindRembgLiveTuner(layer) { 104 if (!layer) return; 105 const w = layer.canvas.width, h = layer.canvas.height; 106 const snap = document.createElement('canvas'); 107 snap.width = w; snap.height = h; 108 snap.getContext('2d').drawImage(layer.canvas, 0, 0); 109 state.rembgLiveLayer = layer; 110 state.rembgLiveSnap = snap; 111 rembgApplyEdgeNow(); // initial pass (no-op at 0/0) 112 } 113 let rembgRaf = null; 114 function scheduleRembgApply() { 115 if (rembgRaf) return; 116 rembgRaf = requestAnimationFrame(() => { rembgRaf = null; rembgApplyEdgeNow(); }); 117 } 118 function rembgApplyEdgeNow() { 119 if (!state.rembgLiveLayer || !state.rembgLiveSnap) return; 120 const feather = parseInt(document.getElementById('ge-rembg-feather')?.value || '0', 10); 121 const grow = parseInt(document.getElementById('ge-rembg-grow')?.value || '0', 10); 122 const layer = state.rembgLiveLayer; 123 const snap = state.rembgLiveSnap; 124 const w = snap.width, h = snap.height; 125 const lctx = layer.ctx; 126 127 // 1) Start fresh from the pristine cutout snapshot. 128 lctx.clearRect(0, 0, w, h); 129 lctx.drawImage(snap, 0, 0); 130 131 // 2) Edge ±N â dilate / erode alpha via blur+threshold: 132 // grow > 0 â low threshold (32) â halo counts as opaque â grows. 133 // grow < 0 â high threshold (200) â only solid interior â shrinks. 134 // RGB is kept; only alpha is replaced. 135 if (grow !== 0) { 136 const blurC = document.createElement('canvas'); 137 blurC.width = w; blurC.height = h; 138 const bctx = blurC.getContext('2d'); 139 bctx.filter = `blur(${Math.abs(grow)}px)`; 140 bctx.drawImage(snap, 0, 0); 141 bctx.filter = 'none'; 142 const blurred = bctx.getImageData(0, 0, w, h).data; 143 const layerData = lctx.getImageData(0, 0, w, h); 144 const out = layerData.data; 145 const thr = grow > 0 ? 32 : 200; 146 for (let i = 0; i < out.length; i += 4) { 147 out[i + 3] = blurred[i + 3] >= thr ? 255 : 0; 148 } 149 lctx.putImageData(layerData, 0, 0); 150 } 151 152 // 3) Feather softens whatever edge we have now. Blur the entire 153 // layer (alpha + RGB) â alpha gets smooth falloff, RGB gets a 154 // faint blur at the edge which actually helps hide residual 155 // colour fringing from the original background. 156 if (feather > 0) { 157 const fC = document.createElement('canvas'); 158 fC.width = w; fC.height = h; 159 const fctx = fC.getContext('2d'); 160 fctx.filter = `blur(${feather}px)`; 161 fctx.drawImage(layer.canvas, 0, 0); 162 fctx.filter = 'none'; 163 lctx.clearRect(0, 0, w, h); 164 lctx.drawImage(fC, 0, 0); 165 } 166 composite(); 167 } 168 169 // ââ Slider preview swatches + wiring ââ 170 const rembgFeatherPrev = document.getElementById('ge-rembg-feather-preview'); 171 const rembgGrowPrev = document.getElementById('ge-rembg-grow-preview'); 172 function syncRembgFeather(v) { 173 if (!rembgFeatherPrev) return; 174 const inner = Math.max(0, 50 - v * 2.5); 175 rembgFeatherPrev.style.background = `radial-gradient(circle, var(--fg) 0%, var(--fg) ${inner}%, transparent 75%)`; 176 } 177 function syncRembgGrow(v) { 178 if (!rembgGrowPrev) return; 179 // -10..+10 â scale 0.6 .. 1.4 so the swatch visibly grows/shrinks. 180 const s = 1 + v * 0.04; 181 rembgGrowPrev.style.transform = `scale(${s})`; 182 rembgGrowPrev.style.background = v < 0 ? 'color-mix(in srgb, var(--fg) 40%, transparent)' : 'var(--fg)'; 183 } 184 syncRembgFeather(2); 185 syncRembgGrow(0); 186 document.getElementById('ge-rembg-feather')?.addEventListener('input', (e) => { 187 const v = parseInt(e.target.value, 10); 188 document.getElementById('ge-rembg-feather-label').textContent = v + 'px'; 189 syncRembgFeather(v); 190 scheduleRembgApply(); 191 }); 192 document.getElementById('ge-rembg-grow')?.addEventListener('input', (e) => { 193 const v = parseInt(e.target.value, 10); 194 document.getElementById('ge-rembg-grow-label').textContent = (v >= 0 ? '+' : '') + v + 'px'; 195 syncRembgGrow(v); 196 scheduleRembgApply(); 197 }); 198 199 // ââ Selection-hint mask builder (used here + by wand-rembg) ââ 200 // Full-image white-on-transparent mask PNG (base64, no `data:` 201 // prefix) of whichever selection is active â wand first, lasso 202 // second. Returns null if neither has a selection. 203 function buildSelectionHintMask() { 204 const w = state.imgWidth, h = state.imgHeight; 205 if (state.wandMask && state.wandLayerId) {
206 const off = state.layerOffsets.get(state.wandLayerId) || { x: 0, y: 0 }; 207 const c = document.createElement('canvas'); 208 c.width = w; c.height = h; 209 c.getContext('2d').drawImage(state.wandMask, off.x, off.y); 210 return c.toDataURL('image/png').split(',')[1]; 211 } 212 if (state.lassoPoints.length >= 3 && !state.lassoActive) { 213 const c = document.createElement('canvas'); 214 c.width = w; c.height = h; 215 const ctx = c.getContext('2d'); 216 ctx.fillStyle = '#fff'; 217 ctx.beginPath(); 218 ctx.moveTo(state.lassoPoints[0].x, state.lassoPoints[0].y); 219 for (let i = 1; i < state.lassoPoints.length; i++) { 220 ctx.lineTo(state.lassoPoints[i].x, state.lassoPoints[i].y); 221 } 222 ctx.closePath(); 223 ctx.fill(); 224 return c.toDataURL('image/png').split(',')[1]; 225 } 226 return null; 227 } 228 229 return { buildSelectionHintMask }; 230}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.