117 lines
4.4 KiB
JavaScript
117 lines
4.4 KiB
JavaScript
/**
|
|
* Mask builders used by the AI Harmonize pipeline.
|
|
*
|
|
* - `layerUnionAlpha` — union of every non-base layer's alpha, as a
|
|
* binary (0/255) mask. Used as the substrate
|
|
* for both seam and body masks.
|
|
* - `seamMask` — feathered band along the alpha edges of all
|
|
* non-base layers. White = "blend here",
|
|
* black = "leave alone". Returned as base64
|
|
* PNG (so it can be POST'd to the diffusion
|
|
* endpoint as JSON).
|
|
* - `layerBodyMask` — feathered FULL shape of every non-base layer.
|
|
* White = "AI may redraw this pixel using the
|
|
* existing pixels as a starting point";
|
|
* black = "preserve exactly". Returned as base64.
|
|
*
|
|
* Each helper takes the visible layer list + the doc dimensions; no
|
|
* module state.
|
|
*
|
|
* @typedef {{ visible: boolean, id: string, canvas: HTMLCanvasElement, offset: {x: number, y: number} }} HarmLayer
|
|
*/
|
|
|
|
/**
|
|
* Build a binary alpha mask = the UNION of every non-base visible
|
|
* layer's pixels. Returns null when fewer than 2 visible layers exist
|
|
* or when the non-base layers are entirely transparent.
|
|
*
|
|
* @param {number} w / h Canvas dimensions in pixels.
|
|
* @param {HarmLayer[]} layers All layers in stack order; the
|
|
* first VISIBLE layer is treated as
|
|
* the base / background.
|
|
* @returns {HTMLCanvasElement|null}
|
|
*/
|
|
export function layerUnionAlpha(w, h, layers) {
|
|
const visible = layers.filter(l => l.visible);
|
|
if (visible.length < 2) return null;
|
|
const bgId = visible[0].id;
|
|
const alphaCanvas = document.createElement('canvas');
|
|
alphaCanvas.width = w; alphaCanvas.height = h;
|
|
const actx = alphaCanvas.getContext('2d');
|
|
let hasFg = false;
|
|
for (const layer of visible) {
|
|
if (layer.id === bgId) continue;
|
|
const off = layer.offset || { x: 0, y: 0 };
|
|
actx.drawImage(layer.canvas, off.x, off.y);
|
|
hasFg = true;
|
|
}
|
|
if (!hasFg) return null;
|
|
const src = actx.getImageData(0, 0, w, h);
|
|
const bin = document.createElement('canvas');
|
|
bin.width = w; bin.height = h;
|
|
const bctx = bin.getContext('2d');
|
|
const binImg = bctx.createImageData(w, h);
|
|
let any = false;
|
|
for (let i = 0; i < src.data.length; i += 4) {
|
|
const v = src.data[i + 3] > 0 ? 255 : 0;
|
|
if (v) any = true;
|
|
binImg.data[i] = binImg.data[i + 1] = binImg.data[i + 2] = v;
|
|
binImg.data[i + 3] = 255;
|
|
}
|
|
if (!any) return null;
|
|
bctx.putImageData(binImg, 0, 0);
|
|
return bin;
|
|
}
|
|
|
|
|
|
/**
|
|
* Build a feathered seam mask along the alpha edges of all non-base
|
|
* visible layers. Returns base64-encoded PNG (no `data:` prefix), or
|
|
* null if there's nothing to harmonize.
|
|
*/
|
|
export function seamMask(w, h, layers, featherPx = 12) {
|
|
const bin = layerUnionAlpha(w, h, layers);
|
|
if (!bin) return null;
|
|
const blur = document.createElement('canvas');
|
|
blur.width = w; blur.height = h;
|
|
const blctx = blur.getContext('2d');
|
|
blctx.filter = `blur(${featherPx}px)`;
|
|
blctx.drawImage(bin, 0, 0);
|
|
blctx.filter = 'none';
|
|
const blurred = blctx.getImageData(0, 0, w, h);
|
|
const mask = blctx.createImageData(w, h);
|
|
// Triangular weight peaked at mid-grey — picks out the alpha-edge band.
|
|
for (let i = 0; i < blurred.data.length; i += 4) {
|
|
const v = blurred.data[i];
|
|
const dist = Math.abs(v - 128);
|
|
const wt = Math.max(0, 255 - dist * 2);
|
|
mask.data[i] = mask.data[i + 1] = mask.data[i + 2] = wt;
|
|
mask.data[i + 3] = 255;
|
|
}
|
|
blctx.putImageData(mask, 0, 0);
|
|
const soft = document.createElement('canvas');
|
|
soft.width = w; soft.height = h;
|
|
const sctx = soft.getContext('2d');
|
|
sctx.filter = `blur(${Math.max(2, Math.floor(featherPx / 4))}px)`;
|
|
sctx.drawImage(blur, 0, 0);
|
|
sctx.filter = 'none';
|
|
return soft.toDataURL('image/png').split(',')[1];
|
|
}
|
|
|
|
|
|
/**
|
|
* Build a feathered FULL-shape mask of every non-base visible layer.
|
|
* Returns base64-encoded PNG, or null if there are no non-base layers.
|
|
*/
|
|
export function layerBodyMask(w, h, layers, featherPx = 12) {
|
|
const bin = layerUnionAlpha(w, h, layers);
|
|
if (!bin) return null;
|
|
const soft = document.createElement('canvas');
|
|
soft.width = w; soft.height = h;
|
|
const sctx = soft.getContext('2d');
|
|
sctx.filter = `blur(${featherPx}px)`;
|
|
sctx.drawImage(bin, 0, 0);
|
|
sctx.filter = 'none';
|
|
return soft.toDataURL('image/png').split(',')[1];
|
|
}
|