PageSourceSearch

https://jayreddinjr-jr-odysseus-hf.hf.space/static/js/editor/wire-inpaint-controls.js

js jayreddinjr-jr-odysseus-hf.hf.space collected 2026-10-04 17:01:27 UTC 8,100 bytes, 164 lines download raw bytes

1/**
2 * Inpaint panel controls — the non-AI side-panel UI for the inpaint
3 * tool (the AI Generate/Remove/Outpaint buttons live in
4 * editor/ai-inpaint.js).
5 *
6 *   Pre-gen sliders (Feather + Strength swatch previews):
7 *     #ge-strength-slider     just-updates-the-label-and-swatch
8 *
9 *   Post-gen live edge tuners — alpha-blur + dilate/erode on the most
10 *   recent Inpaint Result layer, rAF-throttled so dragging stays
11 *   smooth on big canvases:
12 *     #ge-feather-slider       calls applyInpaintFeather + composite
13 *     #ge-edgestroke-slider    same
14 *
15 *   Mask controls:
16 *     #ge-mask-vis             toggle red-overlay visibility
17 *     #ge-inpaint-invert       invert the active mask sub-layer
18 *     #ge-inpaint-clear        wipe the active mask
19 *     #ge-inpaint-mode-paint   set persistent paint mode
20 *     #ge-inpaint-mode-erase   set persistent erase mode
21 *
22 *   Mask tint pickers (wired to keep both visually in sync):
23 *     .ge-inpaint-mask-color   (inpaint section)
24 *     #ge-topbar-mask-color    (topbar swatch — HSV picker attached)
25 *
26 * @param {{
27 *   composite:                () => void,
28 *   applyInpaintFeather:      (layer: object, featherPx: number, edgeShiftPx: number) => void,
29 *   syncToolClearIndicators:  () => void,
30 *   attachColorPicker:        (el: HTMLInputElement) => void,
31 *   uiModule:                 object,
32 * }} deps
33 */
34import { state } from './state.js';
35
36const EYE_OPEN_SM = '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>';
37const EYE_OFF_SM  = '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><line x1="8" y1="16" x2="16" y2="8"/><line x1="8" y1="8" x2="16" y2="16"/></svg>';
38
39export function wireInpaintControls({
40  composite, applyInpaintFeather, syncToolClearIndicators,
41  attachColorPicker, uiModule,
42}) {
43  // ── Feather + Strength preview swatches ──
44  const featherPrev = document.getElementById('ge-feather-preview');
45  const strengthPrev = document.getElementById('ge-strength-preview');
46  function syncFeatherPreview(v) {
47    if (!featherPrev) return;
48    const inner = Math.max(0, 50 - v * 1.25);
49    featherPrev.style.background = `radial-gradient(circle, var(--fg) 0%, var(--fg) ${inner}%, transparent 75%)`;
50  }
51  function syncStrengthPreview(v) {
52    if (!strengthPrev) return;
53    strengthPrev.style.opacity = (v / 100).toFixed(2);
54  }
55
56  // ── Post-inpaint live edge tuner ──
57  // Alpha-blur (Feather) + dilate/erode (Edge Stroke) on the last
58  // Inpaint Result layer. rAF-throttled so dragging stays smooth.
59  let featherRafPending = false;
60  function scheduleInpaintEdgeRefresh() {
61    if (featherRafPending) return;
62    featherRafPending = true;
63    requestAnimationFrame(() => {
64      featherRafPending = false;
65      const layer = state.layers.find(l => l.id === state.lastInpaintLayerId);
66      if (!layer || !layer.inpaintSource) return;
67      const feather = parseInt(document.getElementById('ge-feather-slider')?.value || '0', 10);
68      const edge = parseInt(document.getElementById('ge-edgestroke-slider')?.value || '0', 10);
69      applyInpaintFeather(layer, feather, edge);
70      composite();
71    });
72  }
73  document.getElementById('ge-feather-slider')?.addEventListener('input', (e) => {
74    const v = parseInt(e.target.value, 10);
75    document.getElementById('ge-feather-label').textContent = v + 'px';
76    syncFeatherPreview(v);
77    scheduleInpaintEdgeRefresh();
78  });
79  document.getElementById('ge-edgestroke-slider')?.addEventListener('input', (e) => {
80    const v = parseInt(e.target.value, 10);
81    const label = document.getElementById('ge-edgestroke-label');
82    if (label) label.textContent = (v > 0 ? '+' : '') + v + 'px';
83    const prev = document.getElementById('ge-edgestroke-preview');
84    if (prev) {
85      // Visualise direction: dilate (+) → green, erode (−) → red.
86      const dir = v === 0 ? 'transparent' : (v > 0 ? 'rgba(120,200,120,0.5)' : 'rgba(200,120,120,0.5)');
87      prev.style.background = dir;
88      prev.style.opacity = Math.min(1, Math.abs(v) / 80).toFixed(2);
89    }
90    scheduleInpaintEdgeRefresh();
91  });
92  document.getElementById('ge-strength-slider')?.addEventListener('input', (e) => {
93    document.getElementById('ge-strength-label').textContent = (e.target.value / 100).toFixed(2);
94    syncStrengthPreview(parseInt(e.target.value, 10));
95  });
96  syncFeatherPreview(0);
97  syncStrengthPreview(75);
98
99  // ── Mask vis / invert / clear ──
100  document.getElementById('ge-mask-vis')?.addEventListener('click', () => {
101    state.maskVisible = !state.maskVisible;
102    const btn = document.getElementById('ge-mask-vis');
103    if (!btn) { composite(); return; }
104    btn.innerHTML = `${state.maskVisible ? EYE_OPEN_SM : EYE_OFF_SM}<span id="ge-mask-vis-label">${state.maskVisible ? 'Hide' : 'Show'}</span>`;
105    btn.title = state.maskVisible ? 'Hide mask' : 'Show mask';
106    btn.classList.toggle('visible', state.maskVisible);
107    composite();
108  });
109  document.getElementById('ge-inpaint-invert')?.addEventListener('click', () => {
110    if (!state.maskCtx || !state.maskCanvas) return;
111    const imgData = state.maskCtx.getImageData(0, 0, state.maskCanvas.width, state.maskCanvas.height);
112    const d = imgData.data;
113    for (let i = 0; i < d.length; i += 4) {
114      const alpha = d[i + 3];
115      if (alpha > 0) {
116        d[i] = 0; d[i+1] = 0; d[i+2] = 0; d[i+3] = 0;
117      } else {
118        d[i] = 255; d[i+1] = 255; d[i+2] = 255; d[i+3] = 255;
119      }
120    }
121    state.maskCtx.putImageData(imgData, 0, 0);
122    composite();
123    syncToolClearIndicators();
124    uiModule.showToast('Mask inverted');
125  });
126  document.getElementById('ge-inpaint-clear')?.addEventListener('click', () => {
127    if (state.maskCtx) { state.maskCtx.clearRect(0, 0, state.maskCanvas.width, state.maskCanvas.height); composite(); }
128    syncToolClearIndicators();
129  });
130
131  // ── Paint / Erase segmented toggle ──
132  function setInpaintMode(eraseMode) {
133    state.inpaintEraseMode = !!eraseMode;
134    const paintBtn = document.getElementById('ge-inpaint-mode-paint');
135    const eraseBtn = document.getElementById('ge-inpaint-mode-erase');
136    if (paintBtn) paintBtn.classList.toggle('active', !state.inpaintEraseMode);
137    if (eraseBtn) eraseBtn.classList.toggle('active', state.inpaintEraseMode);
138  }
139  document.getElementById('ge-inpaint-mode-paint')?.addEventListener('click', () => setInpaintMode(false));
140  document.getElementById('ge-inpaint-mode-erase')?.addEventListener('click', () => setInpaintMode(true));
141
142  // ── Mask color picker ──
143  // Updates state.maskTintColor live so the user can pick a colour
144  // that contrasts with their photo. Wire both the topbar picker AND
145  // the inpaint-section picker so changing one syncs the other.
146  function applyMaskTintFromHex(hex) {
147    const r = parseInt(hex.slice(1, 3), 16);
148    const g = parseInt(hex.slice(3, 5), 16);
149    const b = parseInt(hex.slice(5, 7), 16);
150    state.maskTintColor = `rgba(${r}, ${g}, ${b}, 1)`;
151    const inpaintPicker = document.querySelector('.ge-inpaint-mask-color');
152    const topbarPicker = document.getElementById('ge-topbar-mask-color');
153    if (inpaintPicker && inpaintPicker.value !== hex) inpaintPicker.value = hex;
154    if (topbarPicker && topbarPicker.value !== hex) topbarPicker.value = hex;
155    composite();
156  }
157  document.querySelector('.ge-inpaint-mask-color')?.addEventListener('input', (e) => applyMaskTintFromHex(e.target.value));
158  document.getElementById('ge-topbar-mask-color')?.addEventListener('input', (e) => applyMaskTintFromHex(e.target.value));
159  // Use the in-house HSV picker for the topbar swatch.
160  const topbarMaskColor = document.getElementById('ge-topbar-mask-color');
161  if (topbarMaskColor) {
162    try { attachColorPicker(topbarMaskColor); topbarMaskColor.value = topbarMaskColor.value; } catch {}
163  }
164}

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.