canvas fix + minor vis bugfix
This commit is contained in:
+1
-1
@@ -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}),
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user