mirror of
https://github.com/Azornes/Comfyui-LayerForge.git
synced 2026-03-22 13:12:10 -03:00
231 lines
9.3 KiB
JavaScript
231 lines
9.3 KiB
JavaScript
import { createModuleLogger } from "./LoggerUtils.js";
|
|
import { createCanvas } from "./CommonUtils.js";
|
|
import { withErrorHandling, createValidationError } from "../ErrorHandler.js";
|
|
const log = createModuleLogger('ImageAnalysis');
|
|
/**
|
|
* Creates a distance field mask based on the alpha channel of an image.
|
|
* The mask will have gradients from the edges of visible pixels inward.
|
|
* @param image - The source image to analyze
|
|
* @param blendArea - The percentage (0-100) of the area to apply blending
|
|
* @returns HTMLCanvasElement containing the distance field mask
|
|
*/
|
|
/**
|
|
* Synchronous version of createDistanceFieldMask for use in synchronous rendering
|
|
*/
|
|
export function createDistanceFieldMaskSync(image, blendArea) {
|
|
if (!image) {
|
|
log.error("Image is required for distance field mask");
|
|
return createCanvas(1, 1).canvas;
|
|
}
|
|
if (typeof blendArea !== 'number' || blendArea < 0 || blendArea > 100) {
|
|
log.error("Blend area must be a number between 0 and 100");
|
|
return createCanvas(1, 1).canvas;
|
|
}
|
|
const { canvas, ctx } = createCanvas(image.width, image.height, '2d', { willReadFrequently: true });
|
|
if (!ctx) {
|
|
log.error('Failed to create canvas context for distance field mask');
|
|
return canvas;
|
|
}
|
|
// Draw the image to extract pixel data
|
|
ctx.drawImage(image, 0, 0);
|
|
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
|
const data = imageData.data;
|
|
const width = canvas.width;
|
|
const height = canvas.height;
|
|
// Check if image has transparency (any alpha < 255)
|
|
let hasTransparency = false;
|
|
for (let i = 0; i < width * height; i++) {
|
|
if (data[i * 4 + 3] < 255) {
|
|
hasTransparency = true;
|
|
break;
|
|
}
|
|
}
|
|
let distanceField;
|
|
let maxDistance;
|
|
if (hasTransparency) {
|
|
// For images with transparency, use alpha-based distance transform
|
|
const binaryMask = new Uint8Array(width * height);
|
|
for (let i = 0; i < width * height; i++) {
|
|
binaryMask[i] = data[i * 4 + 3] > 0 ? 1 : 0;
|
|
}
|
|
distanceField = calculateDistanceTransform(binaryMask, width, height);
|
|
}
|
|
else {
|
|
// For opaque images, calculate distance from edges of the rectangle
|
|
distanceField = calculateDistanceFromEdges(width, height);
|
|
}
|
|
// Find the maximum distance to normalize
|
|
maxDistance = 0;
|
|
for (let i = 0; i < distanceField.length; i++) {
|
|
if (distanceField[i] > maxDistance) {
|
|
maxDistance = distanceField[i];
|
|
}
|
|
}
|
|
// Create the gradient mask based on blendArea
|
|
const maskData = ctx.createImageData(width, height);
|
|
const threshold = maxDistance * (blendArea / 100);
|
|
for (let i = 0; i < width * height; i++) {
|
|
const distance = distanceField[i];
|
|
const alpha = data[i * 4 + 3];
|
|
if (alpha === 0) {
|
|
// Transparent pixels remain transparent
|
|
maskData.data[i * 4] = 255;
|
|
maskData.data[i * 4 + 1] = 255;
|
|
maskData.data[i * 4 + 2] = 255;
|
|
maskData.data[i * 4 + 3] = 0;
|
|
}
|
|
else if (distance <= threshold) {
|
|
// Edge area - apply gradient alpha
|
|
const gradientValue = distance / threshold;
|
|
const alphaValue = Math.floor(gradientValue * 255);
|
|
maskData.data[i * 4] = 255;
|
|
maskData.data[i * 4 + 1] = 255;
|
|
maskData.data[i * 4 + 2] = 255;
|
|
maskData.data[i * 4 + 3] = alphaValue;
|
|
}
|
|
else {
|
|
// Inner area - full alpha (no blending effect)
|
|
maskData.data[i * 4] = 255;
|
|
maskData.data[i * 4 + 1] = 255;
|
|
maskData.data[i * 4 + 2] = 255;
|
|
maskData.data[i * 4 + 3] = 255;
|
|
}
|
|
}
|
|
// Clear canvas and put the mask data
|
|
ctx.clearRect(0, 0, width, height);
|
|
ctx.putImageData(maskData, 0, 0);
|
|
return canvas;
|
|
}
|
|
/**
|
|
* Async version with error handling for use in async contexts
|
|
*/
|
|
export const createDistanceFieldMask = withErrorHandling(function (image, blendArea) {
|
|
return createDistanceFieldMaskSync(image, blendArea);
|
|
}, 'createDistanceFieldMask');
|
|
/**
|
|
* Calculates the Euclidean distance transform of a binary mask.
|
|
* Uses a two-pass algorithm for efficiency.
|
|
* @param binaryMask - Binary mask where 1 = inside, 0 = outside
|
|
* @param width - Width of the mask
|
|
* @param height - Height of the mask
|
|
* @returns Float32Array containing distance values
|
|
*/
|
|
function calculateDistanceTransform(binaryMask, width, height) {
|
|
const distances = new Float32Array(width * height);
|
|
const infinity = width + height; // A value larger than any possible distance
|
|
// Initialize distances
|
|
for (let i = 0; i < width * height; i++) {
|
|
distances[i] = binaryMask[i] === 1 ? infinity : 0;
|
|
}
|
|
// Forward pass (top-left to bottom-right)
|
|
for (let y = 0; y < height; y++) {
|
|
for (let x = 0; x < width; x++) {
|
|
const idx = y * width + x;
|
|
if (distances[idx] > 0) {
|
|
let minDist = distances[idx];
|
|
// Check top neighbor
|
|
if (y > 0) {
|
|
minDist = Math.min(minDist, distances[(y - 1) * width + x] + 1);
|
|
}
|
|
// Check left neighbor
|
|
if (x > 0) {
|
|
minDist = Math.min(minDist, distances[y * width + (x - 1)] + 1);
|
|
}
|
|
// Check top-left diagonal
|
|
if (x > 0 && y > 0) {
|
|
minDist = Math.min(minDist, distances[(y - 1) * width + (x - 1)] + Math.sqrt(2));
|
|
}
|
|
// Check top-right diagonal
|
|
if (x < width - 1 && y > 0) {
|
|
minDist = Math.min(minDist, distances[(y - 1) * width + (x + 1)] + Math.sqrt(2));
|
|
}
|
|
distances[idx] = minDist;
|
|
}
|
|
}
|
|
}
|
|
// Backward pass (bottom-right to top-left)
|
|
for (let y = height - 1; y >= 0; y--) {
|
|
for (let x = width - 1; x >= 0; x--) {
|
|
const idx = y * width + x;
|
|
if (distances[idx] > 0) {
|
|
let minDist = distances[idx];
|
|
// Check bottom neighbor
|
|
if (y < height - 1) {
|
|
minDist = Math.min(minDist, distances[(y + 1) * width + x] + 1);
|
|
}
|
|
// Check right neighbor
|
|
if (x < width - 1) {
|
|
minDist = Math.min(minDist, distances[y * width + (x + 1)] + 1);
|
|
}
|
|
// Check bottom-right diagonal
|
|
if (x < width - 1 && y < height - 1) {
|
|
minDist = Math.min(minDist, distances[(y + 1) * width + (x + 1)] + Math.sqrt(2));
|
|
}
|
|
// Check bottom-left diagonal
|
|
if (x > 0 && y < height - 1) {
|
|
minDist = Math.min(minDist, distances[(y + 1) * width + (x - 1)] + Math.sqrt(2));
|
|
}
|
|
distances[idx] = minDist;
|
|
}
|
|
}
|
|
}
|
|
return distances;
|
|
}
|
|
/**
|
|
* Calculates distance from edges of a rectangle for opaque images.
|
|
* @param width - Width of the rectangle
|
|
* @param height - Height of the rectangle
|
|
* @returns Float32Array containing distance values from edges
|
|
*/
|
|
function calculateDistanceFromEdges(width, height) {
|
|
const distances = new Float32Array(width * height);
|
|
for (let y = 0; y < height; y++) {
|
|
for (let x = 0; x < width; x++) {
|
|
const idx = y * width + x;
|
|
// Calculate distance to nearest edge
|
|
const distToLeft = x;
|
|
const distToRight = width - 1 - x;
|
|
const distToTop = y;
|
|
const distToBottom = height - 1 - y;
|
|
// Minimum distance to any edge
|
|
const minDistToEdge = Math.min(distToLeft, distToRight, distToTop, distToBottom);
|
|
distances[idx] = minDistToEdge;
|
|
}
|
|
}
|
|
return distances;
|
|
}
|
|
/**
|
|
* Creates a simple radial gradient mask (fallback for rectangular areas).
|
|
* @param width - Width of the mask
|
|
* @param height - Height of the mask
|
|
* @param blendArea - The percentage (0-100) of the area to apply blending
|
|
* @returns HTMLCanvasElement containing the radial gradient mask
|
|
*/
|
|
export const createRadialGradientMask = withErrorHandling(function (width, height, blendArea) {
|
|
if (typeof width !== 'number' || width <= 0) {
|
|
throw createValidationError("Width must be a positive number", { width });
|
|
}
|
|
if (typeof height !== 'number' || height <= 0) {
|
|
throw createValidationError("Height must be a positive number", { height });
|
|
}
|
|
if (typeof blendArea !== 'number' || blendArea < 0 || blendArea > 100) {
|
|
throw createValidationError("Blend area must be a number between 0 and 100", { blendArea });
|
|
}
|
|
const { canvas, ctx } = createCanvas(width, height);
|
|
if (!ctx) {
|
|
log.error('Failed to create canvas context for radial gradient mask');
|
|
return canvas;
|
|
}
|
|
const centerX = width / 2;
|
|
const centerY = height / 2;
|
|
const maxRadius = Math.sqrt(centerX * centerX + centerY * centerY);
|
|
const innerRadius = maxRadius * (1 - blendArea / 100);
|
|
// Create radial gradient
|
|
const gradient = ctx.createRadialGradient(centerX, centerY, innerRadius, centerX, centerY, maxRadius);
|
|
gradient.addColorStop(0, 'white');
|
|
gradient.addColorStop(1, 'black');
|
|
ctx.fillStyle = gradient;
|
|
ctx.fillRect(0, 0, width, height);
|
|
return canvas;
|
|
}, 'createRadialGradientMask');
|