Add MD Color Mod nodes, operators, and web UI

Introduce a complete GIMP/GEGL-inspired color module for ComfyUI. Adds: node declarations (nodes/*) and matching operator implementations (py/*) for many color/image operations (balance, temperature, hue/chroma, hue/saturation, saturation, exposure, levels, invert, auto adjustments, maps, posterize, desaturate, tone mapping, component extract/mix, etc.). Includes support libs (py/libs: color, process, blur), layer compositing (layers_blending), VRAM‑aware chunking, mask handling and alpha preservation. Adds web UI extension and widgets (web/*), package metadata, dynamic __init__.py loader, README update and .gitignore tweaks.
This commit is contained in:
MdONeilsl
2026-07-06 08:17:05 -04:00
parent db644e55b4
commit b6a2d1dd79
45 changed files with 7347 additions and 3 deletions
+43
View File
@@ -0,0 +1,43 @@
import { app } from "../../../scripts/app.js";
import { color_balance_widget } from "./nodes/color_balance_widget.js";
import { color_hue_saturation_widget } from "./nodes/color_hue_saturation_widget.js";
//import { addRGBIntegerWidget } from "./widgets/rgb_int_widget.js";
app.registerExtension({
name: "md.color_mod.frontend_extension",
async beforeRegisterNodeDef(nodeType, nodeData, app) {
const onNodeCreated = nodeType.prototype.onNodeCreated;
if (nodeData.name === "mdColorBalance") {
nodeType.prototype.onNodeCreated = function () {
onNodeCreated?.apply(this, arguments);
this._inst = new color_balance_widget(this);
};
}
if (nodeData.name === "mdColorHueSaturation") {
nodeType.prototype.onNodeCreated = function () {
onNodeCreated?.apply(this, arguments);
this._inst = new color_hue_saturation_widget(this);
};
}
/*
if (nodeData.name === "mdPrimitiveColorRGB") {
nodeType.prototype.onNodeCreated = function () {
const result = onNodeCreated?.apply(this, arguments);
addRGBIntegerWidget(this, "rgb", 16711680);
return result;
};
}
*/
},
});
+9
View File
@@ -0,0 +1,9 @@
export const set_node_widget_value = (node, widget_id, value) => {
let widget = node.widgets?.find(w => w.name === widget_id);
if (!widget) return console.error(`Could not find ${widget_id} widget`);
//console.log(`set ${widget_id} to ${value}`);
widget.value = value;
};
+103
View File
@@ -0,0 +1,103 @@
import { set_node_widget_value } from "../libs/widgets_module.js";
export class color_balance_widget {
constructor(node) {
this.node = node;
this.rangeNames = ["Shadow", "Midtonnes", "Highlights"];
// Find widgets
this.rangeWidget = node.widgets?.find(w => w.name === "range");
this.cyanWidget = node.widgets?.find(w => w.name === "Cyan-Red");
this.magentaWidget = node.widgets?.find(w => w.name === "Magenta-Green");
this.yellowWidget = node.widgets?.find(w => w.name === "Yellow-Blue");
this.dataWidget = node.widgets?.find(w => w.name === "jdata");
if (!this.rangeWidget || !this.cyanWidget || !this.magentaWidget || !this.yellowWidget || !this.dataWidget) {
console.warn("mdColorBalance: some widgets missing");
return;
}
this.dataWidget.hidden = true;
this.setupCallbacks();
// Initial load after the node is fully ready
setTimeout(() => this.loadFromData(), 0);
}
getCurrentRangeIndex() {
return this.rangeNames.indexOf(this.rangeWidget.value);
}
saveCurrentToData() {
let data;
try {
data = JSON.parse(this.dataWidget.value);
} catch {
data = [[0, 0, 0], [0, 0, 0], [0, 0, 0]];
}
const idx = this.getCurrentRangeIndex();
if (idx === -1) return;
data[idx] = [
parseFloat(this.cyanWidget.value),
parseFloat(this.magentaWidget.value),
parseFloat(this.yellowWidget.value)
];
this.dataWidget.value = JSON.stringify(data);
if (this.dataWidget.callback) this.dataWidget.callback(this.dataWidget.value);
}
loadFromData() {
let data;
try {
data = JSON.parse(this.dataWidget.value);
} catch {
data = [[0, 0, 0], [0, 0, 0], [0, 0, 0]];
}
const idx = this.getCurrentRangeIndex();
if (idx === -1) return;
const vals = data[idx] || [0, 0, 0];
// Use the shared utility to set widget values
set_node_widget_value(this.node, "Cyan-Red", vals[0]);
set_node_widget_value(this.node, "Magenta-Green", vals[1]);
set_node_widget_value(this.node, "Yellow-Blue", vals[2]);
// Notify the widget callbacks so the UI updates
if (this.cyanWidget.callback) this.cyanWidget.callback(this.cyanWidget.value);
if (this.magentaWidget.callback) this.magentaWidget.callback(this.magentaWidget.value);
if (this.yellowWidget.callback) this.yellowWidget.callback(this.yellowWidget.value);
}
setupCallbacks() {
// --- Range dropdown ---
const origRangeCb = this.rangeWidget.callback;
this.rangeWidget.callback = (value) => {
// Save current slider values for the old range
this.saveCurrentToData();
// Load slider values for the new range
this.loadFromData();
// Invoke any existing callback
if (origRangeCb) origRangeCb(value);
};
// --- Slider widgets ---
const onSliderChange = () => {
this.saveCurrentToData();
};
// Preserve existing callbacks by wrapping them
const wrapSliderCallback = (widget, extra) => {
const orig = widget.callback;
widget.callback = function(value) {
extra();
if (orig) orig.call(this, value); // ✅ Preserve `this` context
};
};
wrapSliderCallback(this.cyanWidget, onSliderChange);
wrapSliderCallback(this.magentaWidget, onSliderChange);
wrapSliderCallback(this.yellowWidget, onSliderChange);
}
}
+104
View File
@@ -0,0 +1,104 @@
import { set_node_widget_value } from "../libs/widgets_module.js";
export class color_hue_saturation_widget {
constructor(node) {
this.node = node;
// Order must match the dropdown: Master, Red, Yellow, Green, Cyan, Blue, Magenta
this.rangeNames = ["Master", "Red", "Yellow", "Green", "Cyan", "Blue", "Magenta"];
// Find widgets
this.colorWidget = node.widgets?.find(w => w.name === "color");
this.hueWidget = node.widgets?.find(w => w.name === "Hue");
this.lightnessWidget = node.widgets?.find(w => w.name === "Lightness");
this.saturationWidget = node.widgets?.find(w => w.name === "Saturation");
this.dataWidget = node.widgets?.find(w => w.name === "jdata");
if (!this.colorWidget || !this.hueWidget || !this.lightnessWidget ||
!this.saturationWidget || !this.dataWidget) {
console.warn("mdColorHueSaturation: some widgets missing");
return;
}
this.dataWidget.hidden = true;
this.setupCallbacks();
// Initial load after the node is fully ready
setTimeout(() => this.loadFromData(), 0);
}
getCurrentRangeIndex() {
return this.rangeNames.indexOf(this.colorWidget.value);
}
saveCurrentToData() {
let data;
try {
data = JSON.parse(this.dataWidget.value);
} catch {
data = this.rangeNames.map(() => [0, 0, 0]);
}
const idx = this.getCurrentRangeIndex();
if (idx === -1) return;
data[idx] = [
parseFloat(this.hueWidget.value),
parseFloat(this.lightnessWidget.value),
parseFloat(this.saturationWidget.value)
];
this.dataWidget.value = JSON.stringify(data);
if (this.dataWidget.callback) this.dataWidget.callback(this.dataWidget.value);
}
loadFromData() {
let data;
try {
data = JSON.parse(this.dataWidget.value);
} catch {
data = this.rangeNames.map(() => [0, 0, 0]);
}
const idx = this.getCurrentRangeIndex();
if (idx === -1) return;
const vals = data[idx] || [0, 0, 0];
// Update sliders using the shared utility
set_node_widget_value(this.node, "Hue", vals[0]);
set_node_widget_value(this.node, "Lightness", vals[1]);
set_node_widget_value(this.node, "Saturation", vals[2]);
// Notify the widget callbacks so the UI updates
if (this.hueWidget.callback) this.hueWidget.callback(this.hueWidget.value);
if (this.lightnessWidget.callback) this.lightnessWidget.callback(this.lightnessWidget.value);
if (this.saturationWidget.callback) this.saturationWidget.callback(this.saturationWidget.value);
}
setupCallbacks() {
// --- Color dropdown ---
const origColorCb = this.colorWidget.callback;
this.colorWidget.callback = (value) => {
// Save current slider values for the old range
this.saveCurrentToData();
// Load slider values for the new range
this.loadFromData();
// Invoke any existing callback
if (origColorCb) origColorCb(value);
};
// --- Slider widgets ---
const onSliderChange = () => {
this.saveCurrentToData();
};
const wrapSliderCallback = (widget, extra) => {
const orig = widget.callback;
widget.callback = function(value) {
extra();
if (orig) orig.call(this, value);
};
};
wrapSliderCallback(this.hueWidget, onSliderChange);
wrapSliderCallback(this.lightnessWidget, onSliderChange);
wrapSliderCallback(this.saturationWidget, onSliderChange);
}
}