The Sharpen, Vibration and Clarity parameters have been added to the RS Image Adjustments node

This commit is contained in:
Raykosan
2026-07-28 16:09:48 +03:00
parent 3aa7728d74
commit 7f4033d024
6 changed files with 184 additions and 103 deletions
+62 -18
View File
@@ -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
+1 -1
View File
@@ -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__}")
+1 -1
View File
@@ -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",
+1 -1
View File
@@ -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"]
+102 -77
View File
@@ -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);
};
+17 -5
View File
@@ -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);
}