mirror of
https://github.com/comfyanonymous/ComfyUI.git
synced 2025-04-20 03:13:30 +00:00
Animated image output support (#2008)
* Refactor multiline widget into generic DOM widget * wip webp preview * webp support * fix check * fix sizing * show image when zoomed out * Swap webp checkto generic animated image flag * remove duplicate * Fix falsy check
This commit is contained in:
parent
ce67dcbcda
commit
6ff06fa796
@ -4,7 +4,10 @@ import { ComfyUI, $el } from "./ui.js";
|
|||||||
import { api } from "./api.js";
|
import { api } from "./api.js";
|
||||||
import { defaultGraph } from "./defaultGraph.js";
|
import { defaultGraph } from "./defaultGraph.js";
|
||||||
import { getPngMetadata, getWebpMetadata, importA1111, getLatentMetadata } from "./pnginfo.js";
|
import { getPngMetadata, getWebpMetadata, importA1111, getLatentMetadata } from "./pnginfo.js";
|
||||||
|
import { addDomClippingSetting } from "./domWidget.js";
|
||||||
|
import { createImageHost, calculateImageGrid } from "./ui/imagePreview.js"
|
||||||
|
|
||||||
|
export const ANIM_PREVIEW_WIDGET = "$$comfy_animation_preview"
|
||||||
|
|
||||||
function sanitizeNodeName(string) {
|
function sanitizeNodeName(string) {
|
||||||
let entityMap = {
|
let entityMap = {
|
||||||
@ -405,7 +408,9 @@ export class ComfyApp {
|
|||||||
return shiftY;
|
return shiftY;
|
||||||
}
|
}
|
||||||
|
|
||||||
node.prototype.setSizeForImage = function () {
|
node.prototype.setSizeForImage = function (force) {
|
||||||
|
if(!force && this.animatedImages) return;
|
||||||
|
|
||||||
if (this.inputHeight) {
|
if (this.inputHeight) {
|
||||||
this.setSize(this.size);
|
this.setSize(this.size);
|
||||||
return;
|
return;
|
||||||
@ -422,13 +427,20 @@ export class ComfyApp {
|
|||||||
let imagesChanged = false
|
let imagesChanged = false
|
||||||
|
|
||||||
const output = app.nodeOutputs[this.id + ""];
|
const output = app.nodeOutputs[this.id + ""];
|
||||||
if (output && output.images) {
|
if (output?.images) {
|
||||||
|
this.animatedImages = output?.animated?.find(Boolean);
|
||||||
if (this.images !== output.images) {
|
if (this.images !== output.images) {
|
||||||
this.images = output.images;
|
this.images = output.images;
|
||||||
imagesChanged = true;
|
imagesChanged = true;
|
||||||
imgURLs = imgURLs.concat(output.images.map(params => {
|
imgURLs = imgURLs.concat(
|
||||||
return api.apiURL("/view?" + new URLSearchParams(params).toString() + app.getPreviewFormatParam());
|
output.images.map((params) => {
|
||||||
}))
|
return api.apiURL(
|
||||||
|
"/view?" +
|
||||||
|
new URLSearchParams(params).toString() +
|
||||||
|
(this.animatedImages ? "" : app.getPreviewFormatParam())
|
||||||
|
);
|
||||||
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -507,7 +519,34 @@ export class ComfyApp {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.imgs && this.imgs.length) {
|
if (this.imgs?.length) {
|
||||||
|
const widgetIdx = this.widgets?.findIndex((w) => w.name === ANIM_PREVIEW_WIDGET);
|
||||||
|
|
||||||
|
if(this.animatedImages) {
|
||||||
|
// Instead of using the canvas we'll use a IMG
|
||||||
|
if(widgetIdx > -1) {
|
||||||
|
// Replace content
|
||||||
|
const widget = this.widgets[widgetIdx];
|
||||||
|
widget.options.host.updateImages(this.imgs);
|
||||||
|
} else {
|
||||||
|
const host = createImageHost(this);
|
||||||
|
this.setSizeForImage(true);
|
||||||
|
const widget = this.addDOMWidget(ANIM_PREVIEW_WIDGET, "img", host.el, {
|
||||||
|
host,
|
||||||
|
getHeight: host.getHeight,
|
||||||
|
onDraw: host.onDraw,
|
||||||
|
hideOnZoom: false
|
||||||
|
});
|
||||||
|
widget.serializeValue = () => undefined;
|
||||||
|
widget.options.host.updateImages(this.imgs);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (widgetIdx > -1) {
|
||||||
|
this.widgets.splice(widgetIdx, 1);
|
||||||
|
}
|
||||||
|
|
||||||
const canvas = app.graph.list_of_graphcanvas[0];
|
const canvas = app.graph.list_of_graphcanvas[0];
|
||||||
const mouse = canvas.graph_mouse;
|
const mouse = canvas.graph_mouse;
|
||||||
if (!canvas.pointer_is_down && this.pointerDown) {
|
if (!canvas.pointer_is_down && this.pointerDown) {
|
||||||
@ -547,31 +586,7 @@ export class ComfyApp {
|
|||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
cell_padding = 0;
|
cell_padding = 0;
|
||||||
let best = 0;
|
({ cellWidth, cellHeight, cols, shiftX } = calculateImageGrid(this.imgs, dw, dh));
|
||||||
let w = this.imgs[0].naturalWidth;
|
|
||||||
let h = this.imgs[0].naturalHeight;
|
|
||||||
|
|
||||||
// compact style
|
|
||||||
for (let c = 1; c <= numImages; c++) {
|
|
||||||
const rows = Math.ceil(numImages / c);
|
|
||||||
const cW = dw / c;
|
|
||||||
const cH = dh / rows;
|
|
||||||
const scaleX = cW / w;
|
|
||||||
const scaleY = cH / h;
|
|
||||||
|
|
||||||
const scale = Math.min(scaleX, scaleY, 1);
|
|
||||||
const imageW = w * scale;
|
|
||||||
const imageH = h * scale;
|
|
||||||
const area = imageW * imageH * numImages;
|
|
||||||
|
|
||||||
if (area > best) {
|
|
||||||
best = area;
|
|
||||||
cellWidth = imageW;
|
|
||||||
cellHeight = imageH;
|
|
||||||
cols = c;
|
|
||||||
shiftX = c * ((cW - imageW) / 2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let anyHovered = false;
|
let anyHovered = false;
|
||||||
@ -1272,6 +1287,7 @@ export class ComfyApp {
|
|||||||
canvasEl.tabIndex = "1";
|
canvasEl.tabIndex = "1";
|
||||||
document.body.prepend(canvasEl);
|
document.body.prepend(canvasEl);
|
||||||
|
|
||||||
|
addDomClippingSetting();
|
||||||
this.#addProcessMouseHandler();
|
this.#addProcessMouseHandler();
|
||||||
this.#addProcessKeyHandler();
|
this.#addProcessKeyHandler();
|
||||||
this.#addConfigureHandler();
|
this.#addConfigureHandler();
|
||||||
|
312
web/scripts/domWidget.js
Normal file
312
web/scripts/domWidget.js
Normal file
@ -0,0 +1,312 @@
|
|||||||
|
import { app, ANIM_PREVIEW_WIDGET } from "./app.js";
|
||||||
|
|
||||||
|
const SIZE = Symbol();
|
||||||
|
|
||||||
|
function intersect(a, b) {
|
||||||
|
const x = Math.max(a.x, b.x);
|
||||||
|
const num1 = Math.min(a.x + a.width, b.x + b.width);
|
||||||
|
const y = Math.max(a.y, b.y);
|
||||||
|
const num2 = Math.min(a.y + a.height, b.y + b.height);
|
||||||
|
if (num1 >= x && num2 >= y) return [x, y, num1 - x, num2 - y];
|
||||||
|
else return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getClipPath(node, element, elRect) {
|
||||||
|
const selectedNode = Object.values(app.canvas.selected_nodes)[0];
|
||||||
|
if (selectedNode && selectedNode !== node) {
|
||||||
|
const MARGIN = 7;
|
||||||
|
const scale = app.canvas.ds.scale;
|
||||||
|
|
||||||
|
const intersection = intersect(
|
||||||
|
{ x: elRect.x / scale, y: elRect.y / scale, width: elRect.width / scale, height: elRect.height / scale },
|
||||||
|
{
|
||||||
|
x: selectedNode.pos[0] + app.canvas.ds.offset[0] - MARGIN,
|
||||||
|
y: selectedNode.pos[1] + app.canvas.ds.offset[1] - LiteGraph.NODE_TITLE_HEIGHT - MARGIN,
|
||||||
|
width: selectedNode.size[0] + MARGIN + MARGIN,
|
||||||
|
height: selectedNode.size[1] + LiteGraph.NODE_TITLE_HEIGHT + MARGIN + MARGIN,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!intersection) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
const widgetRect = element.getBoundingClientRect();
|
||||||
|
const clipX = intersection[0] - widgetRect.x / scale + "px";
|
||||||
|
const clipY = intersection[1] - widgetRect.y / scale + "px";
|
||||||
|
const clipWidth = intersection[2] + "px";
|
||||||
|
const clipHeight = intersection[3] + "px";
|
||||||
|
const path = `polygon(0% 0%, 0% 100%, ${clipX} 100%, ${clipX} ${clipY}, calc(${clipX} + ${clipWidth}) ${clipY}, calc(${clipX} + ${clipWidth}) calc(${clipY} + ${clipHeight}), ${clipX} calc(${clipY} + ${clipHeight}), ${clipX} 100%, 100% 100%, 100% 0%)`;
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function computeSize(size) {
|
||||||
|
if (this.widgets?.[0].last_y == null) return;
|
||||||
|
|
||||||
|
let y = this.widgets[0].last_y;
|
||||||
|
let freeSpace = size[1] - y;
|
||||||
|
|
||||||
|
let widgetHeight = 0;
|
||||||
|
let dom = [];
|
||||||
|
for (const w of this.widgets) {
|
||||||
|
if (w.type === "converted-widget") {
|
||||||
|
// Ignore
|
||||||
|
delete w.computedHeight;
|
||||||
|
} else if (w.computeSize) {
|
||||||
|
widgetHeight += w.computeSize()[1] + 4;
|
||||||
|
} else if (w.element) {
|
||||||
|
// Extract DOM widget size info
|
||||||
|
const styles = getComputedStyle(w.element);
|
||||||
|
let minHeight = w.options.getMinHeight?.() ?? parseInt(styles.getPropertyValue("--comfy-widget-min-height"));
|
||||||
|
let maxHeight = w.options.getMaxHeight?.() ?? parseInt(styles.getPropertyValue("--comfy-widget-max-height"));
|
||||||
|
|
||||||
|
let prefHeight = w.options.getHeight?.() ?? styles.getPropertyValue("--comfy-widget-height");
|
||||||
|
if (prefHeight.endsWith?.("%")) {
|
||||||
|
prefHeight = size[1] * (parseFloat(prefHeight.substring(0, prefHeight.length - 1)) / 100);
|
||||||
|
} else {
|
||||||
|
prefHeight = parseInt(prefHeight);
|
||||||
|
if (isNaN(minHeight)) {
|
||||||
|
minHeight = prefHeight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (isNaN(minHeight)) {
|
||||||
|
minHeight = 50;
|
||||||
|
}
|
||||||
|
if (!isNaN(maxHeight)) {
|
||||||
|
if (!isNaN(prefHeight)) {
|
||||||
|
prefHeight = Math.min(prefHeight, maxHeight);
|
||||||
|
} else {
|
||||||
|
prefHeight = maxHeight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dom.push({
|
||||||
|
minHeight,
|
||||||
|
prefHeight,
|
||||||
|
w,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
widgetHeight += LiteGraph.NODE_WIDGET_HEIGHT + 4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
freeSpace -= widgetHeight;
|
||||||
|
|
||||||
|
// Calculate sizes with all widgets at their min height
|
||||||
|
const prefGrow = []; // Nodes that want to grow to their prefd size
|
||||||
|
const canGrow = []; // Nodes that can grow to auto size
|
||||||
|
let growBy = 0;
|
||||||
|
for (const d of dom) {
|
||||||
|
freeSpace -= d.minHeight;
|
||||||
|
if (isNaN(d.prefHeight)) {
|
||||||
|
canGrow.push(d);
|
||||||
|
d.w.computedHeight = d.minHeight;
|
||||||
|
} else {
|
||||||
|
const diff = d.prefHeight - d.minHeight;
|
||||||
|
if (diff > 0) {
|
||||||
|
prefGrow.push(d);
|
||||||
|
growBy += diff;
|
||||||
|
d.diff = diff;
|
||||||
|
} else {
|
||||||
|
d.w.computedHeight = d.minHeight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.imgs && !this.widgets.find((w) => w.name === ANIM_PREVIEW_WIDGET)) {
|
||||||
|
// Allocate space for image
|
||||||
|
freeSpace -= 220;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (freeSpace < 0) {
|
||||||
|
// Not enough space for all widgets so we need to grow
|
||||||
|
size[1] -= freeSpace;
|
||||||
|
this.graph.setDirtyCanvas(true);
|
||||||
|
} else {
|
||||||
|
// Share the space between each
|
||||||
|
const growDiff = freeSpace - growBy;
|
||||||
|
if (growDiff > 0) {
|
||||||
|
// All pref sizes can be fulfilled
|
||||||
|
freeSpace = growDiff;
|
||||||
|
for (const d of prefGrow) {
|
||||||
|
d.w.computedHeight = d.prefHeight;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// We need to grow evenly
|
||||||
|
const shared = -growDiff / prefGrow.length;
|
||||||
|
for (const d of prefGrow) {
|
||||||
|
d.w.computedHeight = d.prefHeight - shared;
|
||||||
|
}
|
||||||
|
freeSpace = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (freeSpace > 0 && canGrow.length) {
|
||||||
|
// Grow any that are auto height
|
||||||
|
const shared = freeSpace / canGrow.length;
|
||||||
|
for (const d of canGrow) {
|
||||||
|
d.w.computedHeight += shared;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Position each of the widgets
|
||||||
|
for (const w of this.widgets) {
|
||||||
|
w.y = y;
|
||||||
|
if (w.computedHeight) {
|
||||||
|
y += w.computedHeight;
|
||||||
|
} else if (w.computeSize) {
|
||||||
|
y += w.computeSize()[1] + 4;
|
||||||
|
} else {
|
||||||
|
y += LiteGraph.NODE_WIDGET_HEIGHT + 4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Override the compute visible nodes function to allow us to hide/show DOM elements when the node goes offscreen
|
||||||
|
const elementWidgets = new Set();
|
||||||
|
const computeVisibleNodes = LGraphCanvas.prototype.computeVisibleNodes;
|
||||||
|
LGraphCanvas.prototype.computeVisibleNodes = function () {
|
||||||
|
const visibleNodes = computeVisibleNodes.apply(this, arguments);
|
||||||
|
for (const node of app.graph._nodes) {
|
||||||
|
if (elementWidgets.has(node)) {
|
||||||
|
const hidden = visibleNodes.indexOf(node) === -1;
|
||||||
|
for (const w of node.widgets) {
|
||||||
|
if (w.element) {
|
||||||
|
w.element.hidden = hidden;
|
||||||
|
if (hidden) {
|
||||||
|
w.options.onHide?.(w);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return visibleNodes;
|
||||||
|
};
|
||||||
|
|
||||||
|
let enableDomClipping = true;
|
||||||
|
|
||||||
|
export function addDomClippingSetting() {
|
||||||
|
app.ui.settings.addSetting({
|
||||||
|
id: "Comfy.DOMClippingEnabled",
|
||||||
|
name: "Enable DOM element clipping (enabling may reduce performance)",
|
||||||
|
type: "boolean",
|
||||||
|
defaultValue: enableDomClipping,
|
||||||
|
onChange(value) {
|
||||||
|
console.log("enableDomClipping", enableDomClipping);
|
||||||
|
enableDomClipping = !!value;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
LGraphNode.prototype.addDOMWidget = function (name, type, element, options) {
|
||||||
|
options = { hideOnZoom: true, selectOn: ["focus", "click"], ...options };
|
||||||
|
|
||||||
|
if (!element.parentElement) {
|
||||||
|
document.body.append(element);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mouseDownHandler;
|
||||||
|
if (element.blur) {
|
||||||
|
mouseDownHandler = (event) => {
|
||||||
|
if (!element.contains(event.target)) {
|
||||||
|
element.blur();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
document.addEventListener("mousedown", mouseDownHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
const widget = {
|
||||||
|
type,
|
||||||
|
name,
|
||||||
|
get value() {
|
||||||
|
return options.getValue?.() ?? undefined;
|
||||||
|
},
|
||||||
|
set value(v) {
|
||||||
|
options.setValue?.(v);
|
||||||
|
widget.callback?.(widget.value);
|
||||||
|
},
|
||||||
|
draw: function (ctx, node, widgetWidth, y, widgetHeight) {
|
||||||
|
if (widget.computedHeight == null) {
|
||||||
|
computeSize.call(node, node.size);
|
||||||
|
}
|
||||||
|
|
||||||
|
const hidden =
|
||||||
|
(!!options.hideOnZoom && app.canvas.ds.scale < 0.5) ||
|
||||||
|
widget.computedHeight <= 0 ||
|
||||||
|
widget.type === "converted-widget";
|
||||||
|
element.hidden = hidden;
|
||||||
|
element.style.display = hidden ? "none" : null;
|
||||||
|
if (hidden) {
|
||||||
|
widget.options.onHide?.(widget);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const margin = 10;
|
||||||
|
const elRect = ctx.canvas.getBoundingClientRect();
|
||||||
|
const transform = new DOMMatrix()
|
||||||
|
.scaleSelf(elRect.width / ctx.canvas.width, elRect.height / ctx.canvas.height)
|
||||||
|
.multiplySelf(ctx.getTransform())
|
||||||
|
.translateSelf(margin, margin + y);
|
||||||
|
|
||||||
|
const scale = new DOMMatrix().scaleSelf(transform.a, transform.d);
|
||||||
|
|
||||||
|
Object.assign(element.style, {
|
||||||
|
transformOrigin: "0 0",
|
||||||
|
transform: scale,
|
||||||
|
left: `${transform.a + transform.e}px`,
|
||||||
|
top: `${transform.d + transform.f}px`,
|
||||||
|
width: `${widgetWidth - margin * 2}px`,
|
||||||
|
height: `${(widget.computedHeight ?? 50) - margin * 2}px`,
|
||||||
|
position: "absolute",
|
||||||
|
zIndex: app.graph._nodes.indexOf(node),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (enableDomClipping) {
|
||||||
|
element.style.clipPath = getClipPath(node, element, elRect);
|
||||||
|
element.style.willChange = "clip-path";
|
||||||
|
}
|
||||||
|
|
||||||
|
this.options.onDraw?.(widget);
|
||||||
|
},
|
||||||
|
element,
|
||||||
|
options,
|
||||||
|
onRemove() {
|
||||||
|
if (mouseDownHandler) {
|
||||||
|
document.removeEventListener("mousedown", mouseDownHandler);
|
||||||
|
}
|
||||||
|
element.remove();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const evt of options.selectOn) {
|
||||||
|
element.addEventListener(evt, () => {
|
||||||
|
app.canvas.selectNode(this);
|
||||||
|
app.canvas.bringToFront(this);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.addCustomWidget(widget);
|
||||||
|
elementWidgets.add(this);
|
||||||
|
|
||||||
|
const onRemoved = this.onRemoved;
|
||||||
|
this.onRemoved = function () {
|
||||||
|
element.remove();
|
||||||
|
elementWidgets.delete(this);
|
||||||
|
onRemoved?.apply(this, arguments);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!this[SIZE]) {
|
||||||
|
this[SIZE] = true;
|
||||||
|
const onResize = this.onResize;
|
||||||
|
this.onResize = function (size) {
|
||||||
|
options.beforeResize?.call(widget, this);
|
||||||
|
computeSize.call(this, size);
|
||||||
|
onResize?.apply(this, arguments);
|
||||||
|
options.afterResize?.call(widget, this);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return widget;
|
||||||
|
};
|
97
web/scripts/ui/imagePreview.js
Normal file
97
web/scripts/ui/imagePreview.js
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
import { $el } from "../ui.js";
|
||||||
|
|
||||||
|
export function calculateImageGrid(imgs, dw, dh) {
|
||||||
|
let best = 0;
|
||||||
|
let w = imgs[0].naturalWidth;
|
||||||
|
let h = imgs[0].naturalHeight;
|
||||||
|
const numImages = imgs.length;
|
||||||
|
|
||||||
|
let cellWidth, cellHeight, cols, rows, shiftX;
|
||||||
|
// compact style
|
||||||
|
for (let c = 1; c <= numImages; c++) {
|
||||||
|
const r = Math.ceil(numImages / c);
|
||||||
|
const cW = dw / c;
|
||||||
|
const cH = dh / r;
|
||||||
|
const scaleX = cW / w;
|
||||||
|
const scaleY = cH / h;
|
||||||
|
|
||||||
|
const scale = Math.min(scaleX, scaleY, 1);
|
||||||
|
const imageW = w * scale;
|
||||||
|
const imageH = h * scale;
|
||||||
|
const area = imageW * imageH * numImages;
|
||||||
|
|
||||||
|
if (area > best) {
|
||||||
|
best = area;
|
||||||
|
cellWidth = imageW;
|
||||||
|
cellHeight = imageH;
|
||||||
|
cols = c;
|
||||||
|
rows = r;
|
||||||
|
shiftX = c * ((cW - imageW) / 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { cellWidth, cellHeight, cols, rows, shiftX };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createImageHost(node) {
|
||||||
|
const el = $el("div.comfy-img-preview");
|
||||||
|
let currentImgs;
|
||||||
|
let first = true;
|
||||||
|
|
||||||
|
function updateSize() {
|
||||||
|
let w = null;
|
||||||
|
let h = null;
|
||||||
|
|
||||||
|
if (currentImgs) {
|
||||||
|
let elH = el.clientHeight;
|
||||||
|
if (first) {
|
||||||
|
first = false;
|
||||||
|
// On first run, if we are small then grow a bit
|
||||||
|
if (elH < 190) {
|
||||||
|
elH = 190;
|
||||||
|
}
|
||||||
|
el.style.setProperty("--comfy-widget-min-height", elH);
|
||||||
|
} else {
|
||||||
|
el.style.setProperty("--comfy-widget-min-height", null);
|
||||||
|
}
|
||||||
|
|
||||||
|
const nw = node.size[0];
|
||||||
|
({ cellWidth: w, cellHeight: h } = calculateImageGrid(currentImgs, nw - 20, elH));
|
||||||
|
w += "px";
|
||||||
|
h += "px";
|
||||||
|
|
||||||
|
el.style.setProperty("--comfy-img-preview-width", w);
|
||||||
|
el.style.setProperty("--comfy-img-preview-height", h);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
el,
|
||||||
|
updateImages(imgs) {
|
||||||
|
if (imgs !== currentImgs) {
|
||||||
|
if (currentImgs == null) {
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
updateSize();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
el.replaceChildren(...imgs);
|
||||||
|
currentImgs = imgs;
|
||||||
|
node.onResize(node.size);
|
||||||
|
node.graph.setDirtyCanvas(true, true);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getHeight() {
|
||||||
|
updateSize();
|
||||||
|
},
|
||||||
|
onDraw() {
|
||||||
|
// Element from point uses a hittest find elements so we need to toggle pointer events
|
||||||
|
el.style.pointerEvents = "all";
|
||||||
|
const over = document.elementFromPoint(app.canvas.mouse[0], app.canvas.mouse[1]);
|
||||||
|
el.style.pointerEvents = "none";
|
||||||
|
|
||||||
|
if(!over) return;
|
||||||
|
// Set the overIndex so Open Image etc work
|
||||||
|
const idx = currentImgs.indexOf(over);
|
||||||
|
node.overIndex = idx;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
@ -1,4 +1,5 @@
|
|||||||
import { api } from "./api.js"
|
import { api } from "./api.js"
|
||||||
|
import "./domWidget.js";
|
||||||
|
|
||||||
function getNumberDefaults(inputData, defaultStep, precision, enable_rounding) {
|
function getNumberDefaults(inputData, defaultStep, precision, enable_rounding) {
|
||||||
let defaultVal = inputData[1]["default"];
|
let defaultVal = inputData[1]["default"];
|
||||||
@ -97,166 +98,21 @@ function seedWidget(node, inputName, inputData, app) {
|
|||||||
seed.widget.linkedWidgets = [seedControl];
|
seed.widget.linkedWidgets = [seedControl];
|
||||||
return seed;
|
return seed;
|
||||||
}
|
}
|
||||||
|
|
||||||
const MultilineSymbol = Symbol();
|
|
||||||
const MultilineResizeSymbol = Symbol();
|
|
||||||
|
|
||||||
function addMultilineWidget(node, name, opts, app) {
|
function addMultilineWidget(node, name, opts, app) {
|
||||||
const MIN_SIZE = 50;
|
const inputEl = document.createElement("textarea");
|
||||||
|
inputEl.className = "comfy-multiline-input";
|
||||||
|
inputEl.value = opts.defaultVal;
|
||||||
|
inputEl.placeholder = opts.placeholder || "";
|
||||||
|
|
||||||
function computeSize(size) {
|
const widget = node.addDOMWidget(name, "customtext", inputEl, {
|
||||||
if (node.widgets[0].last_y == null) return;
|
getValue() {
|
||||||
|
return inputEl.value;
|
||||||
let y = node.widgets[0].last_y;
|
|
||||||
let freeSpace = size[1] - y;
|
|
||||||
|
|
||||||
// Compute the height of all non customtext widgets
|
|
||||||
let widgetHeight = 0;
|
|
||||||
const multi = [];
|
|
||||||
for (let i = 0; i < node.widgets.length; i++) {
|
|
||||||
const w = node.widgets[i];
|
|
||||||
if (w.type === "customtext") {
|
|
||||||
multi.push(w);
|
|
||||||
} else {
|
|
||||||
if (w.computeSize) {
|
|
||||||
widgetHeight += w.computeSize()[1] + 4;
|
|
||||||
} else {
|
|
||||||
widgetHeight += LiteGraph.NODE_WIDGET_HEIGHT + 4;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// See how large each text input can be
|
|
||||||
freeSpace -= widgetHeight;
|
|
||||||
freeSpace /= multi.length + (!!node.imgs?.length);
|
|
||||||
|
|
||||||
if (freeSpace < MIN_SIZE) {
|
|
||||||
// There isnt enough space for all the widgets, increase the size of the node
|
|
||||||
freeSpace = MIN_SIZE;
|
|
||||||
node.size[1] = y + widgetHeight + freeSpace * (multi.length + (!!node.imgs?.length));
|
|
||||||
node.graph.setDirtyCanvas(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Position each of the widgets
|
|
||||||
for (const w of node.widgets) {
|
|
||||||
w.y = y;
|
|
||||||
if (w.type === "customtext") {
|
|
||||||
y += freeSpace;
|
|
||||||
w.computedHeight = freeSpace - multi.length*4;
|
|
||||||
} else if (w.computeSize) {
|
|
||||||
y += w.computeSize()[1] + 4;
|
|
||||||
} else {
|
|
||||||
y += LiteGraph.NODE_WIDGET_HEIGHT + 4;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
node.inputHeight = freeSpace;
|
|
||||||
}
|
|
||||||
|
|
||||||
const widget = {
|
|
||||||
type: "customtext",
|
|
||||||
name,
|
|
||||||
get value() {
|
|
||||||
return this.inputEl.value;
|
|
||||||
},
|
},
|
||||||
set value(x) {
|
setValue(v) {
|
||||||
this.inputEl.value = x;
|
inputEl.value = v;
|
||||||
},
|
},
|
||||||
draw: function (ctx, _, widgetWidth, y, widgetHeight) {
|
|
||||||
if (!this.parent.inputHeight) {
|
|
||||||
// If we are initially offscreen when created we wont have received a resize event
|
|
||||||
// Calculate it here instead
|
|
||||||
computeSize(node.size);
|
|
||||||
}
|
|
||||||
const visible = app.canvas.ds.scale > 0.5 && this.type === "customtext";
|
|
||||||
const margin = 10;
|
|
||||||
const elRect = ctx.canvas.getBoundingClientRect();
|
|
||||||
const transform = new DOMMatrix()
|
|
||||||
.scaleSelf(elRect.width / ctx.canvas.width, elRect.height / ctx.canvas.height)
|
|
||||||
.multiplySelf(ctx.getTransform())
|
|
||||||
.translateSelf(margin, margin + y);
|
|
||||||
|
|
||||||
const scale = new DOMMatrix().scaleSelf(transform.a, transform.d)
|
|
||||||
Object.assign(this.inputEl.style, {
|
|
||||||
transformOrigin: "0 0",
|
|
||||||
transform: scale,
|
|
||||||
left: `${transform.a + transform.e}px`,
|
|
||||||
top: `${transform.d + transform.f}px`,
|
|
||||||
width: `${widgetWidth - (margin * 2)}px`,
|
|
||||||
height: `${this.parent.inputHeight - (margin * 2)}px`,
|
|
||||||
position: "absolute",
|
|
||||||
background: (!node.color)?'':node.color,
|
|
||||||
color: (!node.color)?'':'white',
|
|
||||||
zIndex: app.graph._nodes.indexOf(node),
|
|
||||||
});
|
});
|
||||||
this.inputEl.hidden = !visible;
|
widget.inputEl = inputEl;
|
||||||
},
|
|
||||||
};
|
|
||||||
widget.inputEl = document.createElement("textarea");
|
|
||||||
widget.inputEl.className = "comfy-multiline-input";
|
|
||||||
widget.inputEl.value = opts.defaultVal;
|
|
||||||
widget.inputEl.placeholder = opts.placeholder || "";
|
|
||||||
document.addEventListener("mousedown", function (event) {
|
|
||||||
if (!widget.inputEl.contains(event.target)) {
|
|
||||||
widget.inputEl.blur();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
widget.parent = node;
|
|
||||||
document.body.appendChild(widget.inputEl);
|
|
||||||
|
|
||||||
node.addCustomWidget(widget);
|
|
||||||
|
|
||||||
app.canvas.onDrawBackground = function () {
|
|
||||||
// Draw node isnt fired once the node is off the screen
|
|
||||||
// if it goes off screen quickly, the input may not be removed
|
|
||||||
// this shifts it off screen so it can be moved back if the node is visible.
|
|
||||||
for (let n in app.graph._nodes) {
|
|
||||||
n = graph._nodes[n];
|
|
||||||
for (let w in n.widgets) {
|
|
||||||
let wid = n.widgets[w];
|
|
||||||
if (Object.hasOwn(wid, "inputEl")) {
|
|
||||||
wid.inputEl.style.left = -8000 + "px";
|
|
||||||
wid.inputEl.style.position = "absolute";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
node.onRemoved = function () {
|
|
||||||
// When removing this node we need to remove the input from the DOM
|
|
||||||
for (let y in this.widgets) {
|
|
||||||
if (this.widgets[y].inputEl) {
|
|
||||||
this.widgets[y].inputEl.remove();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
widget.onRemove = () => {
|
|
||||||
widget.inputEl?.remove();
|
|
||||||
|
|
||||||
// Restore original size handler if we are the last
|
|
||||||
if (!--node[MultilineSymbol]) {
|
|
||||||
node.onResize = node[MultilineResizeSymbol];
|
|
||||||
delete node[MultilineSymbol];
|
|
||||||
delete node[MultilineResizeSymbol];
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (node[MultilineSymbol]) {
|
|
||||||
node[MultilineSymbol]++;
|
|
||||||
} else {
|
|
||||||
node[MultilineSymbol] = 1;
|
|
||||||
const onResize = (node[MultilineResizeSymbol] = node.onResize);
|
|
||||||
|
|
||||||
node.onResize = function (size) {
|
|
||||||
computeSize(size);
|
|
||||||
|
|
||||||
// Call original resizer handler
|
|
||||||
if (onResize) {
|
|
||||||
onResize.apply(this, arguments);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return { minWidth: 400, minHeight: 200, widget };
|
return { minWidth: 400, minHeight: 200, widget };
|
||||||
}
|
}
|
||||||
|
@ -409,6 +409,21 @@ dialog::backdrop {
|
|||||||
width: calc(100% - 10px);
|
width: calc(100% - 10px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.comfy-img-preview {
|
||||||
|
pointer-events: none;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-content: flex-start;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comfy-img-preview img {
|
||||||
|
object-fit: contain;
|
||||||
|
width: var(--comfy-img-preview-width);
|
||||||
|
height: var(--comfy-img-preview-height);
|
||||||
|
}
|
||||||
|
|
||||||
/* Search box */
|
/* Search box */
|
||||||
|
|
||||||
.litegraph.litesearchbox {
|
.litegraph.litesearchbox {
|
||||||
|
Loading…
Reference in New Issue
Block a user