From 7f4033d0244a52351951f9dd69dc623ea5af2192 Mon Sep 17 00:00:00 2001 From: Raykosan <71210053+Raykosan@users.noreply.github.com> Date: Tue, 28 Jul 2026 16:09:48 +0300 Subject: [PATCH] The Sharpen, Vibration and Clarity parameters have been added to the RS Image Adjustments node --- Rayko_Adjustments.py | 80 ++++++++++++++----- __init__.py | 2 +- package.json | 2 +- pyproject.toml | 2 +- web/rs_adjustments.js | 179 ++++++++++++++++++++++++------------------ web/spline_mask.js | 22 ++++-- 6 files changed, 184 insertions(+), 103 deletions(-) diff --git a/Rayko_Adjustments.py b/Rayko_Adjustments.py index 94354ea..f32b731 100644 --- a/Rayko_Adjustments.py +++ b/Rayko_Adjustments.py @@ -2,7 +2,7 @@ # Copyright 2025-2026 Raykosan (RaykoStudio) # # Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. +# you may not use th is file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 @@ -13,9 +13,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -import aiohttp -import base64 -from io import BytesIO import os import torch import numpy as np @@ -27,6 +24,7 @@ from PIL import Image from collections import OrderedDict from server import PromptServer from aiohttp import web +import aiohttp class LRUCache: def __init__(self, max_size: int = 50): @@ -69,7 +67,6 @@ def apply_basic_adjustments(img_bgr, brightness, contrast, hue, saturation): if brightness != 0 or contrast != 0: alpha = 1.0 + (contrast / 100.0) beta = brightness - img_bgr = img_bgr.astype(np.float32) img_bgr = img_bgr * alpha + beta img_bgr = np.clip(img_bgr, 0, 255).astype(np.uint8) @@ -82,6 +79,44 @@ def apply_basic_adjustments(img_bgr, brightness, contrast, hue, saturation): return img_bgr +def apply_sharpen(img_bgr, amount): + if amount <= 0: + return img_bgr + + amount = min(amount, 200) / 100.0 + blurred = cv2.GaussianBlur(img_bgr, (0, 0), 3) + sharpened = cv2.addWeighted(img_bgr, 1.0 + amount, blurred, -amount, 0) + return sharpened + +def apply_vibrance(img_bgr, amount): + if amount == 0: + return img_bgr + + amount = amount / 100.0 + img_float = img_bgr.astype(np.float32) / 255.0 + + max_rgb = np.max(img_float, axis=2) + avg_rgb = np.mean(img_float, axis=2) + saturation = max_rgb - avg_rgb + + mask = 1.0 - saturation + + result = img_float.copy() + for c in range(3): + result[:, :, c] = img_float[:, :, c] + (img_float[:, :, c] - avg_rgb[:, :, np.newaxis][:, :, 0]) * amount * mask + + result = np.clip(result * 255, 0, 255).astype(np.uint8) + return result + +def apply_clarity(img_bgr, amount): + if amount == 0: + return img_bgr + + amount = amount / 100.0 + blurred = cv2.GaussianBlur(img_bgr, (0, 0), 30) + clarified = cv2.addWeighted(img_bgr, 1.0 + amount, blurred, -amount, 0) + return clarified + def parse_cube_lut(lut_path): try: mtime = os.path.getmtime(lut_path) @@ -109,11 +144,8 @@ def parse_cube_lut(lut_path): continue lut_array = np.array(lut_data, dtype=np.float32).reshape((size, size, size, 3)) - lut_array = np.transpose(lut_array, (2, 1, 0, 3)) - LUT_CACHE.put(cache_key, lut_array) - return lut_array def apply_lut(img_bgr, lut_path, intensity): @@ -168,7 +200,6 @@ def apply_lut(img_bgr, lut_path, intensity): ) result = np.clip(result, 0, 1) - result_bgr = result[:, :, ::-1] if intensity < 1.0: @@ -182,7 +213,6 @@ def apply_levels(img_bgr, input_black, gamma, input_white): return img_bgr img_float = img_bgr.astype(np.float32) / 255.0 - in_min = input_black / 255.0 in_max = input_white / 255.0 in_range = max(0.001, in_max - in_min) @@ -201,7 +231,6 @@ def apply_exposure(img_bgr, exposure, offset): return img_bgr img_float = img_bgr.astype(np.float32) / 255.0 - multiplier = 2.0 ** (exposure / 100.0) img_float = img_float * multiplier + (offset / 100.0) @@ -244,7 +273,7 @@ def apply_black_and_white(img_bgr, red, yellow, green, cyan, blue, magenta): img_float = img_bgr.astype(np.float32) / 255.0 img_rgb = img_float[:, :, ::-1] img_hsv = cv2.cvtColor((img_rgb * 255).astype(np.uint8), cv2.COLOR_RGB2HSV) - hue = img_hsv[:, :, 0].astype(np.float32) * 2.0 # 0..358 + hue = img_hsv[:, :, 0].astype(np.float32) * 2.0 w_red = red / 100.0 w_yellow = yellow / 100.0 @@ -316,11 +345,11 @@ def apply_selective_color(img_bgr, color_name, cyan, magenta, yellow, black): img_float = img_bgr.astype(np.float32) / 255.0 img_hsv = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2HSV).astype(np.float32) - h = img_hsv[:, :, 0] # 0..179 - s = img_hsv[:, :, 1] # 0..255 - v = img_hsv[:, :, 2] # 0..255 + h = img_hsv[:, :, 0] + s = img_hsv[:, :, 1] + v = img_hsv[:, :, 2] - h_deg = h * 2.0 # 0..358 + h_deg = h * 2.0 s_norm = s / 255.0 v_norm = v / 255.0 @@ -339,7 +368,6 @@ def apply_selective_color(img_bgr, color_name, cyan, magenta, yellow, black): diff = np.minimum(diff, 360 - diff) mask = np.exp(-(diff ** 2) / (2 * sigma ** 2)) elif color_name == "Whites": - mask = (v_norm > 0.8) * (1 - s_norm) mask = np.exp(-((v_norm - 1.0)**2) / (2*0.1**2)) * np.exp(-(s_norm**2) / (2*0.2**2)) elif color_name == "Neutrals": mask = np.exp(-((v_norm - 0.5)**2) / (2*0.15**2)) * np.exp(-(s_norm**2) / (2*0.2**2)) @@ -380,10 +408,22 @@ def process_image(img_bgr, adjustments): result_bgr = apply_basic_adjustments(img_bgr, brightness, contrast, hue, saturation) + sharpen = adjustments.get("sharpen", 0) + if sharpen != 0: + result_bgr = apply_sharpen(result_bgr, sharpen) + lut_path = adjustments.get("lut_path", "") lut_intensity = adjustments.get("lut_intensity", 100) if lut_path: result_bgr = apply_lut(result_bgr, lut_path, lut_intensity / 100.0) + + vibrance = adjustments.get("vibrance", 0) + if vibrance != 0: + result_bgr = apply_vibrance(result_bgr, vibrance) + + clarity = adjustments.get("clarity", 0) + if clarity != 0: + result_bgr = apply_clarity(result_bgr, clarity) levels = adjustments.get("levels", {}) result_bgr = apply_levels( @@ -606,6 +646,7 @@ class RS_ImageAdjustments: "image": ("IMAGE",), "brightness": ("INT", {"default": 0, "min": -100, "max": 100, "step": 1}), "contrast": ("INT", {"default": 0, "min": -100, "max": 100, "step": 1}), + "sharpen": ("INT", {"default": 0, "min": 0, "max": 200, "step": 1}), "hue": ("INT", {"default": 0, "min": -180, "max": 180, "step": 1}), "saturation": ("INT", {"default": 0, "min": -100, "max": 100, "step": 1}), }, @@ -621,7 +662,7 @@ class RS_ImageAdjustments: OUTPUT_NODE = True DESCRIPTION = "Interactive image adjustments with real-time preview" - def apply_adjustments(self, image, brightness, contrast, hue, saturation, unique_id=None, advanced_params=None): + def apply_adjustments(self, image, brightness, contrast, sharpen, hue, saturation, unique_id=None, advanced_params=None): unique_id = str(unique_id) if unique_id is not None else "unknown" if image.numel() == 0: @@ -656,6 +697,7 @@ class RS_ImageAdjustments: "timestamp": ts, "brightness": brightness, "contrast": contrast, + "sharpen": sharpen, "hue": hue, "saturation": saturation, "advanced_params": advanced_params or "{}" @@ -686,6 +728,7 @@ class RS_ImageAdjustments: brightness = adjustments.get("brightness", brightness) contrast = adjustments.get("contrast", contrast) + sharpen = adjustments.get("sharpen", sharpen) hue = adjustments.get("hue", hue) saturation = adjustments.get("saturation", saturation) @@ -693,6 +736,7 @@ class RS_ImageAdjustments: adjustments["brightness"] = brightness adjustments["contrast"] = contrast + adjustments["sharpen"] = sharpen adjustments["hue"] = hue adjustments["saturation"] = saturation diff --git a/__init__.py b/__init__.py index a4225ff..7364778 100644 --- a/__init__.py +++ b/__init__.py @@ -29,7 +29,7 @@ IGNORED_MODULES: Set[str] = {'__init__.py'} REQUIRED_MODULES: Set[str] = set() PACKAGE_DISPLAY_NAME = "ComfyUI_RaykoStudio" -__version__ = "0.32.2" +__version__ = "0.32.3" def load_modules(): print(f"🦊 {PACKAGE_DISPLAY_NAME}: Version: {__version__}") diff --git a/package.json b/package.json index 2e4485a..e010b49 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ComfyUI_RaykoStudio", - "version": "0.32.2", + "version": "0.32.3", "description": "Set of custom nodes for ComfyUI providing additional image processing capabilities", "author": "Raykosan", "license": "Apache License 2.0", diff --git a/pyproject.toml b/pyproject.toml index e7b7ce9..4e686a5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "ComfyUI_RaykoStudio" description = "Set of custom nodes for ComfyUI providing additional image processing capabilities" -version = "0.32.2" +version = "0.32.3" license = { file = "LICENSE" } dependencies = ["torch", "numpy", "Pillow>=10.0.0", "freetype-py>=2.5.1", "pycairo>=1.29.0", "scipy>=1.10.0"] diff --git a/web/rs_adjustments.js b/web/rs_adjustments.js index 82ded5a..be92d79 100644 --- a/web/rs_adjustments.js +++ b/web/rs_adjustments.js @@ -4,12 +4,17 @@ import { api } from "../../../scripts/api.js"; let activeNodeId = null; const BASIC_PARAMS = ['brightness', 'contrast', 'hue', 'saturation']; +const HIGHLIGHT_PARAMS = [...BASIC_PARAMS, 'sharpen']; +const HIDDEN_WIDGETS = ['brightness', 'contrast', 'hue', 'saturation', 'sharpen']; const DEFAULT_SLIDER_VALUES = { brightness: 0, contrast: 0, + sharpen: 0, hue: 0, saturation: 0, + vibrance: 0, + clarity: 0, lut_intensity: 100, input_black: 0, gamma: 1.0, @@ -40,10 +45,6 @@ const DEFAULT_SLIDER_VALUES = { blue_red: 0, blue_green: 0, blue_blue: 100, - selective_cyan: 0, - selective_magenta: 0, - selective_yellow: 0, - selective_black: 0, sc_cyan: 0, sc_magenta: 0, sc_yellow: 0, @@ -54,8 +55,11 @@ const DEFAULT_SLIDER_VALUES = { const DEFAULT_ADJUSTMENTS = { brightness: 0, contrast: 0, + sharpen: 0, hue: 0, saturation: 0, + vibrance: 0, + clarity: 0, lut_path: "", lut_intensity: 100, levels: { input_black: 0, gamma: 1.0, input_white: 255 }, @@ -190,6 +194,8 @@ app.registerExtension({ this.sidePanel = null; this.overlayInputs = {}; this.sliderDisplays = {}; + this._basicParamLabels = {}; + this._allSectionsExpanded = false; this.minWidth = 500; this.minHeight = 450; @@ -198,27 +204,18 @@ app.registerExtension({ this.slidersY = 0; this.sliderRects = []; this.resetBtnRects = []; - this.sliderHover = [false, false, false, false]; - this.resetBtnHover = [false, false, false, false]; + this.sliderHover = [false, false, false, false, false]; + this.resetBtnHover = [false, false, false, false, false]; this.sliderDragging = -1; this.btnApplyHover = false; this.btnCancelHover = false; this.btnAdvancedHover = false; this.btnResetAllHover = false; - this._allSectionsExpanded = false; - this._basicParamLabels = {}; this._syncingWidgets = false; - const syncWidgetValue = (widgetName, value) => { - const widget = this.widgets?.find(w => w.name === widgetName); - if (widget) { - widget.value = value; - } - }; - - ["brightness", "contrast", "hue", "saturation"].forEach(n => { + BASIC_PARAMS.forEach(n => { const w = this.widgets?.find(w => w.name === n); if (w) { w.hidden = true; @@ -235,6 +232,25 @@ app.registerExtension({ }; } }); + + const serverParams = ['sharpen']; + serverParams.forEach(n => { + const w = this.widgets?.find(w => w.name === n); + if (w) { + w.hidden = true; + w.computeSize = () => [0, 0]; + w.y = 0; + const origCallback = w.callback; + w.callback = (v) => { + if (origCallback) origCallback.apply(w, arguments); + if (!this._syncingWidgets) { + this.adjustments[n] = v; + this.setDirtyCanvas(true); + this._scheduleHeavyRender(); + } + }; + } + }); const advWidget = this.widgets?.find(w => w.name === "advanced_params"); if (advWidget) { @@ -272,7 +288,7 @@ app.registerExtension({ nodeType.prototype._hideNativeWidgets = function() { if (!this.widgets) return; - BASIC_PARAMS.forEach(param => { + HIDDEN_WIDGETS.forEach(param => { const widget = this.widgets.find(w => w.name === param); if (widget) { widget.hidden = true; @@ -288,7 +304,7 @@ app.registerExtension({ if (!this.widgets) return; this._syncingWidgets = true; try { - BASIC_PARAMS.forEach(param => { + HIDDEN_WIDGETS.forEach(param => { const widget = this.widgets.find(w => w.name === param); if (widget && widget.value !== this.adjustments[param]) { widget.value = this.adjustments[param]; @@ -310,22 +326,24 @@ app.registerExtension({ this.sliderDisplays[key].textContent = String(this.adjustments[key]); } }); + + if (this.overlayInputs.sharpen) { + this.overlayInputs.sharpen.value = this.adjustments.sharpen || 0; + } + if (this.sliderDisplays.sharpen) { + this.sliderDisplays.sharpen.textContent = String(this.adjustments.sharpen || 0); + } }; nodeType.prototype._isParamModified = function(key, parentObj = this.adjustments) { const def = DEFAULT_SLIDER_VALUES[key]; - if (def === undefined) { - return false; - } - + if (def === undefined) return false; + const val = parentObj[key]; - if (val === undefined) { - return false; - } - + if (val === undefined) return false; + if (typeof def === 'number') { - const isModified = Math.abs(val - def) > 0.001; - return isModified; + return Math.abs(val - def) > 0.001; } return val !== def; }; @@ -333,39 +351,30 @@ app.registerExtension({ nodeType.prototype._isSectionModified = function(sectionName) { const checks = { lut: () => (this.adjustments.lut_path && this.adjustments.lut_path !== "") || this._isParamModified('lut_intensity'), - + advanced_tools: () => this._isParamModified('vibrance') || this._isParamModified('clarity'), levels: () => this._isParamModified('input_black', this.adjustments.levels) || this._isParamModified('gamma', this.adjustments.levels) || this._isParamModified('input_white', this.adjustments.levels), - exposure: () => this._isParamModified('exposure', this.adjustments.exposure) || this._isParamModified('offset', this.adjustments.exposure), - color_balance: () => [ 'shadows_cyan_red', 'shadows_magenta_green', 'shadows_yellow_blue', 'midtones_cyan_red', 'midtones_magenta_green', 'midtones_yellow_blue', 'highlights_cyan_red', 'highlights_magenta_green', 'highlights_yellow_blue' ].some(k => this._isParamModified(k, this.adjustments.color_balance)), - black_white: () => ['bw_red', 'bw_yellow', 'bw_green', 'bw_cyan', 'bw_blue', 'bw_magenta'].some(k => this._isParamModified(k, this.adjustments.black_white)), - channel_mixer: () => [ 'red_red', 'red_green', 'red_blue', 'green_red', 'green_green', 'green_blue', 'blue_red', 'blue_green', 'blue_blue' ].some(k => this._isParamModified(k, this.adjustments.channel_mixer)), - selective_color: () => ['sc_cyan', 'sc_magenta', 'sc_yellow', 'sc_black'].some(k => this._isParamModified(k, this.adjustments.selective_color)) }; - return checks[sectionName] ? checks[sectionName]() : false; }; nodeType.prototype._updateSectionHeaders = function() { - if (!this._sectionRegistry) { - return; - } - + if (!this._sectionRegistry) return; this._sectionRegistry.forEach(entry => { const modified = this._isSectionModified(entry.name); if (entry.label) { @@ -375,7 +384,7 @@ app.registerExtension({ }; nodeType.prototype._updateBasicParamLabels = function() { - BASIC_PARAMS.forEach(key => { + HIGHLIGHT_PARAMS.forEach(key => { const lbl = this._basicParamLabels[key]; if (lbl) { const isModified = this._isParamModified(key); @@ -451,11 +460,9 @@ app.registerExtension({ this._updateSectionHeaders(); this._updateBasicParamLabels(); - this.previewImage = null; this.setDirtyCanvas(true); this._scheduleHeavyRender(); - this._syncWidgetsFromAdjustments(); }; @@ -488,12 +495,13 @@ app.registerExtension({ return b; }; - const btnNormalMode = makeBtn("🟢 NORMAL MODE", "#2196F3", () => { this._toggleAdvancedMode(); }); + const btnNormalMode = makeBtn(" NORMAL MODE", "#2196F3", () => { this._toggleAdvancedMode(); }); buttonContainer.appendChild(btnNormalMode); const btnApply = makeBtn("✔️ APPLY", "#4CAF50", () => { this._sendAdjustments(); this._toggleAdvancedMode(); }); buttonContainer.appendChild(btnApply); - const btnCancel = makeBtn("❌ CANCEL", "#dc3545", () => { this._cancelEditing(); this._toggleAdvancedMode(); }); + const btnCancel = makeBtn(" CANCEL", "#dc3545", () => { this._cancelEditing(); this._toggleAdvancedMode(); }); buttonContainer.appendChild(btnCancel); + const bottomRow = document.createElement('div'); bottomRow.style.cssText = 'display:flex;gap:8px;width:100%;'; @@ -503,7 +511,6 @@ app.registerExtension({ this._expandCollapseBtn = makeBtn("▶ EXPAND ALL", "#888", () => { this._allSectionsExpanded = !this._allSectionsExpanded; - const display = this._allSectionsExpanded ? 'block' : 'none'; const icon = this._allSectionsExpanded ? '▼' : '▶'; const text = this._allSectionsExpanded ? '▼ COLLAPSE ALL' : '▶ EXPAND ALL'; @@ -512,7 +519,6 @@ app.registerExtension({ if (entry.content) entry.content.style.display = display; if (entry.chevron) entry.chevron.textContent = icon; }); - this._expandCollapseBtn.textContent = text; }); this._expandCollapseBtn.style.flex = '1'; @@ -527,9 +533,11 @@ app.registerExtension({ lbl.textContent = label; lbl.style.cssText = 'color:#aaa;font-size:11px;font-weight:600;'; container.appendChild(lbl); - if (BASIC_PARAMS.includes(key)) { + + if (HIGHLIGHT_PARAMS.includes(key)) { this._basicParamLabels[key] = lbl; } + const row = document.createElement('div'); row.style.cssText = 'display:flex;align-items:center;gap:8px;'; const slider = document.createElement('input'); @@ -573,10 +581,10 @@ app.registerExtension({ const widget = this.widgets?.find(w => w.name === key); if (widget) widget.value = num; - this.previewImage = null; this.setDirtyCanvas(true); if (BASIC_PARAMS.includes(key)) { + this.previewImage = null; } else { this._scheduleHeavyRender(); } @@ -621,7 +629,7 @@ app.registerExtension({ }; const resetBtn = document.createElement('button'); - resetBtn.textContent = '🔄'; + resetBtn.textContent = '🔄️'; resetBtn.style.cssText = 'width:28px;height:28px;background:#252525;color:#888;border:1px solid #444;border-radius:4px;cursor:pointer;font-size:14px;display:flex;align-items:center;justify-content:center;transition:0.15s;flex-shrink:0;'; resetBtn.onmouseenter = () => { resetBtn.style.background = '#2a2a2a'; resetBtn.style.color = '#fff'; resetBtn.style.borderColor = '#4CAF50'; }; resetBtn.onmouseleave = () => { resetBtn.style.background = '#252525'; resetBtn.style.color = '#888'; resetBtn.style.borderColor = '#444'; }; @@ -635,7 +643,6 @@ app.registerExtension({ this._updateBasicParamLabels(); this.setDirtyCanvas(true); - if (BASIC_PARAMS.includes(key)) { this.previewImage = null; } @@ -709,6 +716,7 @@ app.registerExtension({ this.sidePanel.appendChild(div()); this.sidePanel.appendChild(makeSlider("BRIGHTNESS", "brightness", -100, 100, 1)); this.sidePanel.appendChild(makeSlider("CONTRAST", "contrast", -100, 100, 1)); + this.sidePanel.appendChild(makeSlider("SHARPEN", "sharpen", 0, 200, 1)); this.sidePanel.appendChild(makeSlider("HUE", "hue", -180, 180, 1)); this.sidePanel.appendChild(makeSlider("SATURATION", "saturation", -100, 100, 1)); this.sidePanel.appendChild(div()); @@ -731,7 +739,7 @@ app.registerExtension({ browseBtn.onmouseleave = () => { browseBtn.style.background = '#252525'; browseBtn.style.color = '#aaa'; browseBtn.style.borderColor = '#444'; }; browseBtn.onclick = (e) => { e.stopPropagation(); fileInput.click(); }; const resetBtn = document.createElement('button'); - resetBtn.textContent = '🔄️'; + resetBtn.textContent = '🔄'; resetBtn.style.cssText = 'width:28px;height:28px;background:#252525;color:#888;border:1px solid #444;border-radius:4px;cursor:pointer;font-size:14px;display:flex;align-items:center;justify-content:center;transition:0.15s;flex-shrink:0;'; resetBtn.onmouseenter = () => { resetBtn.style.background = '#2a2a2a'; resetBtn.style.color = '#fff'; resetBtn.style.borderColor = '#4CAF50'; }; resetBtn.onmouseleave = () => { resetBtn.style.background = '#252525'; resetBtn.style.color = '#888'; resetBtn.style.borderColor = '#444'; }; @@ -787,6 +795,12 @@ app.registerExtension({ }); this.sidePanel.appendChild(lutSection); + const advancedToolsSection = makeSection("ADVANCED TOOLS", "advanced_tools", (content) => { + content.appendChild(makeSlider("VIBRANCE", "vibrance", -100, 100, 1)); + content.appendChild(makeSlider("CLARITY", "clarity", -100, 100, 1)); + }); + this.sidePanel.appendChild(advancedToolsSection); + const levelsSection = makeSection("LEVELS", "levels", (content) => { content.appendChild(makeSlider("INPUT BLACK", "input_black", 0, 255, 1, false, this.adjustments.levels)); content.appendChild(makeSlider("GAMMA", "gamma", 0.1, 3.0, 0.01, true, this.adjustments.levels)); @@ -947,7 +961,6 @@ app.registerExtension({ } this.currentRenderAbortController = new AbortController(); const signal = this.currentRenderAbortController.signal; - this._isHeavyRenderPending = true; const payload = { node_id: String(this.id), @@ -972,16 +985,12 @@ app.registerExtension({ if (this._previewTimestamp === ts) { this.previewImage = img; this.setDirtyCanvas(true); - this._isHeavyRenderPending = false; } }; - img.onerror = () => { - this._isHeavyRenderPending = false; - }; + img.onerror = () => {}; img.src = `/view?filename=${data.preview_file}&type=temp&t=${ts}`; } } catch (e) { - this._isHeavyRenderPending = false; if (e.name === 'AbortError') return; } }; @@ -1076,16 +1085,27 @@ app.registerExtension({ const data = this.pendingEditorData; this.adjustments.brightness = data.brightness || 0; this.adjustments.contrast = data.contrast || 0; + this.adjustments.sharpen = data.sharpen || 0; this.adjustments.hue = data.hue || 0; this.adjustments.saturation = data.saturation || 0; try { const adv = JSON.parse(data.advanced_params || "{}"); - Object.keys(adv).forEach(key => { - if (this.adjustments[key] !== undefined) { - this.adjustments[key] = adv[key]; + + const mergeRecursive = (target, source) => { + for (let key in source) { + if (source.hasOwnProperty(key)) { + if (source[key] !== null && typeof source[key] === 'object' && !Array.isArray(source[key])) { + if (!target[key] || typeof target[key] !== 'object') target[key] = {}; + mergeRecursive(target[key], source[key]); + } else { + target[key] = source[key]; + } + } } - }); + }; + + mergeRecursive(this.adjustments, adv); } catch (e) {} this.realBackground = { width: data.bg_width, height: data.bg_height }; @@ -1100,7 +1120,6 @@ app.registerExtension({ this.backgroundImage = img; this.isLoading = false; this.isEditing = true; - this._initWebSocket(); this._tempCanvas.width = img.width; this._tempCanvas.height = img.height; @@ -1112,6 +1131,7 @@ app.registerExtension({ this._syncWidgetsFromAdjustments(); this._syncOverlayUI(); this._updateBasicParamLabels(); + this._initWebSocket(); this._scheduleHeavyRender(); }; img.onerror = () => { @@ -1144,15 +1164,6 @@ app.registerExtension({ cancelAnimationFrame(this._overlayRenderLoop); this._overlayRenderLoop = null; } - if (this._ws) { - this._ws.close(); - this._ws = null; - this._wsConnected = false; - } - if (this._wsReconnectTimer) { - clearTimeout(this._wsReconnectTimer); - this._wsReconnectTimer = null; - } this.setDirtyCanvas(true); return; } @@ -1274,6 +1285,15 @@ app.registerExtension({ cancelAnimationFrame(this._overlayRenderLoop); this._overlayRenderLoop = null; } + if (this._ws) { + this._ws.close(); + this._ws = null; + this._wsConnected = false; + } + if (this._wsReconnectTimer) { + clearTimeout(this._wsReconnectTimer); + this._wsReconnectTimer = null; + } this.setDirtyCanvas(true); }; @@ -1284,7 +1304,7 @@ app.registerExtension({ const cSize = Math.max(200, size[0] - 40); this.canvasPixelSize = cSize; - const slidersH = 4 * 30 + 10; + const slidersH = 5 * 30 + 10; const btnH = 30; const gaps = 10 + 8 + 8 + 20 + 15; const neededHeight = titleH + canvasTopPadding + cSize + slidersH + btnH + gaps; @@ -1493,6 +1513,7 @@ app.registerExtension({ const sliderConfigs = [ { label: "BRIGHTNESS", key: "brightness", min: -100, max: 100 }, { label: "CONTRAST", key: "contrast", min: -100, max: 100 }, + { label: "SHARPEN", key: "sharpen", min: 0, max: 200 }, { label: "HUE", key: "hue", min: -180, max: 180 }, { label: "SATURATION", key: "saturation", min: -100, max: 100 } ]; @@ -1519,7 +1540,7 @@ app.registerExtension({ } }); - const buttonsY = slidersY + 4 * (sliderH + gapSlider) + 10; + const buttonsY = slidersY + 5 * (sliderH + gapSlider) + 10; const btnH2 = 30; const btnGap2 = 10; const btnW2 = (this.size[0] - 35) / 2; @@ -1541,7 +1562,7 @@ app.registerExtension({ ctx.strokeStyle = "#dc3545"; ctx.stroke(); ctx.fillStyle = "#dc3545"; - ctx.fillText("❌ CANCEL", 15 + btnW2 + btnGap2 + btnW2 / 2, buttonsY + btnH2 / 2 + 4); + ctx.fillText(" CANCEL", 15 + btnW2 + btnGap2 + btnW2 / 2, buttonsY + btnH2 / 2 + 4); }; nodeType.prototype.onMouseDown = function(event, pos) { @@ -1605,7 +1626,7 @@ app.registerExtension({ const btnH2 = 30; const btnGap2 = 10; const btnW2 = (this.size[0] - 35) / 2; - const buttonsY = this.slidersY + 4 * 38 + 10; + const buttonsY = this.slidersY + 5 * 38 + 10; const y1 = buttonsY; if (pos[0] >= 15 && pos[0] <= 15 + btnW2 && pos[1] >= y1 && pos[1] <= y1 + btnH2) { @@ -1677,7 +1698,7 @@ app.registerExtension({ const btnH2 = 30; const btnGap2 = 10; const btnW2 = (this.size[0] - 35) / 2; - const buttonsY = this.slidersY + 4 * 38 + 10; + const buttonsY = this.slidersY + 5 * 38 + 10; const y1 = buttonsY; const prevApply = this.btnApplyHover; @@ -1715,7 +1736,11 @@ app.registerExtension({ } } - this.previewImage = null; + if (BASIC_PARAMS.includes(rect.key)) { + this.previewImage = null; + } else { + this._scheduleHeavyRender(); + } this.setDirtyCanvas(true); }; diff --git a/web/spline_mask.js b/web/spline_mask.js index b26a6d8..26a90c5 100644 --- a/web/spline_mask.js +++ b/web/spline_mask.js @@ -1,6 +1,3 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright 2025-2026 Raykosan (RaykoStudio) -console.log("[SPLINE 🦊] spline_mask.js LOADED!"); import { app } from "../../scripts/app.js"; import { api } from "../../scripts/api.js"; @@ -52,8 +49,8 @@ app.registerExtension({ node.imageList = []; node.buttons = [ - { label: "🎨 IMAGE", color: "#2196F3", callback: () => node.showImageSelector(), hover: false }, - { label: "🖼️ UPLOAD", color: "#4CAF50", callback: () => node.triggerFileUpload(), hover: false }, + { label: "📂 INPUT", color: "#2196F3", callback: () => node.showImageSelector(), hover: false }, + { label: "🖥️ UPLOAD", color: "#4CAF50", callback: () => node.triggerFileUpload(), hover: false }, { label: "🔴 CLEAR", color: "#dc3545", callback: () => node.clearPoints(), hover: false } ]; @@ -499,6 +496,20 @@ app.registerExtension({ const btnY = h - 45; const btnW = (w - 50) / 3; + // === РАЗМЕРЫ ИЗОБРАЖЕНИЯ НАД ПРЕВЬЮ === + if (this.imageLoaded && this.image) { + ctx.fillStyle = "#888"; + ctx.font = "11px sans-serif"; + ctx.textAlign = "center"; + ctx.textBaseline = "top"; + ctx.fillText( + `${this.image.width} × ${this.image.height}`, + w / 2, + titleBarHeight + widgetsHeight - 13 + ); + } + // ==================================== + const startY = titleBarHeight + widgetsHeight + padding; const availableHeight = h - startY - footerHeight - padding; const availableWidth = w - (padding * 2); @@ -527,6 +538,7 @@ app.registerExtension({ ctx.fillStyle = "#555"; ctx.font = "14px Arial"; ctx.textAlign = "center"; + ctx.textBaseline = "middle"; ctx.fillText("Select Image...", w / 2, startY + availableHeight / 2); }