Files
AEmotionStudio 829189e15e fix: Address PR #41 review comments (round 6)
- Remove debug console.log statements:
  - matrix_button.ts: 'MatrixButton module loaded'
  - video_comparer.ts: module load, widget constructor/setter, setup logs
  - shader_renderer.ts: 'ShaderRenderer module loaded'
  - advanced_comparer.ts: module load and setup complete logs

- Extract shared animation utilities to golden_eyeball.ts:
  - shouldUpdateFrame(): frame counter advancement
  - resetShadowContext(): context shadow property reset
  - createTitleGradient(): standard vertical background gradient
  - calculateShimmerPosition(): shimmer math calculation
  - TITLE_GRADIENT_COLORS: shared color constants
  - TITLE_CORNER_RADIUS: shared corner radius constant

- Refactor to use shared utilities:
  - gradient_title.ts now imports and uses shared utilities
  - golden_eyeball.ts drawGradientTitle uses its own exports

All 85 tests pass.
2026-01-28 04:26:42 -08:00

752 lines
34 KiB
JavaScript

/**
* advanced_comparer.ts - Advanced Image Comparer Widget for ComfyUI
* Supports multiple comparison modes: Slider, Click, Side-by-Side, Stacked, Grid, Carousel, Batch, Onion Skin
*/
// @ts-ignore - Runtime ComfyUI import
import { app } from "../../../scripts/app.js";
import { drawGradientTitle } from "./golden_eyeball.js";
import { imageDataToUrl } from "./comfy_utils.js";
// Shared modes array used in widget and context menu
const COMPARER_MODES = ["Slider", "Click", "Side-by-Side", "Stacked", "Grid", "Carousel", "Batch", "Onion Skin"];
// ============================
// Animation Cache
// ============================
const CACHE = {
lastTime: 0,
frameCount: 0,
frameSkip: 2,
};
// ============================
// Widget Class
// ============================
class AdvancedImageComparerWidget {
/**
* Calculate scaled dimensions for an image to fit within usable area while maintaining aspect ratio.
* @param img - The image element with naturalWidth and naturalHeight
* @param usableWidth - The maximum width available
* @param usableHeight - The maximum height available
* @returns Object with target width (tw) and height (th)
*/
calculateScaledDimensions(img, usableWidth, usableHeight) {
const imgAspect = img.naturalWidth / img.naturalHeight;
const usableAspect = usableWidth / usableHeight;
if (imgAspect > usableAspect) {
return { tw: usableWidth, th: usableWidth / imgAspect };
}
else {
return { tw: usableHeight * imgAspect, th: usableHeight };
}
}
constructor(name, node) {
this.type = "custom";
this.options = { serialize: false };
this.y = 0;
this.imagesA = [];
this.imagesB = [];
this.currentPairIndex = 0;
this.maxPairs = 0;
this.animationFrame = null;
this.autoPlayEnabled = false;
this.autoPlaySpeed = 2000;
this.currentBatchPage = 0;
this.pairsPerPage = 3;
this.maxBatchPages = 0;
this.name = name;
this.node = node;
this._value = { images: [] };
this.selected = [];
this.imgs = [];
}
set value(v) {
const images = v.images || [];
const imagesA = images.filter((img) => 'is_image_a' in img && img.is_image_a === true);
const imagesB = images.filter((img) => 'is_image_a' in img && img.is_image_a === false);
this.imagesA = imagesA.map((img, index) => ({
name: `A${index + 1}`,
selected: true,
url: imageDataToUrl(img),
img: null,
index
}));
this.imagesB = imagesB.map((img, index) => ({
name: `B${index + 1}`,
selected: true,
url: imageDataToUrl(img),
img: null,
index
}));
this.maxPairs = Math.max(this.imagesA.length, this.imagesB.length);
this.currentPairIndex = 0;
this.maxBatchPages = Math.ceil(this.maxPairs / this.pairsPerPage);
this.currentBatchPage = 0;
this._value = { images: [...this.imagesA, ...this.imagesB] };
this.updateSelectedPair();
this.loadAllImages();
if (this.node?.updateControlsVisibility) {
this.node.updateControlsVisibility();
}
if (this.node) {
const minWidth = 700, minHeight = 600;
const [w, h] = this.node.size;
this.node.setSize([Math.max(w, minWidth), Math.max(h, minHeight)]);
this.node.setDirtyCanvas(true, true);
}
}
get value() {
return this._value || { images: [] };
}
loadAllImages() {
[...this.imagesA, ...this.imagesB].forEach(imageData => {
if (!imageData.img && imageData.url) {
imageData.img = new Image();
imageData.img.onload = () => this.node.setDirtyCanvas(true, false);
imageData.img.onerror = (e) => console.error("Image load failed:", imageData.name, e);
imageData.img.src = imageData.url;
}
});
}
setSelected(selected) {
this.selected = selected;
this.imgs = [];
for (const sel of selected) {
if (!sel.img && sel.url) {
sel.img = new Image();
sel.img.onload = () => this.node.setDirtyCanvas(true, false);
sel.img.src = sel.url;
}
if (sel.img)
this.imgs.push(sel.img);
}
}
draw(ctx, node, width, y, height) {
this.y = y;
const [, nodeHeight] = node.size;
const availableHeight = Math.max(200, nodeHeight - y - 10);
const mode = node.properties?.comparer_mode || "Slider";
switch (mode) {
case "Click":
this.drawClickMode(ctx, y, width, availableHeight);
break;
case "Side-by-Side":
this.drawSideBySideMode(ctx, y, width, availableHeight);
break;
case "Stacked":
this.drawStackedMode(ctx, y, width, availableHeight);
break;
case "Grid":
this.drawGridMode(ctx, y, width, availableHeight);
break;
case "Carousel":
this.drawCarouselMode(ctx, y, width, availableHeight);
break;
case "Batch":
this.drawBatchMode(ctx, y, width, availableHeight);
break;
case "Onion Skin":
this.drawOnionSkinMode(ctx, y, width, availableHeight);
break;
default:
this.drawSliderMode(ctx, y, width, availableHeight);
break;
}
if (["Grid", "Batch"].includes(mode) && this.maxPairs > 1) {
this.drawBatchControls(ctx, y, width, availableHeight);
}
if (mode === "Carousel" && this.maxPairs > 1) {
this.drawPairIndicator(ctx, y + availableHeight - 25, width);
}
}
drawClickMode(ctx, y, width, h) {
const idx = this.node.isPointerDown ? 1 : 0;
this.drawImage(ctx, this.selected[idx], y, width, h);
}
drawSideBySideMode(ctx, y, width, h) {
if (this.selected[0])
this.drawImageSideBySide(ctx, this.selected[0], y, width, h, 0);
if (this.selected[1])
this.drawImageSideBySide(ctx, this.selected[1], y, width, h, 1);
}
drawStackedMode(ctx, y, width, h) {
if (this.selected[0])
this.drawImageStacked(ctx, this.selected[0], y, width, h, 0);
if (this.selected[1])
this.drawImageStacked(ctx, this.selected[1], y, width, h, 1);
}
drawSliderMode(ctx, y, width, h) {
if (this.selected[0])
this.drawImage(ctx, this.selected[0], y, width, h);
if (this.selected[1] && this.node.isPointerOver) {
this.drawImage(ctx, this.selected[1], y, width, h, this.node.pointerOverPos[0]);
}
}
drawGridMode(ctx, y, width, h) {
const pairs = Math.min(this.maxPairs, 64);
const cols = Math.ceil(Math.sqrt(pairs * 2));
const rows = Math.ceil((pairs * 2) / cols);
const cw = width / cols, ch = (h - 40) / rows;
let ci = 0;
for (let i = 0; i < pairs; i++) {
if (this.imagesA[i]?.img) {
this.drawImageInCell(ctx, this.imagesA[i], y + Math.floor(ci / cols) * ch, (ci % cols) * cw, cw, ch, `A${i + 1}`);
ci++;
}
if (this.imagesB[i]?.img) {
this.drawImageInCell(ctx, this.imagesB[i], y + Math.floor(ci / cols) * ch, (ci % cols) * cw, cw, ch, `B${i + 1}`);
ci++;
}
}
}
drawCarouselMode(ctx, y, width, h) {
const a = this.imagesA[this.currentPairIndex], b = this.imagesB[this.currentPairIndex];
if (a?.img)
this.drawImageSideBySide(ctx, a, y, width, h, 0);
if (b?.img)
this.drawImageSideBySide(ctx, b, y, width, h, 1);
}
drawBatchMode(ctx, y, width, h) {
const ph = (h - 40) / this.pairsPerPage;
const start = this.currentBatchPage * this.pairsPerPage;
for (let i = 0; i < this.pairsPerPage && start + i < this.maxPairs; i++) {
const idx = start + i, py = y + i * ph;
if (this.imagesA[idx]?.img)
this.drawImageInPair(ctx, this.imagesA[idx], py, 0, width / 2, ph, 0);
if (this.imagesB[idx]?.img)
this.drawImageInPair(ctx, this.imagesB[idx], py, width / 2, width / 2, ph, 1);
if (i < this.pairsPerPage - 1 && idx < this.maxPairs - 1) {
ctx.beginPath();
ctx.moveTo(0, py + ph);
ctx.lineTo(width, py + ph);
ctx.strokeStyle = "rgba(255,255,255,0.3)";
ctx.lineWidth = 1;
ctx.stroke();
}
}
}
drawOnionSkinMode(ctx, y, width, h) {
const opacity = this.node.properties?.onionSkinOpacity || 0.5;
if (this.selected[0])
this.drawImage(ctx, this.selected[0], y, width, h);
if (this.selected[1]) {
ctx.save();
ctx.globalAlpha = opacity;
this.drawImage(ctx, this.selected[1], y, width, h);
ctx.restore();
}
}
drawImage(ctx, imageData, y, nodeWidth, availableHeight, cropX) {
if (!imageData?.img?.naturalWidth)
return;
const img = imageData.img, pad = 3;
const uw = nodeWidth - pad * 2, uh = availableHeight - pad * 2;
const { tw, th } = this.calculateScaledDimensions(img, uw, uh);
const dx = pad + (uw - tw) / 2, dy = y + pad + (uh - th) / 2;
ctx.save();
ctx.beginPath();
ctx.rect(pad, y + pad, uw, uh);
ctx.clip();
if (cropX && cropX > dx) {
const wm = img.naturalWidth / tw, sw = Math.max(0, (cropX - dx) * wm), dw = Math.max(0, cropX - dx);
ctx.drawImage(img, 0, 0, sw, img.naturalHeight, dx, dy, dw, th);
}
else {
ctx.drawImage(img, 0, 0, img.naturalWidth, img.naturalHeight, dx, dy, tw, th);
}
if (cropX != null && cropX > dx && cropX < dx + tw) {
ctx.beginPath();
ctx.moveTo(cropX, dy);
ctx.lineTo(cropX, dy + th);
ctx.globalCompositeOperation = "difference";
ctx.strokeStyle = "rgba(255,255,255,1)";
ctx.lineWidth = 2;
ctx.stroke();
}
ctx.restore();
}
drawImageSideBySide(ctx, imageData, y, nodeWidth, availableHeight, imageIndex) {
if (!imageData?.img?.naturalWidth)
return;
const img = imageData.img, hw = nodeWidth / 2, pad = 3, sep = 1;
const uw = hw - pad - sep / 2, uh = availableHeight - pad * 2;
const { tw, th } = this.calculateScaledDimensions(img, uw, uh);
const dx = imageIndex === 0 ? pad + (uw - tw) / 2 : hw + sep / 2 + pad + (uw - tw) / 2;
const dy = y + pad + (uh - th) / 2;
ctx.save();
const cx = imageIndex === 0 ? 0 : hw + sep / 2, cw = imageIndex === 0 ? hw - sep / 2 : hw - sep / 2;
ctx.beginPath();
ctx.rect(cx, y, cw, availableHeight);
ctx.clip();
ctx.drawImage(img, 0, 0, img.naturalWidth, img.naturalHeight, dx, dy, tw, th);
ctx.fillStyle = "rgba(0,0,0,0.8)";
ctx.fillRect(dx, dy, 25, 18);
ctx.fillStyle = "white";
ctx.font = "11px Arial";
ctx.textAlign = "center";
ctx.fillText(imageData.name, dx + 12, dy + 13);
ctx.restore();
if (imageIndex === 0) {
ctx.save();
ctx.beginPath();
ctx.moveTo(hw, y + pad);
ctx.lineTo(hw, y + availableHeight - pad);
ctx.strokeStyle = "rgba(255,255,255,0.5)";
ctx.lineWidth = 2;
ctx.stroke();
ctx.restore();
}
}
drawImageStacked(ctx, imageData, y, nodeWidth, availableHeight, imageIndex) {
if (!imageData?.img?.naturalWidth)
return;
const img = imageData.img, hh = availableHeight / 2, pad = 3, sep = 1;
const uw = nodeWidth - pad * 2, uh = hh - pad - sep / 2;
const { tw, th } = this.calculateScaledDimensions(img, uw, uh);
const dx = pad + (uw - tw) / 2;
const dy = imageIndex === 0 ? y + pad + (uh - th) / 2 : y + hh + sep / 2 + pad + (uh - th) / 2;
ctx.save();
const cy = imageIndex === 0 ? y : y + hh + sep / 2, ch = imageIndex === 0 ? hh - sep / 2 : hh - sep / 2;
ctx.beginPath();
ctx.rect(0, cy, nodeWidth, ch);
ctx.clip();
ctx.drawImage(img, 0, 0, img.naturalWidth, img.naturalHeight, dx, dy, tw, th);
ctx.fillStyle = "rgba(0,0,0,0.8)";
ctx.fillRect(dx, dy, 25, 18);
ctx.fillStyle = "white";
ctx.font = "11px Arial";
ctx.textAlign = "center";
ctx.fillText(imageData.name, dx + 12, dy + 13);
ctx.restore();
if (imageIndex === 0) {
ctx.save();
ctx.beginPath();
ctx.moveTo(pad, y + hh);
ctx.lineTo(nodeWidth - pad, y + hh);
ctx.strokeStyle = "rgba(255,255,255,0.5)";
ctx.lineWidth = 2;
ctx.stroke();
ctx.restore();
}
}
drawImageInCell(ctx, imageData, y, x, cw, ch, label) {
if (!imageData?.img?.naturalWidth)
return;
const img = imageData.img, pad = 2, uw = cw - pad * 2, uh = ch - pad * 2;
const { tw, th } = this.calculateScaledDimensions(img, uw, uh);
const dx = x + pad + (uw - tw) / 2, dy = y + pad + (uh - th) / 2;
ctx.save();
ctx.beginPath();
ctx.rect(x + pad, y + pad, uw, uh);
ctx.clip();
ctx.strokeStyle = "rgba(255,255,255,0.3)";
ctx.lineWidth = 1;
ctx.strokeRect(x + pad, y + pad, uw, uh);
ctx.drawImage(img, 0, 0, img.naturalWidth, img.naturalHeight, dx, dy, tw, th);
ctx.fillStyle = "rgba(0,0,0,0.8)";
ctx.fillRect(dx, dy, 28, 16);
ctx.fillStyle = "white";
ctx.font = "10px Arial";
ctx.textAlign = "center";
ctx.fillText(label, dx + 14, dy + 11);
ctx.restore();
}
drawImageInPair(ctx, imageData, y, x, pw, ph, imageIndex) {
if (!imageData?.img?.naturalWidth)
return;
const img = imageData.img, pad = 3, uw = pw - pad * 2, uh = ph - pad * 2;
const { tw, th } = this.calculateScaledDimensions(img, uw, uh);
const dx = x + pad + (uw - tw) / 2, dy = y + pad + (uh - th) / 2;
ctx.save();
ctx.beginPath();
ctx.rect(x + pad, y + pad, uw, uh);
ctx.clip();
ctx.drawImage(img, 0, 0, img.naturalWidth, img.naturalHeight, dx, dy, tw, th);
ctx.fillStyle = "rgba(0,0,0,0.8)";
ctx.fillRect(dx, dy, 30, 16);
ctx.fillStyle = "white";
ctx.font = "11px Arial";
ctx.textAlign = "center";
ctx.fillText(imageData.name, dx + 15, dy + 11);
ctx.restore();
if (imageIndex === 0 && pw < this.node.size[0]) {
ctx.save();
ctx.beginPath();
ctx.moveTo(x + pw, y + pad);
ctx.lineTo(x + pw, y + ph - pad);
ctx.strokeStyle = "rgba(255,255,255,0.5)";
ctx.lineWidth = 2;
ctx.stroke();
ctx.restore();
}
}
drawBatchControls(ctx, y, width, h) {
const cy = y + h - 30;
ctx.save();
ctx.fillStyle = "rgba(0,0,0,0.7)";
ctx.fillRect(0, cy, width, 30);
ctx.fillStyle = "white";
ctx.font = "12px Arial";
ctx.textAlign = "left";
ctx.fillText(`Images: A(${this.imagesA.length}) B(${this.imagesB.length})`, 10, cy + 18);
ctx.textAlign = "right";
const mode = this.node.properties?.comparer_mode || "Slider";
if (mode === "Grid")
ctx.fillText(`Showing ${Math.min(this.maxPairs, 64)} pairs`, width - 10, cy + 18);
else if (mode === "Batch") {
const s = this.currentBatchPage * this.pairsPerPage + 1, e = Math.min((this.currentBatchPage + 1) * this.pairsPerPage, this.maxPairs);
ctx.fillText(`Showing pairs ${s}-${e} of ${this.maxPairs}`, width - 10, cy + 18);
}
ctx.restore();
}
drawPairIndicator(ctx, y, width) {
ctx.save();
const ds = 8, dp = 12, tw = this.maxPairs * dp - (dp - ds), sx = (width - tw) / 2;
for (let i = 0; i < this.maxPairs; i++) {
ctx.beginPath();
ctx.arc(sx + i * dp + ds / 2, y + ds / 2, ds / 2, 0, Math.PI * 2);
ctx.fillStyle = i === this.currentPairIndex ? "rgba(255,255,255,1)" : "rgba(255,255,255,0.4)";
ctx.fill();
}
ctx.restore();
}
nextPair() { if (this.currentPairIndex < this.maxPairs - 1) {
this.currentPairIndex++;
this.updateSelectedPair();
this.node.setDirtyCanvas(true, false);
} }
previousPair() { if (this.currentPairIndex > 0) {
this.currentPairIndex--;
this.updateSelectedPair();
this.node.setDirtyCanvas(true, false);
} }
toggleAutoPlay() { this.autoPlayEnabled = !this.autoPlayEnabled; this.autoPlayEnabled ? this.startAutoPlay() : this.stopAutoPlay(); this.updateNodeControls(); this.node.setDirtyCanvas(true, false); }
nextBatchPage() { if (this.currentBatchPage < this.maxBatchPages - 1) {
this.currentBatchPage++;
this.updateNodeControls();
this.node.setDirtyCanvas(true, false);
} }
previousBatchPage() { if (this.currentBatchPage > 0) {
this.currentBatchPage--;
this.updateNodeControls();
this.node.setDirtyCanvas(true, false);
} }
updateNodeControls() {
if (this.node.pairInfoWidget)
this.node.pairInfoWidget.value = `${this.currentPairIndex + 1} / ${this.maxPairs}`;
if (this.node.autoPlayButton)
this.node.autoPlayButton.name = this.autoPlayEnabled ? "⏸ Pause" : "▶ Play";
if (this.node.batchSelectorWidget)
this.node.batchSelectorWidget.value = (this.currentPairIndex + 1).toString();
if (this.node.batchPageInfoWidget)
this.node.batchPageInfoWidget.value = `Page ${this.currentBatchPage + 1} / ${this.maxBatchPages}`;
}
updateSelectedPair() {
const mode = this.node.properties?.comparer_mode || "Slider";
if (["Slider", "Click", "Side-by-Side", "Stacked", "Onion Skin"].includes(mode)) {
const processed = [];
if (this.imagesA[this.currentPairIndex])
processed.push(this.imagesA[this.currentPairIndex]);
if (this.imagesB[this.currentPairIndex])
processed.push(this.imagesB[this.currentPairIndex]);
this.setSelected(processed);
}
this.updateNodeControls();
}
startAutoPlay() {
if (this.animationFrame)
clearInterval(this.animationFrame);
this.animationFrame = setInterval(() => {
this.currentPairIndex = this.currentPairIndex >= this.maxPairs - 1 ? 0 : this.currentPairIndex + 1;
this.updateSelectedPair();
this.node.setDirtyCanvas(true, false);
}, this.autoPlaySpeed);
}
stopAutoPlay() { if (this.animationFrame) {
clearInterval(this.animationFrame);
this.animationFrame = null;
} }
computeSize(width) {
const mode = this.node?.properties?.comparer_mode || "Slider";
let height = Math.max(500, width);
switch (mode) {
case "Stacked":
height = Math.max(700, width * 1.5);
break;
case "Grid":
const p = Math.min(this.maxPairs || 1, 64), c = Math.ceil(Math.sqrt(p * 2)), r = Math.ceil((p * 2) / c);
height = Math.max(500, (width / c) * r + 100);
break;
case "Carousel":
height = Math.max(450, width * 0.9 + 100);
break;
case "Batch":
height = Math.max(550, Math.min(this.maxPairs || 1, 3) * (width * 0.6) + 100);
break;
default:
height = Math.max(400, width * 0.9);
break;
}
return [width, height];
}
mouse(event, pos, node) {
const mode = node.properties?.comparer_mode || "Slider";
if (event.type === "pointermove") {
node.pointerOverPos = [...pos];
if (mode === "Slider")
node.setDirtyCanvas(true, false);
return true;
}
if (event.type === "pointerdown" && mode === "Grid" && this.maxPairs > 1) {
const p = Math.min(this.maxPairs, 64), c = Math.ceil(Math.sqrt(p * 2)), r = Math.ceil((p * 2) / c);
const cw = node.size[0] / c, wh = node.size[1] - this.y - 10, ch = (wh - 40) / r;
const ci = Math.floor(pos[1] / ch) * c + Math.floor(pos[0] / cw), ii = Math.floor(ci / 2);
if (ii < this.maxPairs) {
this.currentPairIndex = ii;
node.properties.comparer_mode = "Carousel";
if (node.layoutWidget)
node.layoutWidget.value = "Carousel";
node.updateControlsVisibility();
node.setDirtyCanvas(true, false);
return true;
}
}
return false;
}
onRemoved() { this.stopAutoPlay(); }
}
// ============================
// Extension Registration
// ============================
// eslint-disable-next-line @typescript-eslint/no-explicit-any
app.registerExtension({
name: "AdvancedImageComparer",
// eslint-disable-next-line @typescript-eslint/no-explicit-any
async beforeRegisterNodeDef(nodeType, nodeData) {
if (nodeData.name !== "AdvancedImageComparer")
return;
nodeType.prototype.properties = nodeType.prototype.properties || { comparer_mode: "Slider", onionSkinOpacity: 0.5 };
nodeType["@comparer_mode"] = { type: "combo", values: COMPARER_MODES };
const origOnDrawForeground = nodeType.prototype.onDrawForeground;
nodeType.prototype.onDrawForeground = function (ctx) {
if (origOnDrawForeground)
origOnDrawForeground.call(this, ctx);
drawGradientTitle(this, ctx, CACHE, 8);
};
const origOnRemoved = nodeType.prototype.onRemoved;
nodeType.prototype.onRemoved = function () {
if (origOnRemoved)
origOnRemoved.call(this);
// Note: CACHE only holds animation timing state, no cleanup needed
};
const onNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function () {
if (onNodeCreated)
onNodeCreated.call(this);
this.properties = this.properties || { comparer_mode: "Slider", onionSkinOpacity: 0.5 };
this.isPointerDown = false;
this.isPointerOver = false;
this.pointerOverPos = [0, 0];
this.imageIndex = 0;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const self = this;
self.layoutWidget = self.addWidget("combo", "Layout Mode", this.properties.comparer_mode, (value) => {
this.properties.comparer_mode = value;
this.updateControlsVisibility();
self.setDirtyCanvas(true, false);
}, { values: COMPARER_MODES });
self.batchSelectorWidget = self.addWidget("combo", "View Pair", "1", (value) => {
const idx = parseInt(value) - 1;
if (this.comparerWidget && idx >= 0 && idx < this.comparerWidget.maxPairs) {
this.comparerWidget.currentPairIndex = idx;
this.comparerWidget.updateSelectedPair();
self.setDirtyCanvas(true, false);
}
}, { values: ["1"] });
self.prevButton = self.addWidget("button", "◀ Previous", null, () => { this.comparerWidget?.previousPair(); });
self.nextButton = self.addWidget("button", "Next ▶", null, () => { this.comparerWidget?.nextPair(); });
self.autoPlayButton = self.addWidget("button", "▶ Play", null, () => {
this.comparerWidget?.toggleAutoPlay();
self.autoPlayButton.name = this.comparerWidget?.autoPlayEnabled ? "⏸ Pause" : "▶ Play";
});
self.pairInfoWidget = self.addWidget("text", "Pair Info", "1 / 1", () => { }, {});
self.pairInfoWidget.disabled = true;
self.batchPrevButton = self.addWidget("button", "◀ Prev Page", null, () => { this.comparerWidget?.previousBatchPage(); });
self.batchNextButton = self.addWidget("button", "Next Page ▶", null, () => { this.comparerWidget?.nextBatchPage(); });
self.batchPageInfoWidget = self.addWidget("text", "Page Info", "Page 1 / 1", () => { }, {});
self.batchPageInfoWidget.disabled = true;
self.onionSkinOpacitySlider = self.addWidget("slider", "Opacity B", this.properties.onionSkinOpacity, (value) => {
this.properties.onionSkinOpacity = value;
self.setDirtyCanvas(true, false);
}, { min: 0.0, max: 1.0, step: 0.01 });
this.comparerWidget = self.addCustomWidget(new AdvancedImageComparerWidget("advanced_comparer", this));
this.updateControlsVisibility();
self.setSize([700, 600]);
self.setDirtyCanvas(true, true);
};
nodeType.prototype.updateControlsVisibility = function () {
const mode = this.properties.comparer_mode;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const self = this;
const hasMultiplePairs = this.comparerWidget && this.comparerWidget.maxPairs > 1;
const showBatchSelector = hasMultiplePairs && ["Slider", "Click", "Side-by-Side", "Stacked", "Onion Skin"].includes(mode);
const showCarouselControls = mode === "Carousel";
const showBatchPagination = mode === "Batch" && this.comparerWidget && this.comparerWidget.maxBatchPages > 1;
const showOnionSkinSlider = mode === "Onion Skin";
if (self.batchSelectorWidget) {
self.batchSelectorWidget.hidden = !showBatchSelector;
if (showBatchSelector && this.comparerWidget) {
const opts = [];
for (let i = 1; i <= this.comparerWidget.maxPairs; i++)
opts.push(i.toString());
self.batchSelectorWidget.options.values = opts;
self.batchSelectorWidget.value = (this.comparerWidget.currentPairIndex + 1).toString();
}
}
if (self.prevButton)
self.prevButton.hidden = !showCarouselControls;
if (self.nextButton)
self.nextButton.hidden = !showCarouselControls;
if (self.autoPlayButton)
self.autoPlayButton.hidden = !showCarouselControls;
if (self.pairInfoWidget) {
self.pairInfoWidget.hidden = !showCarouselControls;
if (showCarouselControls && this.comparerWidget)
self.pairInfoWidget.value = `${this.comparerWidget.currentPairIndex + 1} / ${this.comparerWidget.maxPairs}`;
}
if (self.batchPrevButton)
self.batchPrevButton.hidden = !showBatchPagination;
if (self.batchNextButton)
self.batchNextButton.hidden = !showBatchPagination;
if (self.batchPageInfoWidget) {
self.batchPageInfoWidget.hidden = !showBatchPagination;
if (showBatchPagination && this.comparerWidget)
self.batchPageInfoWidget.value = `Page ${this.comparerWidget.currentBatchPage + 1} / ${this.comparerWidget.maxBatchPages}`;
}
if (self.onionSkinOpacitySlider) {
self.onionSkinOpacitySlider.hidden = !showOnionSkinSlider;
if (showOnionSkinSlider)
self.onionSkinOpacitySlider.value = this.properties.onionSkinOpacity;
}
};
const originalComputeSize = nodeType.prototype.computeSize;
nodeType.prototype.computeSize = function (out) {
const size = originalComputeSize ? originalComputeSize.call(this, out) : [700, 600];
if (this.comparerWidget) {
const widgetSize = this.comparerWidget.computeSize(size[0]);
let extra = 60;
const mode = this.properties.comparer_mode;
const hasMultiplePairs = this.comparerWidget.maxPairs > 1;
if (mode === "Carousel")
extra += 120;
else if (["Slider", "Click", "Side-by-Side", "Stacked"].includes(mode) && hasMultiplePairs)
extra += 35;
else if (mode === "Batch" && this.comparerWidget.maxBatchPages > 1)
extra += 90;
else if (mode === "Onion Skin") {
extra += 35;
if (hasMultiplePairs)
extra += 35;
}
size[1] = Math.max(size[1], widgetSize[1] + extra);
}
return size;
};
const originalOnExecuted = nodeType.prototype.onExecuted;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
nodeType.prototype.onExecuted = function (message) {
let result;
if (originalOnExecuted)
result = originalOnExecuted.call(this, message);
if (message && typeof message === 'object') {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const self = this;
const images = message.ui?.images || message.images;
if (images && images.length > 0 && this.comparerWidget) {
this.comparerWidget.value = { images };
const [w, h] = self.size;
self.setSize([Math.max(w, 700), Math.max(h, 600)]);
self.setDirtyCanvas(true, true);
}
}
return result || message;
};
nodeType.prototype.setIsPointerDown = function (down = this.isPointerDown) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const self = this;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const appObj = app;
const newIsDown = down && !!appObj.canvas.pointer_is_down;
if (this.isPointerDown !== newIsDown) {
this.isPointerDown = newIsDown;
self.setDirtyCanvas(true, false);
}
this.imageIndex = this.isPointerDown ? 1 : 0;
if (this.isPointerDown)
requestAnimationFrame(() => { this.setIsPointerDown(); });
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
nodeType.prototype.onMouseDown = function () { this.setIsPointerDown(true); return false; };
// eslint-disable-next-line @typescript-eslint/no-explicit-any
nodeType.prototype.onMouseEnter = function () {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const appObj = app;
this.setIsPointerDown(!!appObj.canvas.pointer_is_down);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
this.isPointerOver = true;
this.setDirtyCanvas(true, false);
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
nodeType.prototype.onMouseLeave = function () {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
this.setIsPointerDown(false);
this.isPointerOver = false;
this.setDirtyCanvas(true, false);
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
nodeType.prototype.onMouseMove = function (_event, pos) {
this.pointerOverPos = [...pos];
// eslint-disable-next-line @typescript-eslint/no-explicit-any
if (this.properties.comparer_mode === "Slider")
this.setDirtyCanvas(true, false);
else if (this.properties.comparer_mode === "Click")
this.imageIndex = pos[0] > this.size[0] / 2 ? 1 : 0;
return true;
};
// Context menu options
const origGetExtraMenuOptions = nodeType.prototype.getExtraMenuOptions;
nodeType.prototype.getExtraMenuOptions = function (_, options) {
if (origGetExtraMenuOptions)
origGetExtraMenuOptions.apply(this, arguments);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const self = this;
options.push(null); // Separator
const currentMode = this.properties.comparer_mode || "Slider";
options.push({
content: "Layout Mode",
has_submenu: true,
submenu: {
options: COMPARER_MODES.map(mode => ({
content: mode === currentMode ? `✓ ${mode}` : mode,
callback: () => {
this.properties.comparer_mode = mode;
if (self.layoutWidget)
self.layoutWidget.value = mode;
this.updateControlsVisibility();
self.setDirtyCanvas(true, false);
}
}))
}
});
options.push({
content: "Reset to Default Size",
callback: () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const selfAny = this;
selfAny.properties.user_resized = false;
if (selfAny.setSize && selfAny.computeSize)
selfAny.setSize(selfAny.computeSize());
selfAny.setDirtyCanvas(true, false);
}
});
};
}
});
//# sourceMappingURL=advanced_comparer.js.map