- Implemented ComfyGenerateRequest type and associated functions for generating images using various architectures and modes. - Added functions for listing generation options and handling image uploads. - Created workflows for different generation modes including SDXL, Z-Image, Z-Image Turbo, and Anima. - Introduced GenerationJobStatus component to display the status of ongoing generation jobs. - Developed MaskControls for managing mask operations and displaying mask analysis. - Created palette items for tool selection, layer management, and generation settings.
39 lines
4.2 KiB
TypeScript
39 lines
4.2 KiB
TypeScript
import { blurMaskValues, despeckleMaskValues, dilateMaskValues, erodeMaskValues, maskValueFromRgba } from "./maskRaster";
|
|
|
|
export type MagicWandRasterSettings = { tolerance: number; feather: number; choke: number; despeckle: number; contiguous: boolean; mode: "replace" | "add" | "subtract" };
|
|
|
|
export async function createWandMask(source: string, existingMaskSource: string | undefined, width: number, height: number, startX: number, startY: number, settings: MagicWandRasterSettings) {
|
|
const canvas = document.createElement("canvas");
|
|
canvas.width = Math.max(1, width); canvas.height = Math.max(1, height);
|
|
const context = canvas.getContext("2d");
|
|
if (!context) return source;
|
|
context.drawImage(await loadImage(source), 0, 0, canvas.width, canvas.height);
|
|
const data = context.getImageData(0, 0, canvas.width, canvas.height);
|
|
const start = (startY * canvas.width + startX) * 4;
|
|
const key = [data.data[start] ?? 0, data.data[start + 1] ?? 0, data.data[start + 2] ?? 0];
|
|
const selected = settings.contiguous ? floodSelect(data, width, height, startX, startY, key, settings.tolerance) : globalSelect(data, key, settings.tolerance);
|
|
let values: Uint8ClampedArray<ArrayBufferLike> = toValues(selected);
|
|
if (settings.despeckle > 0) values = despeckleMaskValues(values, width, height, Math.round(settings.despeckle));
|
|
if (settings.choke > 0) values = erodeMaskValues(values, width, height, Math.round(settings.choke));
|
|
if (settings.choke < 0) values = dilateMaskValues(values, width, height, Math.round(-settings.choke));
|
|
if (settings.feather > 0) values = blurMaskValues(values, width, height, Math.round(settings.feather));
|
|
const existing = existingMaskSource ? await loadMask(existingMaskSource, width, height) : undefined;
|
|
for (let pixel = 0; pixel < values.length; pixel++) {
|
|
const current = existing?.[pixel] ?? 255; const selectedValue = values[pixel] ?? 0;
|
|
const alpha = settings.mode === "add" ? Math.min(current, 255 - selectedValue) : settings.mode === "subtract" ? Math.max(current, selectedValue) : 255 - selectedValue;
|
|
const index = pixel * 4; data.data[index] = data.data[index + 1] = data.data[index + 2] = 255; data.data[index + 3] = alpha;
|
|
}
|
|
context.putImageData(data, 0, 0); return canvas.toDataURL("image/png");
|
|
}
|
|
|
|
function floodSelect(data: ImageData, width: number, height: number, x: number, y: number, key: number[], tolerance: number) {
|
|
const result = new Uint8Array(width * height); const queue: Array<[number, number]> = [[x, y]];
|
|
while (queue.length) { const [px, py] = queue.pop()!; if (px < 0 || py < 0 || px >= width || py >= height) continue; const i = py * width + px; if (result[i] || !matches(data, i, key, tolerance)) continue; result[i] = 1; queue.push([px + 1, py], [px - 1, py], [px, py + 1], [px, py - 1]); }
|
|
return result;
|
|
}
|
|
function globalSelect(data: ImageData, key: number[], tolerance: number) { const result = new Uint8Array(data.width * data.height); for (let i = 0; i < result.length; i++) if (matches(data, i, key, tolerance)) result[i] = 1; return result; }
|
|
function matches(data: ImageData, pixel: number, key: number[], tolerance: number) { const i = pixel * 4; return Math.hypot((data.data[i] ?? 0) - (key[0] ?? 0), (data.data[i + 1] ?? 0) - (key[1] ?? 0), (data.data[i + 2] ?? 0) - (key[2] ?? 0)) <= tolerance; }
|
|
function toValues(selected: Uint8Array) { const values = new Uint8ClampedArray(selected.length); for (let i = 0; i < selected.length; i++) values[i] = selected[i] ? 255 : 0; return values; }
|
|
async function loadMask(source: string, width: number, height: number) { const canvas = document.createElement("canvas"); canvas.width = width; canvas.height = height; const context = canvas.getContext("2d"); if (!context) return undefined; context.drawImage(await loadImage(source), 0, 0, width, height); const data = context.getImageData(0, 0, width, height); const values = new Uint8ClampedArray(width * height); for (let i = 0; i < values.length; i++) values[i] = maskValueFromRgba(data.data, i * 4); return values; }
|
|
function loadImage(source: string) { return new Promise<HTMLImageElement>((resolve, reject) => { const image = new Image(); image.onload = () => resolve(image); image.onerror = () => reject(new Error("Failed to load image")); image.src = source; }); }
|