mirror of
https://github.com/justUmen/Bjornulf_custom_nodes.git
synced 2026-03-21 12:42:11 -03:00
315 lines
9.3 KiB
JavaScript
315 lines
9.3 KiB
JavaScript
import { app } from "../../../scripts/app.js";
|
|
import { api } from "../../../scripts/api.js";
|
|
|
|
// Helper function to construct image URLs for ComfyUI
|
|
function imageDataToUrl(data) {
|
|
return api.apiURL(
|
|
`/view?filename=${encodeURIComponent(data.filename)}&type=${
|
|
data.type
|
|
}&subfolder=${data.subfolder}` + app.getPreviewFormatParam()
|
|
);
|
|
}
|
|
|
|
app.registerExtension({
|
|
name: "Bjornulf.FourImageViewer",
|
|
async nodeCreated(node) {
|
|
// Ensure this applies only to the specific node type
|
|
if (node.comfyClass !== "Bjornulf_FourImageViewer") return;
|
|
|
|
// Constants for layout and sizing
|
|
const marginTop = 90; // Space at the top for node UI elements
|
|
const verticalOffset = -30; // Adjustment for canvas positioning
|
|
const minSize = 256; // Minimum size of the node
|
|
const maxSize = 2048; // Maximum size of the node
|
|
const padding = 10; // Padding around the image area
|
|
|
|
// Initialize node properties
|
|
node.size = [512, 512 + marginTop];
|
|
node.data = {
|
|
images: new Array(4).fill(null), // Array to hold up to 4 images
|
|
sliderX: null, // X position of the vertical slider
|
|
sliderY: null, // Y position of the horizontal slider
|
|
};
|
|
|
|
// Enable resizing
|
|
node.flags |= LiteGraph.RESIZABLE;
|
|
|
|
// Handle resizing to maintain square aspect ratio
|
|
node.onResize = function (size) {
|
|
const minDimension = Math.max(
|
|
minSize,
|
|
Math.min(size[0], size[1] - marginTop)
|
|
);
|
|
const maxDimension = Math.min(maxSize, minDimension);
|
|
size[0] = maxDimension;
|
|
size[1] = maxDimension + marginTop;
|
|
|
|
const fullImgWidth = size[0] - 3 * padding;
|
|
const fullImgHeight = size[1] - padding - marginTop;
|
|
|
|
// Update slider positions proportionally
|
|
if (node.data.hasOwnProperty("sliderX")) {
|
|
const oldWidth = node.size[0] - 3 * padding;
|
|
const oldHeight = node.size[1] - padding - marginTop;
|
|
const relativeX = (node.data.sliderX - padding) / oldWidth;
|
|
const relativeY = (node.data.sliderY - marginTop) / oldHeight;
|
|
node.data.sliderX = padding + fullImgWidth * relativeX;
|
|
node.data.sliderY = marginTop + fullImgHeight * relativeY;
|
|
} else {
|
|
node.data.sliderX = padding + fullImgWidth / 2;
|
|
node.data.sliderY = marginTop + fullImgHeight / 2;
|
|
}
|
|
|
|
node.size = size;
|
|
return size;
|
|
};
|
|
|
|
// Handle mouse down to move sliders
|
|
node.onMouseDown = function (e) {
|
|
const rect = node.getBounding();
|
|
const [clickX, clickY] = [
|
|
e.canvasX - rect[0],
|
|
e.canvasY - rect[1] + verticalOffset,
|
|
];
|
|
|
|
const xStart = padding;
|
|
const xEnd = xStart + (node.size[0] - 3 * padding);
|
|
const yStart = marginTop;
|
|
const yEnd = yStart + (node.size[1] - padding - marginTop);
|
|
|
|
if (
|
|
clickX >= xStart &&
|
|
clickX <= xEnd &&
|
|
clickY >= yStart &&
|
|
clickY <= yEnd
|
|
) {
|
|
const hasImage2 = node.data.images[1] !== null;
|
|
const hasImage3 = node.data.images[2] !== null;
|
|
const hasImage4 = node.data.images[3] !== null;
|
|
|
|
// Lock sliderY to bottom if only two images are present
|
|
if (hasImage2 && !hasImage3 && !hasImage4) {
|
|
node.data.sliderY = yEnd;
|
|
}
|
|
|
|
node.data.sliderX = Math.max(xStart, Math.min(clickX, xEnd));
|
|
node.data.sliderY =
|
|
hasImage3 || hasImage4
|
|
? Math.max(yStart, Math.min(clickY, yEnd))
|
|
: yEnd;
|
|
|
|
app.graph.setDirtyCanvas(true, true);
|
|
return true;
|
|
}
|
|
return false;
|
|
};
|
|
|
|
// Load images when the node is executed
|
|
node.onExecuted = async function (message) {
|
|
node.data.images = new Array(4).fill(null);
|
|
for (let i = 1; i <= 4; i++) {
|
|
const images = message[`images_${i}`] || [];
|
|
if (images.length) {
|
|
const imgData = images[0];
|
|
const img = new Image();
|
|
img.src = imageDataToUrl(imgData);
|
|
await new Promise((resolve) => (img.onload = img.onerror = resolve));
|
|
node.data.images[i - 1] = img;
|
|
}
|
|
}
|
|
app.graph.setDirtyCanvas(true, true);
|
|
};
|
|
|
|
// Render images and sliders
|
|
node.onDrawForeground = function (ctx) {
|
|
const xStart = padding;
|
|
const xEnd = xStart + (node.size[0] - 3 * padding);
|
|
const yStart = marginTop;
|
|
const yEnd = yStart + (node.size[1] - padding - marginTop);
|
|
const fullImgWidth = node.size[0] - 3 * padding;
|
|
const fullImgHeight = node.size[1] - padding - marginTop;
|
|
|
|
// Calculate fitted rectangle for image display
|
|
function getFittedDestRect(dx, dy, dWidth, dHeight, targetRatio) {
|
|
let newWidth = dWidth;
|
|
let newHeight = dWidth / targetRatio;
|
|
if (newHeight > dHeight) {
|
|
newHeight = dHeight;
|
|
newWidth = dHeight * targetRatio;
|
|
}
|
|
const offsetX = dx + (dWidth - newWidth) / 2;
|
|
const offsetY = dy + (dHeight - newHeight) / 2;
|
|
return [offsetX, offsetY, newWidth, newHeight];
|
|
}
|
|
|
|
// Draw a cropped image within specified bounds
|
|
function drawCroppedImage(img, dx, dy, dWidth, dHeight) {
|
|
if (!img) return;
|
|
let targetRatio = dWidth / dHeight;
|
|
if (
|
|
node.data.images[0] &&
|
|
node.data.images[0].naturalWidth &&
|
|
node.data.images[0].naturalHeight
|
|
) {
|
|
targetRatio =
|
|
node.data.images[0].naturalWidth /
|
|
node.data.images[0].naturalHeight;
|
|
}
|
|
|
|
const [ndx, ndy, ndWidth, ndHeight] = getFittedDestRect(
|
|
dx,
|
|
dy,
|
|
dWidth,
|
|
dHeight,
|
|
targetRatio
|
|
);
|
|
|
|
const imgRatio = img.naturalWidth / img.naturalHeight;
|
|
let sx = 0,
|
|
sy = 0,
|
|
sWidth = img.naturalWidth,
|
|
sHeight = img.naturalHeight;
|
|
if (imgRatio > targetRatio) {
|
|
sWidth = img.naturalHeight * targetRatio;
|
|
sx = (img.naturalWidth - sWidth) / 2;
|
|
} else if (imgRatio < targetRatio) {
|
|
sHeight = img.naturalWidth / targetRatio;
|
|
sy = (img.naturalHeight - sHeight) / 2;
|
|
}
|
|
ctx.drawImage(
|
|
img,
|
|
sx,
|
|
sy,
|
|
sWidth,
|
|
sHeight,
|
|
ndx,
|
|
ndy,
|
|
ndWidth,
|
|
ndHeight
|
|
);
|
|
}
|
|
|
|
const connectedImages = node.data.images
|
|
.slice(1)
|
|
.filter((img) => img !== null).length;
|
|
|
|
if (connectedImages === 0) {
|
|
// Single image display
|
|
if (node.data.images[0]) {
|
|
drawCroppedImage(
|
|
node.data.images[0],
|
|
xStart,
|
|
yStart,
|
|
fullImgWidth,
|
|
fullImgHeight
|
|
);
|
|
}
|
|
} else if (connectedImages === 1 && node.data.images[1]) {
|
|
// Two images with vertical split
|
|
const splitX = node.data.sliderX;
|
|
ctx.save();
|
|
ctx.beginPath();
|
|
ctx.rect(xStart, yStart, splitX - xStart, fullImgHeight);
|
|
ctx.clip();
|
|
drawCroppedImage(
|
|
node.data.images[0],
|
|
xStart,
|
|
yStart,
|
|
fullImgWidth,
|
|
fullImgHeight
|
|
);
|
|
ctx.restore();
|
|
|
|
ctx.save();
|
|
ctx.beginPath();
|
|
ctx.rect(splitX, yStart, xEnd - splitX, fullImgHeight);
|
|
ctx.clip();
|
|
drawCroppedImage(
|
|
node.data.images[1],
|
|
xStart,
|
|
yStart,
|
|
fullImgWidth,
|
|
fullImgHeight
|
|
);
|
|
ctx.restore();
|
|
} else {
|
|
// Three or four images with quadrants
|
|
const drawQuadrant = (imgIndex, clipX, clipY, clipW, clipH) => {
|
|
if (!node.data.images[imgIndex]) return;
|
|
ctx.save();
|
|
ctx.beginPath();
|
|
ctx.rect(clipX, clipY, clipW, clipH);
|
|
ctx.clip();
|
|
drawCroppedImage(
|
|
node.data.images[imgIndex],
|
|
xStart,
|
|
yStart,
|
|
fullImgWidth,
|
|
fullImgHeight
|
|
);
|
|
ctx.restore();
|
|
};
|
|
|
|
drawQuadrant(
|
|
0,
|
|
xStart,
|
|
yStart,
|
|
node.data.sliderX - xStart,
|
|
node.data.sliderY - yStart
|
|
);
|
|
drawQuadrant(
|
|
1,
|
|
node.data.sliderX,
|
|
yStart,
|
|
xEnd - node.data.sliderX,
|
|
node.data.sliderY - yStart
|
|
);
|
|
|
|
if (node.data.images[3] === null) {
|
|
drawQuadrant(
|
|
2,
|
|
xStart,
|
|
node.data.sliderY,
|
|
xEnd - xStart,
|
|
yEnd - node.data.sliderY
|
|
);
|
|
} else {
|
|
drawQuadrant(
|
|
2,
|
|
xStart,
|
|
node.data.sliderY,
|
|
node.data.sliderX - xStart,
|
|
yEnd - node.data.sliderY
|
|
);
|
|
drawQuadrant(
|
|
3,
|
|
node.data.sliderX,
|
|
node.data.sliderY,
|
|
xEnd - node.data.sliderX,
|
|
yEnd - node.data.sliderY
|
|
);
|
|
}
|
|
}
|
|
|
|
// Draw sliders
|
|
ctx.strokeStyle = "#FFF";
|
|
ctx.lineWidth = 1;
|
|
if (connectedImages > 0) {
|
|
ctx.beginPath();
|
|
if (node.data.images[3] === null && node.data.images[2] !== null) {
|
|
ctx.moveTo(node.data.sliderX, yStart);
|
|
ctx.lineTo(node.data.sliderX, node.data.sliderY);
|
|
} else {
|
|
ctx.moveTo(node.data.sliderX, yStart);
|
|
ctx.lineTo(node.data.sliderX, yEnd);
|
|
}
|
|
if (connectedImages >= 2) {
|
|
ctx.moveTo(xStart, node.data.sliderY);
|
|
ctx.lineTo(xEnd, node.data.sliderY);
|
|
}
|
|
ctx.stroke();
|
|
}
|
|
};
|
|
},
|
|
});
|