Files
2025-04-29 23:14:15 +02:00

241 lines
11 KiB
JavaScript

import { app } from "../../../scripts/app.js";
import { api } from "../../../scripts/api.js";
// Displays an animated video preview using a sequence of images
app.registerExtension({
name: "Comfy.SmartNodes.SmartVideoPreviewScaled",
async beforeRegisterNodeDef(nodeType, nodeData, app) {
if (nodeData.name === "SmartVideoPreviewScaled") {
// Initialize node properties
const initProperties = (node) => {
// node.images = []; // Removed, use imageElements directly
node.fps = 12.0;
node.currentFrameIndex = 0;
node.lastFrameTime = 0;
node.animationFrameId = null;
node.imageElements = []; // Store loaded Image elements
node.isLoading = false;
node.dimensions = null; // Store original dimensions [width, height]
};
// Load images from the backend data
const loadImages = (node, frameInfoList) => {
node.isLoading = true;
node.imageElements = []; // Clear previous images
node.dimensions = null; // Reset dimensions
node.setDirtyCanvas(true, true);
if (!frameInfoList || frameInfoList.length === 0) {
// node.images = []; // Removed
node.isLoading = false;
node.setDirtyCanvas(true, true);
return;
}
// node.images = frameInfoList; // Removed
let imagesLoaded = 0;
const totalImages = frameInfoList.length;
frameInfoList.forEach((imageInfo, index) => {
const img = new Image();
node.imageElements[index] = img; // Store placeholder initially
img.onload = () => {
imagesLoaded++;
if (index === 0) { // Get dimensions from the first image
node.dimensions = [img.naturalWidth, img.naturalHeight];
}
if (imagesLoaded === totalImages) {
node.isLoading = false;
node.currentFrameIndex = 0;
node.lastFrameTime = performance.now();
node.setDirtyCanvas(true, true);
if (node.animationFrameId === null) {
startAnimation(node);
}
}
};
img.onerror = () => {
imagesLoaded++; // Count errors too to avoid getting stuck
console.error("Error loading image:", imageInfo.filename);
node.imageElements[index] = null; // Mark as failed
if (imagesLoaded === totalImages) {
node.isLoading = false;
node.setDirtyCanvas(true, true);
}
};
// Use api.apiURL() to construct the full URL for temp images
img.src = api.apiURL('/view?filename=' + encodeURIComponent(imageInfo.filename) + '&type=' + imageInfo.type + '&subfolder=' + encodeURIComponent(imageInfo.subfolder || ''));
});
};
// Animation loop
const animationLoop = (node, timestamp) => {
if (!node.imageElements || node.imageElements.length === 0 || node.isLoading) {
node.animationFrameId = requestAnimationFrame((t) => animationLoop(node, t));
return;
}
const frameInterval = 1000 / node.fps;
const elapsed = timestamp - node.lastFrameTime;
if (elapsed >= frameInterval) {
node.lastFrameTime = timestamp - (elapsed % frameInterval);
node.currentFrameIndex = (node.currentFrameIndex + 1) % node.imageElements.length;
node.setDirtyCanvas(true, false); // Redraw without resizing
}
node.animationFrameId = requestAnimationFrame((t) => animationLoop(node, t));
};
// Start the animation
const startAnimation = (node) => {
if (node.animationFrameId !== null) {
cancelAnimationFrame(node.animationFrameId);
}
node.lastFrameTime = performance.now();
node.animationFrameId = requestAnimationFrame((t) => animationLoop(node, t));
};
// Stop the animation
const stopAnimation = (node) => {
if (node.animationFrameId !== null) {
cancelAnimationFrame(node.animationFrameId);
node.animationFrameId = null;
}
};
// Initialize on node creation
const onNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function() {
onNodeCreated?.apply(this, arguments);
initProperties(this);
};
// Handle data from the backend
const onExecuted = nodeType.prototype.onExecuted;
nodeType.prototype.onExecuted = function(message) {
onExecuted?.apply(this, arguments);
// Look for 'video_frames' instead of 'images'
if (message?.video_frames) {
// Check if message.fps is an array and take the first element
this.fps = Array.isArray(message.fps) && message.fps.length > 0 ? message.fps[0] : (message.fps || 12.0);
stopAnimation(this); // Stop previous animation before loading new images
// Pass the video_frames data to loadImages
loadImages(this, message.video_frames);
} else {
// No frames received, clear existing ones
// this.images = []; // Removed
this.imageElements = [];
this.dimensions = null;
stopAnimation(this);
this.setDirtyCanvas(true, true);
}
};
// Custom drawing logic
const onDrawForeground = nodeType.prototype.onDrawForeground;
nodeType.prototype.onDrawForeground = function(ctx) {
onDrawForeground?.apply(this, arguments);
if (this.flags.collapsed) return;
// Define explicit padding values
const margin = 10; // Horizontal margin
const topPadding = 75; // Increased top padding
const bottomPadding = 30; // Added bottom padding
const titleHeight = LiteGraph.NODE_TITLE_HEIGHT || 20;
const infoBarHeight = 16; // Height of the bottom info bar
// Calculate available size based on node size and padding
let availableWidth = this.size[0] - margin * 2;
let availableHeight = this.size[1] - titleHeight - topPadding - bottomPadding - infoBarHeight;
const top = titleHeight + topPadding; // Adjusted top position
const left = margin;
// Prevent negative available height/width
availableWidth = Math.max(0, availableWidth);
availableHeight = Math.max(0, availableHeight);
if (this.isLoading) {
ctx.fillStyle = "#CCC";
ctx.font = "12px Arial";
ctx.textAlign = "center";
ctx.fillText("Loading...", this.size[0] / 2, top + availableHeight / 2);
return;
}
if (!this.imageElements || this.imageElements.length === 0 || this.currentFrameIndex >= this.imageElements.length) {
ctx.fillStyle = "#555";
ctx.fillRect(left, top, availableWidth, availableHeight);
ctx.fillStyle = "#CCC";
ctx.font = "12px Arial";
ctx.textAlign = "center";
ctx.fillText("No Video", this.size[0] / 2, top + availableHeight / 2);
return;
}
const currentImage = this.imageElements[this.currentFrameIndex];
if (!currentImage || !currentImage.complete || currentImage.naturalWidth === 0) {
// Image might still be loading or failed
ctx.fillStyle = "#555";
ctx.fillRect(left, top, availableWidth, availableHeight);
ctx.fillStyle = "#CCC";
ctx.font = "14px Arial";
ctx.textAlign = "center";
ctx.fillText("Frame Error", this.size[0] / 2, top + availableHeight / 2);
return;
}
let drawWidth, drawHeight, drawX, drawY;
// Use natural dimensions of the *already scaled* image
const imgWidth = currentImage.naturalWidth;
const imgHeight = currentImage.naturalHeight;
const aspectRatio = imgWidth / imgHeight;
// Fit the image within the available node space while maintaining aspect ratio
if (availableWidth / aspectRatio <= availableHeight) {
// Fit width
drawWidth = availableWidth;
drawHeight = drawWidth / aspectRatio;
} else {
// Fit height
drawHeight = availableHeight;
drawWidth = drawHeight * aspectRatio;
}
// Center the image within the available space
drawX = left + (availableWidth - drawWidth) / 2;
drawY = top + (availableHeight - drawHeight) / 2;
try {
ctx.drawImage(currentImage, drawX, drawY, drawWidth, drawHeight);
} catch (error) {
console.error("Error drawing image:", error);
ctx.fillStyle = "#F55"; // Indicate drawing error
ctx.fillRect(drawX, drawY, drawWidth, drawHeight);
}
// Optionally, draw frame number/info at the very bottom
ctx.fillStyle = "rgba(0, 0, 0, 0.6)";
// Position info bar respecting the new bottom padding
const infoBarY = this.size[1] - bottomPadding - infoBarHeight + 10;
ctx.fillRect(left, infoBarY, availableWidth, infoBarHeight);
ctx.fillStyle = "#FFF";
ctx.font = "12px Arial";
ctx.textAlign = "center";
const frameText = `Frame: ${this.currentFrameIndex + 1}/${this.imageElements.length}`;
// Adjust text Y position within the info bar
ctx.fillText(frameText, left + availableWidth/2, infoBarY + infoBarHeight - 4);
};
// Clean up animation on node removal
const onRemoved = nodeType.prototype.onRemoved;
nodeType.prototype.onRemoved = function() {
stopAnimation(this);
onRemoved?.apply(this, arguments);
};
}
},
});