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.