216 lines
8.7 KiB
JavaScript
216 lines
8.7 KiB
JavaScript
import { app } from "/scripts/app.js";
|
|
|
|
app.registerExtension({
|
|
name: "SBCODE.ImageCompareNode",
|
|
|
|
async beforeRegisterNodeDef(nodeType, nodeData) {
|
|
if (nodeData.name !== "ImageCompareNode") return;
|
|
|
|
// Helper for aspect-correct "contain" fit inside a box
|
|
function fitContain(srcW, srcH, maxW, maxH) {
|
|
if (!srcW || !srcH || !maxW || !maxH) {
|
|
return { x: 0, y: 0, w: 0, h: 0 };
|
|
}
|
|
const s = Math.min(maxW / srcW, maxH / srcH);
|
|
const w = Math.max(1, Math.floor(srcW * s));
|
|
const h = Math.max(1, Math.floor(srcH * s));
|
|
const x = Math.floor((maxW - w) / 2);
|
|
const y = Math.floor((maxH - h) / 2);
|
|
return { x, y, w, h };
|
|
}
|
|
|
|
const origOnNodeCreated = nodeType.prototype.onNodeCreated;
|
|
nodeType.prototype.onNodeCreated = function () {
|
|
if (origOnNodeCreated) origOnNodeCreated.apply(this, arguments);
|
|
|
|
console.log("[SBCODE.ImageCompareNode] Node created:", this.title);
|
|
|
|
if (!this.size || this.size[0] < 100 || this.size[1] < 100) {
|
|
this.size = [532, 582];
|
|
}
|
|
|
|
this.sliderPos = 0.5;
|
|
this.dragging = false;
|
|
this.hovered = false;
|
|
|
|
// Common geometry used across mouse + draw
|
|
const getDrawGeom = () => {
|
|
const margin = 10;
|
|
const topOffset = 40; // leave space for title/widgets
|
|
const drawX = margin;
|
|
const drawY = margin + topOffset;
|
|
const drawW = this.size[0] - margin * 2;
|
|
const drawH = (this.size[1] - margin * 2) - topOffset;
|
|
return { drawX, drawY, drawW, drawH };
|
|
};
|
|
|
|
this.onMouseDown = function (_, pos) {
|
|
const { drawX, drawY, drawW, drawH } = getDrawGeom();
|
|
|
|
const x = pos[0] - drawX;
|
|
const y = pos[1] - drawY;
|
|
|
|
// Only react if inside draw area
|
|
if (x < 0 || x > drawW || y < 0 || y > drawH) return false;
|
|
|
|
const splitX = drawX + Math.floor(drawW * this.sliderPos);
|
|
const handleY = drawY + Math.floor(drawH / 2);
|
|
const dist = Math.hypot(pos[0] - splitX, pos[1] - handleY);
|
|
|
|
// Grab handle if close; otherwise jump slider to clicked x
|
|
if (dist < 15) {
|
|
this.dragging = true;
|
|
return true;
|
|
}
|
|
|
|
this.dragging = true;
|
|
this.sliderPos = Math.max(0, Math.min(1, x / drawW));
|
|
return true;
|
|
};
|
|
|
|
this.onMouseMove = function (e, pos) {
|
|
const { drawX, drawY, drawW, drawH } = getDrawGeom();
|
|
|
|
const splitX = drawX + Math.floor(drawW * this.sliderPos);
|
|
const handleY = drawY + Math.floor(drawH / 2);
|
|
const dist = Math.hypot(pos[0] - splitX, pos[1] - handleY);
|
|
this.hovered = dist < 15;
|
|
|
|
// Check if mouse button is no longer pressed (detect mouse up from event)
|
|
if (this.dragging && e && e.buttons !== undefined && e.buttons === 0) {
|
|
this.dragging = false;
|
|
}
|
|
|
|
if (this.dragging) {
|
|
let x = pos[0] - drawX;
|
|
x = Math.max(0, Math.min(drawW, x));
|
|
const newSliderPos = x / drawW;
|
|
|
|
// Only update canvas if slider position actually changed
|
|
if (Math.abs(newSliderPos - this.sliderPos) > 0.001) {
|
|
this.sliderPos = newSliderPos;
|
|
}
|
|
}
|
|
};
|
|
|
|
this.onDrawForeground = function (ctx) {
|
|
ctx.save();
|
|
|
|
const { drawX, drawY, drawW, drawH } = getDrawGeom();
|
|
|
|
// Background for preview area
|
|
ctx.fillStyle = "#111";
|
|
ctx.fillRect(drawX, drawY, drawW, drawH);
|
|
|
|
// Calculate aspect-correct positioning for images
|
|
let rectA = { x: 0, y: 0, w: drawW, h: drawH };
|
|
let rectB = { x: 0, y: 0, w: drawW, h: drawH };
|
|
|
|
if (this.imgA?.width && this.imgA?.height) {
|
|
rectA = fitContain(this.imgA.width, this.imgA.height, drawW, drawH);
|
|
}
|
|
if (this.imgB?.width && this.imgB?.height) {
|
|
rectB = fitContain(this.imgB.width, this.imgB.height, drawW, drawH);
|
|
}
|
|
|
|
// Draw B as the base (aspect-correct, centered)
|
|
if (this.imgB) {
|
|
ctx.drawImage(
|
|
this.imgB,
|
|
drawX + rectB.x,
|
|
drawY + rectB.y,
|
|
rectB.w,
|
|
rectB.h
|
|
);
|
|
}
|
|
|
|
// Draw A clipped by the slider (left part, aspect-correct, centered)
|
|
if (this.imgA) {
|
|
const splitX = drawX + Math.floor(drawW * this.sliderPos);
|
|
|
|
ctx.save();
|
|
// Clip the left portion of the whole draw area
|
|
ctx.beginPath();
|
|
ctx.rect(drawX, drawY, splitX - drawX, drawH);
|
|
ctx.clip();
|
|
|
|
ctx.drawImage(
|
|
this.imgA,
|
|
drawX + rectA.x,
|
|
drawY + rectA.y,
|
|
rectA.w,
|
|
rectA.h
|
|
);
|
|
ctx.restore();
|
|
|
|
// Slider line
|
|
ctx.strokeStyle = "#00e0ff";
|
|
ctx.lineWidth = 2;
|
|
ctx.beginPath();
|
|
ctx.moveTo(splitX, drawY);
|
|
ctx.lineTo(splitX, drawY + drawH);
|
|
ctx.stroke();
|
|
|
|
// Optional handle highlight when hovered/dragging
|
|
if (this.hovered || this.dragging) {
|
|
ctx.fillStyle = "#00e0ff";
|
|
ctx.beginPath();
|
|
ctx.arc(splitX, drawY + drawH / 2, 5, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
}
|
|
}
|
|
|
|
// Labels
|
|
ctx.fillStyle = "white";
|
|
ctx.font = "bold 14px sans-serif";
|
|
ctx.shadowColor = "black";
|
|
ctx.shadowOffsetX = 2;
|
|
ctx.shadowOffsetY = 2;
|
|
ctx.fillText("A", drawX + 8, drawY + 20);
|
|
ctx.fillText("B", drawX + drawW - 20, drawY + 20);
|
|
//draw dimensions
|
|
ctx.font = "normal 10px sans-serif";
|
|
if (this.imgA && this.imgB) {
|
|
ctx.fillText(this.imgA.width + "x" + this.imgA.height, drawX + 8, drawY + 34);
|
|
ctx.textAlign = "right";
|
|
ctx.fillText(this.imgB.width + "x" + this.imgB.height, drawX + drawW - 10, drawY + 34);
|
|
}
|
|
|
|
ctx.restore();
|
|
};
|
|
|
|
const origOnExecuted = this.onExecuted;
|
|
this.onExecuted = function (output) {
|
|
if (origOnExecuted) origOnExecuted.apply(this, arguments);
|
|
|
|
if (output?.b64_a && output?.b64_b) {
|
|
// Clean up old images
|
|
if (this.imgA) {
|
|
this.imgA.src = "";
|
|
}
|
|
if (this.imgB) {
|
|
this.imgB.src = "";
|
|
}
|
|
|
|
this.imgA = new Image();
|
|
this.imgB = new Image();
|
|
|
|
// Handle errors gracefully
|
|
this.imgA.onerror = () => console.warn("[SBCODE.ImageCompareNode] Failed to load image A");
|
|
this.imgB.onerror = () => console.warn("[SBCODE.ImageCompareNode] Failed to load image B");
|
|
|
|
// Set sources after attaching handlers
|
|
// Handle both chunked arrays and plain strings
|
|
const imgA_data = Array.isArray(output.b64_a) ? output.b64_a.join("") : output.b64_a;
|
|
const imgB_data = Array.isArray(output.b64_b) ? output.b64_b.join("") : output.b64_b;
|
|
|
|
this.imgA.src = "data:image/png;base64," + imgA_data;
|
|
this.imgB.src = "data:image/png;base64," + imgB_data;
|
|
} else {
|
|
console.warn("[SBCODE.ImageCompareNode] Missing image base64 data.");
|
|
}
|
|
};
|
|
};
|
|
},
|
|
});
|