jaceraimi's picture
Upload 3411 files
b4760b6 verified
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.");
}
};
};
},
});