adding image previews on some nodes, will eventually roll out to all nodes that need the ability
This commit is contained in:
@@ -1,15 +1,14 @@
|
||||
import os
|
||||
import re
|
||||
import io
|
||||
import base64
|
||||
import gdown
|
||||
from PIL import Image
|
||||
import torch
|
||||
import numpy as np
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
from server import PromptServer
|
||||
|
||||
|
||||
class FL_GoogleDriveImageDownloader:
|
||||
@@ -26,18 +25,23 @@ class FL_GoogleDriveImageDownloader:
|
||||
"default": True,
|
||||
"label": "Use Cached Image"
|
||||
}),
|
||||
"show_preview": ("BOOLEAN", {
|
||||
"default": False,
|
||||
"label": "Show Preview on Node"
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("IMAGE",)
|
||||
FUNCTION = "download_and_process_image"
|
||||
OUTPUT_NODE = True
|
||||
CATEGORY = "🏵️Fill Nodes/Google Drive"
|
||||
|
||||
def __init__(self):
|
||||
self.cache_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "cache")
|
||||
self.cache_index_file = os.path.join(self.cache_dir, "image_cache_index.json")
|
||||
os.makedirs(self.cache_dir, exist_ok=True)
|
||||
|
||||
|
||||
# Initialize the cache index if it doesn't exist
|
||||
if not os.path.exists(self.cache_index_file):
|
||||
with open(self.cache_index_file, "w") as f:
|
||||
@@ -60,80 +64,94 @@ class FL_GoogleDriveImageDownloader:
|
||||
try:
|
||||
with open(self.cache_index_file, "r") as f:
|
||||
cache_index = json.load(f)
|
||||
|
||||
|
||||
if file_id in cache_index:
|
||||
cached_path = cache_index[file_id]
|
||||
if os.path.exists(cached_path):
|
||||
return cached_path
|
||||
except Exception as e:
|
||||
print(f"Cache lookup error: {str(e)}")
|
||||
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def save_to_cache(self, file_id, image_path):
|
||||
"""Save downloaded image to cache"""
|
||||
try:
|
||||
# Generate a unique filename
|
||||
cache_filename = os.path.join(self.cache_dir, f"{file_id}.png")
|
||||
|
||||
|
||||
# Copy the image to cache
|
||||
img = Image.open(image_path)
|
||||
img.save(cache_filename)
|
||||
|
||||
|
||||
# Update the cache index
|
||||
with open(self.cache_index_file, "r") as f:
|
||||
cache_index = json.load(f)
|
||||
|
||||
|
||||
cache_index[file_id] = cache_filename
|
||||
|
||||
|
||||
with open(self.cache_index_file, "w") as f:
|
||||
json.dump(cache_index, f)
|
||||
|
||||
|
||||
return cache_filename
|
||||
except Exception as e:
|
||||
print(f"Cache save error: {str(e)}")
|
||||
return None
|
||||
|
||||
def download_and_process_image(self, google_drive_link: str, use_cache: bool = True) -> tuple:
|
||||
def prepare_image_for_display(self, pil_image):
|
||||
"""Convert PIL image to base64 for frontend display"""
|
||||
# Create a copy to avoid modifying the original
|
||||
display_img = pil_image.copy()
|
||||
|
||||
# Resize image if it's too large for preview
|
||||
max_size = (512, 512)
|
||||
display_img.thumbnail(max_size, Image.Resampling.LANCZOS)
|
||||
|
||||
buffered = io.BytesIO()
|
||||
display_img.save(buffered, format="PNG")
|
||||
img_str = base64.b64encode(buffered.getvalue()).decode()
|
||||
return f"data:image/png;base64,{img_str}"
|
||||
|
||||
def download_and_process_image(self, google_drive_link: str, use_cache: bool = True, show_preview: bool = True) -> tuple:
|
||||
try:
|
||||
# Extract file ID
|
||||
file_id = self.extract_file_id_from_link(google_drive_link)
|
||||
|
||||
|
||||
image_path = None
|
||||
temp_path = None # Initialize temp_path to ensure it's always defined
|
||||
|
||||
|
||||
# Check cache first if caching is enabled
|
||||
if use_cache:
|
||||
cached_path = self.get_cached_image(file_id)
|
||||
if cached_path:
|
||||
print(f"Using cached image for file ID: {file_id}")
|
||||
image_path = cached_path
|
||||
|
||||
|
||||
# If not in cache or caching disabled, download the image
|
||||
if not image_path:
|
||||
# Create temporary directory for download
|
||||
temp_dir = os.path.join(os.getcwd(), 'temp_downloads')
|
||||
os.makedirs(temp_dir, exist_ok=True)
|
||||
|
||||
|
||||
# Create download URL
|
||||
url = f'https://drive.google.com/uc?id={file_id}'
|
||||
|
||||
|
||||
# Download the file
|
||||
print("Downloading image from Google Drive...")
|
||||
temp_path = os.path.join(temp_dir, f"temp_image_{file_id}")
|
||||
output = gdown.download(url=url, output=temp_path, quiet=False, fuzzy=True)
|
||||
|
||||
|
||||
if not output:
|
||||
raise ValueError("Failed to download image")
|
||||
|
||||
|
||||
image_path = output
|
||||
|
||||
|
||||
# Save to cache if enabled
|
||||
if use_cache:
|
||||
cached_path = self.save_to_cache(file_id, image_path)
|
||||
if cached_path:
|
||||
image_path = cached_path
|
||||
|
||||
|
||||
# Open and process the image
|
||||
try:
|
||||
image = Image.open(image_path)
|
||||
@@ -142,34 +160,39 @@ class FL_GoogleDriveImageDownloader:
|
||||
image = image.convert('RGB')
|
||||
except Exception as e:
|
||||
raise ValueError(f"Invalid image file: {str(e)}")
|
||||
|
||||
|
||||
# Send image to frontend for preview if enabled
|
||||
if show_preview:
|
||||
display_image = self.prepare_image_for_display(image)
|
||||
PromptServer.instance.send_sync("fl_google_drive_image_downloader", {"image": display_image})
|
||||
|
||||
# Convert to the format expected by ComfyUI
|
||||
image_np = np.array(image).astype(np.float32) / 255.0
|
||||
image_tensor = torch.from_numpy(image_np)
|
||||
image_tensor = image_tensor.unsqueeze(0) # Add batch dimension
|
||||
|
||||
|
||||
# Cleanup temp file if it exists and isn't the cached version
|
||||
if temp_path and os.path.exists(temp_path) and temp_path != image_path:
|
||||
os.remove(temp_path)
|
||||
|
||||
|
||||
print(f"Successfully processed image: {image_tensor.shape}")
|
||||
return (image_tensor,)
|
||||
|
||||
|
||||
except Exception as e:
|
||||
raise ValueError(f"Error processing image from Google Drive: {str(e)}")
|
||||
|
||||
@classmethod
|
||||
def IS_CHANGED(cls, google_drive_link, use_cache):
|
||||
def IS_CHANGED(cls, google_drive_link, use_cache, show_preview):
|
||||
# Only signal a change if use_cache is False
|
||||
# This ensures the node won't rerun when caching is enabled
|
||||
if not use_cache:
|
||||
return float("NaN")
|
||||
|
||||
|
||||
# Otherwise, we should check if the link has changed since last run
|
||||
# This is done by hashing the link
|
||||
if not google_drive_link:
|
||||
return 0
|
||||
|
||||
|
||||
# Simple hash of the link string
|
||||
link_hash = hashlib.md5(google_drive_link.encode()).hexdigest()
|
||||
return link_hash
|
||||
return link_hash
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import torch
|
||||
import numpy as np
|
||||
import io
|
||||
import base64
|
||||
from PIL import Image, ImageFilter
|
||||
from ..utils import tensor_to_pil, pil_to_tensor
|
||||
from server import PromptServer
|
||||
|
||||
|
||||
class FL_ImageOverlay:
|
||||
@@ -46,16 +49,18 @@ class FL_ImageOverlay:
|
||||
"invert_mask": ("BOOLEAN", {"default": False}),
|
||||
"mask_feather": ("INT", {"default": 0, "min": 0, "max": 100, "step": 1}),
|
||||
"boundary_behavior": (["clip", "extend_canvas"], {"default": "clip"}),
|
||||
"show_preview": ("BOOLEAN", {"default": False, "label": "Show Preview on Node"}),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("IMAGE",)
|
||||
FUNCTION = "overlay_images"
|
||||
OUTPUT_NODE = True
|
||||
CATEGORY = "🏵️Fill Nodes/Image"
|
||||
|
||||
def overlay_images(self, base_image, overlay_image, mask, x_offset, y_offset,
|
||||
alignment, resize_overlay, blend_mode, opacity, invert_mask,
|
||||
mask_feather, boundary_behavior):
|
||||
mask_feather, boundary_behavior, show_preview=False):
|
||||
# Process batch - use first image from each batch
|
||||
base_pil = tensor_to_pil(base_image, batch_index=0)
|
||||
overlay_pil = tensor_to_pil(overlay_image, batch_index=0)
|
||||
@@ -101,6 +106,11 @@ class FL_ImageOverlay:
|
||||
blend_mode, opacity, boundary_behavior
|
||||
)
|
||||
|
||||
# Send preview to frontend if enabled
|
||||
if show_preview:
|
||||
display_image = self.prepare_image_for_display(result_pil)
|
||||
PromptServer.instance.send_sync("fl_image_overlay", {"image": display_image})
|
||||
|
||||
# Convert back to tensor
|
||||
result_tensor = pil_to_tensor(result_pil)
|
||||
|
||||
@@ -266,3 +276,17 @@ class FL_ImageOverlay:
|
||||
# Convert back to PIL
|
||||
result_np = (result_np * 255).astype(np.uint8)
|
||||
return Image.fromarray(result_np, mode='RGB')
|
||||
|
||||
def prepare_image_for_display(self, pil_image):
|
||||
"""Convert PIL image to base64 for frontend display"""
|
||||
# Create a copy to avoid modifying the original
|
||||
display_img = pil_image.copy()
|
||||
|
||||
# Resize image if it's too large for preview
|
||||
max_size = (512, 512)
|
||||
display_img.thumbnail(max_size, Image.Resampling.LANCZOS)
|
||||
|
||||
buffered = io.BytesIO()
|
||||
display_img.save(buffered, format="PNG")
|
||||
img_str = base64.b64encode(buffered.getvalue()).decode()
|
||||
return f"data:image/png;base64,{img_str}"
|
||||
|
||||
@@ -3,7 +3,10 @@ import torch.nn.functional as F
|
||||
from torchvision.ops import masks_to_boxes
|
||||
from torchvision.transforms.functional import resize as tv_resize, InterpolationMode
|
||||
import numpy as np
|
||||
import io
|
||||
import base64
|
||||
from PIL import Image
|
||||
from server import PromptServer
|
||||
|
||||
class FL_PasteOnCanvas:
|
||||
@classmethod
|
||||
@@ -20,6 +23,7 @@ class FL_PasteOnCanvas:
|
||||
"resize_algorithm": (["bilinear", "nearest", "bicubic", "lanczos"],),
|
||||
"include_alpha": ("BOOLEAN", {"default": False}),
|
||||
"use_full_mask": ("BOOLEAN", {"default": False}),
|
||||
"show_preview": ("BOOLEAN", {"default": False, "label": "Show Preview on Node"}),
|
||||
},
|
||||
"optional": {
|
||||
"mask": ("IMAGE",),
|
||||
@@ -29,10 +33,11 @@ class FL_PasteOnCanvas:
|
||||
|
||||
RETURN_TYPES = ("IMAGE",)
|
||||
FUNCTION = "cut_and_paste"
|
||||
OUTPUT_NODE = True
|
||||
CATEGORY = "🏵️Fill Nodes/Utility"
|
||||
|
||||
def cut_and_paste(self, image, canvas_width, canvas_height, background_red, background_green, background_blue,
|
||||
padding, resize_algorithm, include_alpha, use_full_mask, mask=None, bg_image_optional=None):
|
||||
padding, resize_algorithm, include_alpha, use_full_mask, show_preview=True, mask=None, bg_image_optional=None):
|
||||
# Ensure inputs are in the correct format
|
||||
image = self.tensor_to_rgba(image)
|
||||
B, H, W, C = image.shape
|
||||
@@ -127,6 +132,11 @@ class FL_PasteOnCanvas:
|
||||
if not include_alpha:
|
||||
canvas = canvas[..., :3]
|
||||
|
||||
# Send preview to frontend if enabled
|
||||
if show_preview:
|
||||
display_image = self.prepare_image_for_display(canvas)
|
||||
PromptServer.instance.send_sync("fl_paste_on_canvas", {"image": display_image})
|
||||
|
||||
return (canvas,)
|
||||
|
||||
def prepare_background_image(self, bg_image_optional, canvas_width, canvas_height, batch_size):
|
||||
@@ -175,4 +185,28 @@ class FL_PasteOnCanvas:
|
||||
return None
|
||||
if len(tensor.shape) == 4:
|
||||
return tensor.mean(dim=-1)
|
||||
return tensor
|
||||
return tensor
|
||||
|
||||
def prepare_image_for_display(self, tensor_image):
|
||||
"""Convert tensor image to base64 for frontend display"""
|
||||
# Take the first image from the batch
|
||||
if tensor_image.shape[0] > 0:
|
||||
img_tensor = tensor_image[0]
|
||||
else:
|
||||
img_tensor = tensor_image
|
||||
|
||||
# Convert to numpy and PIL
|
||||
img_np = (img_tensor.cpu().numpy() * 255).astype('uint8')
|
||||
pil_image = Image.fromarray(img_np)
|
||||
|
||||
# Create a copy to avoid modifying the original
|
||||
display_img = pil_image.copy()
|
||||
|
||||
# Resize image if it's too large for preview
|
||||
max_size = (512, 512)
|
||||
display_img.thumbnail(max_size, Image.Resampling.LANCZOS)
|
||||
|
||||
buffered = io.BytesIO()
|
||||
display_img.save(buffered, format="PNG")
|
||||
img_str = base64.b64encode(buffered.getvalue()).decode()
|
||||
return f"data:image/png;base64,{img_str}"
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
[project]
|
||||
name = "comfyui_fill-nodes"
|
||||
description = "Fill-Nodes is a versatile collection of custom nodes for ComfyUI that extends functionality across multiple domains. Features include advanced image processing (pixelation, slicing, masking), visual effects generation (glitch, halftone, pixel art), comprehensive file handling (PDF creation/extraction, Google Drive integration), AI model interfaces (GPT, DALL-E, Hugging Face), utility nodes for workflow enhancement, and specialized tools for video processing, captioning, and batch operations. The pack provides both practical workflow solutions and creative tools within a unified node collection."
|
||||
version = "2.0.3"
|
||||
version = "2.0.4"
|
||||
license = "LICENSE"
|
||||
dependencies = ["diffusers", "librosa", "sounddevice", "glitch_this", "PyOpenGL", "glfw", "scipy>=1.13.1", "requests", "aiohttp", "moviepy", "matplotlib", "reportlab", "openai", "PyPDF2", "pdf2image", "PyMuPDF", "reportlab", "PyPDF2", "ollama", "kornia", "opencv-python", "gdown", "open_clip_torch", "google-genai"]
|
||||
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
import { app } from "../../../../scripts/app.js";
|
||||
import { api } from "../../../../scripts/api.js";
|
||||
|
||||
app.registerExtension({
|
||||
name: "FL.GoogleDriveImageDownloader",
|
||||
async nodeCreated(node) {
|
||||
if (node.comfyClass === "FL_GoogleDriveImageDownloader") {
|
||||
const MIN_WIDTH = 200;
|
||||
const MIN_HEIGHT_WITH_PREVIEW = 300;
|
||||
const MIN_HEIGHT_WITHOUT_PREVIEW = 100;
|
||||
const PADDING = 10;
|
||||
|
||||
// Add image preview
|
||||
const img = new Image();
|
||||
img.onload = () => node.setDirtyCanvas(true);
|
||||
|
||||
node.onDrawBackground = function(ctx) {
|
||||
if (!this.flags.collapsed) {
|
||||
// Get the show_preview widget value
|
||||
const showPreviewWidget = this.widgets?.find(w => w.name === "show_preview");
|
||||
const showPreview = showPreviewWidget ? showPreviewWidget.value : false;
|
||||
|
||||
// Only draw if preview is enabled and image is loaded
|
||||
if (!showPreview || !img.src) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [w, h] = this.size;
|
||||
|
||||
// Calculate the Y position of the last widget
|
||||
const lastWidget = node.widgets[node.widgets.length - 1];
|
||||
const lastWidgetY = lastWidget.last_y || 0;
|
||||
|
||||
// Set the image Y offset to be just below the last widget
|
||||
const IMAGE_Y_OFFSET = lastWidgetY + 30;
|
||||
|
||||
const imageArea = h - IMAGE_Y_OFFSET - PADDING;
|
||||
|
||||
// Draw image
|
||||
if (img.src && imageArea > 50) {
|
||||
const aspectRatio = img.width / img.height;
|
||||
let drawWidth = w - 2 * PADDING;
|
||||
let drawHeight = imageArea;
|
||||
|
||||
if (drawWidth / drawHeight > aspectRatio) {
|
||||
drawWidth = drawHeight * aspectRatio;
|
||||
} else {
|
||||
drawHeight = drawWidth / aspectRatio;
|
||||
}
|
||||
|
||||
const x = PADDING + (w - 2 * PADDING - drawWidth) / 2;
|
||||
const y = IMAGE_Y_OFFSET;
|
||||
|
||||
ctx.drawImage(img, x, y, drawWidth, drawHeight);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Listen for the image from the backend
|
||||
api.addEventListener("fl_google_drive_image_downloader", (event) => {
|
||||
if (event.detail.image) {
|
||||
img.src = event.detail.image;
|
||||
}
|
||||
});
|
||||
|
||||
function updateNodeSize() {
|
||||
// Check if preview is enabled
|
||||
const showPreviewWidget = node.widgets?.find(w => w.name === "show_preview");
|
||||
const showPreview = showPreviewWidget ? showPreviewWidget.value : false;
|
||||
|
||||
// Use different minimum heights based on preview state
|
||||
const minHeight = showPreview ? MIN_HEIGHT_WITH_PREVIEW : MIN_HEIGHT_WITHOUT_PREVIEW;
|
||||
|
||||
node.size[0] = Math.max(MIN_WIDTH, node.size[0]);
|
||||
node.size[1] = Math.max(minHeight, node.size[1]);
|
||||
}
|
||||
|
||||
node.onResize = updateNodeSize;
|
||||
updateNodeSize();
|
||||
|
||||
// Update size when preview toggle changes
|
||||
const showPreviewWidget = node.widgets?.find(w => w.name === "show_preview");
|
||||
if (showPreviewWidget) {
|
||||
const originalCallback = showPreviewWidget.callback;
|
||||
showPreviewWidget.callback = function(value) {
|
||||
if (originalCallback) {
|
||||
originalCallback.apply(this, arguments);
|
||||
}
|
||||
|
||||
// If toggling preview off, shrink the node height to minimum
|
||||
if (!value) {
|
||||
node.size[1] = MIN_HEIGHT_WITHOUT_PREVIEW;
|
||||
} else {
|
||||
// If toggling preview on, grow the node height to minimum with preview
|
||||
node.size[1] = MIN_HEIGHT_WITH_PREVIEW;
|
||||
}
|
||||
|
||||
updateNodeSize();
|
||||
node.setDirtyCanvas(true);
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,104 @@
|
||||
import { app } from "../../../../scripts/app.js";
|
||||
import { api } from "../../../../scripts/api.js";
|
||||
|
||||
app.registerExtension({
|
||||
name: "FL.ImageOverlay",
|
||||
async nodeCreated(node) {
|
||||
if (node.comfyClass === "FL_ImageOverlay") {
|
||||
const MIN_WIDTH = 250;
|
||||
const MIN_HEIGHT_WITH_PREVIEW = 700; // Space for 11 widgets + preview
|
||||
const MIN_HEIGHT_WITHOUT_PREVIEW = 320; // Space for 11 widgets only
|
||||
const PADDING = 10;
|
||||
|
||||
// Add image preview
|
||||
const img = new Image();
|
||||
img.onload = () => node.setDirtyCanvas(true);
|
||||
|
||||
node.onDrawBackground = function(ctx) {
|
||||
if (!this.flags.collapsed) {
|
||||
// Get the show_preview widget value
|
||||
const showPreviewWidget = this.widgets?.find(w => w.name === "show_preview");
|
||||
const showPreview = showPreviewWidget ? showPreviewWidget.value : false;
|
||||
|
||||
// Only draw if preview is enabled and image is loaded
|
||||
if (!showPreview || !img.src) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [w, h] = this.size;
|
||||
|
||||
// Calculate the Y position of the last widget
|
||||
const lastWidget = node.widgets[node.widgets.length - 1];
|
||||
const lastWidgetY = lastWidget.last_y || 0;
|
||||
|
||||
// Set the image Y offset to be just below the last widget
|
||||
const IMAGE_Y_OFFSET = lastWidgetY + 30;
|
||||
|
||||
const imageArea = h - IMAGE_Y_OFFSET - PADDING;
|
||||
|
||||
// Draw image
|
||||
if (img.src && imageArea > 50) {
|
||||
const aspectRatio = img.width / img.height;
|
||||
let drawWidth = w - 2 * PADDING;
|
||||
let drawHeight = imageArea;
|
||||
|
||||
if (drawWidth / drawHeight > aspectRatio) {
|
||||
drawWidth = drawHeight * aspectRatio;
|
||||
} else {
|
||||
drawHeight = drawWidth / aspectRatio;
|
||||
}
|
||||
|
||||
const x = PADDING + (w - 2 * PADDING - drawWidth) / 2;
|
||||
const y = IMAGE_Y_OFFSET;
|
||||
|
||||
ctx.drawImage(img, x, y, drawWidth, drawHeight);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Listen for the image from the backend
|
||||
api.addEventListener("fl_image_overlay", (event) => {
|
||||
if (event.detail.image) {
|
||||
img.src = event.detail.image;
|
||||
}
|
||||
});
|
||||
|
||||
function updateNodeSize() {
|
||||
// Check if preview is enabled
|
||||
const showPreviewWidget = node.widgets?.find(w => w.name === "show_preview");
|
||||
const showPreview = showPreviewWidget ? showPreviewWidget.value : false;
|
||||
|
||||
// Use different minimum heights based on preview state
|
||||
const minHeight = showPreview ? MIN_HEIGHT_WITH_PREVIEW : MIN_HEIGHT_WITHOUT_PREVIEW;
|
||||
|
||||
node.size[0] = Math.max(MIN_WIDTH, node.size[0]);
|
||||
node.size[1] = Math.max(minHeight, node.size[1]);
|
||||
}
|
||||
|
||||
node.onResize = updateNodeSize;
|
||||
updateNodeSize();
|
||||
|
||||
// Update size when preview toggle changes
|
||||
const showPreviewWidget = node.widgets?.find(w => w.name === "show_preview");
|
||||
if (showPreviewWidget) {
|
||||
const originalCallback = showPreviewWidget.callback;
|
||||
showPreviewWidget.callback = function(value) {
|
||||
if (originalCallback) {
|
||||
originalCallback.apply(this, arguments);
|
||||
}
|
||||
|
||||
// If toggling preview off, shrink the node height to minimum
|
||||
if (!value) {
|
||||
node.size[1] = MIN_HEIGHT_WITHOUT_PREVIEW;
|
||||
} else {
|
||||
// If toggling preview on, grow the node height to minimum with preview
|
||||
node.size[1] = MIN_HEIGHT_WITH_PREVIEW;
|
||||
}
|
||||
|
||||
updateNodeSize();
|
||||
node.setDirtyCanvas(true);
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,104 @@
|
||||
import { app } from "../../../../scripts/app.js";
|
||||
import { api } from "../../../../scripts/api.js";
|
||||
|
||||
app.registerExtension({
|
||||
name: "FL.PasteOnCanvas",
|
||||
async nodeCreated(node) {
|
||||
if (node.comfyClass === "FL_PasteOnCanvas") {
|
||||
const MIN_WIDTH = 250;
|
||||
const MIN_HEIGHT_WITH_PREVIEW = 650; // Space for 10 widgets + preview
|
||||
const MIN_HEIGHT_WITHOUT_PREVIEW = 320; // Space for 10 widgets only
|
||||
const PADDING = 10;
|
||||
|
||||
// Add image preview
|
||||
const img = new Image();
|
||||
img.onload = () => node.setDirtyCanvas(true);
|
||||
|
||||
node.onDrawBackground = function(ctx) {
|
||||
if (!this.flags.collapsed) {
|
||||
// Get the show_preview widget value
|
||||
const showPreviewWidget = this.widgets?.find(w => w.name === "show_preview");
|
||||
const showPreview = showPreviewWidget ? showPreviewWidget.value : false;
|
||||
|
||||
// Only draw if preview is enabled and image is loaded
|
||||
if (!showPreview || !img.src) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [w, h] = this.size;
|
||||
|
||||
// Calculate the Y position of the last widget
|
||||
const lastWidget = node.widgets[node.widgets.length - 1];
|
||||
const lastWidgetY = lastWidget.last_y || 0;
|
||||
|
||||
// Set the image Y offset to be just below the last widget
|
||||
const IMAGE_Y_OFFSET = lastWidgetY + 30;
|
||||
|
||||
const imageArea = h - IMAGE_Y_OFFSET - PADDING;
|
||||
|
||||
// Draw image
|
||||
if (img.src && imageArea > 50) {
|
||||
const aspectRatio = img.width / img.height;
|
||||
let drawWidth = w - 2 * PADDING;
|
||||
let drawHeight = imageArea;
|
||||
|
||||
if (drawWidth / drawHeight > aspectRatio) {
|
||||
drawWidth = drawHeight * aspectRatio;
|
||||
} else {
|
||||
drawHeight = drawWidth / aspectRatio;
|
||||
}
|
||||
|
||||
const x = PADDING + (w - 2 * PADDING - drawWidth) / 2;
|
||||
const y = IMAGE_Y_OFFSET;
|
||||
|
||||
ctx.drawImage(img, x, y, drawWidth, drawHeight);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Listen for the image from the backend
|
||||
api.addEventListener("fl_paste_on_canvas", (event) => {
|
||||
if (event.detail.image) {
|
||||
img.src = event.detail.image;
|
||||
}
|
||||
});
|
||||
|
||||
function updateNodeSize() {
|
||||
// Check if preview is enabled
|
||||
const showPreviewWidget = node.widgets?.find(w => w.name === "show_preview");
|
||||
const showPreview = showPreviewWidget ? showPreviewWidget.value : false;
|
||||
|
||||
// Use different minimum heights based on preview state
|
||||
const minHeight = showPreview ? MIN_HEIGHT_WITH_PREVIEW : MIN_HEIGHT_WITHOUT_PREVIEW;
|
||||
|
||||
node.size[0] = Math.max(MIN_WIDTH, node.size[0]);
|
||||
node.size[1] = Math.max(minHeight, node.size[1]);
|
||||
}
|
||||
|
||||
node.onResize = updateNodeSize;
|
||||
updateNodeSize();
|
||||
|
||||
// Update size when preview toggle changes
|
||||
const showPreviewWidget = node.widgets?.find(w => w.name === "show_preview");
|
||||
if (showPreviewWidget) {
|
||||
const originalCallback = showPreviewWidget.callback;
|
||||
showPreviewWidget.callback = function(value) {
|
||||
if (originalCallback) {
|
||||
originalCallback.apply(this, arguments);
|
||||
}
|
||||
|
||||
// If toggling preview off, shrink the node height to minimum
|
||||
if (!value) {
|
||||
node.size[1] = MIN_HEIGHT_WITHOUT_PREVIEW;
|
||||
} else {
|
||||
// If toggling preview on, grow the node height to minimum with preview
|
||||
node.size[1] = MIN_HEIGHT_WITH_PREVIEW;
|
||||
}
|
||||
|
||||
updateNodeSize();
|
||||
node.setDirtyCanvas(true);
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -1,48 +0,0 @@
|
||||
import { app } from "../../../../scripts/app.js";
|
||||
|
||||
app.registerExtension({
|
||||
name: "Fill-Nodes.PasteOnCanvasAnimatedDisplay",
|
||||
async nodeCreated(node) {
|
||||
if (node.comfyClass === "FL_PasteOnCanvas") {
|
||||
addAnimatedDisplay(node);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function addAnimatedDisplay(node) {
|
||||
// Increase the node size
|
||||
node.size = [220, 270];
|
||||
|
||||
// Override the onDrawBackground method to add our animated display
|
||||
node.onDrawBackground = function(ctx) {
|
||||
|
||||
// Calculate the current time to animate the balls
|
||||
const time = Date.now() * 0.001; // Current time in seconds
|
||||
|
||||
// Draw moving circles and their mirrored counterparts
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const x = 60 + (this.size[0] - 20) * (0.2 + 0.02 * Math.sin(time + i));
|
||||
const y = -20 + (this.size[1] - 40) * (0.2 + 0.02 * Math.cos(time * 1.5 + i));
|
||||
const mirrorX = this.size[0] - x; // Mirror X coordinate
|
||||
|
||||
// Original set of circles
|
||||
ctx.beginPath();
|
||||
ctx.arc(x, y, 7, 0, Math.PI * 2);
|
||||
ctx.fillStyle = `hsl(${(time * 100 + i * 50) % 360}, 100%, 75%)`;
|
||||
ctx.fill();
|
||||
|
||||
// Mirrored set of circles
|
||||
ctx.beginPath();
|
||||
ctx.arc(mirrorX, y, 7, 0, Math.PI * 2);
|
||||
ctx.fillStyle = `hsl(${(time * 100 + i * 50) % 360}, 100%, 75%)`;
|
||||
ctx.fill();
|
||||
}
|
||||
|
||||
// Request next frame for continuous animation
|
||||
node.setDirtyCanvas(true);
|
||||
requestAnimationFrame(() => node.setDirtyCanvas(true));
|
||||
};
|
||||
|
||||
// Force the initial redraw to start the animation
|
||||
node.setDirtyCanvas(true);
|
||||
}
|
||||
Reference in New Issue
Block a user