canvas fix + minor vis bugfix

This commit is contained in:
JJ
2025-11-25 16:52:31 +00:00
parent 3a8e94a92f
commit 643d662d67
5 changed files with 358 additions and 57 deletions
+1 -1
View File
@@ -26,11 +26,11 @@ Draws an input image along a coordinate path for each frame, returning the rende
"required": {
"bg_image": ("IMAGE", ),
"coordinates": ("STRING", {"forceInput": True}),
"frames": ("INT", {"default": 0, "min": 0, "max": 10000, "step": 1,"forceInput": True}),
"ref_images": ("IMAGE", ),
},
"optional": {
"ref_masks": ("MASK", ),
"frames": ("INT", {"default": 0, "min": 0, "max": 10000, "step": 1}),
"use_box_rotation": ("BOOLEAN", {"default": True}),
"use_box_scale_size": ("BOOLEAN", {"default": True}),
"fallback_scale": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 100.0, "step": 0.01}),
+10
View File
@@ -14,6 +14,16 @@ app.registerExtension({
}
};
// Override onConfigure to ensure image_ref stays at bottom after page refresh
const originalOnConfigure = nodeType.prototype.onConfigure;
nodeType.prototype.onConfigure = function(info) {
if (originalOnConfigure) {
originalOnConfigure.apply(this, arguments);
}
// Ensure image_ref stays at the bottom after loading
ensureImageRefAtBottom.call(this);
};
nodeType.prototype.onConnectionsChange = function (type, index, connected, link_info) {
const stackTrace = new Error().stack;
+144 -2
View File
@@ -4,6 +4,7 @@
*/
import { app } from '../../../scripts/app.js';
import { applyResizeParams } from './image_resize.js';
/**
* Find the source node connected to a specific input of the current node
@@ -394,6 +395,122 @@ async function loadImageAsBase64(imageUrl) {
}
}
/**
* Check if ImageResizeKJv2 is in the node chain and extract its parameters
* @param {Object} startNode - The starting node
* @param {string} inputName - The input name to trace
* @returns {Object|null} Resize node parameters or null if not found
*/
function findResizeNodeInChain(startNode, inputName) {
const graph = app.graph;
if (!graph || !graph.links) return null;
const visited = new Set();
const toVisit = [{ node: startNode, input: inputName }];
while (toVisit.length > 0) {
const { node, input } = toVisit.shift();
if (visited.has(node.id)) continue;
visited.add(node.id);
// Check if this node is ImageResizeKJv2
if (node.type === 'ImageResizeKJv2') {
// Extract parameters from the node's widgets
const params = {};
if (node.widgets) {
for (const widget of node.widgets) {
params[widget.name] = widget.value;
}
}
// Check if width/height are connected from other nodes
if (node.inputs) {
for (let i = 0; i < node.inputs.length; i++) {
const nodeInput = node.inputs[i];
const inputName = nodeInput.name;
// Find if this input has a connection
let link = null;
if (graph.links instanceof Map) {
for (const [linkId, linkObj] of graph.links) {
if (linkObj && linkObj.target_id === node.id && linkObj.target_slot === i) {
link = linkObj;
break;
}
}
} else if (Array.isArray(graph.links)) {
link = graph.links.find(linkObj =>
linkObj && linkObj.target_id === node.id && linkObj.target_slot === i
);
}
if (link) {
// Found a connection for this input
const sourceNode = graph._nodes?.find(n => n.id === link.origin_id);
if (sourceNode) {
// Try to get the value from the source node's output
const outputSlot = link.origin_slot;
// Check if source node has widgets_values or properties
if (sourceNode.widgets_values && sourceNode.widgets_values.length > outputSlot) {
params[inputName] = sourceNode.widgets_values[outputSlot];
} else if (sourceNode.widgets && sourceNode.widgets[outputSlot]) {
params[inputName] = sourceNode.widgets[outputSlot].value;
} else {
// Try to find matching output name
const outputInfo = sourceNode.outputs?.[outputSlot];
if (outputInfo) {
// If it's a primitive or value node, try to get its value
if (sourceNode.widgets && sourceNode.widgets.length > 0) {
const widget = sourceNode.widgets.find(w => w.name === outputInfo.name || w.name === 'value');
if (widget) {
params[inputName] = widget.value;
}
}
}
}
}
}
}
}
return params;
}
// Continue traversing upstream
if (node.inputs) {
for (const nodeInput of node.inputs) {
const inputIndex = node.inputs.indexOf(nodeInput);
// Find link connected to this input
let link = null;
if (graph.links instanceof Map) {
for (const [linkId, linkObj] of graph.links) {
if (linkObj && linkObj.target_id === node.id && linkObj.target_slot === inputIndex) {
link = linkObj;
break;
}
}
} else if (Array.isArray(graph.links)) {
link = graph.links.find(linkObj =>
linkObj && linkObj.target_id === node.id && linkObj.target_slot === inputIndex
);
}
if (link) {
const sourceNode = graph._nodes?.find(n => n.id === link.origin_id);
if (sourceNode) {
toVisit.push({ node: sourceNode, input: nodeInput.name });
}
}
}
}
}
return null;
}
/**
* Main function to get reference image from connected node
* @param {Object} currentNode - The Power Spline Editor node
@@ -415,12 +532,15 @@ async function getReferenceImageFromConnectedNode(currentNode, inputName = 'ref_
});
}
}
if (!sourceNodeObj) {
console.log(`No source node found for ${inputName} input after deep search`);
return null;
}
// Check if ImageResizeKJv2 is in the chain and get its parameters
const resizeParams = findResizeNodeInChain(currentNode, inputName);
// Step 2: Extract image data from the source node
let imageDataUrl = await extractImageFromSourceNode(sourceNodeObj);
if (!imageDataUrl) {
@@ -445,12 +565,34 @@ async function getReferenceImageFromConnectedNode(currentNode, inputName = 'ref_
}
// Step 3: Load the image as base64
const base64Image = await loadImageAsBase64(imageDataUrl);
let base64Image = await loadImageAsBase64(imageDataUrl);
if (!base64Image) {
console.error('Failed to load image as base64');
return null;
}
// Step 4: Apply resize parameters if ImageResizeKJv2 was detected
if (resizeParams) {
try {
// Load image to apply resize
const img = new Image();
await new Promise((resolve, reject) => {
img.onload = resolve;
img.onerror = reject;
img.src = base64Image;
});
// Apply resize transformation
const { canvas } = await applyResizeParams(img, resizeParams);
// Convert back to base64
base64Image = canvas.toDataURL('image/jpeg', 0.95);
} catch (error) {
console.error('Error applying resize parameters:', error);
// Continue with original image if resize fails
}
}
console.log('Successfully retrieved reference image from connected node');
return base64Image;
}
+196
View File
@@ -0,0 +1,196 @@
/**
* Image Resize Utilities
* JavaScript implementation of ImageResizeKJv2 logic for client-side image processing
*/
/**
* Apply ImageResizeKJv2 resize parameters to an image
* @param {HTMLImageElement} image - The source image
* @param {Object} params - Resize parameters from ImageResizeKJv2 node
* @returns {Promise<{canvas: HTMLCanvasElement, width: number, height: number}>}
*/
export async function applyResizeParams(image, params) {
const {
width: targetWidth,
height: targetHeight,
keep_proportion = 'crop',
upscale_method = 'lanczos',
divisible_by = 2,
pad_color = '0, 0, 0',
crop_position = 'center'
} = params;
const originalWidth = image.width;
const originalHeight = image.height;
// Calculate dimensions based on keep_proportion mode
let resizeWidth = targetWidth;
let resizeHeight = targetHeight;
let padLeft = 0, padRight = 0, padTop = 0, padBottom = 0;
if (keep_proportion === 'resize' || keep_proportion.startsWith('pad')) {
// Calculate new dimensions maintaining aspect ratio
const ratio = Math.min(targetWidth / originalWidth, targetHeight / originalHeight);
resizeWidth = Math.round(originalWidth * ratio);
resizeHeight = Math.round(originalHeight * ratio);
// Calculate padding if needed
if (keep_proportion.startsWith('pad')) {
if (crop_position === 'center') {
padLeft = Math.floor((targetWidth - resizeWidth) / 2);
padRight = targetWidth - resizeWidth - padLeft;
padTop = Math.floor((targetHeight - resizeHeight) / 2);
padBottom = targetHeight - resizeHeight - padTop;
} else if (crop_position === 'top') {
padLeft = Math.floor((targetWidth - resizeWidth) / 2);
padRight = targetWidth - resizeWidth - padLeft;
padTop = 0;
padBottom = targetHeight - resizeHeight;
} else if (crop_position === 'bottom') {
padLeft = Math.floor((targetWidth - resizeWidth) / 2);
padRight = targetWidth - resizeWidth - padLeft;
padTop = targetHeight - resizeHeight;
padBottom = 0;
} else if (crop_position === 'left') {
padLeft = 0;
padRight = targetWidth - resizeWidth;
padTop = Math.floor((targetHeight - resizeHeight) / 2);
padBottom = targetHeight - resizeHeight - padTop;
} else if (crop_position === 'right') {
padLeft = targetWidth - resizeWidth;
padRight = 0;
padTop = Math.floor((targetHeight - resizeHeight) / 2);
padBottom = targetHeight - resizeHeight - padTop;
}
}
} else if (keep_proportion === 'crop') {
// For crop mode, we'll first calculate what to crop, then resize
// This is handled in the drawing phase
} else if (keep_proportion === 'stretch') {
// Use target dimensions as-is
resizeWidth = targetWidth;
resizeHeight = targetHeight;
}
// Apply divisible_by constraint
if (divisible_by > 1) {
resizeWidth = resizeWidth - (resizeWidth % divisible_by);
resizeHeight = resizeHeight - (resizeHeight % divisible_by);
}
// Create canvas for resizing
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
// Handle crop mode
if (keep_proportion === 'crop') {
const oldAspect = originalWidth / originalHeight;
const newAspect = targetWidth / targetHeight;
let cropW, cropH, cropX, cropY;
if (oldAspect > newAspect) {
cropW = Math.round(originalHeight * newAspect);
cropH = originalHeight;
} else {
cropW = originalWidth;
cropH = Math.round(originalWidth / newAspect);
}
// Calculate crop position
if (crop_position === 'center') {
cropX = Math.floor((originalWidth - cropW) / 2);
cropY = Math.floor((originalHeight - cropH) / 2);
} else if (crop_position === 'top') {
cropX = Math.floor((originalWidth - cropW) / 2);
cropY = 0;
} else if (crop_position === 'bottom') {
cropX = Math.floor((originalWidth - cropW) / 2);
cropY = originalHeight - cropH;
} else if (crop_position === 'left') {
cropX = 0;
cropY = Math.floor((originalHeight - cropH) / 2);
} else if (crop_position === 'right') {
cropX = originalWidth - cropW;
cropY = Math.floor((originalHeight - cropH) / 2);
}
// Set canvas to target size
canvas.width = targetWidth;
canvas.height = targetHeight;
// Set image smoothing based on upscale method
setImageSmoothing(ctx, upscale_method);
// Draw cropped and resized image
ctx.drawImage(
image,
cropX, cropY, cropW, cropH, // Source crop
0, 0, targetWidth, targetHeight // Destination
);
} else if (keep_proportion.startsWith('pad')) {
// Resize mode with padding
const paddedWidth = resizeWidth + padLeft + padRight;
const paddedHeight = resizeHeight + padTop + padBottom;
canvas.width = paddedWidth;
canvas.height = paddedHeight;
// Parse pad color
const [r, g, b] = pad_color.split(',').map(s => parseInt(s.trim()));
ctx.fillStyle = `rgb(${r}, ${g}, ${b})`;
ctx.fillRect(0, 0, paddedWidth, paddedHeight);
// Set image smoothing
setImageSmoothing(ctx, upscale_method);
// Draw resized image with padding offset
ctx.drawImage(image, padLeft, padTop, resizeWidth, resizeHeight);
} else {
// Simple resize (stretch or resize mode without padding)
canvas.width = resizeWidth;
canvas.height = resizeHeight;
setImageSmoothing(ctx, upscale_method);
ctx.drawImage(image, 0, 0, resizeWidth, resizeHeight);
}
return {
canvas,
width: canvas.width,
height: canvas.height
};
}
/**
* Set canvas image smoothing based on upscale method
* @param {CanvasRenderingContext2D} ctx - Canvas context
* @param {string} method - Upscale method name
*/
function setImageSmoothing(ctx, method) {
// Map upscale methods to canvas image smoothing
const smoothingMap = {
'nearest-exact': false,
'bilinear': true,
'area': true,
'bicubic': true,
'lanczos': true // Canvas doesn't have lanczos, use high-quality
};
const shouldSmooth = smoothingMap[method] !== undefined ? smoothingMap[method] : true;
ctx.imageSmoothingEnabled = shouldSmooth;
if (shouldSmooth) {
// Use high-quality smoothing for bicubic/lanczos
if (method === 'bicubic' || method === 'lanczos') {
ctx.imageSmoothingQuality = 'high';
} else if (method === 'bilinear') {
ctx.imageSmoothingQuality = 'medium';
} else {
ctx.imageSmoothingQuality = 'low';
}
}
}
+7 -54
View File
@@ -1302,11 +1302,6 @@ app.registerExtension({
console.log('Attempting to update reference image from connected node...');
try {
// Preserve the original canvas dimensions before refresh
const preservedWidth = this.editor?.originalImageWidth;
const preservedHeight = this.editor?.originalImageHeight;
const preservedScale = this.editor?.scale;
// Get reference image from connected bg_image input
const base64Image = await getReferenceImageFromConnectedNode(this, 'bg_image');
if (!base64Image) {
@@ -1316,45 +1311,13 @@ app.registerExtension({
return;
}
// We need to resize the fetched image to match the original canvas dimensions
// to maintain consistent scaling for the green boxes
if (preservedWidth && preservedHeight) {
const tempImg = new Image();
const resizedImageData = await new Promise((resolve, reject) => {
tempImg.onload = () => {
// Create a canvas to resize the image
const resizeCanvas = document.createElement('canvas');
resizeCanvas.width = preservedWidth;
resizeCanvas.height = preservedHeight;
const resizeCtx = resizeCanvas.getContext('2d');
// Draw the image scaled to the preserved dimensions
resizeCtx.drawImage(tempImg, 0, 0, preservedWidth, preservedHeight);
// Get the resized image as base64
const resizedDataUrl = resizeCanvas.toDataURL('image/jpeg', 0.95);
const resizedBase64 = resizedDataUrl.split(',')[1];
resolve(resizedBase64);
};
tempImg.onerror = reject;
tempImg.src = base64Image;
});
// Store the resized reference image
this.originalRefImageData = {
name: 'ref_image_from_connection.jpg',
base64: resizedImageData,
type: 'image/jpeg'
};
} else {
// No preserved dimensions, use the image as-is
this.originalRefImageData = {
name: 'ref_image_from_connection.jpg',
base64: base64Image.split(',')[1],
type: 'image/jpeg'
};
}
// Store the fetched image as-is without resizing
// The canvas system will handle aspect ratio preservation via recenterBackgroundImage()
this.originalRefImageData = {
name: 'ref_image_from_connection.jpg',
base64: base64Image.split(',')[1],
type: 'image/jpeg'
};
// Cache the ref image for future use
await saveRefImageToCache(this.originalRefImageData.base64, 'bg_image.png');
@@ -1374,16 +1337,6 @@ app.registerExtension({
// Wait for the image processing to complete
await new Promise(resolve => setTimeout(resolve, 200));
// Restore the preserved dimensions and scale if they changed
if (preservedWidth && preservedHeight && this.editor) {
if (this.editor.originalImageWidth !== preservedWidth || this.editor.originalImageHeight !== preservedHeight) {
this.editor.originalImageWidth = preservedWidth;
this.editor.originalImageHeight = preservedHeight;
this.editor.scale = preservedScale;
this.editor.recenterBackgroundImage();
}
}
} catch (error) {
console.error('Error updating reference image from connected node:', error);
alert('Error updating reference image: ' + error.message);