From 67f0bbc2c00f80ed92937c48ce5f193ca5b0bde8 Mon Sep 17 00:00:00 2001 From: Raykosan <71210053+Raykosan@users.noreply.github.com> Date: Mon, 27 Jul 2026 21:28:52 +0300 Subject: [PATCH] Adding the RS Image Adjustments node --- Example/RS Image Adjustments.json | 1 + Rayko_Adjustments.py | 653 +++++++++++ Rayko_Saturation.py | 70 -- __init__.py | 2 +- fonts/Boorsok.otf | Bin 0 -> 46904 bytes fonts/Martian Grotesk.otf | Bin 0 -> 66596 bytes package.json | 2 +- pyproject.toml | 2 +- web/rs_adjustments.js | 1712 +++++++++++++++++++++++++++++ Рекоменды.txt | 108 ++ 10 files changed, 2477 insertions(+), 73 deletions(-) create mode 100644 Example/RS Image Adjustments.json create mode 100644 Rayko_Adjustments.py delete mode 100644 Rayko_Saturation.py create mode 100644 fonts/Boorsok.otf create mode 100644 fonts/Martian Grotesk.otf create mode 100644 web/rs_adjustments.js create mode 100644 Рекоменды.txt diff --git a/Example/RS Image Adjustments.json b/Example/RS Image Adjustments.json new file mode 100644 index 0000000..27579e3 --- /dev/null +++ b/Example/RS Image Adjustments.json @@ -0,0 +1 @@ +{"id":"147928be-ba9f-496d-8b40-629122a4ad37","revision":0,"last_node_id":8,"last_link_id":12,"nodes":[{"id":3,"type":"PreviewImage","pos":[60,130],"size":[340,440],"flags":{},"order":2,"mode":0,"inputs":[{"localized_name":"images","name":"images","type":"IMAGE","link":12}],"outputs":[{"localized_name":"images","name":"images","type":"IMAGE","links":null}],"properties":{"cnr_id":"comfy-core","ver":"0.28.0","Node name for S&R":"PreviewImage","ue_properties":{"widget_ue_connectable":{},"input_ue_unconnectable":{},"version":"7.8"}},"widgets_values":[]},{"id":4,"type":"LoadImage","pos":[-1030,80],"size":[370,490],"flags":{},"order":0,"mode":0,"inputs":[{"localized_name":"image","name":"image","type":"COMBO","widget":{"name":"image"},"link":null},{"localized_name":"choose file to upload","name":"upload","type":"IMAGEUPLOAD","widget":{"name":"upload"},"link":null}],"outputs":[{"localized_name":"IMAGE","name":"IMAGE","type":"IMAGE","links":[11]},{"localized_name":"MASK","name":"MASK","type":"MASK","links":null}],"properties":{"cnr_id":"comfy-core","ver":"0.28.0","Node name for S&R":"LoadImage","ue_properties":{"widget_ue_connectable":{},"input_ue_unconnectable":{},"version":"7.8"}},"widgets_values":["ComfyUI_00014_ (3).png","image"]},{"id":8,"type":"RS_ImageAdjustments","pos":[-520,-30],"size":[500,741],"flags":{},"order":1,"mode":0,"inputs":[{"localized_name":"image","name":"image","type":"IMAGE","link":11},{"localized_name":"brightness","name":"brightness","type":"INT","widget":{"name":"brightness"},"link":null},{"localized_name":"contrast","name":"contrast","type":"INT","widget":{"name":"contrast"},"link":null},{"localized_name":"hue","name":"hue","type":"INT","widget":{"name":"hue"},"link":null},{"localized_name":"saturation","name":"saturation","type":"INT","widget":{"name":"saturation"},"link":null}],"outputs":[{"localized_name":"IMAGE","name":"IMAGE","type":"IMAGE","links":[12]}],"properties":{"cnr_id":"ComfyUI_RaykoStudio","ver":"1c7a858cafe7f0e2e581e7527169ec105a6ff474","Node name for S&R":"RS_ImageAdjustments","ue_properties":{"widget_ue_connectable":{},"input_ue_unconnectable":{},"version":"7.8"}},"widgets_values":[0,0,0,0],"color":"#009933"}],"links":[[11,4,0,8,0,"IMAGE"],[12,8,0,3,0,"IMAGE"]],"groups":[],"config":{},"extra":{"ue_links":[],"links_added_by_ue":[],"ds":{"scale":1.2100000000000006,"offset":[1353.2705573948465,117.42593749801291]}},"version":0.4} \ No newline at end of file diff --git a/Rayko_Adjustments.py b/Rayko_Adjustments.py new file mode 100644 index 0000000..01c6fe5 --- /dev/null +++ b/Rayko_Adjustments.py @@ -0,0 +1,653 @@ +import os +import torch +import numpy as np +import cv2 +import json +import time +import folder_paths +from PIL import Image +from collections import OrderedDict +from server import PromptServer +from aiohttp import web + +class LRUCache: + def __init__(self, max_size: int = 50): + self.cache = OrderedDict() + self.max_size = max_size + + def get(self, key): + if key not in self.cache: + return None + self.cache.move_to_end(key) + return self.cache[key] + + def put(self, key, value): + if key in self.cache: + self.cache.move_to_end(key) + self.cache[key] = value + if len(self.cache) > self.max_size: + self.cache.popitem(last=False) + + def clear(self): + self.cache.clear() + +PENDING_ADJUSTMENTS = LRUCache(max_size=50) +PREVIEW_CACHE = LRUCache(max_size=20) +LUT_CACHE = LRUCache(max_size=10) + +def tensor2pil(tensor): + arr = (tensor.cpu().numpy() * 255.0).astype(np.uint8) + if arr.ndim == 4: + arr = arr[0] + if arr.shape[2] >= 4: + return Image.fromarray(arr[:, :, :4], 'RGBA').convert('RGB') + return Image.fromarray(arr, 'RGB') + +def pil2tensor(pil_img): + arr = np.array(pil_img).astype(np.float32) / 255.0 + return torch.from_numpy(arr).unsqueeze(0) + +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) + + if hue != 0 or saturation != 0: + img_hsv = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2HSV).astype(np.float32) + img_hsv[:, :, 0] = (img_hsv[:, :, 0] + hue) % 180 + img_hsv[:, :, 1] = np.clip(img_hsv[:, :, 1] * (1.0 + saturation / 100.0), 0, 255) + img_bgr = cv2.cvtColor(img_hsv.astype(np.uint8), cv2.COLOR_HSV2BGR) + + return img_bgr + +def parse_cube_lut(lut_path): + try: + mtime = os.path.getmtime(lut_path) + cache_key = f"{lut_path}_{int(mtime)}" + except: + cache_key = lut_path + + cached = LUT_CACHE.get(cache_key) + if cached is not None: + return cached + + with open(lut_path, 'r') as f: + lines = f.readlines() + + size = 33 + lut_data = [] + for line in lines: + line = line.strip() + if line.startswith('LUT_3D_SIZE'): + size = int(line.split()[1]) + elif line and not line.startswith('#') and not line.startswith('TITLE') and not line.startswith('DOMAIN'): + try: + lut_data.append([float(x) for x in line.split()]) + except ValueError: + 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): + if intensity <= 0.01: + return img_bgr + + if not os.path.exists(lut_path): + temp_dir = folder_paths.get_temp_directory() + full_path = os.path.join(temp_dir, "rs_adjustments", lut_path) + if not os.path.exists(full_path): + return img_bgr + lut_path = full_path + + lut_array = parse_cube_lut(lut_path) + size = lut_array.shape[0] + + img_float = img_bgr.astype(np.float32) / 255.0 + img_rgb = img_float[:, :, ::-1] + + coords = img_rgb * (size - 1) + idx = np.floor(coords).astype(np.int32) + idx = np.clip(idx, 0, size - 2) + frac = coords - idx + + i0, i1 = idx[:, :, 0], idx[:, :, 0] + 1 + j0, j1 = idx[:, :, 1], idx[:, :, 1] + 1 + k0, k1 = idx[:, :, 2], idx[:, :, 2] + 1 + + f0, f1, f2 = frac[:, :, 0], frac[:, :, 1], frac[:, :, 2] + + w000 = (1 - f0) * (1 - f1) * (1 - f2) + w001 = (1 - f0) * (1 - f1) * f2 + w010 = (1 - f0) * f1 * (1 - f2) + w011 = (1 - f0) * f1 * f2 + w100 = f0 * (1 - f1) * (1 - f2) + w101 = f0 * (1 - f1) * f2 + w110 = f0 * f1 * (1 - f2) + w111 = f0 * f1 * f2 + + result = np.zeros_like(img_rgb) + for c in range(3): + lut_c = lut_array[:, :, :, c] + result[:, :, c] = ( + w000 * lut_c[i0, j0, k0] + + w001 * lut_c[i0, j0, k1] + + w010 * lut_c[i0, j1, k0] + + w011 * lut_c[i0, j1, k1] + + w100 * lut_c[i1, j0, k0] + + w101 * lut_c[i1, j0, k1] + + w110 * lut_c[i1, j1, k0] + + w111 * lut_c[i1, j1, k1] + ) + + result = np.clip(result, 0, 1) + + result_bgr = result[:, :, ::-1] + + if intensity < 1.0: + result_bgr = img_float * (1.0 - intensity) + result_bgr * intensity + return np.clip(result_bgr * 255, 0, 255).astype(np.uint8) + + return (result_bgr * 255).astype(np.uint8) + +def apply_levels(img_bgr, input_black, gamma, input_white): + if input_black == 0 and gamma == 1.0 and input_white == 255: + 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) + + normalized = (img_float - in_min) / in_range + normalized = np.clip(normalized, 0, 1) + + if gamma != 1.0: + normalized = np.power(normalized, 1.0 / gamma) + + result = (normalized * 255).astype(np.uint8) + return result + +def apply_exposure(img_bgr, exposure, offset): + if exposure == 0 and offset == 0: + 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) + + result = np.clip(img_float * 255, 0, 255).astype(np.uint8) + return result + +def apply_color_balance(img_bgr, 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): + if all(v == 0 for v in [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]): + return img_bgr + + img_float = img_bgr.astype(np.float32) / 255.0 + luminance = 0.299 * img_float[:, :, 2] + 0.587 * img_float[:, :, 1] + 0.114 * img_float[:, :, 0] + + shadows_mask = np.clip(1.0 - luminance * 2.0, 0, 1)[:, :, np.newaxis] + midtones_mask = np.clip(1.0 - np.abs(luminance - 0.5) * 4.0, 0, 1)[:, :, np.newaxis] + highlights_mask = np.clip(luminance * 2.0 - 1.0, 0, 1)[:, :, np.newaxis] + + result = img_float.copy() + + result[:, :, 2] += shadows_mask[:, :, 0] * (shadows_cyan_red / 100.0) + result[:, :, 1] += shadows_mask[:, :, 0] * (shadows_magenta_green / 100.0) + result[:, :, 0] += shadows_mask[:, :, 0] * (shadows_yellow_blue / 100.0) + + result[:, :, 2] += midtones_mask[:, :, 0] * (midtones_cyan_red / 100.0) + result[:, :, 1] += midtones_mask[:, :, 0] * (midtones_magenta_green / 100.0) + result[:, :, 0] += midtones_mask[:, :, 0] * (midtones_yellow_blue / 100.0) + + result[:, :, 2] += highlights_mask[:, :, 0] * (highlights_cyan_red / 100.0) + result[:, :, 1] += highlights_mask[:, :, 0] * (highlights_magenta_green / 100.0) + result[:, :, 0] += highlights_mask[:, :, 0] * (highlights_yellow_blue / 100.0) + + result = np.clip(result * 255, 0, 255).astype(np.uint8) + return result + +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 + + w_red = red / 100.0 + w_yellow = yellow / 100.0 + w_green = green / 100.0 + w_cyan = cyan / 100.0 + w_blue = blue / 100.0 + w_magenta = magenta / 100.0 + + # Опорные точки (градусы) и веса (R,G,B) + ref_hues = np.array([0, 30, 60, 90, 120, 150], dtype=np.float32) + ref_weights = np.array([ + [w_red, 0, 0], + [w_red, w_green, 0], + [0, w_green, 0], + [0, w_green, w_blue], + [0, 0, w_blue], + [w_red, 0, w_blue] + ], dtype=np.float32) + + # Интерполяция с учётом цикличности (подробно в предыдущем ответе) + ref_hues_ext = np.array([0, 30, 60, 90, 120, 150, 360]) + ref_weights_ext = np.concatenate([ref_weights, ref_weights[:1]], axis=0) + + hue_norm = hue % 360 + idx = np.digitize(hue_norm, ref_hues_ext, right=False) - 1 + idx = np.clip(idx, 0, 5) + + left_weights = ref_weights[idx] + right_weights = ref_weights_ext[idx + 1] # берём следующий элемент из расширенного массива + left_angle = ref_hues[idx] + right_angle = ref_hues_ext[idx + 1] # аналогично для углов + t = (hue_norm - left_angle) / (right_angle - left_angle + 1e-6) + weights = left_weights + t[:, :, np.newaxis] * (right_weights - left_weights) + + r = img_rgb[:, :, 0] + g = img_rgb[:, :, 1] + b = img_rgb[:, :, 2] + gray = r * weights[:, :, 0] + g * weights[:, :, 1] + b * weights[:, :, 2] + gray = np.clip(gray, 0, 1) + + result = (gray * 255).astype(np.uint8) + result_bgr = cv2.cvtColor(result, cv2.COLOR_GRAY2BGR) + return result_bgr + +def apply_channel_mixer(img_bgr, red_red, red_green, red_blue, + green_red, green_green, green_blue, + blue_red, blue_green, blue_blue): + if (red_red == 100 and red_green == 0 and red_blue == 0 and + green_red == 0 and green_green == 100 and green_blue == 0 and + blue_red == 0 and blue_green == 0 and blue_blue == 100): + return img_bgr + + img_float = img_bgr.astype(np.float32) / 255.0 + img_rgb = img_float[:, :, ::-1] + + r, g, b = img_rgb[:, :, 0], img_rgb[:, :, 1], img_rgb[:, :, 2] + + new_r = (r * (red_red / 100.0) + g * (red_green / 100.0) + b * (red_blue / 100.0)) + new_g = (r * (green_red / 100.0) + g * (green_green / 100.0) + b * (green_blue / 100.0)) + new_b = (r * (blue_red / 100.0) + g * (blue_green / 100.0) + b * (blue_blue / 100.0)) + + result_rgb = np.stack([new_r, new_g, new_b], axis=-1) + result_rgb = np.clip(result_rgb, 0, 1) + result_bgr = (result_rgb[:, :, ::-1] * 255).astype(np.uint8) + + return result_bgr + +def apply_selective_color(img_bgr, color_name, cyan, magenta, yellow, black): + if cyan == 0 and magenta == 0 and yellow == 0 and black == 0: + return img_bgr + + 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_deg = h * 2.0 # 0..358 + s_norm = s / 255.0 + v_norm = v / 255.0 + + # Определяем центральный угол и сигму для каждого цвета + color_configs = { + "Reds": (0, 25), + "Yellows": (30, 20), + "Greens": (60, 20), + "Cyans": (90, 20), + "Blues": (120, 20), + "Magentas": (150, 20), + } + + if color_name in color_configs: + center, sigma = color_configs[color_name] + # Вычисляем угловое расстояние с учётом цикличности (в градусах) + diff = np.abs(h_deg - center) + diff = np.minimum(diff, 360 - diff) + # Гауссова маска + mask = np.exp(-(diff ** 2) / (2 * sigma ** 2)) + # Дополнительно можно учесть насыщенность – для чистых цветов маска выше + # Оставим только по оттенку для простоты + elif color_name == "Whites": + # Белые: высокая яркость >0.8 и низкая насыщенность <0.2 + 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": + # Нейтральные: средняя яркость 0.3..0.7, низкая насыщенность + mask = np.exp(-((v_norm - 0.5)**2) / (2*0.15**2)) * np.exp(-(s_norm**2) / (2*0.2**2)) + elif color_name == "Blacks": + # Чёрные: низкая яркость <0.3 + mask = np.exp(-(v_norm**2) / (2*0.15**2)) * np.exp(-(s_norm**2) / (2*0.2**2)) + else: + mask = np.ones_like(h, dtype=np.float32) + + # Преобразуем RGB в CMYK (как и было) + height, width = img_float.shape[:2] + img_cmyk = np.zeros((height, width, 4), dtype=np.float32) + k = 1.0 - np.max(img_float, axis=2) + img_cmyk[:, :, 0] = (1.0 - img_float[:, :, 2] - k) / np.maximum(1.0 - k, 0.001) + img_cmyk[:, :, 1] = (1.0 - img_float[:, :, 1] - k) / np.maximum(1.0 - k, 0.001) + img_cmyk[:, :, 2] = (1.0 - img_float[:, :, 0] - k) / np.maximum(1.0 - k, 0.001) + img_cmyk[:, :, 3] = k + + # Применяем поправки с учётом плавной маски + img_cmyk[:, :, 0] += mask * (cyan / 100.0) + img_cmyk[:, :, 1] += mask * (magenta / 100.0) + img_cmyk[:, :, 2] += mask * (yellow / 100.0) + img_cmyk[:, :, 3] += mask * (black / 100.0) + + img_cmyk = np.clip(img_cmyk, 0, 1) + + # Обратное преобразование CMYK -> RGB + result_rgb = np.zeros((height, width, 3), dtype=np.float32) + result_rgb[:, :, 2] = 1.0 - img_cmyk[:, :, 0] - img_cmyk[:, :, 3] + result_rgb[:, :, 1] = 1.0 - img_cmyk[:, :, 1] - img_cmyk[:, :, 3] + result_rgb[:, :, 0] = 1.0 - img_cmyk[:, :, 2] - img_cmyk[:, :, 3] + + result_rgb = np.clip(result_rgb, 0, 1) + result_bgr = (result_rgb[:, :, ::-1] * 255).astype(np.uint8) + return result_bgr + +def process_image(img_bgr, adjustments): + brightness = adjustments.get("brightness", 0) + contrast = adjustments.get("contrast", 0) + hue = adjustments.get("hue", 0) + saturation = adjustments.get("saturation", 0) + + result_bgr = apply_basic_adjustments(img_bgr, brightness, contrast, hue, saturation) + + 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) + + levels = adjustments.get("levels", {}) + result_bgr = apply_levels( + result_bgr, + levels.get("input_black", 0), + levels.get("gamma", 1.0), + levels.get("input_white", 255) + ) + + exposure = adjustments.get("exposure", {}) + result_bgr = apply_exposure( + result_bgr, + exposure.get("exposure", 0), + exposure.get("offset", 0) + ) + + color_balance = adjustments.get("color_balance", {}) + result_bgr = apply_color_balance( + result_bgr, + color_balance.get("shadows_cyan_red", 0), + color_balance.get("shadows_magenta_green", 0), + color_balance.get("shadows_yellow_blue", 0), + color_balance.get("midtones_cyan_red", 0), + color_balance.get("midtones_magenta_green", 0), + color_balance.get("midtones_yellow_blue", 0), + color_balance.get("highlights_cyan_red", 0), + color_balance.get("highlights_magenta_green", 0), + color_balance.get("highlights_yellow_blue", 0) + ) + + bw = adjustments.get("black_white", {}) + bw_enabled = adjustments.get("bw_enabled", False) + if bw_enabled: + result_bgr = apply_black_and_white( + result_bgr, + bw.get("bw_red", 40), + bw.get("bw_yellow", 60), + bw.get("bw_green", 40), + bw.get("bw_cyan", 60), + bw.get("bw_blue", 20), + bw.get("bw_magenta", 80) + ) + + channel_mixer = adjustments.get("channel_mixer", {}) + result_bgr = apply_channel_mixer( + result_bgr, + channel_mixer.get("red_red", 100), + channel_mixer.get("red_green", 0), + channel_mixer.get("red_blue", 0), + channel_mixer.get("green_red", 0), + channel_mixer.get("green_green", 100), + channel_mixer.get("green_blue", 0), + channel_mixer.get("blue_red", 0), + channel_mixer.get("blue_green", 0), + channel_mixer.get("blue_blue", 100) + ) + + selective_color = adjustments.get("selective_color", {}) + result_bgr = apply_selective_color( + result_bgr, + selective_color.get("color_name", "Reds"), + selective_color.get("sc_cyan", 0), + selective_color.get("sc_magenta", 0), + selective_color.get("sc_yellow", 0), + selective_color.get("sc_black", 0) + ) + + return result_bgr + +def cleanup_temp_files(node_id, temp_dir, exclude_file=None): + prefix = f"rs_adjustments_{node_id}_" + for f in os.listdir(temp_dir): + if f.startswith(prefix) and f != exclude_file: + try: + os.remove(os.path.join(temp_dir, f)) + except: + pass + +@PromptServer.instance.routes.post("/rayko/rs_adjustments") +async def adjustments_handler(request): + try: + data = await request.json() + node_id = str(data.get("id")) + adjustments = data.get("adjustments", {}) + + if node_id is None or adjustments is None: + return web.json_response({"error": "Missing data"}, status=400) + + PENDING_ADJUSTMENTS.put(node_id, { + "status": "completed", + "adjustments": adjustments + }) + + temp_dir = folder_paths.get_temp_directory() + cleanup_temp_files(node_id, temp_dir) + + return web.json_response({"status": "ok"}) + except Exception as e: + return web.json_response({"error": str(e)}, status=500) + +@PromptServer.instance.routes.post("/rayko/rs_adjustments/cancel") +async def cancel_handler(request): + try: + data = await request.json() + node_id = str(data.get("node_id")) + if node_id: + PENDING_ADJUSTMENTS.put(node_id, {"status": "cancelled"}) + temp_dir = folder_paths.get_temp_directory() + cleanup_temp_files(node_id, temp_dir) + return web.json_response({"status": "cancelled"}) + except Exception as e: + return web.json_response({"error": str(e)}, status=500) + +@PromptServer.instance.routes.post("/rayko/rs_adjustments/preview") +async def preview_handler(request): + try: + data = await request.json() + node_id = str(data.get("node_id", "unknown")) + adjustments = data.get("adjustments", {}) + image_file = data.get("image_file", "") + + cache_key = f"{node_id}_{image_file}_{json.dumps(adjustments, sort_keys=True)}" + cached = PREVIEW_CACHE.get(cache_key) + if cached: + return web.json_response(cached) + + temp_dir = folder_paths.get_temp_directory() + img_path = os.path.join(temp_dir, image_file) + + if not os.path.exists(img_path): + return web.json_response({"error": "Image not found"}, status=404) + + img_pil = Image.open(img_path).convert('RGB') + img_bgr = cv2.cvtColor(np.array(img_pil), cv2.COLOR_RGB2BGR) + + result_bgr = process_image(img_bgr, adjustments) + + ts = int(time.time() * 1000) + filename = f"rs_adjustments_{node_id}_{ts}_preview.png" + filepath = os.path.join(temp_dir, filename) + + result_pil = Image.fromarray(cv2.cvtColor(result_bgr, cv2.COLOR_BGR2RGB)) + result_pil.save(filepath) + + response_data = { + "preview_file": filename, + "timestamp": ts + } + + PREVIEW_CACHE.put(cache_key, response_data) + + return web.json_response(response_data) + except Exception as e: + import traceback + traceback.print_exc() + return web.json_response({"error": str(e)}, status=500) + +class RS_ImageAdjustments: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "image": ("IMAGE",), + "brightness": ("INT", {"default": 0, "min": -100, "max": 100, "step": 1}), + "contrast": ("INT", {"default": 0, "min": -100, "max": 100, "step": 1}), + "hue": ("INT", {"default": 0, "min": -180, "max": 180, "step": 1}), + "saturation": ("INT", {"default": 0, "min": -100, "max": 100, "step": 1}), + }, + "hidden": { + "unique_id": "UNIQUE_ID", + "advanced_params": "STRING" + } + } + + RETURN_TYPES = ("IMAGE",) + FUNCTION = "apply_adjustments" + CATEGORY = "🦊 RaykoStudio" + 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): + unique_id = str(unique_id) if unique_id is not None else "unknown" + + if image.numel() == 0: + return (image,) + + h, w = image.shape[1], image.shape[2] + if h < 8 or w < 8: + return (image,) + + PENDING_ADJUSTMENTS.cache.pop(unique_id, None) + + bg_pil = tensor2pil(image) + bg_w, bg_h = bg_pil.size + + ts = int(time.time() * 1000) + td = folder_paths.get_temp_directory() + + bfn = f"rs_adjustments_{unique_id}_{ts}_bg.png" + bg_pil.save(os.path.join(td, bfn)) + + cleanup_temp_files(unique_id, td, exclude_file=bfn) + + PENDING_ADJUSTMENTS.put(unique_id, {"status": "pending"}) + + time.sleep(0.5) + + PromptServer.instance.send_sync("rs-adjustments-start", { + "id": unique_id, + "bg_file": bfn, + "bg_width": bg_w, + "bg_height": bg_h, + "timestamp": ts, + "brightness": brightness, + "contrast": contrast, + "hue": hue, + "saturation": saturation, + "advanced_params": advanced_params or "{}" + }) + + print(f"[RS Image Adjustments] Node {unique_id} waiting for user input (Apply/Cancel)...") + + while unique_id not in PENDING_ADJUSTMENTS.cache or PENDING_ADJUSTMENTS.cache[unique_id].get("status") == "pending": + if unique_id in PENDING_ADJUSTMENTS.cache and PENDING_ADJUSTMENTS.cache[unique_id].get("status") == "cancelled": + break + time.sleep(0.05) + + if unique_id in PENDING_ADJUSTMENTS.cache: + if PENDING_ADJUSTMENTS.cache[unique_id].get("status") == "cancelled": + print(f"[RS Image Adjustments] Cancelled by user for node {unique_id}") + PENDING_ADJUSTMENTS.cache.pop(unique_id, None) + cleanup_temp_files(unique_id, td) + return (image,) + + print(f"[RS Image Adjustments] Input received for node {unique_id}") + + data = PENDING_ADJUSTMENTS.cache.pop(unique_id, None) + if data is None: + cleanup_temp_files(unique_id, td) + return (image,) + + adjustments = data.get("adjustments", {}) + + brightness = adjustments.get("brightness", brightness) + contrast = adjustments.get("contrast", contrast) + hue = adjustments.get("hue", hue) + saturation = adjustments.get("saturation", saturation) + + img_bgr = cv2.cvtColor(np.array(bg_pil), cv2.COLOR_RGB2BGR) + + adjustments["brightness"] = brightness + adjustments["contrast"] = contrast + adjustments["hue"] = hue + adjustments["saturation"] = saturation + + result_bgr = process_image(img_bgr, adjustments) + + result_pil = Image.fromarray(cv2.cvtColor(result_bgr, cv2.COLOR_BGR2RGB)) + + cleanup_temp_files(unique_id, td) + + return (pil2tensor(result_pil),) + + @classmethod + def IS_CHANGED(cls, **kwargs): + return time.time() + +NODE_CLASS_MAPPINGS = {"RS_ImageAdjustments": RS_ImageAdjustments} +NODE_DISPLAY_NAME_MAPPINGS = {"RS_ImageAdjustments": "🦊 RS Image Adjustments"} \ No newline at end of file diff --git a/Rayko_Saturation.py b/Rayko_Saturation.py deleted file mode 100644 index caf7fc8..0000000 --- a/Rayko_Saturation.py +++ /dev/null @@ -1,70 +0,0 @@ -# SPDX-License-Identifier: Apache-2.0 -# 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 obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import torch -from comfy.model_patcher import ModelPatcher - -class RS_SaturationSwitch: - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "image": ("IMAGE",), - "intensity": ("FLOAT", { - "default": 1.0, - "min": 0.0, - "max": 3.0, - "step": 0.05, - "display_name": "Intensity", - "description": "0.0=Grayscale, 1.0=Original, 1.5=Subtle Boost" - }), - }, - } - - RETURN_TYPES = ("IMAGE",) - FUNCTION = "apply_saturation" - CATEGORY = "🦊 RaykoStudio" - DESCRIPTION = "Artifact-free saturation control with proper tensor handling" - - def apply_saturation(self, image, intensity): - if intensity == 1.0: - return (image,) - - img = image.clone().float() - - r, g, b = img[..., 0], img[..., 1], img[..., 2] - l = 0.299 * r + 0.587 * g + 0.114 * b - l = l.unsqueeze(-1) - - if intensity < 1.0: - result = l + intensity * (img - l) - else: - boost = 1.0 + (intensity - 1.0) * 0.7 - delta = img - l - result = l + boost * delta - - result = torch.where( - (result < 0) | (result > 1), - l + 0.9 * delta, - result - ) - - if result.dim() == 3: - result = result.unsqueeze(0) - - return (result.clamp(0, 1).to(image.dtype),) - -NODE_CLASS_MAPPINGS = {"RS_Saturation": RS_SaturationSwitch} -NODE_DISPLAY_NAME_MAPPINGS = {"RS_Saturation": "🦊 RS Saturation"} \ No newline at end of file diff --git a/__init__.py b/__init__.py index 7739cd8..ffd6787 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.31.3" +__version__ = "0.32.0" def load_modules(): print(f"🦊 {PACKAGE_DISPLAY_NAME}: Version: {__version__}") diff --git a/fonts/Boorsok.otf b/fonts/Boorsok.otf new file mode 100644 index 0000000000000000000000000000000000000000..c43d9520e50fee7da6818757df2796afc2c79ab9 GIT binary patch literal 46904 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 Gb^ih18|qd7 literal 0 HcmV?d00001 diff --git a/package.json b/package.json index d087526..b1ae7f8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ComfyUI_RaykoStudio", - "version": "0.31.3", + "version": "0.32.0", "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 8801542..e48db5d 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.31.3" +version = "0.32.0" 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 new file mode 100644 index 0000000..8f102e2 --- /dev/null +++ b/web/rs_adjustments.js @@ -0,0 +1,1712 @@ +import { app } from "../../../scripts/app.js"; +import { api } from "../../../scripts/api.js"; + +let activeNodeId = null; + +const BASIC_PARAMS = ['brightness', 'contrast', 'hue', 'saturation']; + +const DEFAULT_SLIDER_VALUES = { + brightness: 0, + contrast: 0, + hue: 0, + saturation: 0, + lut_intensity: 100, + input_black: 0, + gamma: 1.0, + input_white: 255, + exposure: 0, + offset: 0, + shadows_cyan_red: 0, + shadows_magenta_green: 0, + shadows_yellow_blue: 0, + midtones_cyan_red: 0, + midtones_magenta_green: 0, + midtones_yellow_blue: 0, + highlights_cyan_red: 0, + highlights_magenta_green: 0, + highlights_yellow_blue: 0, + bw_red: 40, + bw_yellow: 60, + bw_green: 40, + bw_cyan: 60, + bw_blue: 20, + bw_magenta: 80, + red_red: 100, + red_green: 0, + red_blue: 0, + green_red: 0, + green_green: 100, + green_blue: 0, + 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, + sc_black: 0, + bw_enabled: false, +}; + +const DEFAULT_ADJUSTMENTS = { + brightness: 0, + contrast: 0, + hue: 0, + saturation: 0, + lut_path: "", + lut_intensity: 100, + levels: { input_black: 0, gamma: 1.0, input_white: 255 }, + exposure: { exposure: 0, offset: 0 }, + color_balance: { + shadows_cyan_red: 0, shadows_magenta_green: 0, shadows_yellow_blue: 0, + midtones_cyan_red: 0, midtones_magenta_green: 0, midtones_yellow_blue: 0, + highlights_cyan_red: 0, highlights_magenta_green: 0, highlights_yellow_blue: 0 + }, + black_white: { bw_red: 40, bw_yellow: 60, bw_green: 40, bw_cyan: 60, bw_blue: 20, bw_magenta: 80 }, + bw_enabled: false, + channel_mixer: { + red_red: 100, red_green: 0, red_blue: 0, + green_red: 0, green_green: 100, green_blue: 0, + blue_red: 0, blue_green: 0, blue_blue: 100 + }, + selective_color: { color_name: "Reds", sc_cyan: 0, sc_magenta: 0, sc_yellow: 0, sc_black: 0 } +}; + +function applyBasicAdjustmentsJS(imageData, brightness, contrast, hue, saturation) { + const data = imageData.data; + const len = data.length; + + for (let i = 0; i < len; i += 4) { + let r = data[i]; + let g = data[i + 1]; + let b = data[i + 2]; + + if (brightness !== 0 || contrast !== 0) { + const alpha = 1.0 + (contrast / 100.0); + const beta = brightness; + r = Math.min(255, Math.max(0, r * alpha + beta)); + g = Math.min(255, Math.max(0, g * alpha + beta)); + b = Math.min(255, Math.max(0, b * alpha + beta)); + } + + if (hue !== 0 || saturation !== 0) { + const rNorm = r / 255.0; + const gNorm = g / 255.0; + const bNorm = b / 255.0; + const max = Math.max(rNorm, gNorm, bNorm); + const min = Math.min(rNorm, gNorm, bNorm); + const delta = max - min; + let h = 0, s = 0, v = max; + + if (delta !== 0) { + s = delta / max; + if (max === rNorm) h = ((gNorm - bNorm) / delta) % 6; + else if (max === gNorm) h = (bNorm - rNorm) / delta + 2; + else h = (rNorm - gNorm) / delta + 4; + h *= 60; + if (h < 0) h += 360; + } + + h = Math.floor(h); + s = Math.floor(s * 255); + v = Math.floor(v * 255); + h = Math.floor(h / 2); + + h = (h + hue) % 180; + if (h < 0) h += 180; + s = Math.min(255, Math.max(0, Math.floor(s * (1.0 + saturation / 100.0)))); + + h = h * 2; + s = s / 255; + v = v / 255; + + const c = v * s; + const x = c * (1 - Math.abs((h / 60) % 2 - 1)); + const m = v - c; + + let r2 = 0, g2 = 0, b2 = 0; + if (h < 60) { r2 = c; g2 = x; b2 = 0; } + else if (h < 120) { r2 = x; g2 = c; b2 = 0; } + else if (h < 180) { r2 = 0; g2 = c; b2 = x; } + else if (h < 240) { r2 = 0; g2 = x; b2 = c; } + else if (h < 300) { r2 = x; g2 = 0; b2 = c; } + else { r2 = c; g2 = 0; b2 = x; } + + r = Math.floor((r2 + m) * 255); + g = Math.floor((g2 + m) * 255); + b = Math.floor((b2 + m) * 255); + } + + data[i] = Math.min(255, Math.max(0, Math.floor(r))); + data[i + 1] = Math.min(255, Math.max(0, Math.floor(g))); + data[i + 2] = Math.min(255, Math.max(0, Math.floor(b))); + } + return imageData; +} + +app.registerExtension({ + name: "RaykoStudio.RSImageAdjustments", + async beforeRegisterNodeDef(nodeType, nodeData) { + if (nodeData.name !== "RS_ImageAdjustments") return; + + const onNodeCreated = nodeType.prototype.onNodeCreated; + nodeType.prototype.onNodeCreated = function() { + if (onNodeCreated) onNodeCreated.apply(this, arguments); + + this.adjustments = JSON.parse(JSON.stringify(DEFAULT_ADJUSTMENTS)); + this.backgroundImage = null; + this.previewImage = null; + this.originalImageData = null; + this._tempCanvas = document.createElement('canvas'); + this._tempCtx = this._tempCanvas.getContext('2d'); + this.isEditing = false; + this.isLoading = false; + this.advancedMode = false; + this.currentSessionTimestamp = null; + this.pendingEditorData = null; + this.renderTimeout = null; + this.currentRenderAbortController = null; + this._previewTimestamp = null; + this._previewKey = null; + this._overlayRenderLoop = null; + this._lastHeavyRenderTime = 0; + this._isHeavyRenderPending = false; + + this.displayWidth = 420; + this.displayHeight = 420; + this.canvasPixelSize = 420; + this.realBackground = { width: 0, height: 0 }; + + this.overlayContainer = null; + this.overlayCanvas = null; + this.overlayCtx = null; + this.overlayCanvasWrapper = null; + this.sidePanel = null; + this.overlayInputs = {}; + this.sliderDisplays = {}; + + this.minWidth = 500; + this.minHeight = 450; + this.setSize([this.minWidth, this.minHeight]); + + this.slidersY = 0; + this.sliderRects = []; + this.resetBtnRects = []; + this.sliderHover = [false, false, false, false]; + this.resetBtnHover = [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 => { + 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.previewImage = null; + this.setDirtyCanvas(true); + } + }; + } + }); + + const advWidget = this.widgets?.find(w => w.name === "advanced_params"); + if (advWidget) { + advWidget.hidden = true; + advWidget.computeSize = () => [0, 0]; + advWidget.y = 0; + } + + this._hideNativeWidgets(); + this._buildOverlayUI(); + + this._mouseLeaveHandler = () => { + if (this.sliderDragging >= 0) { + this.sliderDragging = -1; + this.setDirtyCanvas(true); + } + }; + + const originalOnMouseLeave = this.onMouseLeave; + this.onMouseLeave = function() { + if (originalOnMouseLeave) originalOnMouseLeave.apply(this, arguments); + this._mouseLeaveHandler(); + }; + + api.addEventListener("rs-adjustments-start", (event) => { + if (event.detail.id != this.id) return; + this.pendingEditorData = event.detail; + this._openDeferredEditor(); + }); + + api.addEventListener("interrupted", () => { + this._cleanup(); + }); + }; + + nodeType.prototype._hideNativeWidgets = function() { + if (!this.widgets) return; + BASIC_PARAMS.forEach(param => { + const widget = this.widgets.find(w => w.name === param); + if (widget) { + widget.hidden = true; + widget.computeSize = () => [0, 0]; + widget.y = 0; + } + }); + this.setSize([this.minWidth, this.minHeight]); + this.setDirtyCanvas(true); + }; + + nodeType.prototype._syncWidgetsFromAdjustments = function() { + if (!this.widgets) return; + this._syncingWidgets = true; + try { + BASIC_PARAMS.forEach(param => { + const widget = this.widgets.find(w => w.name === param); + if (widget && widget.value !== this.adjustments[param]) { + widget.value = this.adjustments[param]; + } + }); + } finally { + this._syncingWidgets = false; + } + this.setDirtyCanvas(true); + }; + + nodeType.prototype._syncOverlayUI = function() { + if (!this.advancedMode) return; + BASIC_PARAMS.forEach(key => { + if (this.overlayInputs[key]) { + this.overlayInputs[key].value = this.adjustments[key]; + } + if (this.sliderDisplays[key]) { + this.sliderDisplays[key].textContent = String(this.adjustments[key]); + } + }); + }; + + nodeType.prototype._isParamModified = function(key, parentObj = this.adjustments) { + const def = DEFAULT_SLIDER_VALUES[key]; + if (def === undefined) { + return false; + } + + const val = parentObj[key]; + if (val === undefined) { + return false; + } + + if (typeof def === 'number') { + const isModified = Math.abs(val - def) > 0.001; + return isModified; + } + return val !== def; + }; + + nodeType.prototype._isSectionModified = function(sectionName) { + const checks = { + lut: () => (this.adjustments.lut_path && this.adjustments.lut_path !== "") || this._isParamModified('lut_intensity'), + + 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; + } + + this._sectionRegistry.forEach(entry => { + const modified = this._isSectionModified(entry.name); + if (entry.label) { + entry.label.style.color = modified ? '#00FF00' : '#666'; + } + }); + }; + + nodeType.prototype._updateBasicParamLabels = function() { + BASIC_PARAMS.forEach(key => { + const lbl = this._basicParamLabels[key]; + if (lbl) { + const isModified = this._isParamModified(key); + lbl.style.color = isModified ? '#00FF00' : '#aaa'; + } + }); + }; + + nodeType.prototype._getDefaultValueForKey = function(key) { + if (DEFAULT_SLIDER_VALUES.hasOwnProperty(key)) { + return DEFAULT_SLIDER_VALUES[key]; + } + const nestedKeys = [ + 'levels', 'exposure', 'color_balance', 'black_white', 'channel_mixer', 'selective_color' + ]; + for (let section of nestedKeys) { + if (DEFAULT_ADJUSTMENTS[section] && DEFAULT_ADJUSTMENTS[section].hasOwnProperty(key)) { + return DEFAULT_ADJUSTMENTS[section][key]; + } + } + return 0; + }; + + nodeType.prototype._resetAllParameters = function() { + const resetRecursive = (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] = {}; + resetRecursive(target[key], source[key]); + } else { + target[key] = source[key]; + } + } + } + }; + resetRecursive(this.adjustments, DEFAULT_ADJUSTMENTS); + + Object.keys(this.overlayInputs).forEach(key => { + const slider = this.overlayInputs[key]; + if (!slider) return; + const defaultVal = this._getDefaultValueForKey(key); + slider.value = defaultVal; + if (this.sliderDisplays[key]) { + const isFloat = Number.isFinite(defaultVal) && !Number.isInteger(defaultVal); + this.sliderDisplays[key].textContent = isFloat ? defaultVal.toFixed(2) : String(Math.round(defaultVal)); + } + }); + + if (this._bwToggle) { + this._bwToggle.checked = this.adjustments.bw_enabled || false; + } + + const colorSelect = this.overlayContainer?.querySelector('#selective-color-select'); + if (colorSelect) { + colorSelect.value = this.adjustments.selective_color.color_name || 'Reds'; + } + + const lutFileInput = this.overlayContainer?.querySelector('input[type="file"][accept=".cube"]'); + const lutFilenameDisplay = this.overlayContainer?.querySelector('.lut-filename-display'); + if (lutFileInput) lutFileInput.value = ''; + if (lutFilenameDisplay) { + lutFilenameDisplay.textContent = 'File not selected'; + lutFilenameDisplay.style.color = '#888'; + } + const lutIntensitySlider = this.overlayInputs['lut_intensity']; + if (lutIntensitySlider) { + lutIntensitySlider.value = this.adjustments.lut_intensity; + if (this.sliderDisplays['lut_intensity']) { + this.sliderDisplays['lut_intensity'].textContent = String(this.adjustments.lut_intensity); + } + } + + this._updateSectionHeaders(); + this._updateBasicParamLabels(); + + this.previewImage = null; + this.setDirtyCanvas(true); + this._scheduleHeavyRender(); + + this._syncWidgetsFromAdjustments(); + }; + + nodeType.prototype._buildOverlayUI = function() { + this.overlayContainer = document.createElement('div'); + this.overlayContainer.style.cssText = 'position:fixed;top:60px;left:0;right:0;bottom:0;background:rgba(10,10,10,0.96);z-index:999;display:none;flex-direction:row;align-items:stretch;font-family:system-ui,-apple-system,sans-serif;'; + + this.overlayCanvasWrapper = document.createElement('div'); + this.overlayCanvasWrapper.style.cssText = 'flex:1;display:flex;align-items:center;justify-content:center;overflow:hidden;position:relative;'; + + this.overlayCanvas = document.createElement('canvas'); + this.overlayCanvas.style.cssText = 'box-shadow:0 8px 32px rgba(0,0,0,0.7);max-width:98%;max-height:98%;border-radius:8px;'; + this.overlayCanvasWrapper.appendChild(this.overlayCanvas); + this.overlayCtx = this.overlayCanvas.getContext('2d'); + + this.sidePanel = document.createElement('div'); + this.sidePanel.style.cssText = 'width:320px;background:#151515;border-left:1px solid #333;padding:12px;display:flex;flex-direction:column;gap:4px;box-sizing:border-box;overflow-y:auto;'; + + const buttonContainer = document.createElement('div'); + buttonContainer.style.cssText = 'position:sticky;top:0;background:#151515;z-index:10;padding:0 0 8px 0;border-bottom:1px solid #333;margin-bottom:8px;'; + this.sidePanel.appendChild(buttonContainer); + + const makeBtn = (txt, col, onClick) => { + const b = document.createElement('button'); + b.textContent = txt; + b.style.cssText = `width:100%;padding:10px;background:#222;color:${col};border:1px solid ${col};border-radius:6px;cursor:pointer;font-weight:600;font-size:12px;margin-top:4px;transition:0.15s;`; + b.onmouseenter = () => { b.style.background = '#2a2a2a'; b.style.transform = 'translateY(-1px)'; }; + b.onmouseleave = () => { b.style.background = '#222'; b.style.transform = 'none'; }; + b.onclick = (e) => { e.stopPropagation(); onClick(); }; + return b; + }; + + 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(); }); + buttonContainer.appendChild(btnCancel); + // Контейнер для нижнего ряда кнопок + const bottomRow = document.createElement('div'); + bottomRow.style.cssText = 'display:flex;gap:8px;width:100%;'; + + const btnResetAll = makeBtn("🔄 RESET ALL", "#FF9800", () => { this._resetAllParameters(); }); + btnResetAll.style.flex = '1'; // Занимает половину ширины + bottomRow.appendChild(btnResetAll); + + // Кнопка Expand/Collapse + 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'; + + // Проходим по всем секциям и меняем состояние + this._sectionRegistry.forEach(entry => { + if (entry.content) entry.content.style.display = display; + if (entry.chevron) entry.chevron.textContent = icon; + }); + + this._expandCollapseBtn.textContent = text; + }); + this._expandCollapseBtn.style.flex = '1'; // Занимает половину ширины + bottomRow.appendChild(this._expandCollapseBtn); + + buttonContainer.appendChild(bottomRow); + + const makeSlider = (label, key, min, max, step, isFloat = false, parentObj = this.adjustments) => { + const container = document.createElement('div'); + container.style.cssText = 'display:flex;flex-direction:column;gap:4px;'; + const lbl = document.createElement('label'); + lbl.textContent = label; + lbl.style.cssText = 'color:#aaa;font-size:11px;font-weight:600;'; + container.appendChild(lbl); + if (BASIC_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'); + slider.type = 'range'; + slider.min = min; + slider.max = max; + slider.step = step; + slider.value = parentObj[key]; + slider.style.cssText = 'flex:1;height:4px;background:#252525;border-radius:2px;outline:none;cursor:pointer;-webkit-appearance:none;'; + const valueDisplay = document.createElement('div'); + valueDisplay.textContent = isFloat ? parseFloat(parentObj[key]).toFixed(2) : String(parseInt(parentObj[key])); + valueDisplay.style.cssText = 'min-width:50px;text-align:center;background:#252525;color:#4CAF50;border:1px solid #444;border-radius:4px;padding:4px 8px;font-size:12px;cursor:pointer;font-weight:600;transition:0.15s;'; + + valueDisplay.onclick = (e) => { + e.stopPropagation(); + const popup = document.createElement('div'); + popup.style.cssText = 'position:fixed;z-index:10004;background:#1a1a1a;border:1px solid #444;border-radius:6px;padding:8px 12px;box-shadow:0 4px 20px rgba(0,0,0,0.5);display:flex;align-items:center;gap:8px;'; + + const input = document.createElement('input'); + input.type = 'number'; + input.value = parentObj[key]; + input.min = min; + input.max = max; + input.step = step; + input.style.cssText = 'width:100px;background:#222;color:#fff;border:1px solid #444;border-radius:4px;padding:6px 10px;font-size:12px;outline:none;'; + + const saveBtn = document.createElement('button'); + saveBtn.textContent = 'OK'; + saveBtn.style.cssText = 'background:#4CAF50;color:#fff;border:none;border-radius:4px;padding:6px 12px;font-size:12px;cursor:pointer;'; + + const doSave = () => { + let num = isFloat ? parseFloat(input.value) : parseInt(input.value); + if (isNaN(num)) num = parentObj[key]; + num = Math.max(min, Math.min(max, num)); + parentObj[key] = num; + slider.value = num; + valueDisplay.textContent = isFloat ? num.toFixed(2) : String(num); + this._updateSectionHeaders(); + this._updateBasicParamLabels(); + + 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)) { + } else { + this._scheduleHeavyRender(); + } + popup.remove(); + }; + + saveBtn.onclick = (ev) => { ev.stopPropagation(); ev.preventDefault(); doSave(); }; + input.onkeydown = (ev) => { if (ev.key === 'Enter') { ev.preventDefault(); doSave(); } }; + + popup.appendChild(input); + popup.appendChild(saveBtn); + document.body.appendChild(popup); + + const popupWidth = popup.offsetWidth || 180; + const popupHeight = popup.offsetHeight || 40; + const rect = valueDisplay.getBoundingClientRect(); + let leftPos = rect.right + 8; + let topPos = rect.top + (rect.height - popupHeight) / 2; + + if (leftPos + popupWidth > window.innerWidth - 10) { + leftPos = rect.left - popupWidth - 8; + } + if (topPos < 10) topPos = 10; + if (topPos + popupHeight > window.innerHeight - 10) { + topPos = window.innerHeight - popupHeight - 10; + } + + popup.style.left = leftPos + 'px'; + popup.style.top = topPos + 'px'; + + setTimeout(() => { input.focus(); input.select(); }, 50); + + setTimeout(() => { + const closeHandler = (ev) => { + if (!popup.contains(ev.target)) { + popup.remove(); + document.removeEventListener('mousedown', closeHandler); + } + }; + document.addEventListener('mousedown', closeHandler); + }, 100); + }; + + const resetBtn = document.createElement('button'); + 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'; }; + resetBtn.onclick = (e) => { + e.stopPropagation(); + const defaultVal = DEFAULT_SLIDER_VALUES[key] !== undefined ? DEFAULT_SLIDER_VALUES[key] : (isFloat ? 0.0 : 0); + parentObj[key] = defaultVal; + slider.value = defaultVal; + valueDisplay.textContent = isFloat ? parseFloat(defaultVal).toFixed(2) : String(parseInt(defaultVal)); + this._updateSectionHeaders(); + this._updateBasicParamLabels(); + + this.setDirtyCanvas(true); + + if (BASIC_PARAMS.includes(key)) { + this.previewImage = null; + } + this._scheduleHeavyRender(); + }; + + slider.oninput = () => { + const val = isFloat ? parseFloat(slider.value) : parseInt(slider.value); + parentObj[key] = val; + valueDisplay.textContent = isFloat ? val.toFixed(2) : String(val); + this._updateSectionHeaders(); + this._updateBasicParamLabels(); + + this.setDirtyCanvas(true); + + if (BASIC_PARAMS.includes(key)) { + this.previewImage = null; + const widget = this.widgets?.find(w => w.name === key); + if (widget) widget.value = val; + } else { + this._scheduleHeavyRender(); + } + }; + this.overlayInputs[key] = slider; + this.sliderDisplays[key] = valueDisplay; + row.appendChild(slider); + row.appendChild(valueDisplay); + row.appendChild(resetBtn); + container.appendChild(row); + return container; + }; + + const makeSection = (title, sectionName, contentBuilder) => { + const section = document.createElement('div'); + section.style.cssText = 'margin-top:8px;'; + const header = document.createElement('div'); + header.style.cssText = 'display:flex;align-items:center;gap:8px;padding:8px 0;cursor:pointer;user-select:none;'; + const chevron = document.createElement('span'); + chevron.textContent = '▶'; + chevron.style.cssText = 'color:#666;font-size:12px;transition:transform 200ms;flex-shrink:0;'; + const lbl = document.createElement('label'); + lbl.textContent = title; + lbl.style.cssText = `flex:1;font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:1px;transition:color 200ms;color:#666;`; + header.appendChild(chevron); + header.appendChild(lbl); + const content = document.createElement('div'); + content.style.cssText = 'display:none;padding-left:8px;'; + header.addEventListener('click', (e) => { + const isOpen = content.style.display !== 'none'; + content.style.display = isOpen ? 'none' : 'block'; + chevron.textContent = isOpen ? '▶' : '▼'; + }); + section.appendChild(header); + section.appendChild(content); + contentBuilder(content); + + if (!this._sectionRegistry) this._sectionRegistry = []; + this._sectionRegistry.push({ name: sectionName, label: lbl, content: content, chevron: chevron }); + + return section; + }; + + const div = () => { const d = document.createElement('div'); d.style.cssText = 'height:1px;background:#333;margin:4px 0;'; return d; }; + + const truncateFilename = (filename) => { + if (!filename) return 'File not selected'; + if (filename.length <= 30) return filename; + return filename.substring(0, 27) + '...'; + }; + + 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("HUE", "hue", -180, 180, 1)); + this.sidePanel.appendChild(makeSlider("SATURATION", "saturation", -100, 100, 1)); + this.sidePanel.appendChild(div()); + + const lutSection = makeSection("COLOR LOOKUP (LUT)", "lut", (content) => { + const fileRow = document.createElement('div'); + fileRow.style.cssText = 'display:flex;align-items:center;gap:5px;'; + const fileInput = document.createElement('input'); + fileInput.type = 'file'; + fileInput.accept = '.cube'; + fileInput.style.display = 'none'; + const filenameDisplay = document.createElement('div'); + filenameDisplay.className = 'lut-filename-display'; + filenameDisplay.textContent = 'File not selected'; + filenameDisplay.style.cssText = 'flex:1;padding:8px;background:#252525;color:#888;border:1px solid #444;border-radius:4px;font-size:11px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-height:36px;display:flex;align-items:center;cursor:pointer;'; + const browseBtn = document.createElement('button'); + browseBtn.textContent = 'Browse'; + browseBtn.style.cssText = 'padding:8px 12px;background:#252525;color:#aaa;border:1px solid #444;border-radius:4px;cursor:pointer;font-size:11px;font-weight:600;transition:0.15s;flex-shrink:0;'; + browseBtn.onmouseenter = () => { browseBtn.style.background = '#2a2a2a'; browseBtn.style.color = '#fff'; browseBtn.style.borderColor = '#4CAF50'; }; + 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.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'; }; + resetBtn.onclick = (e) => { + e.stopPropagation(); + fileInput.value = ''; + filenameDisplay.textContent = 'File not selected'; + filenameDisplay.style.color = '#888'; + this.adjustments.lut_path = ''; + this.adjustments.lut_intensity = 100; + + this.previewImage = null; + this.setDirtyCanvas(true); + this._updateSectionHeaders(); + this._scheduleHeavyRender(); + }; + fileInput.onchange = async (e) => { + const file = e.target.files[0]; + if (!file) return; + filenameDisplay.textContent = truncateFilename(file.name); + filenameDisplay.style.color = '#eee'; + const reader = new FileReader(); + reader.onload = async (ev) => { + const blob = new Blob([ev.target.result], { type: 'text/plain' }); + const formData = new FormData(); + formData.append('image', blob, file.name); + formData.append('subfolder', 'rs_adjustments'); + formData.append('type', 'temp'); + try { + const resp = await api.fetchApi('/upload/image', { method: 'POST', body: formData }); + const data = await resp.json(); + this.adjustments.lut_path = data.name; + this._updateSectionHeaders(); + + this.previewImage = null; + this.setDirtyCanvas(true); + this._scheduleHeavyRender(); + } catch (err) { + filenameDisplay.textContent = 'File not selected'; + filenameDisplay.style.color = '#888'; + } + }; + reader.readAsText(file); + }; + filenameDisplay.onclick = () => fileInput.click(); + fileRow.appendChild(filenameDisplay); + fileRow.appendChild(browseBtn); + fileRow.appendChild(resetBtn); + fileRow.appendChild(fileInput); + content.appendChild(fileRow); + content.appendChild(div()); + content.appendChild(makeSlider("INTENSITY", "lut_intensity", 0, 100, 1)); + }); + this.sidePanel.appendChild(lutSection); + + 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)); + content.appendChild(makeSlider("INPUT WHITE", "input_white", 0, 255, 1, false, this.adjustments.levels)); + }); + this.sidePanel.appendChild(levelsSection); + + const exposureSection = makeSection("EXPOSURE", "exposure", (content) => { + content.appendChild(makeSlider("EXPOSURE", "exposure", -100, 100, 1, false, this.adjustments.exposure)); + content.appendChild(makeSlider("OFFSET", "offset", -100, 100, 1, false, this.adjustments.exposure)); + }); + this.sidePanel.appendChild(exposureSection); + + const colorBalanceSection = makeSection("COLOR BALANCE", "color_balance", (content) => { + const shadowsLabel = document.createElement('div'); + shadowsLabel.textContent = 'SHADOWS'; + shadowsLabel.style.cssText = 'color:#888;font-size:10px;font-weight:600;margin:8px 0 4px 0;'; + content.appendChild(shadowsLabel); + content.appendChild(makeSlider("CYAN / RED", "shadows_cyan_red", -100, 100, 1, false, this.adjustments.color_balance)); + content.appendChild(makeSlider("MAGENTA / GREEN", "shadows_magenta_green", -100, 100, 1, false, this.adjustments.color_balance)); + content.appendChild(makeSlider("YELLOW / BLUE", "shadows_yellow_blue", -100, 100, 1, false, this.adjustments.color_balance)); + const midtonesLabel = document.createElement('div'); + midtonesLabel.textContent = 'MIDTONES'; + midtonesLabel.style.cssText = 'color:#888;font-size:10px;font-weight:600;margin:8px 0 4px 0;'; + content.appendChild(midtonesLabel); + content.appendChild(makeSlider("CYAN / RED", "midtones_cyan_red", -100, 100, 1, false, this.adjustments.color_balance)); + content.appendChild(makeSlider("MAGENTA / GREEN", "midtones_magenta_green", -100, 100, 1, false, this.adjustments.color_balance)); + content.appendChild(makeSlider("YELLOW / BLUE", "midtones_yellow_blue", -100, 100, 1, false, this.adjustments.color_balance)); + const highlightsLabel = document.createElement('div'); + highlightsLabel.textContent = 'HIGHLIGHTS'; + highlightsLabel.style.cssText = 'color:#888;font-size:10px;font-weight:600;margin:8px 0 4px 0;'; + content.appendChild(highlightsLabel); + content.appendChild(makeSlider("CYAN / RED", "highlights_cyan_red", -100, 100, 1, false, this.adjustments.color_balance)); + content.appendChild(makeSlider("MAGENTA / GREEN", "highlights_magenta_green", -100, 100, 1, false, this.adjustments.color_balance)); + content.appendChild(makeSlider("YELLOW / BLUE", "highlights_yellow_blue", -100, 100, 1, false, this.adjustments.color_balance)); + }); + this.sidePanel.appendChild(colorBalanceSection); + + const bwSection = makeSection("BLACK & WHITE", "black_white", (content) => { + const toggleRow = document.createElement('div'); + toggleRow.style.cssText = 'display:flex;align-items:center;gap:10px;margin:4px 0 8px 0;'; + + const toggleLabel = document.createElement('label'); + toggleLabel.textContent = 'Enable B&W'; + toggleLabel.style.cssText = 'color:#aaa;font-size:11px;font-weight:600;'; + + const toggleInput = document.createElement('input'); + toggleInput.type = 'checkbox'; + toggleInput.checked = this.adjustments.bw_enabled || false; + toggleInput.style.cssText = 'width:18px;height:18px;accent-color:#4CAF50;cursor:pointer;'; + + this._bwToggle = toggleInput; + + toggleInput.onchange = () => { + this.adjustments.bw_enabled = toggleInput.checked; + this.previewImage = null; + this.setDirtyCanvas(true); + this._scheduleHeavyRender(); + }; + + toggleRow.appendChild(toggleLabel); + toggleRow.appendChild(toggleInput); + content.appendChild(toggleRow); + + content.appendChild(makeSlider("RED", "bw_red", 0, 200, 1, false, this.adjustments.black_white)); + content.appendChild(makeSlider("YELLOW", "bw_yellow", 0, 200, 1, false, this.adjustments.black_white)); + content.appendChild(makeSlider("GREEN", "bw_green", 0, 200, 1, false, this.adjustments.black_white)); + content.appendChild(makeSlider("CYAN", "bw_cyan", 0, 200, 1, false, this.adjustments.black_white)); + content.appendChild(makeSlider("BLUE", "bw_blue", 0, 200, 1, false, this.adjustments.black_white)); + content.appendChild(makeSlider("MAGENTA", "bw_magenta", 0, 200, 1, false, this.adjustments.black_white)); + }); + this.sidePanel.appendChild(bwSection); + + const channelMixerSection = makeSection("CHANNEL MIXER", "channel_mixer", (content) => { + const redLabel = document.createElement('div'); + redLabel.textContent = 'OUTPUT RED'; + redLabel.style.cssText = 'color:#888;font-size:10px;font-weight:600;margin:8px 0 4px 0;'; + content.appendChild(redLabel); + content.appendChild(makeSlider("RED", "red_red", -200, 200, 1, false, this.adjustments.channel_mixer)); + content.appendChild(makeSlider("GREEN", "red_green", -200, 200, 1, false, this.adjustments.channel_mixer)); + content.appendChild(makeSlider("BLUE", "red_blue", -200, 200, 1, false, this.adjustments.channel_mixer)); + const greenLabel = document.createElement('div'); + greenLabel.textContent = 'OUTPUT GREEN'; + greenLabel.style.cssText = 'color:#888;font-size:10px;font-weight:600;margin:8px 0 4px 0;'; + content.appendChild(greenLabel); + content.appendChild(makeSlider("RED", "green_red", -200, 200, 1, false, this.adjustments.channel_mixer)); + content.appendChild(makeSlider("GREEN", "green_green", -200, 200, 1, false, this.adjustments.channel_mixer)); + content.appendChild(makeSlider("BLUE", "green_blue", -200, 200, 1, false, this.adjustments.channel_mixer)); + const blueLabel = document.createElement('div'); + blueLabel.textContent = 'OUTPUT BLUE'; + blueLabel.style.cssText = 'color:#888;font-size:10px;font-weight:600;margin:8px 0 4px 0;'; + content.appendChild(blueLabel); + content.appendChild(makeSlider("RED", "blue_red", -200, 200, 1, false, this.adjustments.channel_mixer)); + content.appendChild(makeSlider("GREEN", "blue_green", -200, 200, 1, false, this.adjustments.channel_mixer)); + content.appendChild(makeSlider("BLUE", "blue_blue", -200, 200, 1, false, this.adjustments.channel_mixer)); + }); + this.sidePanel.appendChild(channelMixerSection); + + const selectiveColorSection = makeSection("SELECTIVE COLOR", "selective_color", (content) => { + const colorSelect = document.createElement('select'); + colorSelect.id = 'selective-color-select'; + colorSelect.style.cssText = 'width:100%;background:#252525;color:#eee;border:1px solid #444;border-radius:4px;padding:6px;font-size:12px;outline:none;margin-bottom:8px;'; + ['Reds', 'Yellows', 'Greens', 'Cyans', 'Blues', 'Magentas', 'Whites', 'Neutrals', 'Blacks'].forEach(c => { + const opt = document.createElement('option'); + opt.value = c; + opt.textContent = c; + if (c === this.adjustments.selective_color.color_name) opt.selected = true; + colorSelect.appendChild(opt); + }); + colorSelect.onchange = () => { + this.adjustments.selective_color.color_name = colorSelect.value; + this.previewImage = null; + this.setDirtyCanvas(true); + this._scheduleHeavyRender(); + }; + content.appendChild(colorSelect); + content.appendChild(makeSlider("CYAN", "sc_cyan", -100, 100, 1, false, this.adjustments.selective_color)); + content.appendChild(makeSlider("MAGENTA", "sc_magenta", -100, 100, 1, false, this.adjustments.selective_color)); + content.appendChild(makeSlider("YELLOW", "sc_yellow", -100, 100, 1, false, this.adjustments.selective_color)); + content.appendChild(makeSlider("BLACK", "sc_black", -100, 100, 1, false, this.adjustments.selective_color)); + }); + this.sidePanel.appendChild(selectiveColorSection); + + this.overlayContainer.appendChild(this.overlayCanvasWrapper); + this.overlayContainer.appendChild(this.sidePanel); + document.body.appendChild(this.overlayContainer); + }; + + nodeType.prototype._scheduleBasicRender = function() { + this._syncWidgetsFromAdjustments(); + this.setDirtyCanvas(true); + }; + + nodeType.prototype._scheduleHeavyRender = function() { + if (this._isHeavyRenderPending) { + return; + } + + const now = Date.now(); + if (now - this._lastHeavyRenderTime < 50) { + return; + } + + this._lastHeavyRenderTime = now; + this._syncWidgetsFromAdjustments(); + this._fetchPreviewFromServer(); + }; + + nodeType.prototype._fetchPreviewFromServer = async function() { + if (!this.isEditing || !this.backgroundImage) return; + if (this.currentRenderAbortController) { + this.currentRenderAbortController.abort(); + } + this.currentRenderAbortController = new AbortController(); + const signal = this.currentRenderAbortController.signal; + this._isHeavyRenderPending = true; + + const payload = { + node_id: String(this.id), + image_file: this.pendingEditorData.bg_file, + adjustments: this.adjustments + }; + + try { + const response = await api.fetchApi('/rayko/rs_adjustments/preview', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload), + signal: signal + }); + const data = await response.json(); + if (data.preview_file) { + const img = new Image(); + img.crossOrigin = "Anonymous"; + const ts = data.timestamp; + this._previewTimestamp = ts; + img.onload = () => { + if (this._previewTimestamp === ts) { + this.previewImage = img; + this.setDirtyCanvas(true); + this._isHeavyRenderPending = false; + } + }; + img.onerror = () => { + this._isHeavyRenderPending = false; + }; + img.src = `/view?filename=${data.preview_file}&type=temp&t=${ts}`; + } + } catch (e) { + this._isHeavyRenderPending = false; + if (e.name === 'AbortError') return; + } + }; + + nodeType.prototype._openDeferredEditor = function() { + if (!this.pendingEditorData) return; + this.backgroundImage = null; + this.previewImage = null; + this.originalImageData = null; + this.isLoading = true; + this.isEditing = false; + this._hideNativeWidgets(); + + const data = this.pendingEditorData; + this.adjustments.brightness = data.brightness || 0; + this.adjustments.contrast = data.contrast || 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]; + } + }); + } catch (e) {} + + this.realBackground = { width: data.bg_width, height: data.bg_height }; + this._updateDisplaySize(this.canvasPixelSize); + + const bgFile = data.bg_file, ts = data.timestamp; + const img = new Image(); + img.crossOrigin = "Anonymous"; + const imageUrl = `/view?filename=${bgFile}&type=temp&t=${ts}_${Date.now()}`; + + img.onload = () => { + this.backgroundImage = img; + this.isLoading = false; + this.isEditing = true; + + this._tempCanvas.width = img.width; + this._tempCanvas.height = img.height; + this._tempCtx.drawImage(img, 0, 0); + this.originalImageData = this._tempCtx.getImageData(0, 0, img.width, img.height); + + this.setDirtyCanvas(true, true); + if (app.graph) app.graph.setDirtyCanvas(true, true); + this._syncWidgetsFromAdjustments(); + this._syncOverlayUI(); + this._updateBasicParamLabels(); + this._scheduleHeavyRender(); + }; + img.onerror = () => { + this.isLoading = false; + this.isEditing = false; + this.setDirtyCanvas(true); + }; + img.src = imageUrl; + }; + + nodeType.prototype._updateDisplaySize = function(cS) { + this.canvasPixelSize = cS; + const safeHeight = this.realBackground.height || 1; + const bgAR = this.realBackground.width / safeHeight; + if (bgAR >= 1) { + this.displayWidth = cS; + this.displayHeight = cS / bgAR; + } else { + this.displayHeight = cS; + this.displayWidth = cS * bgAR; + } + }; + + nodeType.prototype._toggleAdvancedMode = function(forceClose = false) { + if (forceClose || this.advancedMode) { + this.advancedMode = false; + if (activeNodeId === this.id) activeNodeId = null; + this.overlayContainer.style.display = 'none'; + if (this._overlayRenderLoop) { + cancelAnimationFrame(this._overlayRenderLoop); + this._overlayRenderLoop = null; + } + this.setDirtyCanvas(true); + return; + } + if (activeNodeId && activeNodeId !== this.id) { + const otherNode = app.graph?.nodes.find(n => n.id == activeNodeId); + if (otherNode && typeof otherNode._toggleAdvancedMode === 'function') { + otherNode._toggleAdvancedMode(true); + } + } + this.advancedMode = true; + activeNodeId = this.id; + this.overlayContainer.style.display = 'flex'; + this._syncOverlayUI(); + requestAnimationFrame(() => { + this._resizeOverlayCanvas(); + this._updateSectionHeaders(); + this._startOverlayRenderLoop(); + }); + }; + + nodeType.prototype._resizeOverlayCanvas = function() { + if (!this.overlayCanvasWrapper || !this.overlayCanvas) return; + const w = this.overlayCanvasWrapper.clientWidth; + const h = this.overlayCanvasWrapper.clientHeight; + if (w > 0 && h > 0) { + this.overlayCanvas.width = w; + this.overlayCanvas.height = h; + this.setDirtyCanvas(true); + } + }; + + nodeType.prototype._startOverlayRenderLoop = function() { + if (this._overlayRenderLoop) return; + const render = () => { + if (!this.advancedMode) return; + this._drawOverlayCanvas(); + this._overlayRenderLoop = requestAnimationFrame(render); + }; + this._overlayRenderLoop = requestAnimationFrame(render); + }; + + nodeType.prototype._drawOverlayCanvas = function() { + if (!this.advancedMode || !this.overlayCtx) return; + const ctx = this.overlayCtx; + const w = this.overlayCanvas.width; + const h = this.overlayCanvas.height; + ctx.clearRect(0, 0, w, h); + if (this.isLoading) { + ctx.fillStyle = "#888"; + ctx.font = "14px Arial"; + ctx.textAlign = "center"; + ctx.fillText("Loading...", w / 2, h / 2); + return; + } + if (!this.backgroundImage) return; + const safeDW = this.displayWidth > 0 ? this.displayWidth : 100; + const safeDH = this.displayHeight > 0 ? this.displayHeight : 100; + const scale = Math.min(w / safeDW, h / safeDH); + const drawW = safeDW * scale; + const drawH = safeDH * scale; + const drawX = (w - drawW) / 2; + const drawY = (h - drawH) / 2; + + if (this.previewImage) { + ctx.drawImage(this.previewImage, drawX, drawY, drawW, drawH); + } else if (this.originalImageData) { + const processed = new ImageData( + new Uint8ClampedArray(this.originalImageData.data), + this.originalImageData.width, + this.originalImageData.height + ); + applyBasicAdjustmentsJS(processed, this.adjustments.brightness, this.adjustments.contrast, this.adjustments.hue, this.adjustments.saturation); + this._tempCanvas.width = processed.width; + this._tempCanvas.height = processed.height; + this._tempCtx.putImageData(processed, 0, 0); + ctx.drawImage(this._tempCanvas, drawX, drawY, drawW, drawH); + } else { + ctx.drawImage(this.backgroundImage, drawX, drawY, drawW, drawH); + } + }; + + nodeType.prototype._sendAdjustments = async function() { + if (this.currentRenderAbortController) { + this.currentRenderAbortController.abort(); + this.currentRenderAbortController = null; + } + const payload = { id: String(this.id), adjustments: this.adjustments }; + try { + await api.fetchApi("/rayko/rs_adjustments", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(payload) + }); + this.isEditing = false; + this.setDirtyCanvas(true); + } catch(e) {} + }; + + nodeType.prototype._cancelEditing = async function() { + if (this.currentRenderAbortController) { + this.currentRenderAbortController.abort(); + this.currentRenderAbortController = null; + } + clearTimeout(this.renderTimeout); + try { await api.interrupt(); } catch(e) {} + await fetch("/rayko/rs_adjustments/cancel", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ node_id: String(this.id) }) }); + this._cleanup(); + }; + + nodeType.prototype._cleanup = function() { + this.isEditing = false; + this.isLoading = false; + this.backgroundImage = null; + this.previewImage = null; + this.originalImageData = null; + this.pendingEditorData = null; + this.sliderDragging = -1; + if (this._overlayRenderLoop) { + cancelAnimationFrame(this._overlayRenderLoop); + this._overlayRenderLoop = null; + } + this.setDirtyCanvas(true); + }; + + nodeType.prototype.onResize = function(size) { + if (size[0] < this.minWidth) size[0] = this.minWidth; + const titleH = LiteGraph.NODE_TITLE_HEIGHT || 30; + const canvasTopPadding = 30; + const cSize = Math.max(200, size[0] - 40); + this.canvasPixelSize = cSize; + + const slidersH = 4 * 30 + 10; + const btnH = 30; + const gaps = 10 + 8 + 8 + 20 + 15; + const neededHeight = titleH + canvasTopPadding + cSize + slidersH + btnH + gaps; + if (size[1] < neededHeight) size[1] = neededHeight; + + this.setDirtyCanvas(true); + if (this.isEditing && this.backgroundImage) { + this._updateDisplaySize(cSize); + } + }; + + nodeType.prototype._getCanvasMetrics = function() { + const titleH = LiteGraph.NODE_TITLE_HEIGHT || 30; + const canvasTopPadding = 30; + let cSize, rectX, rectY; + if (this.advancedMode) { + cSize = Math.min(this.overlayCanvas.width || 1000, this.overlayCanvas.height || 800); + rectX = 0; rectY = 0; + } else { + cSize = Math.max(200, this.size[0] - 40); + rectX = (this.size[0] - cSize) / 2; + rectY = titleH + canvasTopPadding; + } + return { cSize, rectX, rectY }; + }; + + nodeType.prototype._roundRect = function(ctx, x, y, w, h, r) { + ctx.beginPath(); + ctx.moveTo(x + r, y); + ctx.lineTo(x + w - r, y); + ctx.quadraticCurveTo(x + w, y, x + w, y + r); + ctx.lineTo(x + w, y + h - r); + ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h); + ctx.lineTo(x + r, y + h); + ctx.quadraticCurveTo(x, y + h, x, y + h - r); + ctx.lineTo(x, y + r); + ctx.quadraticCurveTo(x, y, x + r, y); + ctx.closePath(); + }; + + nodeType.prototype._drawSlider = function(ctx, label, value, min, max, x, y, w, h, isHover, isActive) { + ctx.fillStyle = isHover ? "#2a2a2a" : "#252525"; + ctx.strokeStyle = isActive ? "#4CAF50" : (isHover ? "#4CAF50" : "#444"); + ctx.lineWidth = 1; + this._roundRect(ctx, x, y, w, h, 4); + ctx.fill(); + ctx.stroke(); + + ctx.fillStyle = "#aaa"; + ctx.font = "10px sans-serif"; + ctx.textAlign = "left"; + ctx.textBaseline = "middle"; + ctx.fillText(label, x + 8, y + h / 2); + + const trackX = x + 90; + const trackW = w - 190; + const trackY = y + h / 2; + const trackH = 3; + + ctx.fillStyle = "#444"; + ctx.fillRect(trackX, trackY - trackH / 2, trackW, trackH); + + const ratio = Math.max(0, Math.min(1, (value - min) / (max - min))); + const fillW = trackW * ratio; + ctx.fillStyle = "#4CAF50"; + ctx.fillRect(trackX, trackY - trackH / 2, fillW, trackH); + + const handleX = trackX + fillW; + const handleSize = 12; + ctx.fillStyle = "#fff"; + ctx.beginPath(); + ctx.arc(handleX, trackY, handleSize / 2, 0, Math.PI * 2); + ctx.fill(); + + const valueW = 50; + const valueX = x + w - valueW - 38; + ctx.fillStyle = "#222"; + ctx.strokeStyle = "#444"; + this._roundRect(ctx, valueX, y + 3, valueW, h - 6, 3); + ctx.fill(); + ctx.stroke(); + + ctx.fillStyle = "#4CAF50"; + ctx.font = "10px monospace"; + ctx.textAlign = "center"; + ctx.fillText(String(Math.round(value)), valueX + valueW / 2, y + h / 2); + + const resetBtnSize = 24; + const resetBtnX = x + w - resetBtnSize - 8; + const resetBtnY = y + (h - resetBtnSize) / 2; + ctx.fillStyle = "#252525"; + ctx.strokeStyle = "#444"; + this._roundRect(ctx, resetBtnX, resetBtnY, resetBtnSize, resetBtnSize, 4); + ctx.fill(); + ctx.stroke(); + ctx.fillStyle = "#888"; + ctx.font = "14px sans-serif"; + ctx.textAlign = "center"; + ctx.textBaseline = "middle"; + ctx.fillText("🔄", resetBtnX + resetBtnSize / 2, resetBtnY + resetBtnSize / 2); + + return { x: trackX - 6, y: y, w: trackW + 12, h: h, resetBtnX, resetBtnY, resetBtnSize, valueX, valueY: y, valueW, valueH: h }; + }; + + nodeType.prototype.onDrawForeground = function(ctx) { + if (this.advancedMode) { + ctx.clearRect(0, 0, this.size[0], this.size[1]); + return; + } + + const { cSize, rectX, rectY } = this._getCanvasMetrics(); + ctx.fillStyle = "#1e1e1e"; + ctx.fillRect(rectX, rectY, cSize, cSize); + ctx.strokeStyle = "#555"; + ctx.strokeRect(rectX, rectY, cSize, cSize); + + this._updateDisplaySize(cSize); + + if (this.isLoading) { + ctx.fillStyle = "#888"; + ctx.font = "12px Arial"; + ctx.fillText("Loading...", rectX + cSize / 2 - 35, rectY + cSize / 2); + } else if (this.isEditing && this.backgroundImage) { + try { + const safeDW = this.displayWidth > 0 ? this.displayWidth : 100; + const safeDH = this.displayHeight > 0 ? this.displayHeight : 100; + + ctx.save(); + ctx.translate(rectX, rectY); + + const scale = Math.min(cSize / safeDW, cSize / safeDH); + const drawW = safeDW * scale; + const drawH = safeDH * scale; + const drawX = (cSize - drawW) / 2; + const drawY = (cSize - drawH) / 2; + + if (this.previewImage) { + ctx.drawImage(this.previewImage, drawX, drawY, drawW, drawH); + } else if (this.originalImageData) { + const processed = new ImageData( + new Uint8ClampedArray(this.originalImageData.data), + this.originalImageData.width, + this.originalImageData.height + ); + applyBasicAdjustmentsJS(processed, this.adjustments.brightness, this.adjustments.contrast, this.adjustments.hue, this.adjustments.saturation); + this._tempCanvas.width = processed.width; + this._tempCanvas.height = processed.height; + this._tempCtx.putImageData(processed, 0, 0); + ctx.drawImage(this._tempCanvas, drawX, drawY, drawW, drawH); + } else { + ctx.drawImage(this.backgroundImage, drawX, drawY, drawW, drawH); + } + + ctx.restore(); + } catch (e) { + console.error('[RS Adjustments] FATAL DRAW ERROR:', e); + } + } else { + ctx.fillStyle = "#888"; + ctx.font = "12px Arial"; + ctx.fillText("▶ Run queue to start", rectX + cSize / 2 - 65, rectY + cSize / 2); + } + + const btnW = 140; + const btnH = 24; + const gap = 10; + const totalW = btnW * 2 + gap; + const startX = (this.size[0] - totalW) / 2; + const btnY = 20; + + const advBtnX = startX; + ctx.fillStyle = this.btnAdvancedHover ? "#3a3a3a" : "#2a2a2a"; + ctx.strokeStyle = "#2196F3"; + ctx.lineWidth = 2; + this._roundRect(ctx, advBtnX, btnY, btnW, btnH, 6); + ctx.fill(); + ctx.stroke(); + ctx.fillStyle = "#2196F3"; + ctx.font = "bold 11px Arial"; + ctx.textAlign = "center"; + ctx.textBaseline = "alphabetic"; + ctx.fillText("✨ ADVANCED", advBtnX + btnW/2, btnY + btnH/2 + 4); + + const resetBtnX = startX + btnW + gap; + ctx.fillStyle = this.btnResetAllHover ? "#3a3a3a" : "#2a2a2a"; + ctx.strokeStyle = "#FF9800"; + ctx.lineWidth = 2; + this._roundRect(ctx, resetBtnX, btnY, btnW, btnH, 6); + ctx.fill(); + ctx.stroke(); + ctx.fillStyle = "#FF9800"; + ctx.font = "bold 11px Arial"; + ctx.textAlign = "center"; + ctx.textBaseline = "alphabetic"; + ctx.fillText("🔄 RESET ALL", resetBtnX + btnW/2, btnY + btnH/2 + 4); + + const widgetX = 15; + const widgetW = this.size[0] - 30; + const slidersY = rectY + cSize + 10; + this.slidersY = slidersY; + this.sliderRects = []; + this.resetBtnRects = []; + + const sliderH = 30; + const gapSlider = 8; + const sliderConfigs = [ + { label: "BRIGHTNESS", key: "brightness", min: -100, max: 100 }, + { label: "CONTRAST", key: "contrast", min: -100, max: 100 }, + { label: "HUE", key: "hue", min: -180, max: 180 }, + { label: "SATURATION", key: "saturation", min: -100, max: 100 } + ]; + + sliderConfigs.forEach((cfg, i) => { + const y = slidersY + i * (sliderH + gapSlider); + const rect = this._drawSlider(ctx, cfg.label, this.adjustments[cfg.key], cfg.min, cfg.max, widgetX, y, widgetW, sliderH, this.sliderHover[i], this.sliderDragging === i); + this.sliderRects.push({ ...rect, key: cfg.key, min: cfg.min, max: cfg.max }); + + const resetBtnRect = { + x: rect.resetBtnX, + y: rect.resetBtnY, + w: rect.resetBtnSize, + h: rect.resetBtnSize, + key: cfg.key + }; + this.resetBtnRects.push(resetBtnRect); + + if (this.resetBtnHover[i]) { + ctx.strokeStyle = "#4CAF50"; + ctx.lineWidth = 1.5; + this._roundRect(ctx, resetBtnRect.x, resetBtnRect.y, resetBtnRect.w, resetBtnRect.h, 4); + ctx.stroke(); + } + }); + + const buttonsY = slidersY + 4 * (sliderH + gapSlider) + 10; + const btnH2 = 30; + const btnGap2 = 10; + const btnW2 = (this.size[0] - 35) / 2; + + ctx.fillStyle = this.btnApplyHover ? "#444" : "#2a2a2a"; + this._roundRect(ctx, 15, buttonsY, btnW2, btnH2, 6); + ctx.fill(); + ctx.strokeStyle = "#4CAF50"; + ctx.stroke(); + ctx.fillStyle = "#4CAF50"; + ctx.font = "bold 11px Arial"; + ctx.textAlign = "center"; + ctx.textBaseline = "alphabetic"; + ctx.fillText("✔️ APPLY", 15 + btnW2 / 2, buttonsY + btnH2 / 2 + 4); + + ctx.fillStyle = this.btnCancelHover ? "#444" : "#2a2a2a"; + this._roundRect(ctx, 15 + btnW2 + btnGap2, buttonsY, btnW2, btnH2, 6); + ctx.fill(); + ctx.strokeStyle = "#dc3545"; + ctx.stroke(); + ctx.fillStyle = "#dc3545"; + ctx.fillText("❌ CANCEL", 15 + btnW2 + btnGap2 + btnW2 / 2, buttonsY + btnH2 / 2 + 4); + }; + + nodeType.prototype.onMouseDown = function(event, pos) { + if (!this.advancedMode && pos) { + const btnW = 140; + const btnH = 24; + const gap = 10; + const totalW = btnW * 2 + gap; + const startX = (this.size[0] - totalW) / 2; + const btnY = 20; + const advBtnX = startX; + const resetBtnX = startX + btnW + gap; + + if (pos[0] >= advBtnX && pos[0] <= advBtnX + btnW && pos[1] >= btnY && pos[1] <= btnY + btnH) { + this._toggleAdvancedMode(); + return true; + } + if (pos[0] >= resetBtnX && pos[0] <= resetBtnX + btnW && pos[1] >= btnY && pos[1] <= btnY + btnH) { + this._resetAllParameters(); + return true; + } + } + + if (this.sliderRects && pos) { + for (let i = 0; i < this.sliderRects.length; i++) { + const rect = this.sliderRects[i]; + if (rect.valueX !== undefined && pos[0] >= rect.valueX && pos[0] <= rect.valueX + rect.valueW && pos[1] >= rect.valueY && pos[1] <= rect.valueY + rect.valueH) { + this._showValueInputPopup(i, event); + return true; + } + } + } + + if (this.sliderRects && pos) { + for (let i = 0; i < this.sliderRects.length; i++) { + const rect = this.sliderRects[i]; + if (pos[0] >= rect.x && pos[0] <= rect.x + rect.w && pos[1] >= rect.y && pos[1] <= rect.y + rect.h) { + this.sliderDragging = i; + this._updateSliderFromMouse(i, pos[0]); + return true; + } + } + } + + if (this.resetBtnRects && pos) { + for (let i = 0; i < this.resetBtnRects.length; i++) { + const rect = this.resetBtnRects[i]; + if (pos[0] >= rect.x && pos[0] <= rect.x + rect.w && pos[1] >= rect.y && pos[1] <= rect.y + rect.h) { + const defaultVal = DEFAULT_SLIDER_VALUES[rect.key] !== undefined ? DEFAULT_SLIDER_VALUES[rect.key] : 0; + this.adjustments[rect.key] = defaultVal; + const widget = this.widgets?.find(w => w.name === rect.key); + if (widget) widget.value = defaultVal; + this.previewImage = null; + this.setDirtyCanvas(true); + return true; + } + } + } + + if (pos && this.slidersY !== undefined) { + const btnH2 = 30; + const btnGap2 = 10; + const btnW2 = (this.size[0] - 35) / 2; + const buttonsY = this.slidersY + 4 * 38 + 10; + const y1 = buttonsY; + + if (pos[0] >= 15 && pos[0] <= 15 + btnW2 && pos[1] >= y1 && pos[1] <= y1 + btnH2) { + this._sendAdjustments(); + return true; + } + if (pos[0] >= 15 + btnW2 + btnGap2 && pos[0] <= 15 + btnW2 + btnGap2 + btnW2 && pos[1] >= y1 && pos[1] <= y1 + btnH2) { + this._cancelEditing(); + return true; + } + } + + return false; + }; + + nodeType.prototype.onMouseMove = function(event, pos) { + if (this.sliderDragging >= 0 && event && event.buttons === 0) { + this.sliderDragging = -1; + this.setDirtyCanvas(true); + return; + } + + if (this.sliderDragging >= 0 && pos) { + this._updateSliderFromMouse(this.sliderDragging, pos[0]); + return; + } + + if (pos && !this.advancedMode) { + const btnW = 140; + const btnH = 24; + const gap = 10; + const totalW = btnW * 2 + gap; + const startX = (this.size[0] - totalW) / 2; + const btnY = 20; + const advBtnX = startX; + const resetBtnX = startX + btnW + gap; + + const prevAdv = this.btnAdvancedHover; + const prevReset = this.btnResetAllHover; + this.btnAdvancedHover = pos[0] >= advBtnX && pos[0] <= advBtnX + btnW && pos[1] >= btnY && pos[1] <= btnY + btnH; + this.btnResetAllHover = pos[0] >= resetBtnX && pos[0] <= resetBtnX + btnW && pos[1] >= btnY && pos[1] <= btnY + btnH; + if (prevAdv !== this.btnAdvancedHover || prevReset !== this.btnResetAllHover) { + this.setDirtyCanvas(true); + } + } + + const prevHover = [...this.sliderHover]; + const prevResetHover = [...this.resetBtnHover]; + + if (this.sliderRects && pos) { + for (let i = 0; i < this.sliderRects.length; i++) { + const rect = this.sliderRects[i]; + this.sliderHover[i] = pos[0] >= rect.x && pos[0] <= rect.x + rect.w && pos[1] >= rect.y && pos[1] <= rect.y + rect.h; + } + } + + if (this.resetBtnRects && pos) { + for (let i = 0; i < this.resetBtnRects.length; i++) { + const rect = this.resetBtnRects[i]; + this.resetBtnHover[i] = pos[0] >= rect.x && pos[0] <= rect.x + rect.w && pos[1] >= rect.y && pos[1] <= rect.y + rect.h; + } + } + + if (prevHover.some((v, i) => v !== this.sliderHover[i]) || prevResetHover.some((v, i) => v !== this.resetBtnHover[i])) { + this.setDirtyCanvas(true); + } + + if (pos && this.slidersY !== undefined) { + const btnH2 = 30; + const btnGap2 = 10; + const btnW2 = (this.size[0] - 35) / 2; + const buttonsY = this.slidersY + 4 * 38 + 10; + const y1 = buttonsY; + + const prevApply = this.btnApplyHover; + const prevCancel = this.btnCancelHover; + this.btnApplyHover = pos[0] >= 15 && pos[0] <= 15 + btnW2 && pos[1] >= y1 && pos[1] <= y1 + btnH2; + this.btnCancelHover = pos[0] >= 15 + btnW2 + btnGap2 && pos[0] <= 15 + btnW2 + btnGap2 + btnW2 && pos[1] >= y1 && pos[1] <= y1 + btnH2; + + if (prevApply !== this.btnApplyHover || prevCancel !== this.btnCancelHover) this.setDirtyCanvas(true); + } + }; + + nodeType.prototype.onMouseUp = function() { + if (this.sliderDragging >= 0) { + this.sliderDragging = -1; + this.setDirtyCanvas(true); + } + }; + + nodeType.prototype._updateSliderFromMouse = function(index, mouseX) { + if (!this.sliderRects || !this.sliderRects[index]) return; + const rect = this.sliderRects[index]; + const ratio = Math.max(0, Math.min(1, (mouseX - rect.x) / rect.w)); + const value = Math.round(rect.min + ratio * (rect.max - rect.min)); + this.adjustments[rect.key] = value; + + const widget = this.widgets?.find(w => w.name === rect.key); + if (widget) widget.value = value; + + if (this.advancedMode) { + if (this.overlayInputs[rect.key]) { + this.overlayInputs[rect.key].value = value; + } + if (this.sliderDisplays[rect.key]) { + this.sliderDisplays[rect.key].textContent = String(value); + } + } + + this.previewImage = null; + this.setDirtyCanvas(true); + }; + + nodeType.prototype._showValueInputPopup = function(index, event) { + if (!this.sliderRects || !this.sliderRects[index]) return; + const rect = this.sliderRects[index]; + const currentValue = this.adjustments[rect.key]; + + const screenX = event.clientX; + const screenY = event.clientY; + + const popup = document.createElement('div'); + popup.style.cssText = 'position:fixed;z-index:10003;background:#1a1a1a;border:1px solid #444;border-radius:6px;padding:8px 12px;box-shadow:0 4px 20px rgba(0,0,0,0.5);display:flex;align-items:center;gap:8px;'; + + const input = document.createElement('input'); + input.type = 'number'; + input.value = currentValue; + input.min = rect.min; + input.max = rect.max; + input.step = 1; + input.style.cssText = 'width:100px;background:#222;color:#fff;border:1px solid #444;border-radius:4px;padding:6px 10px;font-size:12px;outline:none;'; + + const saveBtn = document.createElement('button'); + saveBtn.textContent = 'OK'; + saveBtn.style.cssText = 'background:#4CAF50;color:#fff;border:none;border-radius:4px;padding:6px 12px;font-size:12px;cursor:pointer;'; + + const doSave = () => { + let num = parseInt(input.value); + if (isNaN(num)) num = currentValue; + num = Math.max(rect.min, Math.min(rect.max, num)); + this.adjustments[rect.key] = num; + + const widget = this.widgets?.find(w => w.name === rect.key); + if (widget) widget.value = num; + + if (this.advancedMode) { + if (this.overlayInputs[rect.key]) { + this.overlayInputs[rect.key].value = num; + } + if (this.sliderDisplays[rect.key]) { + this.sliderDisplays[rect.key].textContent = String(num); + } + } + + popup.remove(); + this.previewImage = null; + this.setDirtyCanvas(true); + }; + + saveBtn.onclick = (ev) => { ev.stopPropagation(); ev.preventDefault(); doSave(); }; + input.onkeydown = (ev) => { if (ev.key === 'Enter') { ev.preventDefault(); doSave(); } }; + + popup.appendChild(input); + popup.appendChild(saveBtn); + document.body.appendChild(popup); + + const popupWidth = popup.offsetWidth || 180; + const popupHeight = popup.offsetHeight || 40; + + let leftPos = screenX + 15; + let topPos = screenY - popupHeight / 2; + + if (leftPos + popupWidth > window.innerWidth - 10) { + leftPos = screenX - popupWidth - 15; + } + if (topPos < 10) topPos = 10; + if (topPos + popupHeight > window.innerHeight - 10) { + topPos = window.innerHeight - popupHeight - 10; + } + + popup.style.left = leftPos + 'px'; + popup.style.top = topPos + 'px'; + + setTimeout(() => { input.focus(); input.select(); }, 50); + + setTimeout(() => { + const closeHandler = (ev) => { + if (!popup.contains(ev.target)) { + popup.remove(); + document.removeEventListener('mousedown', closeHandler); + } + }; + document.addEventListener('mousedown', closeHandler); + }, 100); + }; + } +}); \ No newline at end of file diff --git a/Рекоменды.txt b/Рекоменды.txt new file mode 100644 index 0000000..311e53f --- /dev/null +++ b/Рекоменды.txt @@ -0,0 +1,108 @@ +## Анализ корректности расчетов параметров + +После проверки кода выявлены следующие проблемы: + +### 1. **Black & White** (функция `apply_black_and_white`) +❌ **Ошибка**: параметры `yellow`, `cyan`, `magenta` не используются. +Формула использует только коэффициенты для красного, зелёного и синего каналов: +```python +gray = r*(red/100) + g*(green/100) + b*(blue/100) +``` +Это игнорирует цветовые тона жёлтого, циана и пурпурного, что не соответствует фотошопу. + +**Правильный подход** (как в Photoshop): +Для каждого пикселя определяется его доминирующий цветовой тон, и веса для преобразования в градации серого интерполируются между соседними цветами. Например, можно использовать HSV и взвешенную сумму яркостей по формуле: +``` +gray = R * w_R + G * w_G + B * w_B +``` +где веса зависят от оттенка: +- для красных тонов → `w_R = red/100`, `w_G = 0`, `w_B = 0` (но с плавным переходом) +- для жёлтых → комбинация `red` и `green` +- для зелёных → `green` +- для цианов → `green` и `blue` +- для синих → `blue` +- для пурпурных → `red` и `blue` + +Реализация через интерполяцию по оттенку (hue): +```python +hue = cv2.cvtColor(img_rgb, cv2.COLOR_RGB2HSV)[:,:,0] # 0..179 +# задаём веса для каждого цвета в градусах (0=красный, 30=жёлтый, 60=зелёный, 90=циан, 120=синий, 150=пурпурный) +color_weights = { + 0: (red/100, 0, 0), + 30: (red/100, green/100, 0), + 60: (0, green/100, 0), + 90: (0, green/100, blue/100), + 120: (0, 0, blue/100), + 150: (red/100, 0, blue/100) +} +# для каждого пикселя интерполируем веса по hue +``` + +--- + +### 2. **Selective Color** (функция `apply_selective_color`) +⚠️ **Неточность**: маска для выбранного цвета является **бинарной** (по жёстким порогам HSV), тогда как в фотошопе коррекция применяется плавно, пропорционально близости пикселя к целевому цвету. + +**Улучшение**: использовать не бинарную маску, а весовой коэффициент от 0 до 1, вычисляемый на основе расстояния в цветовом пространстве (например, в HSV). +Пример для красного: +```python +hue_dist = np.minimum(np.abs(h - 0), np.abs(h - 180)) # красный около 0 или 180 +mask = np.exp(-hue_dist**2 / sigma**2) * (s > 30) # гауссово взвешивание +``` +Аналогично для других цветов. + +Формула преобразования CMYK в RGB **верна**: +- `K = 1 - max(R,G,B)` +- `C = (1 - R - K) / (1 - K + eps)` +- `M = (1 - G - K) / (1 - K + eps)` +- `Y = (1 - B - K) / (1 - K + eps)` +- После добавления поправок обратный переход: + - `R = 1 - C - K` + - `G = 1 - M - K` + - `B = 1 - Y - K` + +--- + +### 3. **Levels** (`apply_levels`) +✅ **Формула верна**: +``` +normalized = (img - in_min) / (in_max - in_min) +result = clamp(normalized^(1/gamma), 0, 1) * 255 +``` + +--- + +### 4. **Exposure** (`apply_exposure`) +✅ **Формула верна** (экспозиция в стопах): +``` +multiplier = 2^(exposure/100) +result = img * multiplier + offset/100 +``` + +--- + +### 5. **Color Balance** (`apply_color_balance`) +✅ **Маски и формулы коррекции** соответствуют распространённому подходу: +- Яркость вычисляется по стандартной формуле `0.299*R + 0.587*G + 0.114*B`. +- Маски теней, средних тонов и светов строятся через кусочно-линейные функции. +- Коррекция добавляется к соответствующим каналам (R,G,B) с учётом знака (положительное → Red/Green/Blue, отрицательное → Cyan/Magenta/Yellow). + +--- + +### 6. **Channel Mixer** (`apply_channel_mixer`) +✅ **Формула полностью корректна**: +``` +new_R = R * (red_red/100) + G * (red_green/100) + B * (red_blue/100) +new_G = R * (green_red/100) + G * (green_green/100) + B * (green_blue/100) +new_B = R * (blue_red/100) + G * (blue_green/100) + B * (blue_blue/100) +``` +с последующим клиппированием в [0,1]. + +--- + +### Рекомендации по исправлению +1. **Переписать `apply_black_and_white`** с учётом всех шести параметров (красный, жёлтый, зелёный, циан, синий, пурпурный) с использованием интерполяции по оттенку. +2. **Заменить бинарные маски в `apply_selective_color`** на плавные веса (например, через гауссово расстояние по оттенку) для более точного соответствия фотошопу. +3. Остальные функции оставить без изменений – они работают корректно. + +При необходимости могу предоставить готовый исправленный код для `apply_black_and_white` и `apply_selective_color`. \ No newline at end of file