// ========================================================================== // Eses Image Effect Levels // ========================================================================== // // Description: // The 'Eses Image Effect Levels' node provides a versatile, image editor style // levels adjustment tool directly within the ComfyUI interface. It allows for // precise, interactive control over the tonal range of images and masks, // complete with a live histogram for visual feedback. // // Key Features: // // - Interactive Controls: // - An interactive preview of the level adjustments in the node. // - Features sliders for input Black, Mid, and White points, as well as // sliders for the final Output Black and White points. // - A live histogram visualizes the tonal distribution of the selected // channel. // // - Multi-Channel Adjustments: // - Apply level adjustments to the combined RGB channels for general // corrections. // - Isolate adjustments to the individual Red, Green, or Blue channels. // - Apply a separate level adjustment directly to an input mask. // // - State Serialization: // - All level adjustments are saved with the workflow and restored on // reload. // - The node's state persists after refreshing the browser page. // // - Live Preview: // - The node displays a preview of the connected image with the level // adjustments applied in real-time as you move the sliders. // // - Quality of Life Features: // - "Set Auto Levels" button to automatically calculate optimal // input levels. // - A simple file-based preset system for saving and loading adjustment // presets // - "Reset" buttons to revert the current channel or all channels to // default values. // - Adjustable auto levels sensitivity for fine-tuned automatic adjustments. // // Version: 1.3.2 // // License: See LICENSE.txt // // ========================================================================== import { app } from "/scripts/app.js"; import { api } from "/scripts/api.js"; const HEADER_HEIGHT = 445; const PADDING = 10; const HISTOGRAM_AREA_HEIGHT = 40; const AREA_SPACING = 8; const SLIDER_BOX_HEIGHT = 15; const OUTPUT_SLIDER_AREA_HEIGHT = 20; // --- PRESET HANDLING START --- // Global cache for presets to avoid // fetching them for every single node let allPresets = null; async function getPresets() { if (allPresets === null) { try { const response = await api.fetchApi("/eses_levels/get_presets", { cache: "no-store" }); allPresets = await response.json(); //console.log("[EsesImageEffectLevels] Presets loaded:", allPresets); } catch (e) { //console.error("[EsesImageEffectLevels] Failed to load presets:", e); allPresets = {}; } } return allPresets; } // Pre-fetch presets when the script is loaded getPresets(); // --- PRESET HANDLING END --- app.registerExtension({ name: "Eses.EsesImageEffectLevels", // --- PRESET HANDLING START --- async beforeRegisterNodeDef(nodeType, nodeData, app) { // Runs once when the node definition is being registered. // Dynamically add the preset names to the combo box definition. if (nodeData.name === "EsesImageEffectLevels") { const presets = await getPresets(); const presetNames = ["None", ...Object.keys(presets)]; // Find the 'preset' input in the node's // definition and update its options const presetInput = nodeData.input.required.preset; if (presetInput) { presetInput[0] = presetNames; } } }, // --- PRESET HANDLING END --- nodeCreated(node) { if (node.comfyClass === "EsesImageEffectLevels") { const originalConfigure = node.configure; const originalSerialize = node.serialize; node.size = [256, 575]; node.imagePreview = null; node.originalImage = null; node.originalMask = null; node.isManuallyResized = false; node.dragging = null; node.resetButtonState = 'idle'; // Add state for auto color button node.autoColorButtonState = 'idle'; node.onResize = function () { this.isManuallyResized = true; }; const defaultLevels = () => ({ black_point: 0.0, white_point: 1.0, mid_point: 1.0, output_black: 0, output_white: 255 }); const resetAllLevels = () => ({ rgb: defaultLevels(), r: defaultLevels(), g: defaultLevels(), b: defaultLevels(), mask: defaultLevels() }); node.allLevels = resetAllLevels(); node.histograms = {}; let blackWidget, midWidget, whiteWidget, outputBlackWidget, outputWhiteWidget; function updateWidgetsFromState() { const levels = node.allLevels[node.activeChannel]; if (!levels) return; if (blackWidget) blackWidget.value = parseFloat(levels.black_point.toFixed(2)); if (midWidget) midWidget.value = parseFloat(levels.mid_point.toFixed(2)); if (whiteWidget) whiteWidget.value = parseFloat(levels.white_point.toFixed(2)); if (outputBlackWidget) outputBlackWidget.value = Math.round(levels.output_black); if (outputWhiteWidget) outputWhiteWidget.value = Math.round(levels.output_white); } node.getResetButtonName = (isConfirm = false) => { const channelWidget = node.widgets.find(w => w.name === "channel"); if (node.activeChannel === 'rgb') return isConfirm ? "CONFIRM RESET ALL" : "Reset All Levels"; const channelName = channelWidget.value; return isConfirm ? `CONFIRM RESET ${channelName}` : `Reset ${channelName} Level`; }; node.resetButtonCallback = function () { const resetButton = node.widgets.find(w => w.name.startsWith("Reset") || w.name.startsWith("CONFIRM")); if (node.resetButtonState === 'armed') { clearTimeout(node.resetTimeout); if (node.activeChannel === 'rgb') { node.allLevels = resetAllLevels(); } else { node.allLevels[node.activeChannel] = defaultLevels(); } updateWidgetsFromState(); node.sendLevelUpdateToPython(); resetButton.name = node.getResetButtonName(); node.resetButtonState = 'idle'; } else { node.resetButtonState = 'armed'; resetButton.name = node.getResetButtonName(true); node.resetTimeout = setTimeout(() => { resetButton.name = node.getResetButtonName(); node.resetButtonState = 'idle'; app.graph.setDirtyCanvas(true); }, 3000); } app.graph.setDirtyCanvas(true); }; function setupCustomWidgets() { const levelsWidget = node.widgets.find(w => w.name === "all_levels_json"); const channelWidget = node.widgets.find(w => w.name === "channel"); // Hide the boolean widgets that are only used for triggering const autoLevelsWidget = node.widgets.find(w => w.name === "auto_levels"); const autoColorWidget = node.widgets.find(w => w.name === "auto_color"); if (!levelsWidget || !channelWidget) return; levelsWidget.type = "hidden"; if (levelsWidget.element) { levelsWidget.element.style.display = "none"; } if (autoLevelsWidget) { autoLevelsWidget.type = "hidden"; if (autoLevelsWidget.element) { autoLevelsWidget.element.style.display = "none"; } } if (autoColorWidget) { autoColorWidget.type = "hidden"; if (autoColorWidget.element) { autoColorWidget.element.style.display = "none"; } } const channelMap = { "RGB": "rgb", "Red": "r", "Green": "g", "Blue": "b", "Mask": "mask" }; node.activeChannel = channelMap[channelWidget.value] || "rgb"; if (!node.widgets.find(w => w.name === "Black")) { blackWidget = node.addWidget("number", "Black", 0.0, (val) => { if (node.allLevels[node.activeChannel]) { node.allLevels[node.activeChannel].black_point = val; node.sendLevelUpdateToPython(); } }, { min: 0.0, max: 1.0, step: 0.01, precision: 2 }); midWidget = node.addWidget("number", "Mid", 1.0, (val) => { if (node.allLevels[node.activeChannel]) { node.allLevels[node.activeChannel].mid_point = val; node.sendLevelUpdateToPython(); } }, { min: 0.01, max: 9.99, step: 0.01, precision: 2 }); whiteWidget = node.addWidget("number", "White", 1.0, (val) => { if (node.allLevels[node.activeChannel]) { node.allLevels[node.activeChannel].white_point = val; node.sendLevelUpdateToPython(); } }, { min: 0.0, max: 1.0, step: 0.01, precision: 2 }); outputBlackWidget = node.addWidget("number", "Output Black", 0, (val) => { if (node.allLevels[node.activeChannel]) { node.allLevels[node.activeChannel].output_black = val; node.sendLevelUpdateToPython(); } }, { min: 0, max: 255, step: 1, precision: 0 }); outputWhiteWidget = node.addWidget("number", "Output White", 255, (val) => { if (node.allLevels[node.activeChannel]) { node.allLevels[node.activeChannel].output_white = val; node.sendLevelUpdateToPython(); } }, { min: 0, max: 255, step: 1, precision: 0 }); } else { blackWidget = node.widgets.find(w => w.name === "Black"); midWidget = node.widgets.find(w => w.name === "Mid"); whiteWidget = node.widgets.find(w => w.name === "White"); outputBlackWidget = node.widgets.find(w => w.name === "Output Black"); outputWhiteWidget = node.widgets.find(w => w.name === "Output White"); } // --- PRESET HANDLING START --- // Remove old "Copy Settings" button if it exists const copySettingsButton = node.widgets.find(w => w.name === "Copy Settings"); if (copySettingsButton) node.widgets.splice(node.widgets.indexOf(copySettingsButton), 1); // Add "Preset Name" text field and "Save Preset" button if (!node.widgets.find(w => w.name === "Preset Name")) { const presetNameWidget = node.addWidget("string", "Preset Name", "", () => { }, {}); node.addWidget("button", "Save Preset", null, async () => { const name = presetNameWidget.value; if (!name || !name.trim()) { alert("Please enter a preset name."); return; } try { const resp = await api.fetchApi("/eses_levels/save_preset", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ preset_name: name, preset_data: node.allLevels, }), }); const res = await resp.json(); if (res.success) { alert(`Preset '${name}' saved successfully!`); // Refresh the presets in the dropdown allPresets = res.presets; const presetWidget = node.widgets.find(w => w.name === "preset"); if (presetWidget) { presetWidget.options.values = ["None", ...Object.keys(allPresets)]; presetWidget.value = name; } presetNameWidget.value = ""; } else { alert(`Error saving preset: ${res.message}`); } } catch (e) { console.error("[EsesImageEffectLevels] Error saving preset:", e); alert(`An error occurred: ${e}`); } }); } const presetWidget = node.widgets.find(w => w.name === "preset"); if (presetWidget) { presetWidget.callback = async (value) => { if (value === "None") return; const presets = await getPresets(); const selectedPreset = presets[value]; if (selectedPreset) { // Create a fresh default state const newLevels = resetAllLevels(); const loadedLevels = JSON.parse(JSON.stringify(selectedPreset)); // Deep merge the loaded preset into the default state. // This ensures compatibility with older presets. for (const channelKey in newLevels) { if (loadedLevels[channelKey]) { Object.assign(newLevels[channelKey], loadedLevels[channelKey]); } } node.allLevels = newLevels; updateWidgetsFromState(); node.sendLevelUpdateToPython(); node.setDirtyCanvas(true, true); } }; } // --- PRESET HANDLING END --- if (!node.widgets.find(w => w.name === "Set Auto Levels")) { node.addWidget("button", "Set Auto Levels", null, () => { const autoLevelsWidget = node.widgets.find(w => w.name === "auto_levels"); if (autoLevelsWidget) { const originalValue = autoLevelsWidget.value; autoLevelsWidget.value = true; app.queuePrompt(); setTimeout(() => { autoLevelsWidget.value = originalValue; }, 100); } }); } // Add "Set Auto Color" button if (!node.widgets.find(w => w.name === "Set Auto Color")) { node.addWidget("button", "Set Auto Color", null, () => { const autoColorWidget = node.widgets.find(w => w.name === "auto_color"); if (autoColorWidget) { const originalValue = autoColorWidget.value; autoColorWidget.value = true; app.queuePrompt(); // Reset the value back to false after a short delay setTimeout(() => { autoColorWidget.value = originalValue; }, 100); } }); } const oldResetBtn = node.widgets.find(w => w && w.name && (w.name.startsWith("Reset") || w.name.startsWith("CONFIRM"))); if (oldResetBtn) node.widgets.splice(node.widgets.indexOf(oldResetBtn), 1); const resetButton = node.addWidget("button", node.getResetButtonName(), null, node.resetButtonCallback.bind(node)); channelWidget.callback = (value) => { node.activeChannel = channelMap[value] || "rgb"; resetButton.name = node.getResetButtonName(); updateWidgetsFromState(); node.updateHistogram(); node.sendLevelUpdateToPython(); }; } node.configure = function (data) { originalConfigure.apply(this, arguments); if (data.custom_all_levels) { this.allLevels = data.custom_all_levels; } setupCustomWidgets(); updateWidgetsFromState(); }; node.serialize = function () { const data = originalSerialize.call(this); data.custom_all_levels = node.allLevels; return data; }; setupCustomWidgets(); Object.assign(node, { getImageArea: function () { const interactiveHeight = HISTOGRAM_AREA_HEIGHT + OUTPUT_SLIDER_AREA_HEIGHT + AREA_SPACING; // Moved image top edge down by the // amount increased by HEADER_HEIGHT const totalContentHeight = this.size[1] - HEADER_HEIGHT - PADDING * 2; const imageAreaHeight = totalContentHeight - interactiveHeight; if (imageAreaHeight < 10) return null; return { x: PADDING, y: HEADER_HEIGHT + PADDING, width: this.size[0] - PADDING * 2, height: imageAreaHeight }; }, getBarArea: function () { const imageArea = this.getImageArea(); if (!imageArea) return null; const barAreaHeight = HISTOGRAM_AREA_HEIGHT - SLIDER_BOX_HEIGHT - 4; return { x: PADDING, y: imageArea.y + imageArea.height + AREA_SPACING, width: this.size[0] - PADDING * 2, height: barAreaHeight }; }, getInputSliderArea: function () { const barArea = this.getBarArea(); if (!barArea) return null; return { x: PADDING, y: barArea.y + barArea.height + 4, width: this.size[0] - PADDING * 2, height: SLIDER_BOX_HEIGHT }; }, getOutputSliderArea: function () { const inputSliderArea = this.getInputSliderArea(); if (!inputSliderArea) return null; return { x: PADDING, y: inputSliderArea.y + inputSliderArea.height + AREA_SPACING, width: this.size[0] - PADDING * 2, height: SLIDER_BOX_HEIGHT }; }, updateHistogram: async function () { const channelWidget = node.widgets.find(w => w.name === "channel"); let base_data_image = (this.activeChannel === 'mask') ? this.originalMask : this.originalImage; if (!base_data_image) return; this.histograms[this.activeChannel] = null; const canvas = document.createElement('canvas'); canvas.width = base_data_image.naturalWidth; canvas.height = base_data_image.naturalHeight; canvas.getContext('2d').drawImage(base_data_image, 0, 0); const base_data_b64 = canvas.toDataURL('image/png').split(',')[1]; try { const result = await api.fetchApi("/eses_levels/get_histogram", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ base_data_b64, channel_mode: channelWidget.value }) }).then(r => r.json()); if (result.histogram) { this.histograms[this.activeChannel] = result.histogram; this.setDirtyCanvas(true, true); } } catch (err) { console.error("Failed to fetch histogram:", err); } }, sendLevelUpdateToPython: async function () { this.setDirtyCanvas(true, true); const levelsWidget = this.widgets.find(w => w.name === "all_levels_json"); if (!levelsWidget) return; levelsWidget.value = JSON.stringify(this.allLevels); let base_data_type, base_data_image; if (this.activeChannel === 'mask') { if (!this.originalMask) { this.imagePreview = null; this.setDirtyCanvas(true, true); return; } base_data_type = 'mask'; base_data_image = this.originalMask; } else { if (!this.originalImage) { this.imagePreview = null; this.setDirtyCanvas(true, true); return; } base_data_type = 'image'; base_data_image = this.originalImage; } const canvas = document.createElement('canvas'); canvas.width = base_data_image.naturalWidth; canvas.height = base_data_image.naturalHeight; canvas.getContext('2d').drawImage(base_data_image, 0, 0); const base_data_b64 = canvas.toDataURL('image/png').split(',')[1]; const body = { base_data_b64, all_levels_json: levelsWidget.value, base_data_type }; try { const result = await api.fetchApi("/eses_levels/apply", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) }).then(r => r.json()); if (result.adjusted_image_data) { const img = new Image(); img.src = `data:image/png;base64,${result.adjusted_image_data}`; img.onload = () => { this.imagePreview = img; this.setDirtyCanvas(true, true); }; } } catch (err) { } }, drawHistogram: function (ctx, area, data, step = 1) { if (!data || data.length === 0) return; step = Math.max(1, Math.floor(step)); ctx.fillStyle = "#999"; const baseBarWidth = area.width / 256; for (let i = 0; i < 256; i += step) { const chunk = data.slice(i, i + step); if (chunk.length === 0) continue; const maxVal = Math.max(...chunk); const barHeight = maxVal * area.height; const barWidth = baseBarWidth * chunk.length; const x = area.x + i * baseBarWidth; ctx.fillRect(x, area.y + area.height - barHeight, barWidth, barHeight); } }, drawHandle: function (ctx, x, y, size, color, strokeColor) { ctx.fillStyle = color; ctx.strokeStyle = strokeColor; ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(x, y); ctx.lineTo(x - size, y + size * 2); ctx.lineTo(x + size, y + size * 2); ctx.closePath(); ctx.fill(); ctx.stroke(); }, onDrawForeground: function (ctx) { if (this.flags?.collapsed) return; const imageArea = this.getImageArea(); const barArea = this.getBarArea(); const inputSliderArea = this.getInputSliderArea(); const outputSliderArea = this.getOutputSliderArea(); if (!imageArea || !barArea || !inputSliderArea || !outputSliderArea) return; if (this.imagePreview) { ctx.drawImage(this.imagePreview, imageArea.x, imageArea.y, imageArea.width, imageArea.height); } else { ctx.save(); ctx.fillStyle = LiteGraph.NODE_TEXT_COLOR || "#CCC"; ctx.textAlign = "center"; ctx.textBaseline = "middle"; ctx.font = "bold 14px Arial"; const message = "Connect inputs and run workflow"; ctx.fillText(message, imageArea.x + imageArea.width / 2, imageArea.y + imageArea.height / 2); ctx.restore(); } // Draw Input Levels Area ctx.fillStyle = "#282828"; ctx.fillRect(barArea.x, barArea.y, barArea.width, barArea.height); ctx.fillStyle = "#202020"; ctx.fillRect(inputSliderArea.x, inputSliderArea.y, inputSliderArea.width, inputSliderArea.height); const histogramData = this.histograms[this.activeChannel]; if (histogramData) { this.drawHistogram(ctx, barArea, histogramData, 1); } const levels = this.allLevels[this.activeChannel]; if (!levels) return; // Draw Input Handles const blackX = inputSliderArea.x + levels.black_point * inputSliderArea.width; const whiteX = inputSliderArea.x + levels.white_point * inputSliderArea.width; const relativeRangeWidth = whiteX - blackX; const midPointToSlider = (gamma) => { if (gamma >= 1.0) return 1.0 - (0.5 + ((gamma - 1.0) / 8.99) * 0.5); else return 1.0 - (((gamma - 0.01) / 0.99) * 0.5); }; const midHandleX = blackX + (midPointToSlider(levels.mid_point) * relativeRangeWidth); const handleTipY = inputSliderArea.y + 2.5; this.drawHandle(ctx, blackX, handleTipY, 5, "black", "white"); this.drawHandle(ctx, whiteX, handleTipY, 5, "white", "black"); this.drawHandle(ctx, midHandleX, handleTipY, 5, "#888", "white"); // Draw Output Levels Area const gradient = ctx.createLinearGradient(outputSliderArea.x, 0, outputSliderArea.x + outputSliderArea.width, 0); const blackOutPos = levels.output_black / 255.0; const whiteOutPos = levels.output_white / 255.0; gradient.addColorStop(0, "black"); gradient.addColorStop(blackOutPos, "black"); gradient.addColorStop(whiteOutPos, "white"); gradient.addColorStop(1, "white"); ctx.fillStyle = gradient; ctx.fillRect(outputSliderArea.x, outputSliderArea.y, outputSliderArea.width, outputSliderArea.height); // Draw Output Handles const outBlackX = outputSliderArea.x + blackOutPos * outputSliderArea.width; const outWhiteX = outputSliderArea.x + whiteOutPos * outputSliderArea.width; const outHandleTipY = outputSliderArea.y + 2.5; this.drawHandle(ctx, outBlackX, outHandleTipY, 5, "black", "white"); this.drawHandle(ctx, outWhiteX, outHandleTipY, 5, "white", "black"); }, onMouseDown: function (event) { if (event.button !== 0) return false; const localPos = app.canvas.convertEventToCanvasOffset(event); const mouseX = localPos[0] - this.pos[0]; const mouseY = localPos[1] - this.pos[1]; const levels = this.allLevels[this.activeChannel]; if (!levels) return false; // Check output slider first const outputSliderArea = this.getOutputSliderArea(); if (outputSliderArea && mouseX > outputSliderArea.x && mouseX < outputSliderArea.x + outputSliderArea.width && mouseY > outputSliderArea.y && mouseY < outputSliderArea.y + outputSliderArea.height) { const outBlackX = outputSliderArea.x + (levels.output_black / 255.0) * outputSliderArea.width; if (Math.abs(mouseX - outBlackX) < 10) { this.dragging = 'output_black'; return true; } const outWhiteX = outputSliderArea.x + (levels.output_white / 255.0) * outputSliderArea.width; if (Math.abs(mouseX - outWhiteX) < 10) { this.dragging = 'output_white'; return true; } } // Check input slider const inputSliderArea = this.getInputSliderArea(); if (inputSliderArea && mouseX > inputSliderArea.x && mouseX < inputSliderArea.x + inputSliderArea.width && mouseY > inputSliderArea.y && mouseY < inputSliderArea.y + inputSliderArea.height) { const blackX = inputSliderArea.x + levels.black_point * inputSliderArea.width; const whiteX = inputSliderArea.x + levels.white_point * inputSliderArea.width; const relativeRangeWidth = whiteX - blackX; const midPointToSlider = (gamma) => { if (gamma >= 1.0) { return 1.0 - (0.5 + ((gamma - 1.0) / 8.99) * 0.5); } else { return 1.0 - (((gamma - 0.01) / 0.99) * 0.5); } }; const midHandleX = blackX + (midPointToSlider(levels.mid_point) * relativeRangeWidth); if (Math.abs(mouseX - midHandleX) < 10) { this.dragging = 'mid'; } else if (Math.abs(mouseX - blackX) < 10) { this.dragging = 'black'; } else if (Math.abs(mouseX - whiteX) < 10) { this.dragging = 'white'; } } return this.dragging !== null; }, onMouseMove: function (event) { if (event.buttons !== 1) { if (this.dragging) { this.dragging = null; this.sendLevelUpdateToPython(); } return; } if (!this.dragging) return; const localPos = app.canvas.convertEventToCanvasOffset(event); const mouseX = localPos[0] - this.pos[0]; const levels = this.allLevels[this.activeChannel]; if (!levels) return; let needsUpdate = false; // Handle input slider dragging if (this.dragging === 'black' || this.dragging === 'white' || this.dragging === 'mid') { const inputSliderArea = this.getInputSliderArea(); if (!inputSliderArea) return; if (this.dragging === 'black' || this.dragging === 'white') { let newLevel = (mouseX - inputSliderArea.x) / inputSliderArea.width; newLevel = Math.max(0.0, Math.min(1.0, newLevel)); if (this.dragging === 'black') levels.black_point = Math.min(newLevel, levels.white_point - 0.01); else levels.white_point = Math.max(newLevel, levels.black_point + 0.01); } else if (this.dragging === 'mid') { const blackX = inputSliderArea.x + levels.black_point * inputSliderArea.width; const whiteX = inputSliderArea.x + levels.white_point * inputSliderArea.width; const relativeRangeWidth = whiteX - blackX; if (relativeRangeWidth < 1) return; let relativePos = (mouseX - blackX) / relativeRangeWidth; relativePos = Math.max(0.0, Math.min(1.0, relativePos)); let invertedPos = 1.0 - relativePos; if (invertedPos >= 0.5) { levels.mid_point = 1.0 + ((invertedPos - 0.5) / 0.5) * (9.99 - 1.0); } else { levels.mid_point = 0.01 + (invertedPos / 0.5) * (1.0 - 0.01); } levels.mid_point = parseFloat(levels.mid_point.toFixed(2)); } needsUpdate = true; } // Handle output slider dragging else if (this.dragging === 'output_black' || this.dragging === 'output_white') { const outputSliderArea = this.getOutputSliderArea(); if (!outputSliderArea) return; let newLevel = ((mouseX - outputSliderArea.x) / outputSliderArea.width) * 255; newLevel = Math.max(0, Math.min(255, newLevel)); if (this.dragging === 'output_black') { levels.output_black = Math.min(newLevel, levels.output_white - 1); } else { levels.output_white = Math.max(newLevel, levels.output_black + 1); } needsUpdate = true; } if (needsUpdate) { updateWidgetsFromState(); this.setDirtyCanvas(true, true); clearTimeout(this.updateTimeout); this.updateTimeout = setTimeout(() => this.sendLevelUpdateToPython(), 50); } }, onMouseUp: function (event) { if (this.dragging) { this.dragging = null; this.sendLevelUpdateToPython(); } } }); const methodsToBind = ['getImageArea', 'getBarArea', 'getInputSliderArea', 'getOutputSliderArea', 'updateHistogram', 'sendLevelUpdateToPython', 'onDrawForeground', 'onMouseDown', 'onMouseMove', 'onMouseUp', 'drawHistogram', 'drawHandle']; methodsToBind.forEach(m => { if (node[m]) node[m] = node[m].bind(node); }); } }, }); // Event listeners ----------- api.addEventListener("eses.levels_preview_base", ({ detail }) => { const node = app.graph.getNodeById(detail.node_id); if (node) { node.isManuallyResized = false; // Check for both auto_levels_updated // and auto_color_updated if (detail.updated_levels) { // Runs for auto-levels or auto-color. // Update all widgets. node.allLevels = detail.updated_levels; const channelWidget = node.widgets.find(w => w.name === "channel"); const channelMap = { "RGB": "rgb", "Red": "r", "Green": "g", "Blue": "b", "Mask": "mask" }; node.activeChannel = channelMap[channelWidget.value] || "rgb"; // Refresh the widgets for the currently // selected channel to show the newly reset // output levels. const blackWidget = node.widgets.find(w => w.name === "Black"); const midWidget = node.widgets.find(w => w.name === "Mid"); const whiteWidget = node.widgets.find(w => w.name === "White"); const outputBlackWidget = node.widgets.find(w => w.name === "Output Black"); const outputWhiteWidget = node.widgets.find(w => w.name === "Output White"); const levels = node.allLevels[node.activeChannel]; if (levels) { if (blackWidget) blackWidget.value = parseFloat(levels.black_point.toFixed(2)); if (midWidget) midWidget.value = parseFloat(levels.mid_point.toFixed(2)); if (whiteWidget) whiteWidget.value = parseFloat(levels.white_point.toFixed(2)); if (outputBlackWidget) outputBlackWidget.value = Math.round(levels.output_black); if (outputWhiteWidget) outputWhiteWidget.value = Math.round(levels.output_white); } } const onAssetLoad = async () => { if (!node.isManuallyResized) { const sizingObj = node.originalImage || node.originalMask; if (sizingObj) { const aspectRatio = sizingObj.naturalWidth / sizingObj.naturalHeight; const imageDrawWidth = node.size[0] - PADDING * 2; const imageDrawHeight = imageDrawWidth / aspectRatio; const interactiveHeight = HISTOGRAM_AREA_HEIGHT + OUTPUT_SLIDER_AREA_HEIGHT + AREA_SPACING; node.size[1] = imageDrawHeight + interactiveHeight + HEADER_HEIGHT + PADDING * 2; node.isManuallyResized = true; // Set to true after auto-resize } } await node.updateHistogram(); await node.sendLevelUpdateToPython(); }; let assetsToLoad = 0; if (detail.image_data) assetsToLoad++; if (detail.mask_data) assetsToLoad++; if (assetsToLoad === 0) { onAssetLoad(); return; } let loadedCount = 0; const assetLoadedCallback = async () => { if (++loadedCount === assetsToLoad) { await onAssetLoad(); } }; if (detail.image_data) { const img = new Image(); img.src = "data:image/png;base64," + detail.image_data; img.onload = assetLoadedCallback; node.originalImage = img; } else { node.originalImage = null; } if (detail.mask_data) { const mask = new Image(); mask.src = "data:image/png;base64," + detail.mask_data; mask.onload = assetLoadedCallback; node.originalMask = mask; } else { node.originalMask = null; } } });