Files
2026-02-17 23:49:14 +02:00

438 lines
13 KiB
JavaScript

import { app } from "../../../scripts/app.js";
import { allow_debug } from "./js_shared.js";
import { Shapes } from "./utils.js";
import { BaseSmartWidget, BaseSmartWidgetManager,} from "./makadi/BaseSmartWidget.js";
import { SmartButton } from "./makadi/SmartButton.js";
app.registerExtension({
name: "iTools.compareNode",
async beforeRegisterNodeDef(nodeType, nodeData, app) {
if (nodeData.name === "iToolsCompareImage") {
}
},
async nodeCreated(node) {
if (node.comfyClass !== "iToolsCompareImage") {
return;
}
// init size
node.size = [285, 330];
let compare = null;
let mouse = {
mouseInNode: false,
x: 0,
y: 0,
};
function turnAllButtonsOff() {
const buttons = node.widgets.filter((widget) => widget instanceof SmartButton);
buttons.forEach((b) => {
b.isActive = false;
b.color = b.originalColor;
b.textColor = b.originalTextColor;
});
}
function toggleButtonActivation(button) {
// turn them all off
turnAllButtonsOff();
// activate this one
if (button.isActive) {
button.color = button.originalColor;
button.textColor = button.originalTextColor;
} else {
button.color = "#80a1c0";
button.textColor = "black";
}
button.isActive = !button.isActive;
}
function createButtons(startVisible = true) {
const bx = 50;
const by = 12;
const r = 25;
const offset = 30;
const buttonFont = "14px Arial";
const a = new SmartButton(bx, by, r, 20, node, "A");
a.allowVisualHover = true;
a.textYoffset = 1.1;
a.isVisible = startVisible;
a.shape = Shapes.CIRCLE;
a.outlineWidth = 1;
a.outlineColor = "#656565";
a.color = "#222222";
a.font = buttonFont;
a.onClick = async () => {
toggleButtonActivation(a);
initCompare("A");
};
const b = new SmartButton(bx + offset, by, r, 20, node, "B");
b.allowVisualHover = true;
b.textYoffset = 1.1;
b.isVisible = startVisible;
b.shape = Shapes.CIRCLE;
b.outlineWidth = 1;
b.outlineColor = "#656565";
b.color = "#222222";
b.font = buttonFont;
b.onClick = async () => {
toggleButtonActivation(b);
initCompare("B");
};
const c = new SmartButton(bx + 2 * offset, by, r, 20, node, "|");
c.allowVisualHover = true;
c.textYoffset = -0.05;
c.isVisible = startVisible;
c.shape = Shapes.CIRCLE;
c.outlineWidth = 1;
c.outlineColor = "#656565";
c.color = "#222222";
c.font = buttonFont;
c.onClick = () => {
toggleButtonActivation(c);
initCompare("|");
};
const o = new SmartButton(bx + 3 * offset, by, r, 20, node, "O");
o.allowVisualHover = true;
o.textYoffset = 1.1;
o.isVisible = startVisible;
o.shape = Shapes.CIRCLE;
o.outlineWidth = 1;
o.outlineColor = "#656565";
o.color = "#222222";
o.font = buttonFont;
o.onClick = () => {
toggleButtonActivation(o);
initCompare("O");
};
}
createButtons();
async function initCompare(compareMode) {
if (compareMode === "|") {
compare = { mode: "|" };
} else if (compareMode === "O") {
compare = { mode: "O" };
} else if (compareMode === "A") {
compare = { mode: "A" };
} else if (compareMode === "B") {
compare = { mode: "B" };
}
}
node.onExecuted = async function (message) {
// await for !node.imgs
for (let i = 0; i < 30 && !node.imgs; i++) {
if (allow_debug) console.log("wait...", i);
await new Promise((resolve) => setTimeout(resolve, 100));
}
// await for ImagePreviewWidget
const previewWidget = node.widgets.find((widget) => !(widget instanceof BaseSmartWidget));
for (let i = 0; i < 30 && !previewWidget; i++) {
if (allow_debug) console.log("wait...", i);
await new Promise((resolve) => setTimeout(resolve, 100));
}
if (!previewWidget) {
if (allow_debug) console.log("ImagePreviewWidget not found");
return;
}
// Override draw function in ImagePreviewWidget
const originalDraw = previewWidget.draw;
previewWidget.draw = function (ctx, node, widget_width, y, widget_height, ...args) {
// originalDraw.apply(this, [ctx, node, widget_width, y, widget_height, ...args]);
overrideDraw(node, widget_width, y, ctx, compare, mouse);
};
// if no compare mode use default
if (!compare && node.imgs) {
compare = { mode: "|" };
const cB = node.widgets.find((widget) => widget.text === "|");
toggleButtonActivation(cB);
}
};
node.onResize = function (newSize) {
// limit width size while resizing
node.size[0] = Math.max(285, newSize[0]);
};
const originalClick = app.canvas.canvas.onclick;
app.canvas.canvas.onclick = (e) => {
if (originalClick) {
originalClick.call(app.canvas.canvas, e);
}
};
// not ready yet 🤔
async function swapO(originalOrder) {
// await new Promise((resolve) => setTimeout(resolve, 200));
if (compare && compare.mode === "O" && node.imgs && node.imgs.length > 1) {
// Swap the first two images
[node.imgs[0], node.imgs[1]] = [node.imgs[1], node.imgs[0]];
// Force canvas redraw
node.setDirtyCanvas(true, false);
} else {
// Restore original order
node.imgs = originalOrder;
node.setDirtyCanvas(true, false);
}
}
// not ready yet
function cycleZoom() {
if (compare.mode === "|" && node.imgs && node.imgs.length > 1) {
// cycle through all zooming steps
node.setDirtyCanvas(true, false);
}
}
node.onMouseEnter = (e) => {
mouse.mouseInNode = true;
};
node.onMouseLeave = (e) => {
mouse.mouseInNode = false;
};
node.onMouseMove = (e, pos) => {
if (mouse.mouseInNode) {
const graphMouse = app.canvas.graph_mouse;
mouse.x = graphMouse[0] - node.pos[0];
mouse.y = graphMouse[1] - node.pos[1];
}
};
const originalMenuOptions = node.getExtraMenuOptions;
node.getExtraMenuOptions = function (_, options) {
if (originalMenuOptions) {
originalMenuOptions.call(node, _, options);
}
if (node.imgs) {
// If node node has images then we add an open in new tab item
let img = null;
if (compare.mode === "A") {
img = node.imgs[0];
} else if (compare.mode === "B") {
img = node.imgs[1];
}
if (img) {
options.unshift(
{
content: "Open Image",
callback: () => {
const url = new URL(img.src);
url.searchParams.delete("preview");
window.open(url, "_blank");
},
},
...getCopyImageOption(img),
{
content: "Save Image",
callback: () => {
const a = document.createElement("a");
const url = new URL(img.src);
url.searchParams.delete("preview");
a.href = url.toString();
a.setAttribute("download", new URLSearchParams(url.search).get("filename"));
document.body.append(a);
a.click();
requestAnimationFrame(() => a.remove());
},
}
);
}
}
};
const m = new BaseSmartWidgetManager(node, "iToolsCompareImage");
const origOnRemoved = node.onRemoved;
node.onRemoved = function () {
origOnRemoved?.apply(this, arguments);
m.destroy()
}
},
});
const compareWay = app.extensionManager.setting.get("iTools.Nodes.Compare Mode", "makadi");
function overrideDraw(node, widget_width, y, ctx, compare, mouse) {
if (!compare || !compare.mode || !node.imgs || node.imgs.length < 2) return;
let img1 = node.imgs[0];
let img2 = node.imgs[node.imgs.length > 2 ? Math.floor(node.imgs.length / 2) : 1];
const dw = widget_width;
const dh = node.size[1] - y;
// 1. Calculate parameters to force both images to the same height (dh)
const getParams = (img) => {
const scale = dh / img.naturalHeight;
const w = img.naturalWidth * scale;
// If the resulting width is wider than the widget, scale down further
const finalScale = w > dw ? dw / w : 1;
const finalW = w * finalScale;
const finalH = dh * finalScale;
return {
x: (dw - finalW) / 2,
y: y + (dh - finalH) / 2,
w: finalW,
h: finalH
};
};
const p1 = getParams(img1);
const p2 = getParams(img2);
// 2. Determine the shared interaction bounds (the container area)
const viewW = Math.max(p1.w, p2.w);
const viewH = Math.max(p1.h, p2.h);
const imgX = (dw - viewW) / 2;
const imgY = y + (dh - viewH) / 2;
// 3. Mouse Logic
let mouseX, mouseY;
if (compareWay === "makadi") {
const graphMouse = app.canvas.graph_mouse;
mouseX = graphMouse[0] - node.pos[0];
mouseY = graphMouse[1] - node.pos[1];
} else {
mouseX = mouse.mouseInNode ? mouse.x : dw / 2;
mouseY = mouse.mouseInNode ? mouse.y : y + dh / 2;
}
// 4. Drawing
if (compare.mode === "|") {
const splitX = Math.max(imgX, Math.min(mouseX, imgX + viewW));
const left = (compareWay === "makadi") ? {img: img1, p: p1} : {img: img2, p: p2};
const right = (compareWay === "makadi") ? {img: img2, p: p2} : {img: img1, p: p1};
// Draw Left Side
ctx.save();
ctx.beginPath();
ctx.rect(0, y, splitX, dh); // Clip left side of the widget
ctx.clip();
ctx.drawImage(left.img, left.p.x, left.p.y, left.p.w, left.p.h);
ctx.restore();
// Draw Right Side
ctx.save();
ctx.beginPath();
ctx.rect(splitX, y, dw - splitX, dh); // Clip right side of the widget
ctx.clip();
ctx.drawImage(right.img, right.p.x, right.p.y, right.p.w, right.p.h);
ctx.restore();
} else if (compare.mode === "O") {
const radius = Math.min(dw, dh) * 0.15;
// Determine background vs overlay based on compareWay
const bg = (compareWay === "makadi") ? {img: img1, p: p1} : {img: img2, p: p2};
const overlay = (compareWay === "makadi") ? {img: img2, p: p2} : {img: img1, p: p1};
// 1. Draw the background image
ctx.drawImage(bg.img, bg.p.x, bg.p.y, bg.p.w, bg.p.h);
// 2. Draw the circular clip for the second image
ctx.save();
ctx.beginPath();
ctx.arc(mouseX, mouseY, radius, 0, Math.PI * 2);
ctx.clip();
ctx.drawImage(overlay.img, overlay.p.x, overlay.p.y, overlay.p.w, overlay.p.h);
ctx.restore();
// Optional: Draw a subtle border around the circle so you can see it better
// ctx.beginPath();
// ctx.arc(mouseX, mouseY, radius, 0, Math.PI * 2);
// ctx.strokeStyle = "rgba(255,255,255,0.5)";
// ctx.stroke();
} else if (compare.mode === "A") {
ctx.drawImage(img1, p1.x, p1.y, p1.w, p1.h);
} else if (compare.mode === "B") {
ctx.drawImage(img2, p2.x, p2.y, p2.w, p2.h);
}
}
function getCopyImageOption(img) {
if (typeof window.ClipboardItem === "undefined") return [];
return [
{
content: "Copy Image",
callback: async () => {
const url = new URL(img.src);
url.searchParams.delete("preview");
const writeImage = async (blob) => {
await navigator.clipboard.write([
new ClipboardItem({
[blob.type]: blob,
}),
]);
};
try {
const data = await fetch(url);
const blob = await data.blob();
try {
await writeImage(blob);
} catch (error) {
// Chrome seems to only support PNG on write, convert and try again
if (blob.type !== "image/png") {
const canvas = $el("canvas", {
width: img.naturalWidth,
height: img.naturalHeight,
});
const ctx = canvas.getContext("2d");
let image;
if (typeof window.createImageBitmap === "undefined") {
image = new Image();
const p = new Promise((resolve, reject) => {
image.onload = resolve;
image.onerror = reject;
}).finally(() => {
URL.revokeObjectURL(image.src);
});
image.src = URL.createObjectURL(blob);
await p;
} else {
image = await createImageBitmap(blob);
}
try {
ctx.drawImage(image, 0, 0);
canvas.toBlob(writeImage, "image/png");
} finally {
if (typeof image.close === "function") {
image.close();
}
}
return;
}
throw error;
}
} catch (error) {
toastStore.addAlert(
t("toastMessages.errorCopyImage", {
error: error.message ?? error,
})
);
}
},
},
];
}