/** * Mask-canvas helpers used by the inpaint pipeline. * * Pure utility functions — they take a canvas (or layer-shape) as * input and return a fresh canvas, with no module-level state. */ /** * Dilate (positive `px`) or erode (negative `px`) a binary alpha mask. * * Strategy: blur the source by `|px|`, then re-threshold the result. * - Dilation keeps anything with non-trivial blurred alpha (low cutoff). * - Erosion keeps only pixels that retained near-full alpha after blur. * * @param {HTMLCanvasElement} src Source mask canvas. * @param {number} px Pixels to dilate (>0) or erode (<0). 0 = copy. * @returns {HTMLCanvasElement} Fresh canvas with the same dimensions. */ export function dilateMask(src, px) { const w = src.width, h = src.height; const tmp = document.createElement('canvas'); tmp.width = w; tmp.height = h; const ctx = tmp.getContext('2d'); if (px === 0) { ctx.drawImage(src, 0, 0); return tmp; } const dilate = px > 0; const radius = Math.abs(px); ctx.filter = `blur(${radius}px)`; ctx.drawImage(src, 0, 0); ctx.filter = 'none'; const img = ctx.getImageData(0, 0, w, h); const threshold = dilate ? 8 : 247; for (let i = 0; i < img.data.length; i += 4) { const a = img.data[i + 3]; const keep = dilate ? a > threshold : a >= threshold; if (keep) { img.data[i] = img.data[i + 1] = img.data[i + 2] = 255; img.data[i + 3] = 255; } else { img.data[i + 3] = 0; } } ctx.putImageData(img, 0, 0); return tmp; } /** * Re-derive an inpaint-result layer's alpha from its cached AI image + * the hard mask, applying a feather + optional dilate/erode of the * boundary. Mutates `layer.canvas` in place via `layer.ctx`. * * The layer must carry an `inpaintSource = { ai, mask }` cache from the * original inpaint call so we can re-shape the alpha cheaply (no * second model call required). * * @param {{canvas: HTMLCanvasElement, ctx: CanvasRenderingContext2D, * inpaintSource?: {ai: CanvasImageSource, mask: HTMLCanvasElement}}} layer * @param {number} featherPx Gaussian blur radius applied to the mask alpha. * @param {number} [edgeShiftPx] Dilate (+) or erode (-) the mask before blurring. */ export function applyInpaintFeather(layer, featherPx, edgeShiftPx = 0) { if (!layer || !layer.inpaintSource) return; const { ai, mask } = layer.inpaintSource; const w = layer.canvas.width; const h = layer.canvas.height; // 1) Optional dilate/erode, then optional blur, into a fresh mask. let shaped = mask; if (edgeShiftPx !== 0) shaped = dilateMask(mask, edgeShiftPx); const softMask = document.createElement('canvas'); softMask.width = w; softMask.height = h; const smCtx = softMask.getContext('2d'); if (featherPx > 0) { smCtx.filter = `blur(${featherPx}px)`; smCtx.drawImage(shaped, 0, 0, w, h); smCtx.filter = 'none'; } else { smCtx.drawImage(shaped, 0, 0, w, h); } // 2) Draw the AI image fresh, then multiply alpha by the soft mask. const ctx = layer.ctx; ctx.save(); ctx.globalCompositeOperation = 'source-over'; ctx.clearRect(0, 0, w, h); ctx.drawImage(ai, 0, 0); ctx.globalCompositeOperation = 'destination-in'; ctx.drawImage(softMask, 0, 0); ctx.restore(); }