314 lines
11 KiB
JavaScript
314 lines
11 KiB
JavaScript
import { app } from "../../scripts/app.js";
|
|
import { api } from "../../scripts/api.js";
|
|
|
|
function imageDataToUrl(data) {
|
|
return api.apiURL(`/view?filename=${encodeURIComponent(data.filename)}&type=${data.type}&subfolder=${data.subfolder}${app.getPreviewFormatParam()}${app.getRandParam()}`);
|
|
}
|
|
|
|
class ImageComparerWidget {
|
|
constructor(name, node) {
|
|
this.name = name;
|
|
this.type = 'custom';
|
|
this.hitAreas = {};
|
|
this.selected = [];
|
|
this._value = { images: [] };
|
|
this.node = node;
|
|
}
|
|
|
|
set value(v) {
|
|
let cleanedVal;
|
|
if (Array.isArray(v)) {
|
|
cleanedVal = v.map((d, i) => {
|
|
if (!d || typeof d === "string") {
|
|
d = { url: d, name: i == 0 ? "A" : "B", selected: true };
|
|
}
|
|
return d;
|
|
});
|
|
} else {
|
|
cleanedVal = v.images || [];
|
|
}
|
|
|
|
if (cleanedVal.length > 2) {
|
|
const hasAAndB = cleanedVal.some((i) => i.name.startsWith("A")) &&
|
|
cleanedVal.some((i) => i.name.startsWith("B"));
|
|
if (!hasAAndB) {
|
|
cleanedVal = [cleanedVal[0], cleanedVal[1]];
|
|
}
|
|
}
|
|
|
|
let selected = cleanedVal.filter((d) => d.selected);
|
|
if (!selected.length && cleanedVal.length) {
|
|
cleanedVal[0].selected = true;
|
|
}
|
|
|
|
selected = cleanedVal.filter((d) => d.selected);
|
|
if (selected.length === 1 && cleanedVal.length > 1) {
|
|
cleanedVal.find((d) => !d.selected).selected = true;
|
|
}
|
|
|
|
this._value.images = cleanedVal;
|
|
selected = cleanedVal.filter((d) => d.selected);
|
|
this.setSelected(selected);
|
|
}
|
|
|
|
get value() {
|
|
return this._value;
|
|
}
|
|
|
|
setSelected(selected) {
|
|
this._value.images.forEach((d) => (d.selected = false));
|
|
this.node.imgs.length = 0;
|
|
|
|
for (const sel of selected) {
|
|
if (!sel.img) {
|
|
sel.img = new Image();
|
|
sel.img.src = sel.url;
|
|
sel.img.onload = () => {
|
|
this.node.setDirtyCanvas(true);
|
|
};
|
|
this.node.imgs.push(sel.img);
|
|
}
|
|
sel.selected = true;
|
|
}
|
|
this.selected = selected;
|
|
}
|
|
|
|
draw(ctx, node, width, y) {
|
|
this.hitAreas = {};
|
|
|
|
// Draw selector buttons if multiple images
|
|
if (this.value.images.length > 2) {
|
|
ctx.textAlign = "left";
|
|
ctx.textBaseline = "top";
|
|
ctx.font = `14px Arial`;
|
|
const drawData = [];
|
|
const spacing = 5;
|
|
let x = 0;
|
|
|
|
for (const img of this.value.images) {
|
|
const textWidth = ctx.measureText(img.name).width;
|
|
drawData.push({
|
|
img,
|
|
text: img.name,
|
|
x,
|
|
width: textWidth,
|
|
});
|
|
x += textWidth + spacing;
|
|
}
|
|
|
|
x = (node.size[0] - (x - spacing)) / 2;
|
|
for (const d of drawData) {
|
|
ctx.fillStyle = d.img.selected ? "rgba(180, 180, 180, 1)" : "rgba(180, 180, 180, 0.5)";
|
|
ctx.fillText(d.text, x, y);
|
|
this.hitAreas[d.text] = {
|
|
bounds: [x, y, d.width, 14],
|
|
data: d.img,
|
|
onDown: this.onSelectionDown.bind(this),
|
|
};
|
|
x += d.width + spacing;
|
|
}
|
|
y += 20;
|
|
}
|
|
|
|
// Draw images
|
|
if (node.properties?.["comparer_mode"] === "Click") {
|
|
this.drawImage(ctx, this.selected[node.isPointerDown ? 1 : 0], node, y);
|
|
} else {
|
|
// Slide mode - draw base image
|
|
this.drawImage(ctx, this.selected[0], node, y);
|
|
// Draw overlay image if hovering
|
|
if (node.isPointerOver && this.selected[1]) {
|
|
this.drawImage(ctx, this.selected[1], node, y, node.pointerOverPos[0]);
|
|
}
|
|
}
|
|
}
|
|
|
|
onSelectionDown(event, pos, node, bounds) {
|
|
const selected = [...this.selected];
|
|
if (bounds?.data.name.startsWith("A")) {
|
|
selected[0] = bounds.data;
|
|
} else if (bounds?.data.name.startsWith("B")) {
|
|
selected[1] = bounds.data;
|
|
}
|
|
this.setSelected(selected);
|
|
node.setDirtyCanvas(true);
|
|
}
|
|
|
|
drawImage(ctx, image, node, y, cropX) {
|
|
if (!image?.img?.naturalWidth || !image?.img?.naturalHeight) {
|
|
return;
|
|
}
|
|
|
|
let [nodeWidth, nodeHeight] = node.size;
|
|
const imageAspect = image.img.naturalWidth / image.img.naturalHeight;
|
|
let height = nodeHeight - y;
|
|
const widgetAspect = nodeWidth / height;
|
|
let targetWidth, targetHeight;
|
|
let offsetX = 0;
|
|
|
|
if (imageAspect > widgetAspect) {
|
|
targetWidth = nodeWidth;
|
|
targetHeight = nodeWidth / imageAspect;
|
|
} else {
|
|
targetHeight = height;
|
|
targetWidth = height * imageAspect;
|
|
offsetX = (nodeWidth - targetWidth) / 2;
|
|
}
|
|
|
|
const widthMultiplier = image.img.naturalWidth / targetWidth;
|
|
const sourceX = 0;
|
|
const sourceY = 0;
|
|
const sourceWidth = cropX != null ? (cropX - offsetX) * widthMultiplier : image.img.naturalWidth;
|
|
const sourceHeight = image.img.naturalHeight;
|
|
const destX = (nodeWidth - targetWidth) / 2;
|
|
const destY = y + (height - targetHeight) / 2;
|
|
const destWidth = cropX != null ? cropX - offsetX : targetWidth;
|
|
const destHeight = targetHeight;
|
|
|
|
ctx.save();
|
|
ctx.beginPath();
|
|
|
|
if (cropX) {
|
|
ctx.rect(destX, destY, destWidth, destHeight);
|
|
ctx.clip();
|
|
}
|
|
|
|
ctx.drawImage(image.img, sourceX, sourceY, sourceWidth, sourceHeight, destX, destY, destWidth, destHeight);
|
|
|
|
if (cropX != null && cropX >= destX && cropX <= destX + targetWidth) {
|
|
ctx.beginPath();
|
|
ctx.moveTo(cropX, destY);
|
|
ctx.lineTo(cropX, destY + destHeight);
|
|
ctx.globalCompositeOperation = "difference";
|
|
ctx.strokeStyle = "rgba(255,255,255, 1)";
|
|
ctx.lineWidth = 2;
|
|
ctx.stroke();
|
|
}
|
|
|
|
ctx.restore();
|
|
}
|
|
|
|
computeSize(width) {
|
|
return [width, Math.max(200, width * 0.75)];
|
|
}
|
|
|
|
serializeValue() {
|
|
const v = [];
|
|
for (const data of this._value.images) {
|
|
const d = { ...data };
|
|
delete d.img;
|
|
v.push(d);
|
|
}
|
|
return { images: v };
|
|
}
|
|
}
|
|
|
|
app.registerExtension({
|
|
name: "llmtoolkit.ImageComparer",
|
|
async beforeRegisterNodeDef(nodeType, nodeData) {
|
|
if (nodeData.name === "ImageComparer") {
|
|
const origOnExecuted = nodeType.prototype.onExecuted;
|
|
|
|
nodeType.prototype.onNodeCreated = function() {
|
|
this.imageIndex = 0;
|
|
this.imgs = [];
|
|
this.serialize_widgets = true;
|
|
this.isPointerDown = false;
|
|
this.isPointerOver = false;
|
|
this.pointerOverPos = [0, 0];
|
|
this.properties = this.properties || {};
|
|
this.properties["comparer_mode"] = "Slide";
|
|
|
|
this.canvasWidget = this.addCustomWidget(new ImageComparerWidget("llmtoolkit_comparer", this));
|
|
this.setSize(this.computeSize());
|
|
this.setDirtyCanvas(true, true);
|
|
};
|
|
|
|
nodeType.prototype.onExecuted = function(output) {
|
|
if (origOnExecuted) {
|
|
origOnExecuted.apply(this, arguments);
|
|
}
|
|
|
|
if (!this.canvasWidget) return;
|
|
|
|
if ("images" in output) {
|
|
this.canvasWidget.value = {
|
|
images: (output.images || []).map((d, i) => {
|
|
return {
|
|
name: i === 0 ? "A" : "B",
|
|
selected: true,
|
|
url: imageDataToUrl(d),
|
|
};
|
|
}),
|
|
};
|
|
} else {
|
|
output.a_images = output.a_images || [];
|
|
output.b_images = output.b_images || [];
|
|
const imagesToChoose = [];
|
|
const multiple = output.a_images.length + output.b_images.length > 2;
|
|
|
|
for (const [i, d] of output.a_images.entries()) {
|
|
imagesToChoose.push({
|
|
name: output.a_images.length > 1 || multiple ? `A${i + 1}` : "A",
|
|
selected: i === 0,
|
|
url: imageDataToUrl(d),
|
|
});
|
|
}
|
|
|
|
for (const [i, d] of output.b_images.entries()) {
|
|
imagesToChoose.push({
|
|
name: output.b_images.length > 1 || multiple ? `B${i + 1}` : "B",
|
|
selected: i === 0,
|
|
url: imageDataToUrl(d),
|
|
});
|
|
}
|
|
|
|
this.canvasWidget.value = { images: imagesToChoose };
|
|
}
|
|
|
|
this.setDirtyCanvas(true);
|
|
};
|
|
|
|
nodeType.prototype.setIsPointerDown = function(down = this.isPointerDown) {
|
|
const newIsDown = down && !!app.canvas.pointer_is_down;
|
|
if (this.isPointerDown !== newIsDown) {
|
|
this.isPointerDown = newIsDown;
|
|
this.setDirtyCanvas(true, false);
|
|
}
|
|
this.imageIndex = this.isPointerDown ? 1 : 0;
|
|
if (this.isPointerDown) {
|
|
requestAnimationFrame(() => {
|
|
this.setIsPointerDown();
|
|
});
|
|
}
|
|
};
|
|
|
|
nodeType.prototype.onMouseDown = function(event, pos, canvas) {
|
|
this.setIsPointerDown(true);
|
|
return false;
|
|
};
|
|
|
|
nodeType.prototype.onMouseEnter = function(event) {
|
|
this.setIsPointerDown(!!app.canvas.pointer_is_down);
|
|
this.isPointerOver = true;
|
|
};
|
|
|
|
nodeType.prototype.onMouseLeave = function(event) {
|
|
this.setIsPointerDown(false);
|
|
this.isPointerOver = false;
|
|
};
|
|
|
|
nodeType.prototype.onMouseMove = function(event, pos, canvas) {
|
|
this.pointerOverPos = [...pos];
|
|
this.imageIndex = this.pointerOverPos[0] > this.size[0] / 2 ? 1 : 0;
|
|
this.setDirtyCanvas(true, false);
|
|
};
|
|
|
|
// Add property configuration
|
|
nodeType["@comparer_mode"] = {
|
|
type: "combo",
|
|
values: ["Slide", "Click"],
|
|
};
|
|
}
|
|
},
|
|
}); |