PageSourceSearch

https://jayreddinjr-jr-odysseus-hf.hf.space/static/js/editor/ai-rembg.js

js jayreddinjr-jr-odysseus-hf.hf.space collected 2026-10-04 17:01:22 UTC 10,050 bytes, 230 lines download raw bytes

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.