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]); } }; }, });