Files
2026-07-06 16:15:07 -03:00

167 lines
6.7 KiB
JavaScript

import { app } from "../../scripts/app.js";
import { api } from "../../scripts/api.js";
function viewURL(img) {
const p = new URLSearchParams();
p.set("filename", img.filename);
p.set("type", img.type || "temp");
p.set("subfolder", img.subfolder || "");
p.set("t", "" + (+new Date()));
return api.apiURL(`/view?${p.toString()}`);
}
app.registerExtension({
name: "PixelArt.PreviewMagnifier",
async beforeRegisterNodeDef(nodeType, nodeData) {
if (nodeData.name !== "PixelArtPreview") return;
// Remove the blurry preview the core draws; we use our own crisp widget.
nodeType.prototype.onDrawBackground = function () {};
const onNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function () {
const r = onNodeCreated ? onNodeCreated.apply(this, arguments) : undefined;
const self = this;
const container = document.createElement("div");
Object.assign(container.style, {
position: "relative",
width: "100%",
height: "100%",
minHeight: "120px",
overflow: "hidden",
display: "flex",
alignItems: "center",
justifyContent: "center",
background: "#111",
borderRadius: "6px",
cursor: "crosshair",
});
const img = document.createElement("img");
Object.assign(img.style, {
maxWidth: "100%",
maxHeight: "100%",
imageRendering: "pixelated",
display: "none",
});
container.appendChild(img);
const hint = document.createElement("div");
hint.textContent = "no image — run the workflow";
Object.assign(hint.style, {
color: "#666",
fontSize: "11px",
fontFamily: "monospace",
pointerEvents: "none",
});
container.appendChild(hint);
const lens = document.createElement("div");
Object.assign(lens.style, {
position: "absolute",
boxSizing: "border-box",
border: "2px solid #fff",
borderRadius: "50%",
boxShadow: "0 0 0 1px #000, 0 2px 10px rgba(0,0,0,.7)",
pointerEvents: "none",
display: "none",
backgroundRepeat: "no-repeat",
backgroundOrigin: "border-box",
backgroundClip: "border-box",
imageRendering: "pixelated",
zIndex: "10",
});
container.appendChild(lens);
const getW = (name, def) => {
const w = self.widgets && self.widgets.find((x) => x.name === name);
return w ? w.value : def;
};
const onMove = (e) => {
if (img.style.display === "none" || !getW("magnifier", true)) {
lens.style.display = "none";
return;
}
const rect = img.getBoundingClientRect(); // screen space (graph zoom applied)
const iw = img.offsetWidth || rect.width; // local (untransformed) px
const ih = img.offsetHeight || rect.height;
// Screen->local scale from the ComfyUI graph transform.
const scale = rect.width / iw || 1;
// Mouse position over the image, in the image's LOCAL pixels.
const xLocal = (e.clientX - rect.left) / scale;
const yLocal = (e.clientY - rect.top) / scale;
if (xLocal < 0 || yLocal < 0 || xLocal > iw || yLocal > ih) {
lens.style.display = "none";
return;
}
const zoom = getW("zoom", 6);
const lensSize = getW("lens_size", 140);
const crect = container.getBoundingClientRect();
// Mouse position inside the container, also in LOCAL pixels.
const cxLocal = (e.clientX - crect.left) / scale;
const cyLocal = (e.clientY - crect.top) / scale;
lens.style.display = "block";
lens.style.width = lensSize + "px";
lens.style.height = lensSize + "px";
// Center the lens exactly on the cursor.
lens.style.left = (cxLocal - lensSize / 2) + "px";
lens.style.top = (cyLocal - lensSize / 2) + "px";
lens.style.backgroundImage = `url("${img.src}")`;
// Background is the image blown up in LOCAL px; put the pixel under
// the cursor at the lens center.
lens.style.backgroundSize = `${iw * zoom}px ${ih * zoom}px`;
lens.style.backgroundPosition =
`${lensSize / 2 - xLocal * zoom}px ${lensSize / 2 - yLocal * zoom}px`;
};
container.addEventListener("mousemove", onMove);
container.addEventListener("mouseleave", () => {
lens.style.display = "none";
});
img.onload = () => {
img.style.display = "block";
hint.style.display = "none";
self._paAspect = (img.naturalHeight && img.naturalWidth)
? img.naturalHeight / img.naturalWidth
: 0.75;
// Resize the node once to fit the image (no feedback loop).
const sz = self.computeSize();
self.setSize([self.size[0], sz[1]]);
app.graph.setDirtyCanvas(true, true);
};
this._paImg = img;
this._paAspect = 0.75;
const widget = this.addDOMWidget("pixelart_preview", "preview", container, {
serialize: false,
hideOnZoom: false,
});
// Height depends ONLY on width + image aspect ratio, never on the node
// height -> avoids the infinite vertical growth feedback loop.
widget.computeSize = function (width) {
const w = width || (self.size ? self.size[0] : 280);
let h = Math.round(w * (self._paAspect || 0.75));
h = Math.max(120, Math.min(h, 1024));
return [width, h];
};
return r;
};
const onExecuted = nodeType.prototype.onExecuted;
nodeType.prototype.onExecuted = function (message) {
onExecuted && onExecuted.apply(this, arguments);
const imgs = message && message.images;
if (imgs && imgs.length && this._paImg) {
this._paImg.src = viewURL(imgs[imgs.length - 1]);
}
};
},
});