fix: clean up duplicate extractions in preview ops
This commit is contained in:
@@ -6,48 +6,3 @@ export const blendOps = {
|
||||
composite: ops.composite,
|
||||
comp: ops.comp,
|
||||
};
|
||||
|
||||
|
||||
// Extracted with ts-morph
|
||||
|
||||
export function merge(ctx: CanvasRenderingContext2D, W: number, node: ComfyNode, topCanvasOrInputs: HTMLCanvasElement | HTMLCanvasElement[], _opts?: any, frameIndex: number = 0): HTMLCanvasElement {
|
||||
const inputs = Array.isArray(topCanvasOrInputs)
|
||||
? topCanvasOrInputs
|
||||
: [ctx.canvas, topCanvasOrInputs];
|
||||
const base = inputs[0] ?? ctx.canvas;
|
||||
const topCanvas = inputs[1] ?? null;
|
||||
if (!topCanvas) return fitCanvas(base, base.width || 1, base.height || 1);
|
||||
const mode = strAny(node, ["mode", "blend_mode"], "over", frameIndex);
|
||||
const foregroundFit = strAny(node, ["foreground_fit", "fit_mode"], "stretch", frameIndex);
|
||||
const blendSpace = strAny(node, ["blend_space", "color_space"], "linear", frameIndex);
|
||||
const rawOpacity = w(node, "opacity") ? num(node, "opacity", 100, frameIndex) : numAny(node, ["mix", "factor", "fade_factor", "blend_factor", "start_level", "end_level"], 1, frameIndex);
|
||||
const opacity = rawOpacity > 1 ? rawOpacity / 100 : rawOpacity;
|
||||
const merged = applyEffectToCanvas(base, (effectCtx, width, height) => {
|
||||
blend(effectCtx, width, height, topCanvas, mode, opacity, foregroundFit, blendSpace);
|
||||
});
|
||||
const effectMask = resolvePreviewMaskCanvas(node, base, inputs[2] ?? null, frameIndex);
|
||||
return effectMask ? compositeProcessedWithMask(base, merged, effectMask) : merged;
|
||||
}
|
||||
|
||||
export function composite(ctx: CanvasRenderingContext2D, W: number, node: ComfyNode, inputs: HTMLCanvasElement[]): HTMLCanvasElement {
|
||||
const base = inputs[0] ?? ctx.canvas;
|
||||
const topInput = inputs[1] ?? null;
|
||||
if (!topInput) return base;
|
||||
const rawMask = inputs[2] ?? null;
|
||||
const maskInput = rawMask && boolAny(node, ["invert_mask"], false) ? invertMaskCanvas(rawMask) : rawMask;
|
||||
const top = premultLayerWithMask(topInput, maskInput);
|
||||
const x = Math.round(numAny(node, ["x"], 0) + numAny(node, ["offset_x"], 0));
|
||||
const y = Math.round(numAny(node, ["y"], 0) + numAny(node, ["offset_y"], 0));
|
||||
const mode = strAny(node, ["mode", "blend_mode"], "over");
|
||||
const rawOpacity = w(node, "opacity") ? num(node, "opacity", 100) : numAny(node, ["mix", "factor", "blend_factor", "start_level", "end_level"], 1);
|
||||
const opacity = rawOpacity > 1 ? rawOpacity / 100 : rawOpacity;
|
||||
return compositeAt(base, top, mode, opacity, x, y, top.width || 1, top.height || 1);
|
||||
}
|
||||
|
||||
export function comp(ctx: CanvasRenderingContext2D, W: number, node: ComfyNode, inputs: HTMLCanvasElement[]): HTMLCanvasElement {
|
||||
return renderCompPreview(
|
||||
node,
|
||||
resolveCompPreviewInputs(node, inputs),
|
||||
).canvas;
|
||||
}
|
||||
|
||||
|
||||
@@ -16,141 +16,3 @@ export const colorOps = {
|
||||
edgeDetect: ops.edgeDetect,
|
||||
glow: ops.glow,
|
||||
};
|
||||
|
||||
|
||||
// Extracted with ts-morph
|
||||
|
||||
export function colorAjust(ctx: CanvasRenderingContext2D, W: number, node: ComfyNode, inputs: HTMLCanvasElement[] = [], frameIndex: number = 0): HTMLCanvasElement {
|
||||
const source = inputs[0] ?? ctx.canvas;
|
||||
const rawMask = inputs[1] ?? null;
|
||||
return renderMaskedEffectPreview(
|
||||
node,
|
||||
source,
|
||||
rawMask,
|
||||
(input) => applyEffectToCanvas(input, (effectCtx, width, height) => {
|
||||
applyColorCorrectReference(effectCtx, width, height,
|
||||
numAny(node, ["temperature"], 0, frameIndex),
|
||||
numAny(node, ["tint"], 0, frameIndex),
|
||||
numAny(node, ["hue", "hue_deg"], 0, frameIndex),
|
||||
numAny(node, ["brightness"], 0, frameIndex),
|
||||
numAny(node, ["contrast"], 0, frameIndex),
|
||||
numAny(node, ["saturation", "sat"], 0, frameIndex),
|
||||
numAny(node, ["vibrance"], 0, frameIndex),
|
||||
numAny(node, ["gamma"], 1, frameIndex),
|
||||
numAny(node, ["shadows_hue"], 0, frameIndex),
|
||||
numAny(node, ["shadows_amount"], 0, frameIndex),
|
||||
numAny(node, ["midtones_hue"], 0, frameIndex),
|
||||
numAny(node, ["midtones_amount"], 0, frameIndex),
|
||||
numAny(node, ["highlights_hue"], 0, frameIndex),
|
||||
numAny(node, ["highlights_amount"], 0, frameIndex),
|
||||
{
|
||||
shadowsTemperature: numAny(node, ["shadows_temperature"], 0, frameIndex),
|
||||
shadowsTint: numAny(node, ["shadows_tint"], 0, frameIndex),
|
||||
shadowsContrast: numAny(node, ["shadows_contrast"], 0, frameIndex),
|
||||
shadowsSaturation: numAny(node, ["shadows_saturation"], 0, frameIndex),
|
||||
shadowsVibrance: numAny(node, ["shadows_vibrance"], 0, frameIndex),
|
||||
shadowsGamma: numAny(node, ["shadows_gamma"], 1, frameIndex),
|
||||
shadowsBrightness: numAny(node, ["shadows_brightness"], 0, frameIndex),
|
||||
midtonesTemperature: numAny(node, ["midtones_temperature"], 0, frameIndex),
|
||||
midtonesTint: numAny(node, ["midtones_tint"], 0, frameIndex),
|
||||
midtonesContrast: numAny(node, ["midtones_contrast"], 0, frameIndex),
|
||||
midtonesSaturation: numAny(node, ["midtones_saturation"], 0, frameIndex),
|
||||
midtonesVibrance: numAny(node, ["midtones_vibrance"], 0, frameIndex),
|
||||
midtonesGamma: numAny(node, ["midtones_gamma"], 1, frameIndex),
|
||||
midtonesBrightness: numAny(node, ["midtones_brightness"], 0, frameIndex),
|
||||
highlightsTemperature: numAny(node, ["highlights_temperature"], 0, frameIndex),
|
||||
highlightsTint: numAny(node, ["highlights_tint"], 0, frameIndex),
|
||||
highlightsContrast: numAny(node, ["highlights_contrast"], 0, frameIndex),
|
||||
highlightsSaturation: numAny(node, ["highlights_saturation"], 0, frameIndex),
|
||||
highlightsVibrance: numAny(node, ["highlights_vibrance"], 0, frameIndex),
|
||||
highlightsGamma: numAny(node, ["highlights_gamma"], 1, frameIndex),
|
||||
highlightsBrightness: numAny(node, ["highlights_brightness"], 0, frameIndex),
|
||||
},
|
||||
);
|
||||
}),
|
||||
{ frameIndex },
|
||||
);
|
||||
}
|
||||
|
||||
export function colorCorrect(ctx: CanvasRenderingContext2D, W: number, node: ComfyNode, inputs: HTMLCanvasElement[] = [], frameIndex: number = 0): HTMLCanvasElement {
|
||||
return ops.colorAjust(ctx, W, node, inputs, frameIndex);
|
||||
}
|
||||
|
||||
export function channel(ctx: CanvasRenderingContext2D, W: number, node: ComfyNode, outputSlot?: number | null, inputs: HTMLCanvasElement[] = [], frameIndex: number = 0): HTMLCanvasElement {
|
||||
const source = inputs[0] ?? ctx.canvas;
|
||||
const splitMode = strAny(node, ["mode"], "RGBA", frameIndex);
|
||||
const hasSingleChannelWidget = !!wAny(node, ["channel"]);
|
||||
if (!hasSingleChannelWidget && outputSlot != null) {
|
||||
return extractSplitChannelCanvas(source, outputSlot, splitMode);
|
||||
}
|
||||
const extracted = applyEffectToCanvas(source, (effectCtx, width, height) => {
|
||||
applyChannel(effectCtx, width, height, strAny(node, ["channel"], "Red", frameIndex));
|
||||
});
|
||||
const ectx = extracted.getContext("2d", { willReadFrequently: true })!;
|
||||
const img = ectx.getImageData(0, 0, extracted.width, extracted.height);
|
||||
const data = img.data;
|
||||
for (let i = 3; i < data.length; i += 4) data[i] = 255;
|
||||
ectx.putImageData(img, 0, 0);
|
||||
return extracted;
|
||||
}
|
||||
|
||||
export function levels(ctx: CanvasRenderingContext2D, W: number, node: ComfyNode, _opts?: any): void {
|
||||
const { width, height } = getCanvasDimensions(ctx);
|
||||
applyLevels(ctx,width,height,
|
||||
numAny(node, ["in_min", "min"], 0),
|
||||
numAny(node, ["in_max", "max"], 1),
|
||||
numAny(node, ["gamma", "mid"], 1),
|
||||
numAny(node, ["out_min"], 0),
|
||||
numAny(node, ["out_max"], 1),
|
||||
);
|
||||
}
|
||||
|
||||
export function hueSat(ctx: CanvasRenderingContext2D, W: number, node: ComfyNode, _opts?: any): void {
|
||||
const { width, height } = getCanvasDimensions(ctx);
|
||||
applyHueSat(ctx,width,height,
|
||||
numAny(node, ["hue_deg", "hue"], 0),
|
||||
numAny(node, ["saturation", "sat"], 1),
|
||||
numAny(node, ["value", "val"], 1),
|
||||
);
|
||||
}
|
||||
|
||||
export function invert(ctx: CanvasRenderingContext2D, W: number, node: ComfyNode, inputs: HTMLCanvasElement[] = [], frameIndex: number = 0): HTMLCanvasElement {
|
||||
const source = inputs[0] ?? ctx.canvas;
|
||||
return applyEffectToCanvas(source, (effectCtx, width, height) => {
|
||||
applyInvert(effectCtx, width, height, boolAny(node, ["invert_alpha"], false, frameIndex));
|
||||
});
|
||||
}
|
||||
|
||||
export function clamp(ctx: CanvasRenderingContext2D, W: number, node: ComfyNode, inputs: HTMLCanvasElement[] = [], frameIndex: number = 0): HTMLCanvasElement {
|
||||
const source = inputs[0] ?? ctx.canvas;
|
||||
return applyEffectToCanvas(source, (effectCtx, width, height) => {
|
||||
applyClamp(effectCtx, width, height, numAny(node, ["min_v", "min"], 0, frameIndex), numAny(node, ["max_v", "max"], 1, frameIndex));
|
||||
});
|
||||
}
|
||||
|
||||
export function sharpen(ctx: CanvasRenderingContext2D, W: number, node: ComfyNode): void {
|
||||
const { width, height } = getCanvasDimensions(ctx);
|
||||
applyUnsharp(ctx,width,height, numAny(node,["amount", "strength", "factor"],1));
|
||||
}
|
||||
|
||||
export function edgeDetect(ctx: CanvasRenderingContext2D, W: number, node: ComfyNode): void {
|
||||
const { width, height } = getCanvasDimensions(ctx);
|
||||
applyEdgeDetect(ctx,width,height, numAny(node,["strength"],1));
|
||||
}
|
||||
|
||||
export function glow(ctx: CanvasRenderingContext2D, W: number, node: ComfyNode): void {
|
||||
const { width, height } = getCanvasDimensions(ctx);
|
||||
applyGlow(ctx,width,height, numAny(node,["threshold"],0.8), numAny(node,["intensity"],0.75), Math.round(numAny(node,["blur_px", "blur", "radius"],6)));
|
||||
}
|
||||
|
||||
export function lumaKey(ctx: CanvasRenderingContext2D, W: number, node: ComfyNode): void {
|
||||
const { width, height } = getCanvasDimensions(ctx);
|
||||
applyLumaKey(ctx,width,height, numAny(node,["low"],0.1), numAny(node,["high"],0.9), numAny(node,["softness"],0.05));
|
||||
}
|
||||
|
||||
export function desaturate(ctx: CanvasRenderingContext2D, W: number, node: ComfyNode): void {
|
||||
const { width, height } = getCanvasDimensions(ctx);
|
||||
const factor = numAny(node, ["factor", "amount"], 1);
|
||||
applyDesaturate(ctx, width, height, factor);
|
||||
}
|
||||
|
||||
|
||||
@@ -16,248 +16,3 @@ export const geometryOps = {
|
||||
distort: ops.distort,
|
||||
spherize: ops.spherize,
|
||||
};
|
||||
|
||||
|
||||
// Extracted with ts-morph
|
||||
|
||||
export function crop(ctx: CanvasRenderingContext2D, W: number, node: ComfyNode, inputs: HTMLCanvasElement[] = [], frameIndex: number = 0): HTMLCanvasElement {
|
||||
const source = inputs[0] ?? ctx.canvas;
|
||||
const rawMask = inputs[1] ?? null;
|
||||
return renderMaskedEffectPreview(
|
||||
node,
|
||||
source,
|
||||
rawMask,
|
||||
(input) => applyEffectToCanvas(input, (effectCtx, width, height) => applyCrop(
|
||||
effectCtx,
|
||||
node,
|
||||
width,
|
||||
height,
|
||||
str(node, "aspect_ratio", "custom", frameIndex),
|
||||
num(node, "width", width, frameIndex),
|
||||
num(node, "height", height, frameIndex),
|
||||
)),
|
||||
{
|
||||
frameIndex,
|
||||
premultBeforeProcess: true,
|
||||
compositeWithBase: false,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export function cropStitch(ctx: CanvasRenderingContext2D, W: number, node: ComfyNode, inputs: HTMLCanvasElement[] = [], frameIndex: number = 0): HTMLCanvasElement {
|
||||
if (inputs.length < 2) return inputs[0] ?? ctx.canvas;
|
||||
const rendered = renderCropStitchCanvases(node, inputs, frameIndex);
|
||||
return composeCropStitchPreview(rendered.image, rendered.crop, rendered.mask, rendered.bbox);
|
||||
}
|
||||
|
||||
export function padOut(ctx: CanvasRenderingContext2D, W: number, node: ComfyNode, inputs: HTMLCanvasElement[] = [], frameIndex: number = 0): HTMLCanvasElement {
|
||||
const source = inputs[0] ?? ctx.canvas;
|
||||
return renderPadOutCanvases(node, source, frameIndex).image;
|
||||
}
|
||||
|
||||
export function cornerPin(ctx: CanvasRenderingContext2D, W: number, node: ComfyNode, inputs: HTMLCanvasElement[] = [], frameIndex: number = 0): HTMLCanvasElement {
|
||||
const source = inputs[0] ?? ctx.canvas;
|
||||
return renderCornerPinCanvases(node, source, frameIndex).image;
|
||||
}
|
||||
|
||||
export function cropGeneric(ctx: CanvasRenderingContext2D, W: number, node: ComfyNode, inputs: HTMLCanvasElement[] = [], inputInfos: RenderInputInfo[] = []): HTMLCanvasElement {
|
||||
const source = inputs[0] ?? ctx.canvas;
|
||||
const sourceWidth = source.width || 1;
|
||||
const sourceHeight = source.height || 1;
|
||||
let targetWidth = Math.max(1, Math.round(numAny(node, ["width", "target_width", "base_resolution"], sourceWidth)));
|
||||
let targetHeight = Math.max(1, Math.round(numAny(node, ["height", "target_height", "base_resolution"], sourceHeight)));
|
||||
|
||||
const maskInput = inputs[1] ?? null;
|
||||
if (maskInput) {
|
||||
if (!wAny(node, ["width", "target_width", "height", "target_height"]) && !!w(node, "base_resolution")) {
|
||||
const fittedMask = fitCanvas(maskInput, sourceWidth, sourceHeight);
|
||||
const bounds = computeMaskBounds(fittedMask);
|
||||
if (bounds) {
|
||||
const aspect = bounds.width / Math.max(1, bounds.height);
|
||||
const baseResolution = Math.max(1, Math.round(numAny(node, ["base_resolution"], Math.max(bounds.width, bounds.height))));
|
||||
if (aspect >= 1) {
|
||||
targetWidth = baseResolution;
|
||||
targetHeight = Math.max(1, Math.round(baseResolution / aspect));
|
||||
} else {
|
||||
targetHeight = baseResolution;
|
||||
targetWidth = Math.max(1, Math.round(baseResolution * aspect));
|
||||
}
|
||||
}
|
||||
}
|
||||
return extractMaskDrivenCrop(source, maskInput, numAny(node, ["padding"], 0), targetWidth, targetHeight);
|
||||
}
|
||||
|
||||
const cropRegion = w(node, "crop_region")?.value as { x?: number; y?: number; width?: number; height?: number } | null;
|
||||
const bboxNode = inputInfos[1]?.upstreamNode ?? null;
|
||||
const x = Math.max(0, Math.round(cropRegion?.x ?? numAny(bboxNode ?? node, ["x", "x_offset"], 0)));
|
||||
const y = Math.max(0, Math.round(cropRegion?.y ?? numAny(bboxNode ?? node, ["y", "y_offset"], 0)));
|
||||
const width = Math.max(1, Math.round(cropRegion?.width ?? numAny(bboxNode ?? node, ["width", "crop_w"], sourceWidth)));
|
||||
const height = Math.max(1, Math.round(cropRegion?.height ?? numAny(bboxNode ?? node, ["height", "crop_h"], sourceHeight)));
|
||||
return cropRectCanvas(source, x, y, Math.min(width, sourceWidth - x), Math.min(height, sourceHeight - y));
|
||||
}
|
||||
|
||||
export function transform(ctx: CanvasRenderingContext2D, W: number, node: ComfyNode, inputs: HTMLCanvasElement[] = [], frameIndex: number = 0): HTMLCanvasElement {
|
||||
const source = inputs[0] ?? ctx.canvas;
|
||||
const rawMask = inputs[1] ?? null;
|
||||
const transformImage = (input: HTMLCanvasElement): HTMLCanvasElement => {
|
||||
let working = input;
|
||||
const flip = strAny(node, ["flip", "mirror"], "none", frameIndex).toLowerCase();
|
||||
const flipMethod = strAny(node, ["flip_method"], "", frameIndex);
|
||||
const horizontal = flip === "horizontal" || flipMethod.startsWith("y");
|
||||
const vertical = flip === "vertical" || flipMethod.startsWith("x");
|
||||
working = flipCanvas(working, horizontal, vertical);
|
||||
|
||||
const rotationLabel = strAny(node, ["rotation"], "", frameIndex);
|
||||
if (rotationLabel.startsWith("90")) working = rotateDiscrete(working, 1);
|
||||
else if (rotationLabel.startsWith("180")) working = rotateDiscrete(working, 2);
|
||||
else if (rotationLabel.startsWith("270")) working = rotateDiscrete(working, 3);
|
||||
|
||||
const aspectRatio = numAny(node, ["aspect_ratio"], 1, frameIndex);
|
||||
if (Math.abs(aspectRatio - 1) > 0.0001) {
|
||||
const scaled = makeCanvas(working.width || 1, Math.max(1, Math.round((working.height || 1) * aspectRatio)));
|
||||
const sctx = scaled.getContext("2d", { willReadFrequently: true })!;
|
||||
setResampleMode(sctx, normalizeFilterName(strAny(node, ["upscale_method", "interpolation", "transform_method", "filter"], "bilinear", frameIndex)));
|
||||
sctx.drawImage(working, 0, 0, scaled.width, scaled.height);
|
||||
working = scaled;
|
||||
}
|
||||
|
||||
const tx = numAny(node, ["translate_x", "x", "shift_x"], 0, frameIndex);
|
||||
const ty = numAny(node, ["translate_y", "y", "shift_y"], 0, frameIndex);
|
||||
const rot = numAny(node, ["rotate_deg", "rotate"], 0, frameIndex);
|
||||
const scale = numAny(node, ["scale"], 1, frameIndex);
|
||||
const filter = normalizeFilterName(strAny(node, ["filter", "upscale_method", "interpolation", "transform_method"], "bilinear", frameIndex));
|
||||
const expand = boolAny(node, ["expand"], false, frameIndex);
|
||||
const fillMode = strAny(node, ["fill_mode", "edge_mode"], "transparent", frameIndex);
|
||||
const fillColor = strAny(node, ["fill_color", "background_color", "color"], "#000000", frameIndex);
|
||||
|
||||
return applyEffectToCanvas(working, (effectCtx, width, height) => {
|
||||
return applyTransform(effectCtx, width, height, tx, ty, rot, scale, filter, expand, fillMode, fillColor);
|
||||
});
|
||||
};
|
||||
|
||||
return renderMaskedEffectPreview(
|
||||
node,
|
||||
source,
|
||||
rawMask,
|
||||
transformImage,
|
||||
{
|
||||
frameIndex,
|
||||
premultBeforeProcess: true,
|
||||
compositeWithBase: false,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export function cameraShake(ctx: CanvasRenderingContext2D, W: number, node: ComfyNode, inputs: HTMLCanvasElement[] = [], frameIndex: number = 0): HTMLCanvasElement {
|
||||
const source = inputs[0] ?? ctx.canvas;
|
||||
const transformImage = (input: HTMLCanvasElement): HTMLCanvasElement => {
|
||||
const translate = Math.max(0, numAny(node, ["translate_px"], 12, frameIndex));
|
||||
const rotate = Math.max(0, numAny(node, ["rotate_deg"], 1.5, frameIndex));
|
||||
const zoom = Math.max(0, numAny(node, ["zoom"], 0.03, frameIndex));
|
||||
const smoothing = numAny(node, ["smoothing"], 0.65, frameIndex);
|
||||
const frequency = Math.max(0.01, numAny(node, ["shake_frequency", "frequency"], 1, frameIndex));
|
||||
const seed = Math.max(0, Math.round(numAny(node, ["seed"], 12345, frameIndex)));
|
||||
const tx = smoothShakeValue(seed + 11, frameIndex, 1, translate, smoothing, frequency);
|
||||
const ty = smoothShakeValue(seed + 23, frameIndex, 2, translate, smoothing, frequency);
|
||||
const rot = smoothShakeValue(seed + 37, frameIndex, 3, rotate, smoothing, frequency);
|
||||
const scale = Math.max(0.01, 1 + smoothShakeValue(seed + 53, frameIndex, 4, zoom, smoothing, frequency));
|
||||
return applyEffectToCanvas(input, (effectCtx, width, height) => {
|
||||
return applyTransform(
|
||||
effectCtx,
|
||||
width,
|
||||
height,
|
||||
tx,
|
||||
ty,
|
||||
rot,
|
||||
scale,
|
||||
strAny(node, ["filter"], "bilinear", frameIndex),
|
||||
false,
|
||||
strAny(node, ["fill_mode"], "mirror", frameIndex),
|
||||
strAny(node, ["fill_color"], "#000000", frameIndex),
|
||||
);
|
||||
});
|
||||
};
|
||||
return renderMaskedEffectPreview(node, source, inputs[1] ?? null, transformImage, {
|
||||
frameIndex,
|
||||
premultBeforeProcess: true,
|
||||
compositeWithBase: false,
|
||||
});
|
||||
}
|
||||
|
||||
export function cropReformat(ctx: CanvasRenderingContext2D, W: number, node: ComfyNode): void {
|
||||
const { width, height } = getCanvasDimensions(ctx);
|
||||
applyCropReformat(ctx,width,height,
|
||||
numAny(node,["x"],0), numAny(node,["y"],0),
|
||||
numAny(node,["crop_w", "width"],width), numAny(node,["crop_h", "height"],height),
|
||||
numAny(node,["padding"],0),
|
||||
numAny(node,["out_w", "target_width"],0), numAny(node,["out_h", "target_height"],0),
|
||||
strAny(node,["mode", "method"],"fit")
|
||||
);
|
||||
}
|
||||
|
||||
export function resize(ctx: CanvasRenderingContext2D, W: number, node: ComfyNode): HTMLCanvasElement {
|
||||
const { width, height } = getCanvasDimensions(ctx);
|
||||
const resolved = resolveResizeDimensions(node, width, height);
|
||||
return resizeWithMode(ctx.canvas, resolved.width, resolved.height, resolved.filter, resolved.mode, resolved.fillColor, resolved.cropPosition);
|
||||
}
|
||||
|
||||
export function pad(ctx: CanvasRenderingContext2D, W: number, node: ComfyNode, inputs: HTMLCanvasElement[] = []): HTMLCanvasElement {
|
||||
const source = inputs[0] ?? ctx.canvas;
|
||||
const top = Math.max(0, Math.round(numAny(node, ["top"], 0)));
|
||||
const bottom = Math.max(0, Math.round(numAny(node, ["bottom"], 0)));
|
||||
const left = Math.max(0, Math.round(numAny(node, ["left"], 0)));
|
||||
const right = Math.max(0, Math.round(numAny(node, ["right"], 0)));
|
||||
const output = makeCanvas((source.width || 1) + left + right, (source.height || 1) + top + bottom);
|
||||
const octx = output.getContext("2d", { willReadFrequently: true })!;
|
||||
octx.fillStyle = parseHexColor(strAny(node, ["color", "background_color", "pad_color", "padding_color"], "#808080"));
|
||||
octx.fillRect(0, 0, output.width, output.height);
|
||||
octx.drawImage(source, left, top, source.width || 1, source.height || 1);
|
||||
return output;
|
||||
}
|
||||
|
||||
export function flipRotate(ctx: CanvasRenderingContext2D, W: number, node: ComfyNode): HTMLCanvasElement {
|
||||
const flipMethod = strAny(node, ["flip_method"], "");
|
||||
const horizontal = flipMethod.startsWith("y");
|
||||
const vertical = flipMethod.startsWith("x");
|
||||
let working = flipCanvas(ctx.canvas, horizontal, vertical);
|
||||
const rotation = strAny(node, ["rotation"], "");
|
||||
if (rotation.startsWith("90")) working = rotateDiscrete(working, 1);
|
||||
else if (rotation.startsWith("180")) working = rotateDiscrete(working, 2);
|
||||
else if (rotation.startsWith("270")) working = rotateDiscrete(working, 3);
|
||||
return working;
|
||||
}
|
||||
|
||||
export function distort(ctx: CanvasRenderingContext2D, W: number, node: ComfyNode, inputs: HTMLCanvasElement[], frameIndex: number = 0): HTMLCanvasElement {
|
||||
return renderDistortCanvas(node, inputs, frameIndex).image;
|
||||
}
|
||||
|
||||
export function spherize(ctx: CanvasRenderingContext2D, W: number, node: ComfyNode, inputs: HTMLCanvasElement[] = [], frameIndex: number = 0): HTMLCanvasElement {
|
||||
let source = inputs[0] ?? ctx.canvas;
|
||||
|
||||
const sizeMode = strAny(node, ["size_mode"], "from_input", frameIndex).toLowerCase().trim();
|
||||
if (sizeMode === "custom") {
|
||||
const tw = Math.max(64, Math.round(numAny(node, ["width"], 512, frameIndex)));
|
||||
const th = Math.max(64, Math.round(numAny(node, ["height"], 512, frameIndex)));
|
||||
if (tw !== source.width || th !== source.height) {
|
||||
const resized = makeCanvas(tw, th);
|
||||
resized.getContext("2d", { willReadFrequently: true })!.drawImage(source, 0, 0, tw, th);
|
||||
source = resized;
|
||||
}
|
||||
} else {
|
||||
// from_input: sync width/height widgets to actual source dimensions
|
||||
const ww = w(node, "width");
|
||||
const hw = w(node, "height");
|
||||
setWidgetValue(ww, Math.max(64, source.width));
|
||||
setWidgetValue(hw, Math.max(64, source.height));
|
||||
}
|
||||
|
||||
return applyEffectToCanvas(source, (effectCtx, width, height) => {
|
||||
applySpherize(
|
||||
effectCtx, width, height,
|
||||
strAny(node, ["mode"], "spherize", frameIndex),
|
||||
numAny(node, ["strength"], 1.0, frameIndex),
|
||||
boolAny(node, ["invert"], false, frameIndex),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -4,148 +4,3 @@ export const maskOps = {
|
||||
imageOpsMask: ops.imageOpsMask,
|
||||
channelApply: ops.channelApply,
|
||||
};
|
||||
|
||||
|
||||
// Extracted with ts-morph
|
||||
|
||||
export function channelApply(ctx: CanvasRenderingContext2D, W: number, node: ComfyNode, inputs: HTMLCanvasElement[]): HTMLCanvasElement {
|
||||
const base = fitCanvas(inputs[0] ?? ctx.canvas, (inputs[0] ?? ctx.canvas).width || 1, (inputs[0] ?? ctx.canvas).height || 1);
|
||||
const mask = inputs[1] ? fitCanvas(inputs[1], base.width, base.height) : null;
|
||||
if (!mask) return base;
|
||||
|
||||
const bctx = base.getContext("2d", { willReadFrequently: true })!;
|
||||
const image = bctx.getImageData(0, 0, base.width, base.height);
|
||||
const data = image.data;
|
||||
const matte = mask.getContext("2d", { willReadFrequently: true })!.getImageData(0, 0, base.width, base.height).data;
|
||||
const channel = strAny(node, ["channel"], "A").toLowerCase();
|
||||
const channelIndex = channel === "g" || channel === "green" ? 1 : channel === "b" || channel === "blue" ? 2 : channel === "a" || channel === "alpha" ? 3 : 0;
|
||||
|
||||
for (let i = 0; i < data.length; i += 4) {
|
||||
const value = Math.round(clamp01((matte[i] / 255) * (matte[i + 3] / 255)) * 255);
|
||||
data[i + channelIndex] = value;
|
||||
}
|
||||
bctx.putImageData(image, 0, 0);
|
||||
return base;
|
||||
}
|
||||
|
||||
export function imageOpsMask(ctx: CanvasRenderingContext2D, W: number, node: ComfyNode, cls: string, inputs: HTMLCanvasElement[] = [], frameIndex: number = 0): HTMLCanvasElement | null {
|
||||
const source = inputs[0] ?? ctx.canvas;
|
||||
const rawMask = inputs[1] ?? null;
|
||||
const resolvedMask = resolvePreviewMaskCanvas(node, source, rawMask, frameIndex);
|
||||
|
||||
if (cls === "ImageOpsMaskConvert") {
|
||||
return boolAny(node, ["reverse"], false, frameIndex)
|
||||
? imageToMaskPreviewCanvas(source, node, frameIndex)
|
||||
: buildMaskAlphaCanvas(source, source.width || 1, source.height || 1);
|
||||
}
|
||||
|
||||
if (cls === "ImageOpsNoise") {
|
||||
return renderNoiseCanvas(node, true, frameIndex, W);
|
||||
}
|
||||
|
||||
if (cls === "ImageOpsConstant") {
|
||||
return renderConstantCanvas(node, true);
|
||||
}
|
||||
|
||||
if (cls === "ImageOpsRamp") {
|
||||
return renderRampCanvas(node, true);
|
||||
}
|
||||
|
||||
if (cls === "ImageOpsDistort") {
|
||||
return renderDistortCanvas(node, inputs, frameIndex).mask;
|
||||
}
|
||||
|
||||
if (cls === "ImageOpsBlur") {
|
||||
// Backend blur.py returns the *original* prepared mask as MASK output
|
||||
// (output_mask = output_mask_source) — the blur affects the image, not the
|
||||
// mask itself. Mirror that here so the Preview MASK matches what's
|
||||
// actually sent downstream. If no upstream mask, fall back to the
|
||||
// implicit alpha matte of the source.
|
||||
return resolvedMask ?? alphaMaskCanvas(source);
|
||||
}
|
||||
|
||||
if (cls === "ImageOpsTransform") {
|
||||
return ops.transform(ctx, W, node, [resolvedMask ?? alphaMaskCanvas(source)], frameIndex);
|
||||
}
|
||||
|
||||
if (cls === "ImageOpsCrop") {
|
||||
return ops.crop(ctx, W, node, [resolvedMask ?? alphaMaskCanvas(source)], frameIndex);
|
||||
}
|
||||
|
||||
if (cls === "ImageOpsCropStitch") {
|
||||
return renderCropStitchCanvases(node, inputs, frameIndex).mask;
|
||||
}
|
||||
|
||||
if (cls === "ImageOpsPadOut") {
|
||||
return renderPadOutCanvases(node, source, frameIndex).mask;
|
||||
}
|
||||
|
||||
if (cls === "ImageOpsCornerPin") {
|
||||
return renderCornerPinCanvases(node, source, frameIndex).mask;
|
||||
}
|
||||
|
||||
if (cls === "ImageOpsChannel") {
|
||||
const extracted = applyEffectToCanvas(source, (effectCtx, width, height) => {
|
||||
applyChannel(effectCtx, width, height, strAny(node, ["channel"], "Red", frameIndex));
|
||||
});
|
||||
return buildMaskAlphaCanvas(extracted, extracted.width || 1, extracted.height || 1);
|
||||
}
|
||||
|
||||
if (cls === "ImageOpsClamp") {
|
||||
if (!resolvedMask) return alphaMaskCanvas(source);
|
||||
// Clamp only the alpha channel of the prepared mask (which encodes mask value in A,
|
||||
// RGB=255). Using applyClamp on the full canvas would turn RGB gray and cause
|
||||
// buildMaskAlphaCanvas to double-attenuate via luma*alpha.
|
||||
const lo = numAny(node, ["min_v", "min"], 0, frameIndex);
|
||||
const hi = numAny(node, ["max_v", "max"], 1, frameIndex);
|
||||
const mn = Math.round(clamp01(Math.min(lo, hi)) * 255);
|
||||
const mx = Math.round(clamp01(Math.max(lo, hi)) * 255);
|
||||
const clampMaskOut = makeCanvas(resolvedMask.width || 1, resolvedMask.height || 1);
|
||||
const clampMaskCtx = clampMaskOut.getContext("2d", { willReadFrequently: true })!;
|
||||
clampMaskCtx.drawImage(resolvedMask, 0, 0);
|
||||
const clampImg = clampMaskCtx.getImageData(0, 0, clampMaskOut.width, clampMaskOut.height);
|
||||
const clampData = clampImg.data;
|
||||
for (let ci = 0; ci < clampData.length; ci += 4) {
|
||||
clampData[ci] = 255; clampData[ci + 1] = 255; clampData[ci + 2] = 255;
|
||||
clampData[ci + 3] = Math.max(mn, Math.min(mx, clampData[ci + 3]));
|
||||
}
|
||||
clampMaskCtx.putImageData(clampImg, 0, 0);
|
||||
return markPreparedMaskCanvas(clampMaskOut);
|
||||
}
|
||||
|
||||
if (cls === "ImageOpsKeyer") {
|
||||
return renderKeyerCanvases(node, source, rawMask, frameIndex).mask;
|
||||
}
|
||||
|
||||
if (cls === "ImageOpsInvert") {
|
||||
const mask = resolvedMask ?? alphaMaskCanvas(source);
|
||||
return mask;
|
||||
}
|
||||
|
||||
if (cls === "ImageOpsSpherize") {
|
||||
return renderSpherizeMaskCanvas(node, source, rawMask, frameIndex);
|
||||
}
|
||||
|
||||
if (cls === "ImageOpsMerge") {
|
||||
// inputs = [A, B, mask?] — inputs[1] is foreground B, not the mask input
|
||||
const mergeMaskInput = inputs[2] ?? null;
|
||||
if (mergeMaskInput) {
|
||||
const mergeResolvedMask = resolvePreviewMaskCanvas(node, source, mergeMaskInput, frameIndex);
|
||||
if (mergeResolvedMask) return mergeResolvedMask;
|
||||
}
|
||||
const merged = ops.merge(ctx, W, node, inputs, undefined, frameIndex);
|
||||
return alphaMaskCanvas(merged);
|
||||
}
|
||||
|
||||
if (cls === "ImageOpsComp") {
|
||||
const mask = alphaMaskCanvas(ops.comp(ctx, W, node, inputs));
|
||||
return boolAny(node, ["invert_mask"], false, frameIndex) ? invertMaskCanvas(mask) : mask;
|
||||
}
|
||||
|
||||
if (cls === "ImageOpsColorAjust") {
|
||||
return resolvedMask ?? alphaMaskCanvas(source);
|
||||
}
|
||||
|
||||
return resolvedMask ?? alphaMaskCanvas(source);
|
||||
}
|
||||
|
||||
|
||||
@@ -7,80 +7,3 @@ export const proceduralOps = {
|
||||
grain: ops.grain,
|
||||
text: ops.text,
|
||||
};
|
||||
|
||||
|
||||
// Extracted with ts-morph
|
||||
|
||||
export function grain(ctx: CanvasRenderingContext2D, W: number, node: ComfyNode, inputs: HTMLCanvasElement[] = [], frameIndex: number = 0): HTMLCanvasElement {
|
||||
const source = inputs[0] ?? ctx.canvas;
|
||||
return renderGrainCanvas(node, source, inputs[1] ?? null, frameIndex);
|
||||
}
|
||||
|
||||
export function text(ctx: CanvasRenderingContext2D, W: number, node: ComfyNode, inputs: HTMLCanvasElement[] = [], frameIndex: number = 0): HTMLCanvasElement {
|
||||
const source = inputs[0] ?? ctx.canvas;
|
||||
return renderTextCanvas(node, source, inputs[1] ?? null, frameIndex);
|
||||
}
|
||||
|
||||
export function keyer(ctx: CanvasRenderingContext2D, W: number, node: ComfyNode, inputs: HTMLCanvasElement[] = [], frameIndex: number = 0): HTMLCanvasElement {
|
||||
const source = inputs[0] ?? ctx.canvas;
|
||||
return renderKeyerCanvases(node, source, inputs[1] ?? null, frameIndex).image;
|
||||
}
|
||||
|
||||
export function stitch(ctx: CanvasRenderingContext2D, W: number, node: ComfyNode, inputs: HTMLCanvasElement[]): HTMLCanvasElement {
|
||||
const first = inputs[0] ?? ctx.canvas;
|
||||
const second = inputs[1] ?? null;
|
||||
if (!second) return first;
|
||||
return stitchCanvases(
|
||||
first,
|
||||
second,
|
||||
strAny(node, ["direction"], "right"),
|
||||
numAny(node, ["spacing_width"], 0),
|
||||
strAny(node, ["spacing_color"], "black"),
|
||||
boolAny(node, ["match_image_size"], true),
|
||||
);
|
||||
}
|
||||
|
||||
export function constant(ctx: CanvasRenderingContext2D, W: number, node: ComfyNode): HTMLCanvasElement {
|
||||
return renderConstantCanvas(node, false);
|
||||
}
|
||||
|
||||
export function ramp(ctx: CanvasRenderingContext2D, W: number, node: ComfyNode): HTMLCanvasElement {
|
||||
return renderRampCanvas(node, false);
|
||||
}
|
||||
|
||||
export function noise(ctx: CanvasRenderingContext2D, W: number, node: ComfyNode, frameIndex: number = 0): HTMLCanvasElement {
|
||||
return renderNoiseCanvas(node, false, frameIndex, W);
|
||||
}
|
||||
|
||||
async draw(ctx: CanvasRenderingContext2D, W: number, node: ComfyNode, inputs: HTMLCanvasElement[]): Promise<HTMLCanvasElement> {
|
||||
return await renderDrawPreview(node, inputs[0] ?? null);
|
||||
}
|
||||
|
||||
async drawMask(ctx: CanvasRenderingContext2D, W: number, node: ComfyNode, inputs: HTMLCanvasElement[]): Promise<HTMLCanvasElement> {
|
||||
const base = inputs[0] ?? null;
|
||||
const width = base?.width || Math.max(1, Math.round(numAny(node, ["width"], 1024)));
|
||||
const height = base?.height || Math.max(1, Math.round(numAny(node, ["height"], 1024)));
|
||||
const overlay = await resolveDrawOverlayCanvas(node, width, height);
|
||||
// Backend draw.py returns overlay_alpha[..., 0] as the mask (alpha channel
|
||||
// only, brush colour is irrelevant). buildMaskAlphaCanvas applies luma*alpha
|
||||
// which would dim the preview mask whenever the user paints with a non-white
|
||||
// colour — that diverges from the backend. Build an alpha-only matte instead.
|
||||
const ow = overlay.width || 1;
|
||||
const oh = overlay.height || 1;
|
||||
const matte = makeCanvas(ow, oh);
|
||||
const mctx = matte.getContext("2d", { willReadFrequently: true })!;
|
||||
mctx.drawImage(overlay, 0, 0);
|
||||
const img = mctx.getImageData(0, 0, ow, oh);
|
||||
const data = img.data;
|
||||
for (let i = 0; i < data.length; i += 4) {
|
||||
const a = data[i + 3];
|
||||
data[i] = 255;
|
||||
data[i + 1] = 255;
|
||||
data[i + 2] = 255;
|
||||
data[i + 3] = a;
|
||||
}
|
||||
mctx.putImageData(img, 0, 0);
|
||||
const mask = markPreparedMaskCanvas(matte);
|
||||
return boolAny(node, ["invert_mask"], false) ? invertMaskCanvas(mask) : mask;
|
||||
}
|
||||
|
||||
|
||||
@@ -8,15 +8,3 @@ export const videoOps = {
|
||||
channelSplit: ops.channelSplit,
|
||||
channelMerge: ops.channelMerge,
|
||||
};
|
||||
|
||||
|
||||
// Extracted with ts-morph
|
||||
|
||||
export function channelSplit(ctx: CanvasRenderingContext2D, W: number, node: ComfyNode, outputSlot: number | null): HTMLCanvasElement {
|
||||
return extractSplitChannelCanvas(ctx.canvas, outputSlot, strAny(node, ["mode"], "RGBA"));
|
||||
}
|
||||
|
||||
export function channelMerge(ctx: CanvasRenderingContext2D, W: number, node: ComfyNode, inputs: HTMLCanvasElement[]): HTMLCanvasElement {
|
||||
return mergeChannelInputs(inputs, strAny(node, ["mode"], "RGBA")) ?? (inputs[0] ?? ctx.canvas);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user