167 lines
6.7 KiB
JavaScript
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]);
|
|
}
|
|
};
|
|
},
|
|
});
|