/* ===== Extra Input Module (img2img / inpaint) ===== */ window.NAIExtraInput = { async handleUpload(file) { if (!file || !file.type.startsWith('image/')) return; const dataUrl = await NAIUtils.fileToDataUrl(file); NAIState.extraInputImage = { file, dataUrl, b64: NAIUtils.dataUrlToBase64(dataUrl) }; this.renderPreview(); }, clear() { NAIState.extraInputImage = null; NAIState.maskHistory = []; NAIState.maskHistoryIndex = -1; NAIState.maskCanvas = null; NAIState.maskCtx = null; NAIState.overlayCanvas = null; NAIState.overlayCtx = null; this.renderPreview(); }, setMode(mode) { NAIState.extraInputMode = mode; if (NAIState.extraInputImage) this.renderPreview(); }, renderPreview() { const state = NAIState; const previewArea = $('#extra-input-preview'); const slidersArea = $('#extra-input-sliders'); const clearBtn = $('#btn-extra-clear'); const U = NAIUtils; if (!state.extraInputImage) { previewArea.innerHTML = ''; previewArea.style.display = 'none'; slidersArea.innerHTML = ''; clearBtn.style.display = 'none'; return; } clearBtn.style.display = ''; previewArea.style.display = 'block'; previewArea.innerHTML = ''; const mode = state.extraInputMode; const container = document.createElement('div'); container.className = 'extra-preview-container'; const img = document.createElement('img'); img.src = state.extraInputImage.dataUrl; img.className = 'extra-preview-img'; container.appendChild(img); if (mode === 'inpaint') { const overlayCanvas = document.createElement('canvas'); overlayCanvas.className = 'extra-mask-canvas'; const maskCanvas = document.createElement('canvas'); maskCanvas.style.display = 'none'; container.appendChild(overlayCanvas); container.appendChild(maskCanvas); const initCanvas = () => { const nw = img.naturalWidth; const nh = img.naturalHeight; overlayCanvas.width = nw; overlayCanvas.height = nh; maskCanvas.width = nw; maskCanvas.height = nh; // CSS sizing: match the displayed image exactly // Use the image's rendered size const updateCanvasSize = () => { const containerRect = img.parentElement.getBoundingClientRect(); const imgRect = img.getBoundingClientRect(); // Position canvas exactly over the rendered image const offsetLeft = imgRect.left - containerRect.left; const offsetTop = imgRect.top - containerRect.top; overlayCanvas.style.width = imgRect.width + 'px'; overlayCanvas.style.height = imgRect.height + 'px'; overlayCanvas.style.left = offsetLeft + 'px'; overlayCanvas.style.top = offsetTop + 'px'; overlayCanvas.style.transform = 'none'; }; updateCanvasSize(); state.overlayCanvas = overlayCanvas; state.overlayCtx = overlayCanvas.getContext('2d'); state.maskCanvas = maskCanvas; state.maskCtx = maskCanvas.getContext('2d'); state.maskCtx.fillStyle = '#000000'; state.maskCtx.fillRect(0, 0, maskCanvas.width, maskCanvas.height); if (state.maskHistory.length > 0 && state.maskHistoryIndex >= 0) { state.maskCtx.putImageData(state.maskHistory[state.maskHistoryIndex].mask, 0, 0); state.overlayCtx.putImageData(state.maskHistory[state.maskHistoryIndex].overlay, 0, 0); } else { this.saveMaskState(); } this.setupPainting(overlayCanvas); new ResizeObserver(() => { updateCanvasSize(); }).observe(img); }; img.onload = initCanvas; if (img.complete && img.naturalWidth > 0) initCanvas(); } previewArea.appendChild(container); // Sliders slidersArea.innerHTML = ''; if (mode === 'img2img') { slidersArea.appendChild(U.createDynSlider('去噪强度', 'extra-denoise', 0, 0.99, 0.01, 0.7)); slidersArea.appendChild(U.createDynSlider('噪声', 'extra-noise', 0, 1, 0.1, 0)); } else { slidersArea.appendChild(U.createDynSlider('重绘强度', 'extra-inpaint-strength', 0.01, 1, 0.01, 1)); // Brush size const brushGroup = document.createElement('div'); brushGroup.className = 'form-group dyn-slider-group'; brushGroup.innerHTML = `
${state.brushSize}
`; slidersArea.appendChild(brushGroup); setTimeout(() => { const bs = $('#extra-brush-size'); const bsv = $('#extra-brush-size-val'); if (bs && bsv) bs.addEventListener('input', () => { state.brushSize = parseInt(bs.value); bsv.textContent = bs.value; }); }, 0); // Add original image checkbox const overlayGroup = document.createElement('div'); overlayGroup.className = 'extra-checkbox-row'; overlayGroup.innerHTML = ` `; slidersArea.appendChild(overlayGroup); // Undo / Clear const maskBtns = document.createElement('div'); maskBtns.className = 'mask-btn-row'; maskBtns.innerHTML = ` `; slidersArea.appendChild(maskBtns); setTimeout(() => { $('#btn-mask-undo').addEventListener('click', () => this.undoMask()); $('#btn-mask-clear').addEventListener('click', () => this.clearMask()); }, 0); } }, // ── Mask painting ── setupPainting(canvas) { let lastX, lastY; const state = NAIState; const getCoords = (e) => { const rect = canvas.getBoundingClientRect(); const scaleX = canvas.width / rect.width; const scaleY = canvas.height / rect.height; const [cx, cy] = e.touches ? [e.touches[0].clientX, e.touches[0].clientY] : [e.clientX, e.clientY]; return { x: (cx - rect.left) * scaleX, y: (cy - rect.top) * scaleY }; }; const start = (e) => { e.preventDefault(); state.isPainting = true; const c = getCoords(e); lastX = c.x; lastY = c.y; this.paintDot(c.x, c.y); }; const move = (e) => { if (!state.isPainting) return; e.preventDefault(); const c = getCoords(e); this.paintLine(lastX, lastY, c.x, c.y); lastX = c.x; lastY = c.y; }; const end = () => { if (!state.isPainting) return; state.isPainting = false; this.saveMaskState(); }; canvas.addEventListener('mousedown', start); canvas.addEventListener('mousemove', move); canvas.addEventListener('mouseup', end); canvas.addEventListener('mouseleave', end); canvas.addEventListener('touchstart', start, { passive: false }); canvas.addEventListener('touchmove', move, { passive: false }); canvas.addEventListener('touchend', end); }, paintDot(x, y) { const s = NAIState; const r = s.brushSize / 2; s.overlayCtx.fillStyle = 'rgba(255,255,255,0.25)'; s.overlayCtx.beginPath(); s.overlayCtx.arc(x, y, r, 0, Math.PI * 2); s.overlayCtx.fill(); s.maskCtx.fillStyle = '#ffffff'; s.maskCtx.beginPath(); s.maskCtx.arc(x, y, r, 0, Math.PI * 2); s.maskCtx.fill(); }, paintLine(x1, y1, x2, y2) { const s = NAIState; const props = { lineWidth: s.brushSize, lineCap: 'round', lineJoin: 'round' }; Object.assign(s.overlayCtx, props); s.overlayCtx.strokeStyle = 'rgba(255,255,255,0.25)'; s.overlayCtx.beginPath(); s.overlayCtx.moveTo(x1, y1); s.overlayCtx.lineTo(x2, y2); s.overlayCtx.stroke(); Object.assign(s.maskCtx, props); s.maskCtx.strokeStyle = '#ffffff'; s.maskCtx.beginPath(); s.maskCtx.moveTo(x1, y1); s.maskCtx.lineTo(x2, y2); s.maskCtx.stroke(); }, saveMaskState() { const s = NAIState; if (!s.maskCanvas || !s.overlayCanvas) return; s.maskHistory = s.maskHistory.slice(0, s.maskHistoryIndex + 1); s.maskHistory.push({ mask: s.maskCtx.getImageData(0, 0, s.maskCanvas.width, s.maskCanvas.height), overlay: s.overlayCtx.getImageData(0, 0, s.overlayCanvas.width, s.overlayCanvas.height), }); s.maskHistoryIndex = s.maskHistory.length - 1; if (s.maskHistory.length > 30) { s.maskHistory.shift(); s.maskHistoryIndex--; } }, undoMask() { const s = NAIState; if (s.maskHistoryIndex <= 0) return; s.maskHistoryIndex--; const snap = s.maskHistory[s.maskHistoryIndex]; s.maskCtx.putImageData(snap.mask, 0, 0); s.overlayCtx.putImageData(snap.overlay, 0, 0); }, clearMask() { const s = NAIState; if (!s.maskCanvas || !s.overlayCanvas) return; s.maskCtx.fillStyle = '#000000'; s.maskCtx.fillRect(0, 0, s.maskCanvas.width, s.maskCanvas.height); s.overlayCtx.clearRect(0, 0, s.overlayCanvas.width, s.overlayCanvas.height); this.saveMaskState(); }, processMask(maskImage) { const width = maskImage.width; const height = maskImage.height; const smallWidth = Math.floor(width / 8); const smallHeight = Math.floor(height / 8); const smallCanvas = document.createElement('canvas'); smallCanvas.width = smallWidth; smallCanvas.height = smallHeight; const smallCtx = smallCanvas.getContext('2d'); smallCtx.imageSmoothingEnabled = false; smallCtx.drawImage(maskImage, 0, 0, width, height, 0, 0, smallWidth, smallHeight); const largeCanvas = document.createElement('canvas'); largeCanvas.width = width; largeCanvas.height = height; const largeCtx = largeCanvas.getContext('2d'); largeCtx.imageSmoothingEnabled = false; largeCtx.drawImage(smallCanvas, 0, 0, smallWidth, smallHeight, 0, 0, width, height); const imageData = largeCtx.getImageData(0, 0, width, height); const data = imageData.data; for (let i = 0; i < data.length; i += 4) { data[i] = data[i] < 255 ? 0 : 255; // R data[i+1] = data[i+1] < 255 ? 0 : 255; // G data[i+2] = data[i+2] < 255 ? 0 : 255; // B } largeCtx.putImageData(imageData, 0, 0); return largeCanvas; }, getMaskBase64() { if (!NAIState.maskCanvas) return null; const processed_mask = this.processMask(NAIState.maskCanvas); return NAIUtils.dataUrlToBase64(processed_mask.toDataURL('image/png')); }, collectParams() { if (!NAIState.extraInputImage) return null; const mode = NAIState.extraInputMode; const result = { mode, image: NAIState.extraInputImage.b64 }; if (mode === 'img2img') { const d = $('#extra-denoise'); const n = $('#extra-noise'); result.strength = d ? parseFloat(d.value) : 0.7; result.noise = n ? parseFloat(n.value) : 0; } else { const s = $('#extra-inpaint-strength'); result.strength = s ? parseFloat(s.value) : 1; result.mask = this.getMaskBase64(); const addOrig = $('#extra-add-original'); result.add_original_image = addOrig ? addOrig.checked : true; } return result; }, useLastGenerated() { const imgEl = document.querySelector('#result-image-area img'); if (!imgEl || !imgEl.src) return; const dataUrl = imgEl.src; const b64 = NAIUtils.dataUrlToBase64(dataUrl); NAIState.extraInputImage = { file: null, dataUrl, b64 }; // Reset mask when loading new image NAIState.maskHistory = []; NAIState.maskHistoryIndex = -1; NAIState.maskCanvas = null; NAIState.maskCtx = null; NAIState.overlayCanvas = null; NAIState.overlayCtx = null; this.renderPreview(); }, async handleUploadFromDataUrl(dataUrl) { const b64 = NAIUtils.dataUrlToBase64(dataUrl); NAIState.extraInputImage = { file: null, dataUrl, b64 }; NAIState.maskHistory = []; NAIState.maskHistoryIndex = -1; NAIState.maskCanvas = null; NAIState.maskCtx = null; NAIState.overlayCanvas = null; NAIState.overlayCtx = null; this.renderPreview(); }, init() { // Toggle $('#extra-input-toggle').addEventListener('click', () => { const c = $('#extra-input-content'); const a = $('#extra-input-arrow'); const open = c.classList.toggle('open'); a.textContent = open ? '▼' : '▶'; }); // Upload const input = $('#extra-input-file'); const drop = $('#extra-input-dropzone'); drop.addEventListener('click', () => input.click()); drop.addEventListener('dragover', e => { e.preventDefault(); drop.classList.add('dragover'); }); drop.addEventListener('dragleave', () => drop.classList.remove('dragover')); drop.addEventListener('drop', e => { e.preventDefault(); drop.classList.remove('dragover'); if (e.dataTransfer.files.length > 0) this.handleUpload(e.dataTransfer.files[0]); }); input.addEventListener('change', () => { if (input.files.length > 0) this.handleUpload(input.files[0]); input.value = ''; }); // Mode $$('input[name="extra-mode"]').forEach(radio => { radio.addEventListener('change', () => this.setMode(radio.value)); }); // Clear $('#btn-extra-clear').addEventListener('click', () => this.clear()); // Use last generated image $('#btn-extra-use-last').addEventListener('click', () => this.useLastGenerated()); }, };