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:
@@ -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;
|
||||
};
|
||||
}
|
||||
*/
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user