92 lines
3.2 KiB
JavaScript
92 lines
3.2 KiB
JavaScript
/**
|
|
* 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();
|
|
}
|