Files
Hullabalo-ComfyUI-Loop/js/loop_CropImage.js
T
Hullabaloo 075932d3f6 Add files via upload
added js code for Crop Image node
2025-04-30 22:37:28 +02:00

475 lines
19 KiB
JavaScript

import { app } from "../../../scripts/app.js";
import { $el } from "../../../scripts/ui.js";
import { ComfyWidgets } from "../../scripts/widgets.js";
app.registerExtension({
name: "loop.clickCrop",
async beforeRegisterNodeDef(nodeType, nodeData, app) {
if (nodeType.comfyClass !== "ImageCropLoop") return;
const DEFAULT_DIMENSIONS = {
WIDTH: 200,
HEIGHT: 200,
MARGIN: 0,
TITLE_HEIGHT: 24
};
class CoordinateConverter {
/* Core class that handles coordinate transformations */
constructor(originalDimensions) {
this.originalDimensions = originalDimensions;
this.previewDimensions = { width: 0, height: 0 };
}
toOriginalCoords(previewX, previewY, frame) {
/* Converts preview coordinates to original image coordinates */
if (!this.isValidDimensions()) return [previewX, previewY];
const scaleX = this.originalDimensions.width / frame.width;
const scaleY = this.originalDimensions.height / frame.height;
return [
Math.round(previewX * scaleX),
Math.round(previewY * scaleY)
];
}
toPreviewCoords(originalX, originalY, frame) {
/* Converts original coordinates to preview coordinates */
if (!this.isValidDimensions()) return [originalX, originalY];
const scaleX = frame.width / this.originalDimensions.width;
const scaleY = frame.height / this.originalDimensions.height;
return [
Math.round(originalX * scaleX),
Math.round(originalY * scaleY)
];
}
toOriginalSize(previewSize, frame) {
/* Converts preview size to original image size */
if (!this.isValidDimensions()) return previewSize;
return Math.round(previewSize * (this.originalDimensions.width / frame.width));
}
toPreviewSize(originalSize, frame) {
/* Converts original size to preview size */
if (!this.isValidDimensions()) return originalSize;
// First, ensure originalSize doesn't exceed original image dimensions
const maxOriginalSize = Math.min(originalSize, this.originalDimensions.width);
return Math.round(maxOriginalSize * (frame.width / this.originalDimensions.width));
}
constrainOriginalCoords(x, y, size) {
/* Ensures coordinates stay within image bounds */
if (!this.isValidDimensions()) return [x, y];
// Ensure size doesn't exceed original dimensions
const constrainedSize = Math.min(size, this.originalDimensions.width);
const maxX = Math.max(0, this.originalDimensions.width - constrainedSize);
const maxY = Math.max(0, this.originalDimensions.height - constrainedSize);
return [
Math.max(0, Math.min(x, maxX)),
Math.max(0, Math.min(y, maxY))
];
}
isValidDimensions() {
return this.originalDimensions?.width && this.originalDimensions?.height;
}
updateDimensions(originalWidth, originalHeight, previewWidth, previewHeight) {
this.originalDimensions = { width: originalWidth, height: originalHeight };
this.previewDimensions = { width: previewWidth, height: previewHeight };
}
}
// Define ClickCropWidget class inside the extension scope
class ClickCropWidget {
/* Handles the visual interface */
constructor(node) {
this.node = node;
this.type = "HTML";
this.name = "clickableFrame";
this.value = null;
// this.originalOnMouseEnter = node.onMouseEnter;
// this.originalOnMouseLeave = node.onMouseLeave;
// node.onMouseEnter = (e, pos) => {
// if (this.node) {
// this.node.flags = this.node.flags || {};
// this.node.flags.pinned = true;
// app.graph.setDirtyCanvas(true, false);
// }
// if (this.originalOnMouseEnter) {
// this.originalOnMouseEnter.call(node, e, pos);
// }
// };
// node.onMouseLeave = (e, pos) => {
// if (this.node) {
// this.node.flags = this.node.flags || {};
// this.node.flags.pinned = false;
// app.graph.setDirtyCanvas(true, false);
// }
// if (this.originalOnMouseLeave) {
// this.originalOnMouseLeave.call(node, e, pos);
// }
// };
}
draw(ctx, node, widget_width, y, widget_height) {
/* Main rendering function for the widget */
if (!this.isValidImage(node)) return;
this.updateOriginalDimensions(node);
const frame = this.calculateFrame(y, node.size[0], node.size[1]);
node.frameArea = frame;
this.drawFrame(ctx, frame);
this.drawMarker(ctx, node, frame);
}
isValidImage(node) {
return node.imgs?.[0]?.width && node.imgs?.[0]?.height;
}
updateOriginalDimensions(node) {
const img = node.imgs[0];
const match = img.src.match(/_(\d+)_(\d+)_/);
if (match) {
node.converter.updateDimensions(parseInt(match[1]), parseInt(match[2]));
this.updateMarkerPosition(node);
}
}
calculateFrame(y, nodeWidth, nodeHeight) {
/* Determines the frame dimensions and position */
const TITLE_HEIGHT = 24;
const img = this.node.imgs[0];
const availWidth = nodeWidth;
const availHeight = nodeHeight - TITLE_HEIGHT - y;
const imgRatio = img.naturalWidth / img.naturalHeight;
const containerRatio = availWidth / availHeight;
let frameWidth, frameHeight;
if (containerRatio > imgRatio) {
frameHeight = availHeight;
frameWidth = frameHeight * imgRatio;
} else {
frameWidth = availWidth;
frameHeight = frameWidth / imgRatio;
}
return {
left: (availWidth - frameWidth) / 2,
top: y + TITLE_HEIGHT + (availHeight - frameHeight) / 2,
width: frameWidth,
height: frameHeight
};
}
drawFrame(ctx, frame) {
/* Renders the frame border */
// ctx.strokeStyle = "#ff7500";
ctx.strokeStyle = "green";
ctx.lineWidth = 1;
ctx.strokeRect(frame.left, frame.top, frame.width, frame.height);
}
drawMarker(ctx, node, frame) {
/* Renders the selection marker */
if (!node.clickMarker) return;
const previewSize = node.converter.toPreviewSize(
node.widgets.find(w => w.name === 'size').value,
frame
);
ctx.strokeStyle = "green";
ctx.lineWidth = 1;
ctx.strokeRect(
node.clickMarker.x,
node.clickMarker.y,
previewSize,
previewSize
);
}
updateMarkerPosition(node) {
/* Updates marker position when coordinates change */
const frame = node.frameArea;
if (!node.clickMarker || !frame) return;
const xWidget = node.widgets.find(w => w.name === 'x');
const yWidget = node.widgets.find(w => w.name === 'y');
const [newPreviewX, newPreviewY] = node.converter.toPreviewCoords(
xWidget.value,
yWidget.value,
frame
);
node.clickMarker.x = frame.left + newPreviewX;
node.clickMarker.y = frame.top + newPreviewY;
}
}
nodeType.prototype.setupNode = function() {
/* Initializes the node with default values and widgets */
this.converter = new CoordinateConverter({ width: 0, height: 0 });
this.size = [DEFAULT_DIMENSIONS.WIDTH, DEFAULT_DIMENSIONS.HEIGHT];
const widgets = {
x: this.widgets.find(w => w.name === 'x'),
y: this.widgets.find(w => w.name === 'y'),
size: this.widgets.find(w => w.name === 'size')
};
this.setupWidgetCallbacks(widgets);
this.addCustomWidget(new ClickCropWidget(this));
this.setupMouseHandler(widgets);
this.serialize_widgets = false;
};
nodeType.prototype.initializeMarker = function(frame, widgets) {
/* Initializes the marker with default values and widgets */
if (!frame) return;
const { x: xWidget, y: yWidget, size: sizeWidget } = widgets;
// Apply initial constraints
const [constrainedX, constrainedY] = this.converter.constrainOriginalCoords(
xWidget.value,
yWidget.value,
sizeWidget.value
);
// Update widget values
xWidget.value = constrainedX;
yWidget.value = constrainedY;
// Convert to preview coordinates
const [previewX, previewY] = this.converter.toPreviewCoords(
constrainedX,
constrainedY,
frame
);
// Create marker
this.clickMarker = {
x: frame.left + previewX,
y: frame.top + previewY
};
this.setDirtyCanvas(true);
};
nodeType.prototype.setupWidgetCallbacks = function(widgets) {
/* Sets up event handlers for widget value changes */
const { x: xWidget, y: yWidget, size: sizeWidget } = widgets;
// Size widget callback with dynamic coordinate updates
sizeWidget.callback = function(value) {
const frame = this.frameArea;
if (!frame) return;
// Constrain size to original image dimensions
const maxSize = Math.min(this.converter.originalDimensions.width, this.converter.originalDimensions.height);
const constrainedValue = Math.min(value, maxSize);
// Round to nearest multiple of 8
const roundedValue = Math.round(constrainedValue / 8) * 8;
if (value !== roundedValue) {
sizeWidget.value = roundedValue;
this.onSizeChange(roundedValue, frame, widgets);
return;
}
if (!this.clickMarker) {
this.initializeMarker(frame, widgets);
} else {
this.onSizeChange(roundedValue, frame, widgets);
}
}.bind(this);
// X and Y widget callbacks with floor to ensure integer values
const createAutoMarkerCallback = (axis) => {
/* Creates handler for marker changes */
return function(value) {
const frame = this.frameArea;
if (!frame) return;
// Ensure integer values
const intValue = Math.floor(value);
if (value !== intValue) {
if (axis === 'x') {
xWidget.value = intValue;
} else {
yWidget.value = intValue;
}
value = intValue;
}
if (!this.clickMarker) {
this.initializeMarker(frame, widgets);
} else {
this.createCoordinateCallback(axis, xWidget, yWidget, sizeWidget).call(this, value);
}
}.bind(this);
};
xWidget.callback = createAutoMarkerCallback('x');
yWidget.callback = createAutoMarkerCallback('y');
};
nodeType.prototype.onSizeChange = function(newSize, frame, widgets) {
/* Handles size change events */
const { x: xWidget, y: yWidget, size: sizeWidget } = widgets;
if (!frame || !this.converter.isValidDimensions()) return;
// Get current original coordinates
let currentX = Math.floor(xWidget.value);
let currentY = Math.floor(yWidget.value);
// Apply constraints with new size
const [constrainedX, constrainedY] = this.converter.constrainOriginalCoords(
currentX,
currentY,
newSize
);
// Update widget values if they were constrained
if (currentX !== constrainedX || currentY !== constrainedY) {
xWidget.value = constrainedX;
yWidget.value = constrainedY;
}
// Convert to preview coordinates and update marker
const [previewX, previewY] = this.converter.toPreviewCoords(
constrainedX,
constrainedY,
frame
);
this.clickMarker = {
x: frame.left + previewX,
y: frame.top + previewY
};
this.setDirtyCanvas(true);
};
nodeType.prototype.createCoordinateCallback = function(axis, xWidget, yWidget, sizeWidget) {
/* Creates handlers for coordinate changes */
return function(value) {
const frame = this.frameArea;
if (!frame || !this.clickMarker) return;
// Ensure integer values for coordinates
const currentCoords = axis === 'x'
? [Math.floor(value), Math.floor(yWidget.value)]
: [Math.floor(xWidget.value), Math.floor(value)];
const [constrainedX, constrainedY] = this.converter.constrainOriginalCoords(
...currentCoords,
sizeWidget.value
);
const updatedValue = axis === 'x' ? constrainedX : constrainedY;
if (value !== updatedValue) {
if (axis === 'x') xWidget.value = updatedValue;
else yWidget.value = updatedValue;
return;
}
const [previewX, previewY] = this.converter.toPreviewCoords(
axis === 'x' ? constrainedX : xWidget.value,
axis === 'y' ? constrainedY : yWidget.value,
frame
);
const markerSize = this.converter.toPreviewSize(sizeWidget.value, frame);
this.clickMarker.x = frame.left + (axis === 'x' ? previewX : this.clickMarker.x - frame.left);
this.clickMarker.y = frame.top + (axis === 'y' ? previewY : this.clickMarker.y - frame.top);
this.setDirtyCanvas(true);
}.bind(this);
};
nodeType.prototype.createSizeCallback = function(xWidget, yWidget, sizeWidget) {
/* Creates handler for size changes */
return function(value) {
const frame = this.frameArea;
if (!frame || !this.clickMarker) return;
// Get current original coordinates
const [origX, origY] = this.converter.toOriginalCoords(
this.clickMarker.x - frame.left,
this.clickMarker.y - frame.top,
frame
);
// Constrain coordinates with new size value
const [constrainedX, constrainedY] = this.converter.constrainOriginalCoords(origX, origY, value);
// Update x and y widgets if they were constrained
if (xWidget.value !== constrainedX) xWidget.value = constrainedX;
if (yWidget.value !== constrainedY) yWidget.value = constrainedY;
// Convert back to preview coordinates
const [previewX, previewY] = this.converter.toPreviewCoords(constrainedX, constrainedY, frame);
// Update marker position
this.clickMarker.x = frame.left + previewX;
this.clickMarker.y = frame.top + previewY;
this.setDirtyCanvas(true);
}.bind(this);
};
nodeType.prototype.setupMouseHandler = function(widgets) {
/* Handles mouse interactions for selection */
this.onMouseDown = (e, pos) => {
const frame = this.frameArea;
if (!frame ||
pos[0] < frame.left || pos[0] > frame.left + frame.width ||
pos[1] < frame.top || pos[1] > frame.top + frame.height) return;
const { x: xWidget, y: yWidget, size: sizeWidget } = widgets;
const relativeX = pos[0] - frame.left;
const relativeY = pos[1] - frame.top;
const previewSize = this.converter.toPreviewSize(sizeWidget.value, frame);
const markerX = Math.max(0, Math.min(relativeX - previewSize / 2, frame.width - previewSize));
const markerY = Math.max(0, Math.min(relativeY - previewSize / 2, frame.height - previewSize));
const [origX, origY] = this.converter.toOriginalCoords(markerX, markerY, frame);
const [constrainedX, constrainedY] = this.converter.constrainOriginalCoords(origX, origY, sizeWidget.value);
xWidget.value = constrainedX;
yWidget.value = constrainedY;
const [previewX, previewY] = this.converter.toPreviewCoords(constrainedX, constrainedY, frame);
this.clickMarker = { x: frame.left + previewX, y: frame.top + previewY };
this.setDirtyCanvas(true);
};
};
const onNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function() {
onNodeCreated?.apply(this, arguments);
this.setupNode();
};
}
});