From a052b4497e7985b03dec0ad12364f204a67c8e05 Mon Sep 17 00:00:00 2001 From: "Alexander G. Morano" Date: Thu, 1 May 2025 03:38:35 -0400 Subject: [PATCH] * cleaned up old JS supports * new comfy-cozy version * widget_vector unified to match Jovimetrix * added circular and linear gradients * reverse and vertical options for all gradients --- README.md | 10 +- core/glsl.py | 28 ++-- glsl/create/create-gradient_circular.frag | 23 ++++ glsl/create/create-gradient_cone.frag | 10 +- glsl/create/create-gradient_linear.frag | 21 +++ glsl/create/create-palette.frag | 16 ++- glsl/filter/filter-bloom.frag | 8 +- glsl/filter/filter-range.frag | 6 +- node_list.json | 4 +- pyproject.toml | 2 +- web/node_glsl_dynamic.js | 38 ++++- web/util_jov.js | 161 ---------------------- web/widget_vector.js | 69 ++++++---- 13 files changed, 175 insertions(+), 221 deletions(-) create mode 100644 glsl/create/create-gradient_circular.frag create mode 100644 glsl/create/create-gradient_linear.frag delete mode 100644 web/util_jov.js diff --git a/README.md b/README.md index e476952..15bcd15 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,13 @@ Please consider sponsoring me if you enjoy the results of my work, code or docum ## UPDATES +**2025/05/01** @1.1.34: +* cleaned up old JS supports +* new comfy-cozy version +* widget_vector unified to match Jovimetrix +* added circular and linear gradients +* reverse and vertical options for all gradients + **2025/04/22** @1.1.33: * realigned categories @@ -57,9 +64,6 @@ Please consider sponsoring me if you enjoy the results of my work, code or docum * core supports switched to [cozy_comfyui](https://github.com/cozy-comfyui/cozy_comfyui) * updated requirements for numpy to be < 2.0.0 -**2025/03/28** @1.1.27: -* updated requirements for numpy to only be >=1.26.4 - # INSTALLATION [Please see the wiki for advanced use of the environment variables that can be used at startup](https://github.com/Amorano/Jovi_GLSL/wiki) diff --git a/core/glsl.py b/core/glsl.py index c935e0e..046082c 100644 --- a/core/glsl.py +++ b/core/glsl.py @@ -133,9 +133,9 @@ class GLSLNodeDynamic(CozyImageNode): original_params = super().INPUT_TYPES() optional = original_params.get('optional', {}) if 'RES' in cls.CONTROL: - optional["iRes"] = ("VEC2INT", { + optional["iRes"] = ("VEC2", { "default": (IMAGE_SIZE_DEFAULT, IMAGE_SIZE_DEFAULT), - "mij":IMAGE_SIZE_MIN, "label": ['W', 'H'], + "mij":IMAGE_SIZE_MIN, "label": ['W', 'H'], "int": True, "tooltip": "Width and Height as a Vector2 Integer (x, y)" }) @@ -164,7 +164,7 @@ class GLSLNodeDynamic(CozyImageNode): }) if 'MATTE' in cls.CONTROL: - optional["matte"] = ("VEC4INT", { + optional["matte"] = ("VEC4", { "default": (0, 0, 0, 255), "rgb": True, "tooltip": "Define a background color for padding. Useful when images do not fit and need a filler color" @@ -215,6 +215,13 @@ class GLSLNodeDynamic(CozyImageNode): type_name = "IMAGE" if glsl_type != 'sampler2D': type_name = typ.name + if type_name == "VEC2INT": + type_name = "VEC2" + elif type_name == "VEC3INT": + type_name = "VEC3" + elif type_name == "VEC4INT": + type_name = "VEC4" + if default is not None: if default.startswith('EnumGLSL'): params['default'] = 0 @@ -243,15 +250,18 @@ class GLSLNodeDynamic(CozyImageNode): params[minmax('maj', 'max')] = parse_value(val_max, EnumConvertType.FLOAT, sys.maxsize) if val_step is not None: - d = 1 if typ.name.endswith('INT') else 0.01 + d = 0.01 + params['precision'] = 3 + if typ.name.endswith('INT'): + d = 1 + params['precision'] = 0 params['step'] = parse_value(val_step, EnumConvertType.FLOAT, d) if meta is not None: if "rgb" in meta: - if glsl_type.startswith('vec'): - params['linear'] = True - else: - params['rgb'] = True + params['rgb'] = True + elif "linear" in meta: + params['linear'] = True if tooltip is not None: params["tooltip"] = tooltip @@ -314,7 +324,7 @@ class GLSLNodeDynamic(CozyImageNode): # iResolution, iFrame, iFrameRate, iTime, iSeed params = list(zip_longest_fill(iResolution, iFrame, iFrameRate, iTime, matte, edge, seed)) pbar = ProgressBar(len(params)) - logger.debug(f"batch size {batch} :: param count {len(params)}") + # logger.debug(f"batch size {batch} :: param count {len(params)}") for idx, (iResolution, iFrame, iFrameRate, iTime, matte, edge, seed) in enumerate(params): vars = {} firstImage = None diff --git a/glsl/create/create-gradient_circular.frag b/glsl/create/create-gradient_circular.frag new file mode 100644 index 0000000..d977ef0 --- /dev/null +++ b/glsl/create/create-gradient_circular.frag @@ -0,0 +1,23 @@ +// name: CIRCULAR GRADIENT +// desc: Generate a two color circular gradient +// category: CREATE +// control: res, + +uniform ivec4 start; // 0,0,0,255; 0; 255;; rgb | Start color +uniform ivec4 end; // 255,255,255,255; 0; 255;; rgb | End color +uniform vec2 center; // 0.5,0.5; 0; 1 | Center point +uniform float radius; // 0.5; 0; 1 | Max distance to outer edge +uniform bool reverse; // | Reverse the starting direction + +void mainImage( out vec4 fragColor, in vec2 fragCoord ) +{ + vec4 color_start = start / 255.0; + vec4 color_end = end / 255.0; + vec2 uv = fragCoord / iResolution.xy; + float dist = distance(uv, center); + float pos = clamp(dist / radius, 0.0, 1.0); + if (reverse) { + pos = 1.0 - pos; + } + fragColor = mix(color_start, color_end, pos); +} diff --git a/glsl/create/create-gradient_cone.frag b/glsl/create/create-gradient_cone.frag index 1c2f87d..d7ef12c 100644 --- a/glsl/create/create-gradient_cone.frag +++ b/glsl/create/create-gradient_cone.frag @@ -1,13 +1,15 @@ // name: CONICAL GRADIENT -// desc: Generate a conical gradient from black to white +// desc: Generate a two color conical gradient // category: CREATE // control: res, #include .lib/const.lib -uniform vec2 origin; // 0.5,0.5; 0; 1; 0.01 | Intensity of base normal -uniform vec2 range; // 0.0,1.0; 0; 1; 0.01 | start of range. 0=start. size of range. 1=full range. -uniform float angle; // 0.0; ; ; 0.5 | offset of the gradient starting angle +uniform vec2 origin; // 0.5,0.5; 0; 1 | Center point for gradient +uniform vec2 range; // 0.0,1.0; 0; 1 | start of range. 0=start. size of range. 1=full range. +uniform float angle; // 0.0;;; 0.5 | offset of the gradient starting angle +uniform bool vertical; // | if the gradient is top-bottom +uniform bool reverse; // | reverse the starting direction void mainImage( out vec4 fragColor, in vec2 fragCoord ) { diff --git a/glsl/create/create-gradient_linear.frag b/glsl/create/create-gradient_linear.frag new file mode 100644 index 0000000..f4ee080 --- /dev/null +++ b/glsl/create/create-gradient_linear.frag @@ -0,0 +1,21 @@ +// name: LINEAR GRADIENT +// desc: Generate a two color linear gradient +// category: CREATE +// control: res, + +uniform ivec4 start; // 0,0,0,255; 0; 255;; rgb | start color +uniform ivec4 end; // 255,255,255,255; 0; 255;; rgb | end color +uniform bool vertical; // | if the gradient is top-bottom +uniform bool reverse; // | reverse the starting direction + +void mainImage( out vec4 fragColor, in vec2 fragCoord ) +{ + vec4 color_start = start / 255.0; + vec4 color_end = end / 255.0; + vec2 uv = fragCoord / iResolution.xy; + float pos = vertical ? uv.y : uv.x; + if (reverse) { + pos = 1.0 - pos; + } + fragColor = mix(color_start, color_end, pos); +} diff --git a/glsl/create/create-palette.frag b/glsl/create/create-palette.frag index 7a59494..7c180d0 100644 --- a/glsl/create/create-palette.frag +++ b/glsl/create/create-palette.frag @@ -12,14 +12,20 @@ #include .lib/const.lib -uniform vec3 bias; // 0.5,0.5,0.5;0 | scale and bias (dc offset) -uniform vec3 amp; // 0.5,0.5,0.5 | contrast and brightness (amplitude) -uniform vec3 freq; // 1,1,1 | color cycle (R, G and B) (frequency) -uniform vec3 phase; // 0,0,0 | starting offset for the cycle +uniform vec3 bias; // 0.5,0.5,0.5;0 | scale and bias (dc offset) +uniform vec3 amp; // 0.5,0.5,0.5 | contrast and brightness (amplitude) +uniform vec3 freq; // 1,1,1 | color cycle (R, G and B) (frequency) +uniform vec3 phase; // 0,0,0 | starting offset for the cycle +uniform bool vertical; // | if the gradient is top-bottom +uniform bool reverse; // | reverse the starting direction void mainImage( out vec4 fragColor, in vec2 fragCoord ) { vec2 uv = fragCoord.xy / iResolution.xy; - vec3 col = bias + amp * cos(M_PI * (freq * uv.x + phase)); + float pos = vertical ? uv.y : uv.x; + if (reverse) { + pos = 1.0 - pos; + } + vec3 col = bias + amp * cos(M_PI * (freq * pos + phase)); fragColor = vec4(col, 1.0); } \ No newline at end of file diff --git a/glsl/filter/filter-bloom.frag b/glsl/filter/filter-bloom.frag index 23da68c..0548f73 100644 --- a/glsl/filter/filter-bloom.frag +++ b/glsl/filter/filter-bloom.frag @@ -2,10 +2,10 @@ // desc: Bloom filter pass // category: FILTER -uniform sampler2D image; // | RGB(A) image -uniform float threshold; // 0.25; 0; 1; 0.01; | Brightness threshold to start the bloom effect -uniform float intensity; // 2; 0; 200; 0.1; | Ramp up to intensity of bloom -uniform float blurSize; // 3; 0; 100; 0.1; | Number of pixels to fade effect across +uniform sampler2D image; // | RGB(A) image +uniform float threshold; // 0.25; 0; 1; 0.01 | Brightness threshold to start the bloom effect +uniform float intensity; // 2; 0; 200; 0.01 | Ramp up to intensity of bloom +uniform float blurSize; // 3; 0; 100; 0.01 | Number of pixels to fade effect across void mainImage( out vec4 fragColor, in vec2 fragCoord ) { diff --git a/glsl/filter/filter-range.frag b/glsl/filter/filter-range.frag index dd4b2db..26176ec 100644 --- a/glsl/filter/filter-range.frag +++ b/glsl/filter/filter-range.frag @@ -2,9 +2,9 @@ // desc: Select pixels from start color through end color. Maintains alpha/mask. // category: FILTER -uniform sampler2D image; // | RGB(A) image -uniform vec3 start; // 0,0,0; 0; 1; 0.01; rgb | Start of the Range -uniform vec3 end; // 1,1,1; 0; 1; 0.01; rgb | End of the Range +uniform sampler2D image; // | RGB(A) image +uniform vec3 start; // 0,0,0; 0; 1;; rgb | Start of the Range +uniform vec3 end; // 1,1,1; 0; 1;; rgb | End of the Range void mainImage( out vec4 fragColor, in vec2 fragCoord ) { vec2 uv = fragCoord / iResolution.xy; diff --git a/node_list.json b/node_list.json index b7c0da4..2184e7b 100644 --- a/node_list.json +++ b/node_list.json @@ -1,14 +1,16 @@ { "BLEND LINEAR (JOV_GL)": "Simple linear blend between two images", "BLOOM (JOV_GL)": "Bloom filter pass", + "CIRCULAR GRADIENT (JOV_GL)": "Generate a two color circular gradient", "COLOR CONVERSION (JOV_GL)": "Convert an image from one color space (RGB, HSV, LAB, XYZ) to another", "COLOR PALETTE (JOV_GL)": "Color palette creation using the formula: color(t) = a + b * cos[tau(c*t+d)]", - "CONICAL GRADIENT (JOV_GL)": "Generate a conical gradient from black to white", + "CONICAL GRADIENT (JOV_GL)": "Generate a two color conical gradient", "DIRECTIONAL WARP (JOV_GL)": "Domain warp an image with a direction and distortion map", "FILTER RANGE (JOV_GL)": "Select pixels from start color through end color", "GRAYSCALE (JOV_GL)": "Convert input to grayscale", "HSV ADJUST (JOV_GL)": "Hue, Saturation and Value adjustment control", "INVERT (JOV_GL)": "Invert the channels of an image along a scalar [0", + "LINEAR GRADIENT (JOV_GL)": "Generate a two color linear gradient", "MIN MAX (JOV_GL)": "Gets the minimum and maximum of an image", "NOISE PERLIN (JOV_GL)": "Classic Perlin noise", "NOISE SIMPLEX (JOV_GL)": "Simplex noise, simply", diff --git a/pyproject.toml b/pyproject.toml index 32246fc..772ec26 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "jovi_glsl" description = "ComfyUI Nodes that integrate GLSL shader support." -version = "1.1.33" +version = "1.1.34" license = { file = "LICENSE" } readme = "README.md" authors = [{ name = "Alexander G. Morano", email = "amorano@gmail.com" }] diff --git a/web/node_glsl_dynamic.js b/web/node_glsl_dynamic.js index 7b9aec1..f5af0eb 100644 --- a/web/node_glsl_dynamic.js +++ b/web/node_glsl_dynamic.js @@ -1,10 +1,46 @@ /**/ import { app } from "../../scripts/app.js"; -import { widgetFind, widgetHide } from './util_jov.js' const _id = "GLSL DYNAMIC (JOV_GL)"; +const CONVERTED_TYPE = "converted-widget" + +const widgetFind = (widgets, name) => widgets.find(w => w.name == name); + +function widgetHide(node, widget, suffix = '') { + if ((widget?.hidden || false) || widget.type?.startsWith(CONVERTED_TYPE + suffix)) { + return; + } + widget.origType = widget.type; + widget.type = CONVERTED_TYPE + suffix; + widget.hidden = true; + + widget.origComputeSize = widget.computeSize; + widget.computeSize = () => [0, -4]; + + widget.origSerializeValue = widget.serializeValue; + widget.serializeValue = async () => { + // Prevent serializing the widget if we have no input linked + if (!node.inputs) { + return undefined; + } + + let node_input = node.inputs.find((i) => i.widget?.name == widget.name); + if (!node_input || !node_input.link) { + return undefined; + } + return widget.origSerializeValue ? widget.origSerializeValue() : widget.value; + } + + // Hide any linked widgets, e.g. seed+seedControl + if (widget.linkedWidgets) { + for (const w of widget.linkedWidgets) { + widgetHide(node, w, ':' + widget.name); + } + } +} + app.registerExtension({ name: 'jovi_glsl.node.' + _id, async beforeRegisterNodeDef(nodeType, nodeData) { diff --git a/web/util_jov.js b/web/util_jov.js deleted file mode 100644 index 6f14a7b..0000000 --- a/web/util_jov.js +++ /dev/null @@ -1,161 +0,0 @@ -/**/ - -import { app } from "../../scripts/app.js" - -export const CONVERTED_TYPE = "converted-widget" - -export function widget_get_type(config) { - // Special handling for COMBO so we restrict links based on the entries - let type = config?.[0] - let linkType = type - if (type instanceof Array) { - type = 'COMBO' - linkType = linkType.join(',') - } - return { type, linkType } -} - -export const widgetFind = (widgets, name) => widgets.find(w => w.name == name); - -export function widgetHide(node, widget, suffix = '') { - if ((widget?.hidden || false) || widget.type?.startsWith(CONVERTED_TYPE + suffix)) { - return; - } - widget.origType = widget.type; - widget.type = CONVERTED_TYPE + suffix; - widget.hidden = true; - - widget.origComputeSize = widget.computeSize; - widget.computeSize = () => [0, -4]; - - widget.origSerializeValue = widget.serializeValue; - widget.serializeValue = async () => { - // Prevent serializing the widget if we have no input linked - if (!node.inputs) { - return undefined; - } - - let node_input = node.inputs.find((i) => i.widget?.name == widget.name); - if (!node_input || !node_input.link) { - return undefined; - } - return widget.origSerializeValue ? widget.origSerializeValue() : widget.value; - } - - // Hide any linked widgets, e.g. seed+seedControl - if (widget.linkedWidgets) { - for (const w of widget.linkedWidgets) { - widgetHide(node, w, ':' + widget.name); - } - } -} - -export function widgetShow(widget) { - if (widget?.origType) { - widget.type = widget.origType; - delete widget.origType; - } - - widget.computeSize = widget.origComputeSize; - delete widget.origComputeSize; - - if (widget.origSerializeValue) { - widget.serializeValue = widget.origSerializeValue; - delete widget.origSerializeValue; - } - - widget.hidden = false; - if (widget?.linkedWidgets) { - for (const w of widget.linkedWidgets) { - widgetShow(w) - } - } -} - -export function widgetToWidget(node, widget) { - widgetShow(widget); - //const sz = node.size; - node.removeInput(node.inputs.findIndex((i) => i.widget?.name == widget.name)); - - for (const widget of node.widgets) { - widget.last_y -= LiteGraph.NODE_SLOT_HEIGHT; - } - nodeFitHeight(node); -} - -export function widgetToInput(node, widget, config) { - widgetHide(node, widget, "-jov"); - - const { linkType } = widget_get_type(config); - - // Add input and store widget config for creating on primitive node - //const sz = node.size - node.addInput(widget.name, linkType, { - widget: { name: widget.name, config }, - }) - - for (const widget of node.widgets) { - widget.last_y += LiteGraph.NODE_SLOT_HEIGHT; - } - nodeFitHeight(node); - - // Restore original size but grow if needed - //node.setSize([Math.max(sz[0], node.size[0]), Math.max(sz[1], node.size[1])]) -} - -export function nodeFitHeight(node) { - const size_old = node.size; - node.computeSize(); - node.setSize([Math.max(size_old[0], node.size[0]), Math.min(size_old[1], node.size[1])]); - node.setDirtyCanvas(!0, !1); - app.graph.setDirtyCanvas(!0, !1); -} - -function arrayToObject(values, length, parseFn) { - const result = {}; - for (let i = 0; i < length; i++) { - result[i] = parseFn(values[i]); - } - return result; -} - -export function domInnerValueChange(node, pos, widget, value, event=undefined) { - const type = widget.type.includes("INT") ? Number : parseFloat - widget.value = arrayToObject(value, Object.keys(value).length, type); - if ( - widget.options && - widget.options.property && - node.properties[widget.options.property] !== undefined - ) { - node.setProperty(widget.options.property, widget.value) - } - if (widget.callback) { - - widget.callback(widget.value, app.canvas, node, pos, event) - } -} - -export function colorHex2RGB(hex) { - hex = hex.replace(/^#/, ''); - const bigint = parseInt(hex, 16); - const r = (bigint >> 16) & 255; - const g = (bigint >> 8) & 255; - const b = bigint & 255; - return [r, g, b]; -} - -/* -* Parse a string "255,255,255,255" or a List[255,255,255,255] into hex -*/ -export function colorRGB2Hex(input) { - const rgbArray = typeof input == 'string' ? input.match(/\d+/g) : input; - if (rgbArray.length < 3) { - throw new Error('input not 3 or 4 values'); - } - const hexValues = rgbArray.map((value, index) => { - if (index == 3 && !value) return 'ff'; - const hex = parseInt(value).toString(16); - return hex.length == 1 ? '0' + hex : hex; - }); - return '#' + hexValues.slice(0, 3).join('') + (hexValues[3] || ''); -} diff --git a/web/widget_vector.js b/web/widget_vector.js index 5f7b303..b535909 100644 --- a/web/widget_vector.js +++ b/web/widget_vector.js @@ -2,9 +2,31 @@ import { app } from "../../scripts/app.js" import { $el } from "../../scripts/ui.js" -import { domInnerValueChange } from './util_jov.js' /** @import { IWidget, LGraphCanvas } from '../../types/litegraph/litegraph.d.ts' */ +function arrayToObject(values, length, parseFn) { + const result = {}; + for (let i = 0; i < length; i++) { + result[i] = parseFn(values[i]); + } + return result; +} + +function domInnerValueChange(node, pos, widget, value, event=undefined) { + //const numtype = widget.type.includes("INT") ? Number : parseFloat + widget.value = arrayToObject(value, Object.keys(value).length, widget.convert); + if ( + widget.options && + widget.options.property && + node.properties[widget.options.property] !== undefined + ) { + node.setProperty(widget.options.property, widget.value) + } + if (widget.callback) { + widget.callback(widget.value, app.canvas, node, pos, event) + } +} + function colorHex2RGB(hex) { hex = hex.replace(/^#/, ''); const bigint = parseInt(hex, 16); @@ -32,13 +54,24 @@ const VectorWidget = (app, inputName, options, initial) => { /** @type {IWidget} */ const widget = { name: inputName, - type2: options[0], + //type2: options[0], type: options[0], y: 0, value: values, options: options[1] } - //widget.options.type = "STRING"; //`BOOLEAN,INT,FLOAT,${options[0]}`; + + widget.convert = parseFloat; + widget.options.precision = 3; + widget.options.step = 0.01; + widget.options.round = 1 / 10 ** widget.options.step; + + if (widget.options?.rgb || widget.options?.int || false) { + widget.options.step = 1; + widget.options.round = 1; + widget.options.precision = 0; + widget.convert = Number; + } if (widget.options?.rgb || false) { widget.options.maj = 255; @@ -46,31 +79,18 @@ const VectorWidget = (app, inputName, options, initial) => { widget.options.label = ['🟥', '🟩', '🟦', 'ALPHA']; } - widget.options.precision = 3; - widget.options.step = 0.0075; - widget.options.round = 1 / 10 ** widget.options.step; - - if (widget.type2.endsWith('INT')) { - widget.options.step = 1; - widget.options.round = 1; - widget.options.precision = 0; - widget.options.step = 1; - } else { - if (widget.options?.rgb || false) { - widget.options.maj = 1; - } - } - const offset_y = 4; const widget_padding_left = 13; const widget_padding = 30; const label_full = 72; - const label_center = label_full/2; + const label_center = label_full / 2; + /** @type {HTMLInputElement} */ let picker; widget.draw = function(ctx, node, width, Y, height) { - if ((app.canvas.ds.scale < 0.50) || (!this.type2.startsWith("VEC") && this.type2 != "COORD2D")) return; + // if ((app.canvas.ds.scale < 0.50) || (!this.type2.startsWith("VEC") && this.type2 != "COORD2D")) return; + if ((app.canvas.ds.scale < 0.50) || (!this.type.startsWith("VEC"))) return; ctx.save() ctx.beginPath() ctx.lineWidth = 1 @@ -274,15 +294,6 @@ app.registerExtension({ }), VEC4: (node, inputName, inputData, app) => ({ widget: node.addCustomWidget(VectorWidget(app, inputName, inputData, [0, 0, 0, 0])), - }), - VEC2INT: (node, inputName, inputData, app) => ({ - widget: node.addCustomWidget(VectorWidget(app, inputName, inputData, [0, 0])), - }), - VEC3INT: (node, inputName, inputData, app) => ({ - widget: node.addCustomWidget(VectorWidget(app, inputName, inputData, [0, 0, 0])), - }), - VEC4INT: (node, inputName, inputData, app) => ({ - widget: node.addCustomWidget(VectorWidget(app, inputName, inputData, [0, 0, 0, 0])), }) } }