diff --git a/nodes/draw_image.py b/nodes/draw_image.py index 1f6198f..36a99c5 100644 --- a/nodes/draw_image.py +++ b/nodes/draw_image.py @@ -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}), diff --git a/web/create_image_list.js b/web/create_image_list.js index 552ef28..874c22c 100644 --- a/web/create_image_list.js +++ b/web/create_image_list.js @@ -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; diff --git a/web/power_spline_editor/graph_query.js b/web/power_spline_editor/graph_query.js index 762974a..62b867c 100644 --- a/web/power_spline_editor/graph_query.js +++ b/web/power_spline_editor/graph_query.js @@ -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; } diff --git a/web/power_spline_editor/image_resize.js b/web/power_spline_editor/image_resize.js new file mode 100644 index 0000000..b1de670 --- /dev/null +++ b/web/power_spline_editor/image_resize.js @@ -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'; + } + } +} diff --git a/web/power_spline_editor/power_spline_editor.js b/web/power_spline_editor/power_spline_editor.js index 5bbdffe..edeaf36 100644 --- a/web/power_spline_editor/power_spline_editor.js +++ b/web/power_spline_editor/power_spline_editor.js @@ -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);