diff --git a/js/SmartColorWidget.js b/js/SmartColorWidget.js new file mode 100644 index 0000000..13afec0 --- /dev/null +++ b/js/SmartColorWidget.js @@ -0,0 +1,124 @@ +import { app } from "../../../scripts/app.js"; + +function normalizeHexColor(value, fallback = "#000000") { + if (typeof value !== "string") { + return fallback; + } + + const normalized = value.startsWith("#") ? value : `#${value}`; + return /^#[0-9a-fA-F]{6}$/.test(normalized) ? normalized : fallback; +} + +function getBrightness(hexColor) { + const hex = normalizeHexColor(hexColor).slice(1); + const r = parseInt(hex.slice(0, 2), 16); + const g = parseInt(hex.slice(2, 4), 16); + const b = parseInt(hex.slice(4, 6), 16); + return (r * 299 + g * 587 + b * 114) / 1000; +} + +function createColorWidget(name, value) { + const widget = { + name, + type: "SMART_COLOR", + value: normalizeHexColor(value), + options: { default: "#000000" }, + draw(ctx, node, widgetWidth, widgetY, height) { + const border = 3; + + ctx.fillStyle = "#000"; + ctx.fillRect(0, widgetY, widgetWidth, height); + + ctx.fillStyle = this.value; + ctx.fillRect( + border, + widgetY + border, + widgetWidth - border * 2, + height - border * 2 + ); + + ctx.fillStyle = getBrightness(this.value) > 125 ? "#000" : "#fff"; + ctx.font = "14px Arial"; + ctx.textAlign = "center"; + ctx.fillText(this.name, widgetWidth * 0.5, widgetY + 14); + }, + mouse(event, pos, node) { + if (event.type !== "pointerdown") { + return false; + } + + const widgets = node.widgets.filter((widgetItem) => widgetItem.type === "SMART_COLOR"); + + for (const colorWidget of widgets) { + const rect = [colorWidget.last_y, colorWidget.last_y + 32]; + if (pos[1] <= rect[0] || pos[1] >= rect[1]) { + continue; + } + + const picker = document.createElement("input"); + picker.type = "color"; + picker.value = normalizeHexColor(this.value); + + Object.assign(picker.style, { + position: "fixed", + left: `${event.clientX}px`, + top: `${event.clientY}px`, + height: "0px", + width: "0px", + padding: "0px", + opacity: 0, + }); + + picker.addEventListener("blur", () => { + this.callback?.(this.value); + node.graph._version++; + picker.remove(); + }); + + picker.addEventListener("input", () => { + if (!picker.value) { + return; + } + + this.value = normalizeHexColor(picker.value, this.value); + app.canvas.setDirty(true); + }); + + document.body.appendChild(picker); + + requestAnimationFrame(() => { + if (picker.showPicker) { + picker.showPicker(); + } else { + picker.click(); + } + picker.focus(); + }); + + return true; + } + + return false; + }, + computeSize(width) { + return [width, 32]; + }, + }; + + return widget; +} + +app.registerExtension({ + name: "Comfy.SmartNodes.SmartColorWidget", + getCustomWidgets() { + return { + SMART_COLOR: (node, inputName, inputData) => ({ + widget: node.addCustomWidget( + createColorWidget(inputName, inputData?.[1]?.default || "#000000") + ), + minWidth: 150, + minHeight: 32, + }), + }; + }, +}); diff --git a/nodes.py b/nodes.py index 4a206fc..f5a8666 100644 --- a/nodes.py +++ b/nodes.py @@ -1068,6 +1068,7 @@ class SmartBackgroundRemove: "tolerance": ("FLOAT", {"default": 0.01, "min": 0.0, "max": 1.0, "step": 0.01}), "each_point_own_color": ("BOOLEAN", {"default": True}), "debug_points": ("BOOLEAN", {"default": False}), + "clearEdges": ("INT", {"default": 0, "min": 0, "max": 1000, "step": 1}), }, "optional": { "start_point": ("POINT",), @@ -1140,7 +1141,7 @@ class SmartBackgroundRemove: return background_mask # Return the final uint8 background mask - def flood_remove(self, image, fill_size, tolerance, each_point_own_color, debug_points, start_point=None, start_points=None): + def flood_remove(self, image, fill_size, tolerance, each_point_own_color, debug_points, clearEdges=0, start_point=None, start_points=None): # Convert from tensor to numpy array input_image = 255. * image.cpu().numpy() batch_size = input_image.shape[0] @@ -1228,6 +1229,14 @@ class SmartBackgroundRemove: result[:, :, :3] = img result[:, :, 3] = mask * 255 + # Clear edge pixels to transparent + if clearEdges > 0: + ce = clearEdges + result[:ce, :, 3] = 0 # Top + result[-ce:, :, 3] = 0 # Bottom + result[:, :ce, 3] = 0 # Left + result[:, -ce:, 3] = 0 # Right + # Draw debug points if requested if debug_points and points_to_use: # Ensure result is uint8 for OpenCV drawing functions @@ -2204,9 +2213,11 @@ class SmartSaveAnimatedPNG: else: filename = filename + '.gif' - # Convert relative path to absolute path within ComfyUI's output directory output_dir = folder_paths.get_output_directory() - full_path = os.path.join(output_dir, filename) + if os.path.isabs(filename): + full_path = filename + else: + full_path = os.path.join(output_dir, filename) # Create directories if they don't exist directory = os.path.dirname(full_path) @@ -2397,9 +2408,18 @@ class SmartSaveAnimatedPNG: return (full_path,) def save_apng(self, images, fps, filename_prefix="ComfyUI", lossless=True, save_metadata=True, format="APNG", prompt=None, extra_pnginfo=None): - full_output_folder, filename, counter, subfolder, filename_prefix_ = folder_paths.get_save_image_path(filename_prefix, self.output_dir, images[0].shape[1], images[0].shape[0]) - # Extract the base filename part from the potentially path-like filename_prefix_ - base_filename = os.path.basename(filename_prefix_) + abs_prefix = os.path.abspath(filename_prefix) + is_absolute = os.path.isabs(filename_prefix) or (len(filename_prefix) >= 2 and filename_prefix[1] == ':') + cross_drive = is_absolute and os.path.splitdrive(abs_prefix)[0].lower() != os.path.splitdrive(self.output_dir)[0].lower() + + if cross_drive: + full_output_folder = os.path.dirname(abs_prefix) + base_filename = os.path.basename(abs_prefix) + subfolder = "" + os.makedirs(full_output_folder, exist_ok=True) + else: + full_output_folder, filename, counter, subfolder, filename_prefix_ = folder_paths.get_save_image_path(filename_prefix, self.output_dir, images[0].shape[1], images[0].shape[0]) + base_filename = os.path.basename(filename_prefix_) # Use a single filename, not numbered sequence if format == "APNG": ext = "apng" @@ -2414,7 +2434,10 @@ class SmartSaveAnimatedPNG: if format == "GIF": results = list() try: - relative_filename = os.path.join(subfolder, file) if subfolder else file + if cross_drive: + relative_filename = file_path + else: + relative_filename = os.path.join(subfolder, file) if subfolder else file self.save_animated_gif( images=images, filename=relative_filename, @@ -2726,11 +2749,11 @@ class SmartImagePaletteCreate: return { "required": { "colors": ("INT", {"default": 5, "min": 1, "max": 5, "step": 1}), - "color_1": ("COLOR", {"default": "#000000"}), # Black - "color_2": ("COLOR", {"default": "#000000"}), # Black - "color_3": ("COLOR", {"default": "#000000"}), # Black - "color_4": ("COLOR", {"default": "#000000"}), # Black - "color_5": ("COLOR", {"default": "#000000"}), # Black + "color_1": ("SMART_COLOR", {"default": "#000000"}), + "color_2": ("SMART_COLOR", {"default": "#000000"}), + "color_3": ("SMART_COLOR", {"default": "#000000"}), + "color_4": ("SMART_COLOR", {"default": "#000000"}), + "color_5": ("SMART_COLOR", {"default": "#000000"}), } }