Defining NODE_CLASS_MAPPINGS made ComfyUI skip comfy_entrypoint, so /imageops/viewmedia and node replacements were never registered. Load as a V3 extension only, move server.py to routes.py so it stops shadowing ComfyUI's server module, and transcode animated previews with PyAV instead of an external ffmpeg. - Require ComfyUI 0.19.0 (has_intermediate_output); CI floor updated. - Process 11 nodes on ComfyUI's torch device when the frames fit in free memory (IMAGEOPS_COMPUTE_DEVICE=cpu disables it). - Add core CURVE input to Color Correct, BOUNDING_BOX output/input to Crop and Crop Stitch, LAYERS output to Comp (ComfyUI 0.31+), essentials_category, and Merge blend modes with golden parity. - Report progress through the public set_progress API and drop the hidden unique_id input. - Add ImageOps settings and a refresh-previews command. - Fix ImageOpsComp rotation and preview saving (undefined to_display_range) and masked Transform edge colors. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
264 lines
9.2 KiB
JavaScript
264 lines
9.2 KiB
JavaScript
const COMP_BLEND_MODES = [
|
|
"over",
|
|
"add",
|
|
"multiply",
|
|
"screen",
|
|
"overlay",
|
|
"soft_light",
|
|
"difference",
|
|
"lighten",
|
|
"darken",
|
|
"color_dodge",
|
|
"color_burn",
|
|
"exclusion",
|
|
"hard_light"
|
|
];
|
|
function inputName(input) {
|
|
return String(input?.name ?? "");
|
|
}
|
|
function clampCompCenter(value) {
|
|
if (!Number.isFinite(value)) return 0.5;
|
|
return Math.max(-2, Math.min(3, value));
|
|
}
|
|
function clampCompScale(value) {
|
|
if (!Number.isFinite(value)) return 1;
|
|
return Math.max(0.05, Math.min(8, value));
|
|
}
|
|
function clampCompRotation(value) {
|
|
if (!Number.isFinite(value)) return 0;
|
|
const normalized = ((value + 180) % 360 + 360) % 360 - 180;
|
|
return Math.max(-180, Math.min(180, normalized));
|
|
}
|
|
function clampCompPoint(value) {
|
|
if (value == null || value === "") return null;
|
|
const numeric = Number(value);
|
|
if (!Number.isFinite(numeric)) return null;
|
|
return clampCompCenter(numeric);
|
|
}
|
|
function hasFiniteCompPoint(value) {
|
|
if (value == null || value === "") return false;
|
|
return Number.isFinite(Number(value));
|
|
}
|
|
function hasNonDegenerateCompQuad(layer) {
|
|
const xs = [Number(layer.tlX), Number(layer.trX), Number(layer.blX), Number(layer.brX)];
|
|
const ys = [Number(layer.tlY), Number(layer.trY), Number(layer.blY), Number(layer.brY)];
|
|
return Math.max(...xs) - Math.min(...xs) > 1e-4 && Math.max(...ys) - Math.min(...ys) > 1e-4;
|
|
}
|
|
function defaultCompLayer(slot, index) {
|
|
const offset = Math.min(index, 4) * 0.04;
|
|
return {
|
|
slot,
|
|
centerX: 0.5 + offset,
|
|
centerY: 0.5 + offset,
|
|
scale: index === 0 ? 1 : 0.5,
|
|
rotationDeg: 0,
|
|
opacity: 1,
|
|
mode: "over",
|
|
enabled: true
|
|
};
|
|
}
|
|
function normalizeCompLayer(slot, index, layer) {
|
|
const fallback = defaultCompLayer(slot, index);
|
|
const anyLayer = layer ?? {};
|
|
const mode = String(anyLayer.mode ?? fallback.mode).trim().toLowerCase().replace(/[-\s]+/g, "_");
|
|
return {
|
|
slot,
|
|
centerX: clampCompCenter(Number(anyLayer.centerX ?? anyLayer.center_x ?? fallback.centerX)),
|
|
centerY: clampCompCenter(Number(anyLayer.centerY ?? anyLayer.center_y ?? fallback.centerY)),
|
|
scale: clampCompScale(Number(anyLayer.scale ?? fallback.scale)),
|
|
rotationDeg: clampCompRotation(Number(anyLayer.rotationDeg ?? anyLayer.rotate_deg ?? anyLayer.rotation_deg ?? anyLayer.rotation ?? fallback.rotationDeg)),
|
|
opacity: Math.max(0, Math.min(1, Number(anyLayer.opacity ?? fallback.opacity))),
|
|
mode: COMP_BLEND_MODES.includes(mode) ? mode : "over",
|
|
enabled: anyLayer.enabled !== false,
|
|
tlX: clampCompPoint(anyLayer.tlX ?? anyLayer.tl_x),
|
|
tlY: clampCompPoint(anyLayer.tlY ?? anyLayer.tl_y),
|
|
trX: clampCompPoint(anyLayer.trX ?? anyLayer.tr_x),
|
|
trY: clampCompPoint(anyLayer.trY ?? anyLayer.tr_y),
|
|
blX: clampCompPoint(anyLayer.blX ?? anyLayer.bl_x),
|
|
blY: clampCompPoint(anyLayer.blY ?? anyLayer.bl_y),
|
|
brX: clampCompPoint(anyLayer.brX ?? anyLayer.br_x),
|
|
brY: clampCompPoint(anyLayer.brY ?? anyLayer.br_y)
|
|
};
|
|
}
|
|
function getCompSlots(node) {
|
|
const slotMap = /* @__PURE__ */ new Map();
|
|
for (let inputIndex = 0; inputIndex < (node.inputs?.length ?? 0); inputIndex++) {
|
|
const name = inputName(node.inputs?.[inputIndex]);
|
|
const match = /^image_(\d+)$/.exec(name);
|
|
if (match) {
|
|
const layerNumber = Number(match[1]);
|
|
slotMap.set(layerNumber, {
|
|
slot: name,
|
|
inputIndex,
|
|
layerNumber,
|
|
maskSlot: `mask_${layerNumber}`,
|
|
maskInputIndex: null
|
|
});
|
|
}
|
|
}
|
|
for (let inputIndex = 0; inputIndex < (node.inputs?.length ?? 0); inputIndex++) {
|
|
const name = inputName(node.inputs?.[inputIndex]);
|
|
const match = /^mask_(\d+)$/.exec(name);
|
|
if (!match) continue;
|
|
const layerNumber = Number(match[1]);
|
|
const existing = slotMap.get(layerNumber);
|
|
if (existing) {
|
|
existing.maskInputIndex = inputIndex;
|
|
} else {
|
|
slotMap.set(layerNumber, {
|
|
slot: `image_${layerNumber}`,
|
|
inputIndex: -1,
|
|
layerNumber,
|
|
maskSlot: name,
|
|
maskInputIndex: inputIndex
|
|
});
|
|
}
|
|
}
|
|
return [...slotMap.values()].filter((slot) => slot.inputIndex >= 0).sort((a, b) => a.layerNumber - b.layerNumber);
|
|
}
|
|
function parseCompLayers(raw) {
|
|
if (!raw) return [];
|
|
let parsed = raw;
|
|
if (typeof raw === "string") {
|
|
try {
|
|
parsed = JSON.parse(raw);
|
|
} catch {
|
|
return [];
|
|
}
|
|
}
|
|
const source = Array.isArray(parsed) ? parsed : parsed && typeof parsed === "object" && Array.isArray(parsed.layers) ? parsed.layers : [];
|
|
const layers = [];
|
|
for (let index = 0; index < source.length; index++) {
|
|
const entry = source[index];
|
|
if (!entry || typeof entry !== "object") continue;
|
|
const slot = String(entry.slot ?? "").trim();
|
|
if (!slot) continue;
|
|
layers.push(normalizeCompLayer(slot, index, entry));
|
|
}
|
|
return layers;
|
|
}
|
|
function syncCompLayers(raw, slots) {
|
|
const existing = parseCompLayers(raw);
|
|
const bySlot = new Map(existing.map((layer) => [layer.slot, layer]));
|
|
return slots.map((slot, index) => normalizeCompLayer(slot.slot, index, bySlot.get(slot.slot)));
|
|
}
|
|
function hasCompLayerCornerPin(layer) {
|
|
if (!layer) return false;
|
|
const hasAllPoints = [layer.tlX, layer.tlY, layer.trX, layer.trY, layer.blX, layer.blY, layer.brX, layer.brY].every(hasFiniteCompPoint);
|
|
return hasAllPoints && hasNonDegenerateCompQuad(layer);
|
|
}
|
|
function serializeCompLayers(layers) {
|
|
return JSON.stringify({
|
|
version: 1,
|
|
layers: layers.map((layer) => ({
|
|
slot: layer.slot,
|
|
center_x: layer.centerX,
|
|
center_y: layer.centerY,
|
|
scale: layer.scale,
|
|
rotate_deg: layer.rotationDeg,
|
|
opacity: layer.opacity,
|
|
mode: layer.mode,
|
|
enabled: layer.enabled,
|
|
...hasCompLayerCornerPin(layer) ? {
|
|
tl_x: layer.tlX,
|
|
tl_y: layer.tlY,
|
|
tr_x: layer.trX,
|
|
tr_y: layer.trY,
|
|
bl_x: layer.blX,
|
|
bl_y: layer.blY,
|
|
br_x: layer.brX,
|
|
br_y: layer.brY
|
|
} : {}
|
|
}))
|
|
});
|
|
}
|
|
function getCompLayerOutputCorners(outputWidth, outputHeight, sourceWidth, sourceHeight, layer) {
|
|
if (hasCompLayerCornerPin(layer)) {
|
|
const maxX = Math.max(1, outputWidth - 1);
|
|
const maxY = Math.max(1, outputHeight - 1);
|
|
return {
|
|
tl: { x: Number(layer.tlX) * maxX, y: Number(layer.tlY) * maxY },
|
|
tr: { x: Number(layer.trX) * maxX, y: Number(layer.trY) * maxY },
|
|
bl: { x: Number(layer.blX) * maxX, y: Number(layer.blY) * maxY },
|
|
br: { x: Number(layer.brX) * maxX, y: Number(layer.brY) * maxY }
|
|
};
|
|
}
|
|
const rect = computeCompRect(outputWidth, outputHeight, sourceWidth, sourceHeight, { ...layer, tlX: null, tlY: null, trX: null, trY: null, blX: null, blY: null, brX: null, brY: null });
|
|
const angleRad = rect.rotationDeg * Math.PI / 180;
|
|
const cos = Math.cos(angleRad);
|
|
const sin = Math.sin(angleRad);
|
|
const halfWidth = rect.drawWidth / 2;
|
|
const halfHeight = rect.drawHeight / 2;
|
|
const makeCorner = (localX, localY) => ({
|
|
x: rect.centerX + localX * cos - localY * sin,
|
|
y: rect.centerY + localX * sin + localY * cos
|
|
});
|
|
return {
|
|
tl: makeCorner(-halfWidth, -halfHeight),
|
|
tr: makeCorner(halfWidth, -halfHeight),
|
|
bl: makeCorner(-halfWidth, halfHeight),
|
|
br: makeCorner(halfWidth, halfHeight)
|
|
};
|
|
}
|
|
function computeCompRect(outputWidth, outputHeight, sourceWidth, sourceHeight, layer) {
|
|
const drawWidth = Math.max(1, Math.round(Math.max(1, sourceWidth) * clampCompScale(layer.scale)));
|
|
const drawHeight = Math.max(1, Math.round(Math.max(1, sourceHeight) * clampCompScale(layer.scale)));
|
|
if (hasCompLayerCornerPin(layer)) {
|
|
const points = getCompLayerOutputCorners(outputWidth, outputHeight, sourceWidth, sourceHeight, layer);
|
|
const xs = [points.tl.x, points.tr.x, points.bl.x, points.br.x];
|
|
const ys = [points.tl.y, points.tr.y, points.bl.y, points.br.y];
|
|
const left2 = Math.round(Math.min(...xs));
|
|
const top2 = Math.round(Math.min(...ys));
|
|
const right = Math.round(Math.max(...xs));
|
|
const bottom = Math.round(Math.max(...ys));
|
|
return {
|
|
left: left2,
|
|
top: top2,
|
|
width: Math.max(1, right - left2),
|
|
height: Math.max(1, bottom - top2),
|
|
right,
|
|
bottom,
|
|
drawWidth,
|
|
drawHeight,
|
|
centerX: (points.tl.x + points.tr.x + points.bl.x + points.br.x) / 4,
|
|
centerY: (points.tl.y + points.tr.y + points.bl.y + points.br.y) / 4,
|
|
rotationDeg: clampCompRotation(layer.rotationDeg)
|
|
};
|
|
}
|
|
const centerX = clampCompCenter(layer.centerX) * Math.max(1, outputWidth);
|
|
const centerY = clampCompCenter(layer.centerY) * Math.max(1, outputHeight);
|
|
const rad = clampCompRotation(layer.rotationDeg) * Math.PI / 180;
|
|
const absCos = Math.abs(Math.cos(rad));
|
|
const absSin = Math.abs(Math.sin(rad));
|
|
const width = Math.max(1, Math.ceil(drawWidth * absCos + drawHeight * absSin));
|
|
const height = Math.max(1, Math.ceil(drawWidth * absSin + drawHeight * absCos));
|
|
const left = Math.round(centerX - width / 2);
|
|
const top = Math.round(centerY - height / 2);
|
|
return {
|
|
left,
|
|
top,
|
|
width,
|
|
height,
|
|
right: left + width,
|
|
bottom: top + height,
|
|
drawWidth,
|
|
drawHeight,
|
|
centerX,
|
|
centerY,
|
|
rotationDeg: clampCompRotation(layer.rotationDeg)
|
|
};
|
|
}
|
|
export {
|
|
COMP_BLEND_MODES,
|
|
clampCompCenter,
|
|
clampCompRotation,
|
|
clampCompScale,
|
|
computeCompRect,
|
|
getCompLayerOutputCorners,
|
|
getCompSlots,
|
|
hasCompLayerCornerPin,
|
|
parseCompLayers,
|
|
serializeCompLayers,
|
|
syncCompLayers
|
|
};
|