736 lines
22 KiB
JavaScript
736 lines
22 KiB
JavaScript
import { app } from "/scripts/app.js";
|
|
import { applyAdaptiveCanvasOnly, isVueNodes } from "../shared/nodes2.mjs";
|
|
|
|
const NODE_NAME = "DumasImageCompare";
|
|
const BRAND = "#e66a2c";
|
|
const BG = "#1c1d1f";
|
|
const PANEL = "#2a2c2e";
|
|
const BORDER = "#444";
|
|
const TEXT = "#d8d8d8";
|
|
const MUTED = "#9a9a9a";
|
|
|
|
const MODE_LABELS = ["Show 1", "Show 2", "Left Right", "Up Down", "Overlay", "Difference"];
|
|
const MODE_HINTS = ["", "", "Hover to slide left / right", "Hover to slide up / down", "", "Shows pixel differences"];
|
|
const INIT_W = 440;
|
|
const IMAGE_TOP = 54;
|
|
const INIT_H = INIT_W + IMAGE_TOP;
|
|
const BUTTON_GAP = 3;
|
|
const BUTTON_H = 18;
|
|
const BUTTON_TOP = 10;
|
|
const INFO_TOP = 30;
|
|
const BTN_X = 80;
|
|
const MIN_W = BTN_X + 6 * 56 + BUTTON_GAP * 5 + 6;
|
|
const MIN_H = IMAGE_TOP + 100;
|
|
const TOP_TRIM = 8;
|
|
const SIZE_FONT = "12px 'Segoe UI',sans-serif";
|
|
const BADGE_FONT = "10px 'Segoe UI',sans-serif";
|
|
const BADGE_R = 8;
|
|
const SIZE_RESERVE = 86;
|
|
const SIZE_GAP = 6;
|
|
const SLIDER_PAD = 72;
|
|
|
|
function clamp(value, min, max) {
|
|
return Math.max(min, Math.min(max, value));
|
|
}
|
|
|
|
function loadCompareImage(node, meta, key) {
|
|
const image = new Image();
|
|
image.crossOrigin = "anonymous";
|
|
image.onload = () => {
|
|
node[key] = image;
|
|
repaintNode(node);
|
|
};
|
|
image.src = `/view?filename=${encodeURIComponent(meta.filename)}&type=${encodeURIComponent(meta.type)}&subfolder=${encodeURIComponent(meta.subfolder || "")}&t=${Date.now()}`;
|
|
}
|
|
|
|
function repaintNode(node) {
|
|
node.setDirtyCanvas?.(true, true);
|
|
node._cmpDomRender?.();
|
|
}
|
|
|
|
function rowLayout(width) {
|
|
const leftPad = isVueNodes() ? 12 : BTN_X;
|
|
const rightPad = isVueNodes() ? 12 : 6;
|
|
const buttonWidth = Math.max(
|
|
30,
|
|
Math.floor((width - leftPad - rightPad - BUTTON_GAP * (MODE_LABELS.length - 1)) / MODE_LABELS.length),
|
|
);
|
|
return { leftPad, rightPad, buttonWidth };
|
|
}
|
|
|
|
function getButtons(node, width) {
|
|
const layout = rowLayout(width);
|
|
return MODE_LABELS.map((label, index) => ({
|
|
label,
|
|
index,
|
|
x: layout.leftPad + index * (layout.buttonWidth + BUTTON_GAP),
|
|
y: BUTTON_TOP,
|
|
w: layout.buttonWidth,
|
|
h: BUTTON_H,
|
|
active: node._cmpMode === index,
|
|
}));
|
|
}
|
|
|
|
function getInfoRect(node, width) {
|
|
const layout = rowLayout(width);
|
|
const full = {
|
|
x: layout.leftPad,
|
|
y: INFO_TOP,
|
|
w: layout.buttonWidth * MODE_LABELS.length + BUTTON_GAP * (MODE_LABELS.length - 1),
|
|
h: BUTTON_H,
|
|
};
|
|
const reserveLeft = node?._cmpImg1 ? SIZE_RESERVE + SIZE_GAP : 0;
|
|
const reserveRight = node?._cmpImg2 ? SIZE_RESERVE + SIZE_GAP : 0;
|
|
return {
|
|
x: full.x + reserveLeft,
|
|
y: full.y,
|
|
w: Math.max(40, full.w - reserveLeft - reserveRight),
|
|
h: full.h,
|
|
};
|
|
}
|
|
|
|
function getSliderRect(node, width) {
|
|
const info = getInfoRect(node, width);
|
|
return {
|
|
x: info.x + SLIDER_PAD,
|
|
y: info.y + info.h / 2 - 3,
|
|
w: Math.max(20, info.w - SLIDER_PAD - 24),
|
|
h: 6,
|
|
};
|
|
}
|
|
|
|
function getImageRect(width, height) {
|
|
return {
|
|
x: 6,
|
|
y: IMAGE_TOP,
|
|
w: Math.max(40, width - 12),
|
|
h: Math.max(80, height - IMAGE_TOP - 6),
|
|
};
|
|
}
|
|
|
|
function pointInRect(x, y, rect) {
|
|
return x >= rect.x && x <= rect.x + rect.w && y >= rect.y && y <= rect.y + rect.h;
|
|
}
|
|
|
|
function getAvailableImages(node) {
|
|
const entries = [];
|
|
if (node._cmpImg1) entries.push({ slot: 1, image: node._cmpImg1 });
|
|
if (node._cmpImg2) entries.push({ slot: 2, image: node._cmpImg2 });
|
|
return entries;
|
|
}
|
|
|
|
function getPrimaryImage(node) {
|
|
return node._cmpImg1 || node._cmpImg2 || null;
|
|
}
|
|
|
|
function getSecondaryImage(node) {
|
|
return node._cmpImg2 || node._cmpImg1 || null;
|
|
}
|
|
|
|
function fitImage(rect, image) {
|
|
if (!image) return null;
|
|
const scale = Math.min(rect.w / image.naturalWidth, rect.h / image.naturalHeight);
|
|
const drawWidth = image.naturalWidth * scale;
|
|
const drawHeight = image.naturalHeight * scale;
|
|
return {
|
|
x: rect.x + (rect.w - drawWidth) / 2,
|
|
y: rect.y + (rect.h - drawHeight) / 2,
|
|
w: drawWidth,
|
|
h: drawHeight,
|
|
};
|
|
}
|
|
|
|
function drawButton(ctx, button, hovered) {
|
|
ctx.save();
|
|
ctx.fillStyle = button.active ? BRAND : PANEL;
|
|
ctx.strokeStyle = button.active ? BRAND : hovered ? BRAND : BORDER;
|
|
ctx.lineWidth = 1;
|
|
ctx.beginPath();
|
|
if (ctx.roundRect) ctx.roundRect(button.x, button.y, button.w, button.h, 4);
|
|
else ctx.rect(button.x, button.y, button.w, button.h);
|
|
ctx.fill();
|
|
ctx.stroke();
|
|
ctx.fillStyle = button.active ? "#ffffff" : hovered ? "#ebebeb" : MUTED;
|
|
let fontSize = 9;
|
|
ctx.font = `${fontSize}px 'Segoe UI',sans-serif`;
|
|
while (fontSize > 7 && ctx.measureText(button.label).width > button.w - 8) {
|
|
fontSize -= 0.5;
|
|
ctx.font = `${fontSize}px 'Segoe UI',sans-serif`;
|
|
}
|
|
ctx.textAlign = "center";
|
|
ctx.textBaseline = "middle";
|
|
ctx.fillText(button.label, button.x + button.w / 2, button.y + button.h / 2);
|
|
ctx.restore();
|
|
}
|
|
|
|
function dimsText(image) {
|
|
return image ? `${image.naturalWidth}x${image.naturalHeight}` : "";
|
|
}
|
|
|
|
function sizesDiffer(node) {
|
|
return !!(
|
|
node?._cmpImg1
|
|
&& node?._cmpImg2
|
|
&& (node._cmpImg1.naturalWidth !== node._cmpImg2.naturalWidth
|
|
|| node._cmpImg1.naturalHeight !== node._cmpImg2.naturalHeight)
|
|
);
|
|
}
|
|
|
|
function fillTextVCenter(ctx, text, x, yMid) {
|
|
const metrics = ctx.measureText(text);
|
|
if (metrics.actualBoundingBoxAscent != null && metrics.actualBoundingBoxDescent != null) {
|
|
ctx.textBaseline = "alphabetic";
|
|
ctx.fillText(text, x, yMid + (metrics.actualBoundingBoxAscent - metrics.actualBoundingBoxDescent) / 2);
|
|
return;
|
|
}
|
|
ctx.textBaseline = "middle";
|
|
ctx.fillText(text, x, yMid);
|
|
}
|
|
|
|
function drawSizeBadge(ctx, cx, cy, label) {
|
|
ctx.save();
|
|
ctx.fillStyle = BRAND;
|
|
ctx.beginPath();
|
|
ctx.arc(cx, cy, BADGE_R, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
ctx.fillStyle = "#ffffff";
|
|
ctx.font = BADGE_FONT;
|
|
ctx.textAlign = "center";
|
|
ctx.textBaseline = "middle";
|
|
ctx.fillText(String(label), cx, cy + 0.5);
|
|
ctx.restore();
|
|
}
|
|
|
|
function drawSizeLabels(ctx, node, width) {
|
|
if (!node._cmpImg1 && !node._cmpImg2) return;
|
|
|
|
const layout = rowLayout(width);
|
|
const infoFull = {
|
|
x: layout.leftPad,
|
|
y: INFO_TOP,
|
|
w: layout.buttonWidth * MODE_LABELS.length + BUTTON_GAP * (MODE_LABELS.length - 1),
|
|
h: BUTTON_H,
|
|
};
|
|
const rightEdge = infoFull.x + infoFull.w;
|
|
const yMid = infoFull.y + infoFull.h / 2;
|
|
const textColor = sizesDiffer(node) ? BRAND : "#cfcfcf";
|
|
|
|
if (node._cmpImg1) {
|
|
ctx.save();
|
|
ctx.beginPath();
|
|
ctx.rect(infoFull.x, infoFull.y, SIZE_RESERVE, infoFull.h);
|
|
ctx.clip();
|
|
const cx = infoFull.x + BADGE_R;
|
|
drawSizeBadge(ctx, cx, yMid, 1);
|
|
ctx.font = SIZE_FONT;
|
|
ctx.fillStyle = textColor;
|
|
ctx.textAlign = "left";
|
|
fillTextVCenter(ctx, dimsText(node._cmpImg1), cx + BADGE_R + 5, yMid);
|
|
ctx.restore();
|
|
}
|
|
|
|
if (node._cmpImg2) {
|
|
ctx.save();
|
|
ctx.beginPath();
|
|
ctx.rect(rightEdge - SIZE_RESERVE, infoFull.y, SIZE_RESERVE, infoFull.h);
|
|
ctx.clip();
|
|
const cx = rightEdge - BADGE_R;
|
|
drawSizeBadge(ctx, cx, yMid, 2);
|
|
ctx.font = SIZE_FONT;
|
|
ctx.fillStyle = textColor;
|
|
ctx.textAlign = "right";
|
|
fillTextVCenter(ctx, dimsText(node._cmpImg2), cx - BADGE_R - 5, yMid);
|
|
ctx.restore();
|
|
}
|
|
}
|
|
|
|
function drawInfoBar(ctx, node, width) {
|
|
const info = getInfoRect(node, width);
|
|
ctx.save();
|
|
ctx.fillStyle = PANEL;
|
|
ctx.strokeStyle = BORDER;
|
|
ctx.lineWidth = 1;
|
|
ctx.beginPath();
|
|
if (ctx.roundRect) ctx.roundRect(info.x, info.y, info.w, info.h, 4);
|
|
else ctx.rect(info.x, info.y, info.w, info.h);
|
|
ctx.fill();
|
|
ctx.stroke();
|
|
|
|
const images = getAvailableImages(node);
|
|
if (!images.length) {
|
|
ctx.fillStyle = MUTED;
|
|
ctx.font = "11px 'Segoe UI',sans-serif";
|
|
ctx.textAlign = "center";
|
|
ctx.textBaseline = "middle";
|
|
ctx.fillText("Connect images and run to compare", info.x + info.w / 2, info.y + info.h / 2);
|
|
ctx.restore();
|
|
return;
|
|
}
|
|
|
|
if (node._cmpMode === 4) {
|
|
const slider = getSliderRect(node, width);
|
|
ctx.fillStyle = TEXT;
|
|
ctx.font = "11px 'Segoe UI',sans-serif";
|
|
ctx.textAlign = "left";
|
|
ctx.textBaseline = "middle";
|
|
ctx.fillText(`Opacity ${Math.round(node._cmpOpacity * 100)}%`, info.x + 10, info.y + info.h / 2);
|
|
|
|
ctx.fillStyle = "#161719";
|
|
ctx.beginPath();
|
|
if (ctx.roundRect) ctx.roundRect(slider.x, slider.y, slider.w, slider.h, 3);
|
|
else ctx.rect(slider.x, slider.y, slider.w, slider.h);
|
|
ctx.fill();
|
|
|
|
ctx.fillStyle = BRAND;
|
|
ctx.beginPath();
|
|
if (ctx.roundRect) ctx.roundRect(slider.x, slider.y, slider.w * node._cmpOpacity, slider.h, 3);
|
|
else ctx.rect(slider.x, slider.y, slider.w * node._cmpOpacity, slider.h);
|
|
ctx.fill();
|
|
|
|
const knobX = slider.x + slider.w * node._cmpOpacity;
|
|
ctx.fillStyle = "#ffffff";
|
|
ctx.beginPath();
|
|
ctx.arc(knobX, slider.y + slider.h / 2, 7, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
ctx.restore();
|
|
return;
|
|
}
|
|
|
|
if (node._cmpMode >= 2) {
|
|
const hint = MODE_HINTS[node._cmpMode];
|
|
if (hint) {
|
|
ctx.fillStyle = TEXT;
|
|
ctx.font = "11px 'Segoe UI',sans-serif";
|
|
ctx.textAlign = "center";
|
|
ctx.textBaseline = "middle";
|
|
ctx.fillText(hint, info.x + info.w / 2, info.y + info.h / 2);
|
|
}
|
|
}
|
|
ctx.restore();
|
|
drawSizeLabels(ctx, node, width);
|
|
}
|
|
|
|
function drawEmptyState(ctx, rect) {
|
|
ctx.save();
|
|
ctx.fillStyle = BG;
|
|
ctx.fillRect(rect.x, rect.y, rect.w, rect.h);
|
|
ctx.strokeStyle = BORDER;
|
|
ctx.lineWidth = 1;
|
|
ctx.strokeRect(rect.x, rect.y, rect.w, rect.h);
|
|
ctx.fillStyle = MUTED;
|
|
ctx.font = "13px sans-serif";
|
|
ctx.textAlign = "center";
|
|
ctx.textBaseline = "middle";
|
|
ctx.fillText("No images loaded yet", rect.x + rect.w / 2, rect.y + rect.h / 2 - 10);
|
|
ctx.font = "11px sans-serif";
|
|
ctx.fillText("Run the workflow after connecting one or two IMAGE inputs", rect.x + rect.w / 2, rect.y + rect.h / 2 + 12);
|
|
ctx.restore();
|
|
}
|
|
|
|
function drawSingleImage(ctx, rect, image) {
|
|
const fitted = fitImage(rect, image);
|
|
if (!fitted) return;
|
|
ctx.drawImage(image, fitted.x, fitted.y, fitted.w, fitted.h);
|
|
}
|
|
|
|
function drawCompare(ctx, node, rect) {
|
|
const image1 = getPrimaryImage(node);
|
|
const image2 = getSecondaryImage(node);
|
|
|
|
if (!image1 && !image2) {
|
|
drawEmptyState(ctx, rect);
|
|
return;
|
|
}
|
|
|
|
ctx.save();
|
|
ctx.fillStyle = BG;
|
|
ctx.fillRect(rect.x, rect.y, rect.w, rect.h);
|
|
ctx.strokeStyle = BORDER;
|
|
ctx.lineWidth = 1;
|
|
ctx.strokeRect(rect.x, rect.y, rect.w, rect.h);
|
|
|
|
if (!image1 || !image2 || node._cmpMode === 0 || node._cmpMode === 1) {
|
|
const chosenImage = node._cmpMode === 0 ? node._cmpImg1 || node._cmpImg2 : node._cmpImg2 || node._cmpImg1;
|
|
drawSingleImage(ctx, rect, chosenImage || image1 || image2);
|
|
ctx.restore();
|
|
return;
|
|
}
|
|
|
|
const first = fitImage(rect, image1);
|
|
const second = fitImage(rect, image2);
|
|
if (!first || !second) {
|
|
ctx.restore();
|
|
return;
|
|
}
|
|
|
|
if (node._cmpMode === 2) {
|
|
ctx.save();
|
|
ctx.beginPath();
|
|
ctx.rect(rect.x, rect.y, rect.w * node._cmpSplitX, rect.h);
|
|
ctx.clip();
|
|
ctx.drawImage(image1, first.x, first.y, first.w, first.h);
|
|
ctx.restore();
|
|
|
|
ctx.save();
|
|
ctx.beginPath();
|
|
ctx.rect(rect.x + rect.w * node._cmpSplitX, rect.y, rect.w * (1 - node._cmpSplitX), rect.h);
|
|
ctx.clip();
|
|
ctx.drawImage(image2, second.x, second.y, second.w, second.h);
|
|
ctx.restore();
|
|
|
|
const guideX = rect.x + rect.w * node._cmpSplitX;
|
|
ctx.strokeStyle = "rgba(255,255,255,0.5)";
|
|
ctx.beginPath();
|
|
ctx.moveTo(guideX, rect.y);
|
|
ctx.lineTo(guideX, rect.y + rect.h);
|
|
ctx.stroke();
|
|
ctx.restore();
|
|
return;
|
|
}
|
|
|
|
if (node._cmpMode === 3) {
|
|
ctx.save();
|
|
ctx.beginPath();
|
|
ctx.rect(rect.x, rect.y, rect.w, rect.h * node._cmpSplitY);
|
|
ctx.clip();
|
|
ctx.drawImage(image1, first.x, first.y, first.w, first.h);
|
|
ctx.restore();
|
|
|
|
ctx.save();
|
|
ctx.beginPath();
|
|
ctx.rect(rect.x, rect.y + rect.h * node._cmpSplitY, rect.w, rect.h * (1 - node._cmpSplitY));
|
|
ctx.clip();
|
|
ctx.drawImage(image2, second.x, second.y, second.w, second.h);
|
|
ctx.restore();
|
|
|
|
const guideY = rect.y + rect.h * node._cmpSplitY;
|
|
ctx.strokeStyle = "rgba(255,255,255,0.5)";
|
|
ctx.beginPath();
|
|
ctx.moveTo(rect.x, guideY);
|
|
ctx.lineTo(rect.x + rect.w, guideY);
|
|
ctx.stroke();
|
|
ctx.restore();
|
|
return;
|
|
}
|
|
|
|
if (node._cmpMode === 4) {
|
|
ctx.drawImage(image1, first.x, first.y, first.w, first.h);
|
|
ctx.globalAlpha = node._cmpOpacity;
|
|
ctx.drawImage(image2, second.x, second.y, second.w, second.h);
|
|
ctx.globalAlpha = 1;
|
|
ctx.restore();
|
|
return;
|
|
}
|
|
|
|
ctx.drawImage(image1, first.x, first.y, first.w, first.h);
|
|
ctx.globalCompositeOperation = "difference";
|
|
ctx.drawImage(image2, second.x, second.y, second.w, second.h);
|
|
ctx.globalCompositeOperation = "source-over";
|
|
ctx.restore();
|
|
}
|
|
|
|
function paintNode(ctx, node, width, height, mouse) {
|
|
const buttons = getButtons(node, width);
|
|
for (const button of buttons) {
|
|
drawButton(ctx, button, mouse ? pointInRect(mouse.x, mouse.y, button) : false);
|
|
}
|
|
drawInfoBar(ctx, node, width);
|
|
drawCompare(ctx, node, getImageRect(width, height));
|
|
if (node._cmpMode === 2 || node._cmpMode === 3) {
|
|
ctx.save();
|
|
ctx.fillStyle = MUTED;
|
|
ctx.font = "11px 'Segoe UI',sans-serif";
|
|
ctx.textAlign = "right";
|
|
ctx.textBaseline = "bottom";
|
|
ctx.fillText("Move over the image to slide", width - 8, height - 4);
|
|
ctx.restore();
|
|
}
|
|
}
|
|
|
|
function updateImageInteraction(node, x, y, width, height) {
|
|
const rect = getImageRect(width, height);
|
|
if (!pointInRect(x, y, rect)) return false;
|
|
if (node._cmpMode === 2) {
|
|
node._cmpSplitX = clamp((x - rect.x) / rect.w, 0, 1);
|
|
return true;
|
|
}
|
|
if (node._cmpMode === 3) {
|
|
node._cmpSplitY = clamp((y - rect.y) / rect.h, 0, 1);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function updateOpacityFromX(node, x, width) {
|
|
const slider = getSliderRect(node, width);
|
|
node._cmpOpacity = clamp((x - slider.x) / slider.w, 0, 1);
|
|
}
|
|
|
|
function getCursor(node, x, y, width, height) {
|
|
const buttons = getButtons(node, width);
|
|
if (buttons.some((button) => pointInRect(x, y, button))) return "pointer";
|
|
if (node._cmpMode === 4) {
|
|
const slider = getSliderRect(node, width);
|
|
if (pointInRect(x, y, {
|
|
x: slider.x - 10,
|
|
y: slider.y - 10,
|
|
w: slider.w + 20,
|
|
h: slider.h + 20,
|
|
})) {
|
|
return "pointer";
|
|
}
|
|
}
|
|
const rect = getImageRect(width, height);
|
|
if (pointInRect(x, y, rect)) {
|
|
if (node._cmpMode === 2) return "ew-resize";
|
|
if (node._cmpMode === 3) return "ns-resize";
|
|
}
|
|
return "default";
|
|
}
|
|
|
|
function pointerDown(node, x, y, width, height) {
|
|
const buttons = getButtons(node, width);
|
|
for (const button of buttons) {
|
|
if (pointInRect(x, y, button)) {
|
|
node._cmpMode = button.index;
|
|
repaintNode(node);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
const slider = getSliderRect(node, width);
|
|
if (node._cmpMode === 4 && pointInRect(x, y, {
|
|
x: slider.x - 10,
|
|
y: slider.y - 10,
|
|
w: slider.w + 20,
|
|
h: slider.h + 20,
|
|
})) {
|
|
node._cmpDraggingOpacity = true;
|
|
updateOpacityFromX(node, x, width);
|
|
repaintNode(node);
|
|
return true;
|
|
}
|
|
|
|
if (updateImageInteraction(node, x, y, width, height)) {
|
|
repaintNode(node);
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
function pointerMove(node, x, y, width, height) {
|
|
let changed = false;
|
|
if (node._cmpDraggingOpacity && node._cmpMode === 4) {
|
|
updateOpacityFromX(node, x, width);
|
|
changed = true;
|
|
} else if (updateImageInteraction(node, x, y, width, height)) {
|
|
changed = true;
|
|
}
|
|
node._cmpMouse = { x, y };
|
|
if (changed) repaintNode(node);
|
|
}
|
|
|
|
function pointerUp(node) {
|
|
node._cmpDraggingOpacity = false;
|
|
}
|
|
|
|
function createDomWidget(node) {
|
|
const root = document.createElement("div");
|
|
root.style.cssText = "position:relative;width:100%;flex:1 1 0;min-height:0;box-sizing:border-box;cursor:default;";
|
|
const canvas = document.createElement("canvas");
|
|
canvas.style.cssText = "position:absolute;inset:0;width:100%;height:100%;display:block;";
|
|
root.appendChild(canvas);
|
|
|
|
const widget = node.addDOMWidget("dumas_compare", "dumas_compare", root, {
|
|
serialize: false,
|
|
hideOnZoom: false,
|
|
getMinHeight: () => MIN_H,
|
|
});
|
|
widget.computeLayoutSize = () => ({ minHeight: MIN_H, minWidth: 1 });
|
|
applyAdaptiveCanvasOnly(widget);
|
|
|
|
const render = () => {
|
|
const cssWidth = root.clientWidth;
|
|
const cssHeight = root.clientHeight;
|
|
if (cssWidth <= 0 || cssHeight <= 0) return;
|
|
|
|
const scale = (window.devicePixelRatio || 1) * Math.max(1, app.canvas?.ds?.scale || 1);
|
|
const width = Math.round(cssWidth * scale);
|
|
const height = Math.round(cssHeight * scale);
|
|
if (canvas.width !== width) canvas.width = width;
|
|
if (canvas.height !== height) canvas.height = height;
|
|
|
|
const ctx = canvas.getContext("2d");
|
|
ctx.setTransform(scale, 0, 0, scale, 0, 0);
|
|
ctx.clearRect(0, 0, cssWidth, cssHeight);
|
|
ctx.translate(0, -TOP_TRIM);
|
|
paintNode(ctx, node, cssWidth, cssHeight + TOP_TRIM, node._cmpMouse || null);
|
|
};
|
|
|
|
node._cmpDomRender = render;
|
|
|
|
const localPos = (event) => {
|
|
const rect = root.getBoundingClientRect();
|
|
const scaleX = rect.width ? root.clientWidth / rect.width : 1;
|
|
const scaleY = rect.height ? root.clientHeight / rect.height : 1;
|
|
return [
|
|
(event.clientX - rect.left) * scaleX,
|
|
(event.clientY - rect.top) * scaleY + TOP_TRIM,
|
|
];
|
|
};
|
|
|
|
root.addEventListener("pointerdown", (event) => {
|
|
const [x, y] = localPos(event);
|
|
root.style.cursor = getCursor(node, x, y, root.clientWidth, root.clientHeight + TOP_TRIM);
|
|
if (pointerDown(node, x, y, root.clientWidth, root.clientHeight + TOP_TRIM)) {
|
|
event.stopPropagation();
|
|
if (node._cmpDraggingOpacity) {
|
|
try {
|
|
root.setPointerCapture(event.pointerId);
|
|
} catch (_error) {
|
|
// Ignore capture failures and keep the node functional.
|
|
}
|
|
}
|
|
render();
|
|
}
|
|
});
|
|
|
|
root.addEventListener("pointermove", (event) => {
|
|
const [x, y] = localPos(event);
|
|
pointerMove(node, x, y, root.clientWidth, root.clientHeight + TOP_TRIM);
|
|
root.style.cursor = getCursor(node, x, y, root.clientWidth, root.clientHeight + TOP_TRIM);
|
|
render();
|
|
});
|
|
|
|
root.addEventListener("pointerup", (event) => {
|
|
pointerUp(node);
|
|
const [x, y] = localPos(event);
|
|
root.style.cursor = getCursor(node, x, y, root.clientWidth, root.clientHeight + TOP_TRIM);
|
|
try {
|
|
root.releasePointerCapture(event.pointerId);
|
|
} catch (_error) {
|
|
// Ignore release failures.
|
|
}
|
|
render();
|
|
});
|
|
|
|
root.addEventListener("pointerleave", () => {
|
|
pointerUp(node);
|
|
node._cmpMouse = null;
|
|
root.style.cursor = "default";
|
|
render();
|
|
});
|
|
|
|
const resizeObserver = new ResizeObserver(() => render());
|
|
resizeObserver.observe(root);
|
|
node._cmpDomResizeObserver = resizeObserver;
|
|
requestAnimationFrame(render);
|
|
}
|
|
|
|
app.registerExtension({
|
|
name: "Dumas.Compare",
|
|
async beforeRegisterNodeDef(nodeType, nodeData) {
|
|
if (nodeData.name !== NODE_NAME) return;
|
|
|
|
const originalNodeCreated = nodeType.prototype.onNodeCreated;
|
|
nodeType.prototype.onNodeCreated = function () {
|
|
originalNodeCreated?.apply(this, arguments);
|
|
this._cmpMode = 1;
|
|
this._cmpOpacity = 0.5;
|
|
this._cmpSplitX = 0.5;
|
|
this._cmpSplitY = 0.5;
|
|
this._cmpImg1 = null;
|
|
this._cmpImg2 = null;
|
|
this._cmpDraggingOpacity = false;
|
|
this._cmpMouse = null;
|
|
this.hideOutputImages = true;
|
|
this.size[0] = Math.max(this.size[0] || 0, INIT_W);
|
|
this.size[1] = Math.max(this.size[1] || 0, INIT_H);
|
|
if (isVueNodes()) createDomWidget(this);
|
|
};
|
|
|
|
nodeType.prototype.onExecuted = function (output) {
|
|
this.imgs = null;
|
|
const images = Array.isArray(output?.images) ? output.images : [];
|
|
let slot1 = null;
|
|
let slot2 = null;
|
|
for (const image of images) {
|
|
if (image?.slot === 1) slot1 = image;
|
|
if (image?.slot === 2) slot2 = image;
|
|
}
|
|
if (!slot1 && !slot2 && images.length) {
|
|
slot1 = images[0] || null;
|
|
slot2 = images[1] || null;
|
|
}
|
|
if (slot1) loadCompareImage(this, slot1, "_cmpImg1");
|
|
else this._cmpImg1 = null;
|
|
if (slot2) loadCompareImage(this, slot2, "_cmpImg2");
|
|
else this._cmpImg2 = null;
|
|
repaintNode(this);
|
|
};
|
|
|
|
nodeType.prototype.onDrawBackground = function () {
|
|
if (this.flags?.collapsed) return;
|
|
this.imgs = null;
|
|
};
|
|
|
|
const originalDrawForeground = nodeType.prototype.onDrawForeground;
|
|
nodeType.prototype.onDrawForeground = function (ctx) {
|
|
originalDrawForeground?.call(this, ctx);
|
|
if (this.flags?.collapsed || isVueNodes()) return;
|
|
this.size[0] = Math.max(this.size[0], MIN_W);
|
|
this.size[1] = Math.max(this.size[1], MIN_H);
|
|
const graphMouse = app.canvas?.graph_mouse;
|
|
const mouse = graphMouse ? { x: graphMouse[0] - this.pos[0], y: graphMouse[1] - this.pos[1] } : null;
|
|
paintNode(ctx, this, this.size[0], this.size[1], mouse);
|
|
};
|
|
|
|
const originalMouseDown = nodeType.prototype.onMouseDown;
|
|
nodeType.prototype.onMouseDown = function (event, pos) {
|
|
if (!isVueNodes() && pointerDown(this, pos[0], pos[1], this.size[0], this.size[1])) {
|
|
repaintNode(this);
|
|
return true;
|
|
}
|
|
return originalMouseDown?.call(this, event, pos);
|
|
};
|
|
|
|
const originalMouseMove = nodeType.prototype.onMouseMove;
|
|
nodeType.prototype.onMouseMove = function (event, pos) {
|
|
if (!isVueNodes()) pointerMove(this, pos[0], pos[1], this.size[0], this.size[1]);
|
|
return originalMouseMove?.call(this, event, pos);
|
|
};
|
|
|
|
const originalMouseUp = nodeType.prototype.onMouseUp;
|
|
nodeType.prototype.onMouseUp = function (event, pos) {
|
|
if (!isVueNodes()) {
|
|
pointerUp(this);
|
|
repaintNode(this);
|
|
}
|
|
return originalMouseUp?.call(this, event, pos);
|
|
};
|
|
|
|
const originalMouseLeave = nodeType.prototype.onMouseLeave;
|
|
nodeType.prototype.onMouseLeave = function (event) {
|
|
if (!isVueNodes()) {
|
|
pointerUp(this);
|
|
this._cmpMouse = null;
|
|
repaintNode(this);
|
|
}
|
|
return originalMouseLeave?.call(this, event);
|
|
};
|
|
|
|
const originalRemoved = nodeType.prototype.onRemoved;
|
|
nodeType.prototype.onRemoved = function () {
|
|
try {
|
|
this._cmpDomResizeObserver?.disconnect();
|
|
} catch (_error) {
|
|
// Ignore cleanup failures.
|
|
}
|
|
this._cmpDomResizeObserver = null;
|
|
this._cmpDomRender = null;
|
|
return originalRemoved?.apply(this, arguments);
|
|
};
|
|
},
|
|
});
|