Files

384 lines
17 KiB
JavaScript

import { app } from "../../scripts/app.js";
import { api } from "../../scripts/api.js";
function imgToCanvasBase64 (img) {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
canvas.width = img.width;
canvas.height = img.height;
ctx.drawImage(img, 0, 0);
const base64 = canvas.toDataURL('image/png');
return base64;
}
function convertImageToBase64 (img) {
try {
const base64 = imgToCanvasBase64(img);
return base64;
} catch (error) {
console.error(error);
}
}
// Helper function to compute SHA-256 hash of a string and return it as hex
async function computeHash(str) {
const encoder = new TextEncoder();
const data = encoder.encode(str);
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
const hashArray = Array.from(new Uint8Array(hashBuffer));
const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
return hashHex;
}
// Global flag to enable/disable debug logging
const ENABLE_DEBUG = false;
app.registerExtension({
name: "vrch.ImagePreviewBackgroundNode",
async nodeCreated(node) {
// Only process nodes of type "VrchImagePreviewBackgroundNode"
if (node.comfyClass === "VrchImagePreviewBackgroundNode") {
// Get the relevant widgets
const channelWidget = node.widgets.find(w => w.name === "channel");
const enableWidget = node.widgets.find(w => w.name === "background_display");
const intervalWidget = node.widgets.find(w => w.name === "refresh_interval_ms");
const displayOptionWidget = node.widgets.find(w => w.name === "display_option");
const batchDisplayWidget = node.widgets.find(w => w.name === "batch_display");
const batchDisplayIntervalWidget = node.widgets.find(w => w.name === "batch_display_interval_ms");
const batchImagesSizeWidget = node.widgets.find(w => w.name === "batch_images_size");
const widgets = [channelWidget, enableWidget, intervalWidget, displayOptionWidget,
batchDisplayWidget, batchDisplayIntervalWidget, batchImagesSizeWidget];
// Global variable for batch index and timer
let batchIndex = 0;
let batchTimer = null;
// Function to set up the batch display timer if enabled
function setupBatchInterval() {
if (batchTimer) {
clearInterval(batchTimer);
batchTimer = null;
}
if (batchDisplayWidget && batchDisplayWidget.value) {
let ms = parseInt(batchDisplayIntervalWidget.value || 1000, 10);
let imagesSize = parseInt(batchImagesSizeWidget.value || 1, 10);
// Reset batchIndex to 0 when (re)starting timer
batchIndex = 0;
batchTimer = setInterval(() => {
batchIndex = (batchIndex + 1) % imagesSize;
reinitBackground();
updateBackground();
if (ENABLE_DEBUG) {
console.log(`Batch display timer updated index to ${batchIndex}`);
}
}, ms);
}
}
// Set callbacks for widget changes to update the background immediately
widgets.forEach(widget => {
if (widget) {
widget.callback = () => {
if (ENABLE_DEBUG) {
console.log(`callback:::${widget.name}:::${widget.value}`);
}
// For batch related widgets, update the batch timer
if (["batch_display", "batch_display_interval_ms", "batch_images_size"].includes(widget.name)) {
setupBatchInterval();
}
reinitBackground();
updateBackground();
};
}
});
// Function that returns the final image path, with different logic based on batchDisplayWidget
function getImagePath() {
const channel = channelWidget.value || "1";
const extension = "jpeg";
const folder = "preview_background";
let filename;
if (batchDisplayWidget && batchDisplayWidget.value) {
// Use batch display logic: include current batch index formatted为两位数
filename = `channel_${channel}_${String(batchIndex).padStart(2, '0')}.${extension}`;
} else {
filename = `channel_${channel}.${extension}`;
}
const basePath = window.location.href;
return `${basePath}view?filename=${filename}&subfolder=${folder}&type=output&rand=${Math.random()}`;
}
// Store the last loaded path to avoid reloading if not changed
let lastLoadedPath = null;
// Reset lastLoadedPath so the next poll triggers a reload
function reinitBackground() {
lastLoadedPath = null;
}
// Global interval timer for polling new images
let pollTimer = null;
function setupInterval() {
// Clear old timer if exists
if (pollTimer) {
clearInterval(pollTimer);
}
let ms = parseInt(intervalWidget.value || 300, 10);
pollTimer = setInterval(() => {
updateBackground();
}, ms);
}
// Global image object for background
if (!window._td_bg_img) {
window._td_bg_img = new Image();
window._td_bg_img.onload = () => {
// Trigger canvas redraw when image is loaded
if (app.canvas) {
app.canvas.draw(true, true);
}
};
}
// Variable to store the last image hash
let lastImageHash = null;
// Main function to update the background image
function updateBackground() {
if (!enableWidget.value) {
// If disabled, clear the image and trigger redraw
if (window._td_bg_img.src) {
window._td_bg_img.src = "";
lastLoadedPath = null;
lastImageHash = null;
}
return;
}
const path = getImagePath();
if (path !== lastLoadedPath) {
lastLoadedPath = path;
// Create a new image object to load the image
let imgObj = new Image();
imgObj.onload = async () => {
let base64 = convertImageToBase64(imgObj);
let newHash = await computeHash(base64);
// If the hash is unchanged from the last one, do not update the background
if (newHash === lastImageHash) {
return;
}
lastImageHash = newHash;
window._td_bg_img.src = base64;
};
imgObj.onerror = err => {
console.warn("Background image not found or load error:", path);
};
if (ENABLE_DEBUG) {
console.log("Loading background image:", path);
}
imgObj.src = path; // Prevent cache
}
}
// Initialize polling and update
setupInterval();
setupBatchInterval();
updateBackground();
}
// Handle new direct UI preview node
if (node.comfyClass === "VrchImagePreviewBackgroundNewNode") {
const enableWidget = node.widgets.find(w => w.name === "background_display");
const displayOptionWidget = node.widgets.find(w => w.name === "display_option");
const batchDisplayWidget = node.widgets.find(w => w.name === "batch_display");
const batchDisplayIntervalWidget = node.widgets.find(w => w.name === "batch_display_interval_ms");
const widgets = [enableWidget, displayOptionWidget,
batchDisplayWidget, batchDisplayIntervalWidget];
widgets.forEach(widget => {
if (widget) {
widget.callback = () => {
if (ENABLE_DEBUG) {
console.log(`callback:::${widget.name}:::${widget.value}`);
}
// For batch related widgets, update the batch timer
if (["batch_display", "batch_display_interval_ms"].includes(widget.name)) {
setupBatchInterval();
}
// For display option, reinitialize the background
if (widget.name === "display_option") {
window._td_bg_display_option = widget.value;
if (app.canvas) {
app.canvas.draw(true, true);
}
}
updateBackground();
};
}
});
// Global variable for batch index and timer
let batchIndex = 0;
let batchTimer = null;
// Function to set up the batch display timer if enabled
function setupBatchInterval() {
if (batchTimer) {
clearInterval(batchTimer);
batchTimer = null;
}
if (batchDisplayWidget && batchDisplayWidget.value) {
let ms = parseInt(batchDisplayIntervalWidget.value || 1000, 10);
let imageSize = window._td_bg_imgs.length; // Reset batchIndex to 0 when (re)starting timer
batchIndex = 0;
if (imageSize > 0) {
batchTimer = setInterval(() => {
batchIndex = (batchIndex + 1) % imageSize;
updateBackground();
if (ENABLE_DEBUG) {
console.log(`Batch display timer updated index to ${batchIndex}`);
}
}, ms);
}
}
}
function updateBackground() {
if (!enableWidget.value) {
// If disabled, clear the image and trigger redraw
if (window._td_bg_img.src) {
window._td_bg_img.src = "";
}
// Reset batch index
batchIndex = 0;
// Clear batch timer
if (batchTimer) {
clearInterval(batchTimer);
batchTimer = null;
}
return;
}
const base64 = window._td_bg_imgs?.[batchIndex];
if (base64) {
window._td_bg_img.src = base64;
}
}
// initialize enabled flag
window._td_bg_enabled = window._td_bg_enabled ?? false;
// Global image object for background
if (!window._td_bg_img) {
window._td_bg_img = new Image();
window._td_bg_img.onload = () => {
// Trigger canvas redraw when image is loaded
if (app.canvas) {
app.canvas.draw(true, true);
}
};
}
// Initialize polling and update
setupBatchInterval();
updateBackground();
// onExecuted receives images as base64 strings
node.onExecuted = function(message) {
window._td_bg_enabled = message.background_display?.[0] ?? false;
window._td_bg_imgs = message.background_images || [];
window._td_bg_display_option = message.display_option?.[0] || 'fit';
updateBackground();
};
}
},
init() {
// Overwrite LGraphCanvas drawBackCanvas for the entire app
const LGraphCanvas = window.LiteGraph.LGraphCanvas;
const originalDrawBack = LGraphCanvas.prototype.drawBackCanvas;
LGraphCanvas.prototype.drawBackCanvas = function(...args) {
// 1) Call original background drawing (grid, etc.)
originalDrawBack.apply(this, args);
// 2) If a background image is loaded, draw it with "fit" scaling
if (window._td_bg_img && window._td_bg_img.width) {
// Determine enable and display mode from old or new preview node
const nodes = app.graph._nodes.filter(n => ["VrchImagePreviewBackgroundNode","VrchImagePreviewBackgroundNewNode"].includes(n?.comfyClass));
let enableVal = false;
let displayOptionVal = 'fit';
const oldNode = nodes.find(n => n.comfyClass === 'VrchImagePreviewBackgroundNode');
const newNode = nodes.find(n => n.comfyClass === 'VrchImagePreviewBackgroundNewNode');
if (newNode) {
enableVal = window._td_bg_enabled;
displayOptionVal = window._td_bg_display_option || displayOptionVal;
} else if (oldNode) {
const eW = oldNode.widgets.find(w => w.name === 'background_display');
const dW = oldNode.widgets.find(w => w.name === 'display_option');
enableVal = eW? eW.value:false;
displayOptionVal = dW? dW.value:'fit';
}
if (!enableVal) return;
const ctx = this.bgcanvas.getContext("2d");
ctx.save();
// Reset transform if viewport is not defined
if (!this.viewport) {
ctx.setTransform(1, 0, 0, 1, 0, 0);
}
let offsetX, offsetY, drawWidth, drawHeight;
const canvasWidth = this.bgcanvas.width;
const canvasHeight = this.bgcanvas.height;
const imageWidth = window._td_bg_img.width;
const imageHeight = window._td_bg_img.height;
switch (displayOptionVal) {
case "original":
drawWidth = imageWidth;
drawHeight = imageHeight;
offsetX = (canvasWidth - imageWidth) / 2;
offsetY = (canvasHeight - imageHeight) / 2;
break;
case "fit": {
const scaleRatio = Math.min(canvasWidth / imageWidth, canvasHeight / imageHeight);
drawWidth = imageWidth * scaleRatio;
drawHeight = imageHeight * scaleRatio;
offsetX = (canvasWidth - drawWidth) / 2;
offsetY = (canvasHeight - drawHeight) / 2;
break;
}
case "stretch":
offsetX = 0;
offsetY = 0;
drawWidth = canvasWidth;
drawHeight = canvasHeight;
break;
case "crop": {
const scaleRatio = Math.max(canvasWidth / imageWidth, canvasHeight / imageHeight);
drawWidth = imageWidth * scaleRatio;
drawHeight = imageHeight * scaleRatio;
offsetX = (canvasWidth - drawWidth) / 2;
offsetY = (canvasHeight - drawHeight) / 2;
break;
}
default: {
const scaleRatio = Math.min(canvasWidth / imageWidth, canvasHeight / imageHeight);
drawWidth = imageWidth * scaleRatio;
drawHeight = imageHeight * scaleRatio;
offsetX = (canvasWidth - drawWidth) / 2;
offsetY = (canvasHeight - drawHeight) / 2;
}
}
ctx.drawImage(window._td_bg_img, offsetX, offsetY, drawWidth, drawHeight);
ctx.restore();
}
};
}
});