diff --git a/src/preview/ops/blend.ts b/src/preview/ops/blend.ts index c1eb5e3..3e53b93 100644 --- a/src/preview/ops/blend.ts +++ b/src/preview/ops/blend.ts @@ -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; - } - diff --git a/src/preview/ops/color.ts b/src/preview/ops/color.ts index 36c6d05..a34e189 100644 --- a/src/preview/ops/color.ts +++ b/src/preview/ops/color.ts @@ -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); - } - diff --git a/src/preview/ops/geometry.ts b/src/preview/ops/geometry.ts index 454c485..7d27bae 100644 --- a/src/preview/ops/geometry.ts +++ b/src/preview/ops/geometry.ts @@ -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), - ); - }); - } - diff --git a/src/preview/ops/masks.ts b/src/preview/ops/masks.ts index 2cba492..4607d70 100644 --- a/src/preview/ops/masks.ts +++ b/src/preview/ops/masks.ts @@ -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); - } - diff --git a/src/preview/ops/procedural.ts b/src/preview/ops/procedural.ts index 9ae23e8..44df42e 100644 --- a/src/preview/ops/procedural.ts +++ b/src/preview/ops/procedural.ts @@ -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 { - return await renderDrawPreview(node, inputs[0] ?? null); - } - -async drawMask(ctx: CanvasRenderingContext2D, W: number, node: ComfyNode, inputs: HTMLCanvasElement[]): Promise { - 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; - } - diff --git a/src/preview/ops/video.ts b/src/preview/ops/video.ts index af392c6..da1c02f 100644 --- a/src/preview/ops/video.ts +++ b/src/preview/ops/video.ts @@ -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); - } -