fix: clean up duplicate extractions in preview ops

This commit is contained in:
MajoorWaldi
2026-08-22 12:25:14 +02:00
parent 9f3edbe033
commit 3728ceddee
6 changed files with 0 additions and 662 deletions
-45
View File
@@ -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;
}
-138
View File
@@ -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);
}
-245
View File
@@ -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),
);
});
}
-145
View File
@@ -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);
}
-77
View File
@@ -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;
}
-12
View File
@@ -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);
}