From 4c9a28fede03072ddd0a1e8043854df0c04ef675 Mon Sep 17 00:00:00 2001 From: "Alexander G. Morano" Date: Sat, 7 Sep 2024 23:36:24 -0700 Subject: [PATCH] new edge mode for GLSL shaders (clamp, wrap, mirror) comparison node value conversion fixed cleaned up midi filtering --- __init__.py | 2 + core/calc.py | 25 ++-- core/create_glsl.py | 31 +++-- core/device_midi.py | 187 ++++++++++++-------------- node_list.json | 2 +- res/glsl/modify/modify-repatile.frag | 15 --- res/glsl/modify/modify-transform.frag | 27 ++++ sup/shader.py | 28 ++-- web/widget/widget_vector.js | 24 ++-- 9 files changed, 170 insertions(+), 171 deletions(-) delete mode 100644 res/glsl/modify/modify-repatile.frag create mode 100644 res/glsl/modify/modify-transform.frag diff --git a/__init__.py b/__init__.py index 6506d55..810eec3 100644 --- a/__init__.py +++ b/__init__.py @@ -188,6 +188,8 @@ class Lexicon(metaclass=LexiconMeta): DPI = 'DPI', "Use DPI mode from OS" EASE = 'EASE', "Easing function" EDGE = 'EDGE', "Clip or Wrap the Canvas Edge" + EDGE_X = 'EDGE_X', "Clip or Wrap the Canvas Edge" + EDGE_Y = 'EDGE_Y', "Clip or Wrap the Canvas Edge" END = 'END', "End of the range" FALSE = 'πŸ‡«', "False" FILEN = 'πŸ’Ύ', "File Name" diff --git a/core/calc.py b/core/calc.py index a2b8f49..dc426cf 100644 --- a/core/calc.py +++ b/core/calc.py @@ -447,7 +447,7 @@ def INPUT_TYPES(cls) -> dict: Lexicon.COMP_B: (JOV_TYPE_ANY, {"default": 0}), Lexicon.COMPARE: (EnumComparison._member_names_, {"default": EnumComparison.EQUAL.name}), Lexicon.FLIP: ("BOOLEAN", {"default": False}), - Lexicon.INVERT: ("BOOLEAN", {"default": False}), + Lexicon.INVERT: ("BOOLEAN", {"default": False, "tooltips":"reverse the successful and failure inputs"}), }, "outputs": { 0: (Lexicon.TRIGGER, {"tooltips":f"Outputs the input at {Lexicon.IN_A} or {Lexicon.IN_B} depending on which evaluated `TRUE`"}), @@ -457,8 +457,8 @@ def INPUT_TYPES(cls) -> dict: return Lexicon._parse(d, cls) def run(self, **kw) -> Tuple[Any, Any]: - A = parse_param(kw, Lexicon.IN_A, EnumConvertType.VEC4, 0) - B = parse_param(kw, Lexicon.IN_B, EnumConvertType.VEC4, 0) + A = parse_param(kw, Lexicon.IN_A, EnumConvertType.ANY, 0) + B = parse_param(kw, Lexicon.IN_B, EnumConvertType.ANY, 0) good = parse_param(kw, Lexicon.COMP_A, EnumConvertType.ANY, 0) fail = parse_param(kw, Lexicon.COMP_B, EnumConvertType.ANY, 0) op = parse_param(kw, Lexicon.COMPARE, EnumConvertType.STRING, EnumComparison.EQUAL.name) @@ -469,22 +469,23 @@ def run(self, **kw) -> Tuple[Any, Any]: vals = [] results = [] for idx, (A, B, good, fail, op, flip, invert) in enumerate(params): - if not isinstance(A, (list, set, tuple)): + if not isinstance(A, (list,)): A = [A] - if not isinstance(B, (list, set, tuple)): + if not isinstance(B, (list,)): B = [B] size = min(4, max(len(A), len(B))) - 1 typ = [EnumConvertType.FLOAT, EnumConvertType.VEC2, EnumConvertType.VEC3, EnumConvertType.VEC4][size] - val_a = parse_value(A, typ, [A[-1]] * size) - val_b = parse_value(B, typ, [B[-1]] * size) - if flip: - val_a, val_b = val_b, val_a - if not isinstance(val_a, (list, tuple)): + val_a = parse_value(A, typ, [A[-1]] * size) + if not isinstance(val_a, (list,)): val_a = [val_a] - if not isinstance(val_b, (list, tuple)): + + val_b = parse_value(B, typ, [B[-1]] * size) + if not isinstance(val_b, (list,)): val_b = [val_b] + if flip: + val_a, val_b = val_b, val_a op = EnumComparison[op] match op: case EnumComparison.EQUAL: @@ -539,7 +540,7 @@ def run(self, **kw) -> Tuple[Any, Any]: outs = outs[0].unsqueeze(0) else: outs = list(outs) - return outs, list(vals) + return outs, vals, class DelayNode(JOVBaseNode): NAME = "DELAY (JOV) βœ‹πŸ½" diff --git a/core/create_glsl.py b/core/create_glsl.py index ff3e8f5..af8cb6c 100644 --- a/core/create_glsl.py +++ b/core/create_glsl.py @@ -5,7 +5,6 @@ import os import sys -from enum import Enum from pathlib import Path from typing import Any, Tuple @@ -19,15 +18,17 @@ pass from comfy.utils import ProgressBar -from Jovimetrix import ROOT, JOV_TYPE_ANY, Lexicon, JOVImageNode, comfy_message, \ - deep_merge +from Jovimetrix import ROOT, JOV_TYPE_ANY, Lexicon, JOVImageNode, \ + comfy_message, deep_merge -from Jovimetrix.sup.util import EnumConvertType, load_file, parse_param, parse_value +from Jovimetrix.sup.util import EnumConvertType, load_file, parse_param, \ + parse_value -from Jovimetrix.sup.image import MIN_IMAGE_SIZE, EnumInterpolation, EnumScaleMode, \ - cv2tensor_full, image_convert, image_scalefit, tensor2cv +from Jovimetrix.sup.image import MIN_IMAGE_SIZE, EnumInterpolation, \ + EnumScaleMode, cv2tensor_full, image_convert, image_scalefit, tensor2cv -from Jovimetrix.sup.shader import PTYPE, CompileException, GLSLShader, shader_meta +from Jovimetrix.sup.shader import PTYPE, CompileException, EnumEdgeGLSL, \ + GLSLShader, shader_meta # ============================================================================= @@ -52,12 +53,6 @@ JOV_CATEGORY = "CREATE" -class EnumEdgeGLSL(Enum): - CLIP = 1 - WRAP = 2 - WRAPX = 3 - WRAPY = 4 - # ============================================================================= try: @@ -107,7 +102,8 @@ def INPUT_TYPES(cls) -> dict: Lexicon.WH: ("VEC2INT", {"default": (512, 512), "mij":MIN_IMAGE_SIZE, "label": [Lexicon.W, Lexicon.H]}), Lexicon.SAMPLE: (EnumInterpolation._member_names_, {"default": EnumInterpolation.LANCZOS4.name}), Lexicon.MATTE: ("VEC4INT", {"default": (0, 0, 0, 255), "rgb": True}), - Lexicon.EDGE: (EnumInterpolation._member_names_, {"default": EnumInterpolation.LANCZOS4.name}), + Lexicon.EDGE_X: (EnumEdgeGLSL._member_names_, {"default": EnumEdgeGLSL.CLAMP.name}), + Lexicon.EDGE_Y: (EnumEdgeGLSL._member_names_, {"default": EnumEdgeGLSL.CLAMP.name}), } }) return Lexicon._parse(d, cls) @@ -128,6 +124,9 @@ def run(self, ident, **kw) -> tuple[torch.Tensor]: sample = parse_param(kw, Lexicon.SAMPLE, EnumConvertType.STRING, EnumInterpolation.LANCZOS4.name)[0] sample = EnumInterpolation[sample] matte = parse_param(kw, Lexicon.MATTE, EnumConvertType.VEC4INT, [(0, 0, 0, 255)], 0, 255)[0] + edge_x = parse_param(kw, Lexicon.EDGE_X, EnumConvertType.STRING, EnumEdgeGLSL.CLAMP.name)[0] + edge_y = parse_param(kw, Lexicon.EDGE_Y, EnumConvertType.STRING, EnumEdgeGLSL.CLAMP.name)[0] + edge = (edge_x, edge_y) try: self.__glsl.vertex = kw.pop(Lexicon.PROG_VERT, self.VERTEX) @@ -139,7 +138,7 @@ def run(self, ident, **kw) -> tuple[torch.Tensor]: return variables = kw.copy() - for p in [Lexicon.MODE, Lexicon.WH, Lexicon.SAMPLE, Lexicon.MATTE, Lexicon.BATCH, Lexicon.TIME, Lexicon.FPS]: + for p in [Lexicon.MODE, Lexicon.WH, Lexicon.SAMPLE, Lexicon.MATTE, Lexicon.BATCH, Lexicon.TIME, Lexicon.FPS, Lexicon.EDGE]: variables.pop(p, None) self.__glsl.fps = parse_param(kw, Lexicon.FPS, EnumConvertType.INT, 24, 1, 120)[0] @@ -173,7 +172,7 @@ def run(self, ident, **kw) -> tuple[torch.Tensor]: self.__glsl.size = (w, h) - img = self.__glsl.render(self.__delta, **vars) + img = self.__glsl.render(self.__delta, edge, **vars) if mode != EnumScaleMode.MATTE: img = image_scalefit(img, w, h, mode, sample) img = cv2tensor_full(img, matte) diff --git a/core/device_midi.py b/core/device_midi.py index 1ed4deb..3301fea 100644 --- a/core/device_midi.py +++ b/core/device_midi.py @@ -7,7 +7,7 @@ type 2 (asynchronous): each track is independent of the others """ -from typing import Tuple +from typing import Any, Tuple from math import isclose from queue import Queue @@ -15,10 +15,12 @@ from comfy.utils import ProgressBar -from Jovimetrix import deep_merge, JOVBaseNode, Lexicon -from Jovimetrix.sup.util import parse_param, zip_longest_fill, EnumConvertType -from Jovimetrix.sup.midi import midi_device_names, \ - MIDIMessage, MIDINoteOnFilter, MIDIServerThread +from Jovimetrix import JOV_TYPE_ANY, JOVBaseNode, Lexicon, deep_merge + +from Jovimetrix.sup.util import EnumConvertType, parse_param, zip_longest_fill + +from Jovimetrix.sup.midi import MIDIMessage, MIDINoteOnFilter, MIDIServerThread,\ + midi_device_names # ============================================================================= @@ -122,76 +124,10 @@ def run(self, **kw) -> Tuple[MIDIMessage, bool, int, int, int, int, float]: msg = MIDIMessage(self.__note_on, self.__channel, self.__control, self.__note, self.__value) return msg, self.__note_on, self.__channel, self.__control, self.__note, self.__value, normalize, -class MIDIFilterEZNode(JOVBaseNode): - NAME = "MIDI FILTER EZ (JOV) ❇️" - CATEGORY = f"JOVIMETRIX πŸ”ΊπŸŸ©πŸ”΅/{JOV_CATEGORY}" - RETURN_TYPES = ('JMIDIMSG', 'BOOLEAN',) - RETURN_NAMES = (Lexicon.MIDI, Lexicon.TRIGGER,) - SORT = 25 - DESCRIPTION = """ -Filter MIDI messages based on various criteria, including MIDI mode (such as note on or note off), MIDI channel, control number, note number, value, and normalized value. This node is useful for processing MIDI input and selectively passing through only the desired messages. It helps simplify MIDI data handling by allowing you to focus on specific types of MIDI events. -""" - - @classmethod - def INPUT_TYPES(cls) -> dict: - d = super().INPUT_TYPES() - d = deep_merge(d, { - "optional": { - Lexicon.MIDI: ('JMIDIMSG', {"default": None}), - Lexicon.MODE: (MIDINoteOnFilter._member_names_, {"default": MIDINoteOnFilter.IGNORE.name}), - Lexicon.CHANNEL: ("INT", {"default": -1, "mij": -1, "maj": 127}), - Lexicon.CONTROL: ("INT", {"default": -1, "mij": -1, "maj": 127}), - Lexicon.NOTE: ("INT", {"default": -1, "mij": -1, "maj": 127}), - Lexicon.VALUE: ("INT", {"default": -1, "mij": -1, "maj": 127}), - Lexicon.NORMALIZE: ("FLOAT", {"default": -1, "mij": -1, "maj": 1, "step": 0.01}) - } - }) - return Lexicon._parse(d, cls) - - def run(self, **kw) -> Tuple[MIDIMessage, bool]: - - message: MIDIMessage = parse_param(kw, Lexicon.MIDI, EnumConvertType.ANY, None) - mode = parse_param(kw, Lexicon.MODE, EnumConvertType.STRING, MIDINoteOnFilter.IGNORE.name) - chan = parse_param(kw, Lexicon.CHANNEL, EnumConvertType.INT, -1) - ctrl = parse_param(kw, Lexicon.CONTROL, EnumConvertType.INT, -1) - note = parse_param(kw, Lexicon.NOTE, EnumConvertType.INT, -1) - value = parse_param(kw, Lexicon.VALUE, EnumConvertType.INT, -1) - normal = parse_param(kw, Lexicon.NORMALIZE, EnumConvertType.FLOAT, -1) - params = list(zip_longest_fill(message, mode, chan, ctrl, note, value, normal)) - ret = [] - pbar = ProgressBar(len(params)) - for idx, (message, mode, chan, ctrl, note, value, normal) in enumerate(params): - mode = MIDINoteOnFilter[mode] - if mode != MIDINoteOnFilter.IGNORE: - if mode == MIDINoteOnFilter.NOTE_ON and message.note_on == False: - ret.append((message, False, )) - continue - elif mode == MIDINoteOnFilter.NOTE_OFF and message.note_on == False: - ret.append((message, False, )) - continue - if chan != -1 and chan != message.channel: - ret.append((message, False, )) - continue - if ctrl != -1 and ctrl != message.control: - ret.append((message, False, )) - continue - if note != -1 and note != message.note: - ret.append((message, False, )) - continue - if value != -1 and value != message.value: - ret.append((message, False, )) - continue - if normal > 0 and not isclose(message.normal): - ret.append((message, False, )) - continue - ret.append((message, True, )) - pbar.update_absolute(idx) - return [list(x) for x in (zip(*ret))] - class MIDIFilterNode(JOVBaseNode): NAME = "MIDI FILTER (JOV) ✳️" CATEGORY = f"JOVIMETRIX πŸ”ΊπŸŸ©πŸ”΅/{JOV_CATEGORY}" - RETURN_TYPES = ('JMIDIMSG', 'BOOLEAN', ) + RETURN_TYPES = ("JMIDIMSG", "BOOLEAN", ) RETURN_NAMES = (Lexicon.MIDI, Lexicon.TRIGGER,) SORT = 20 EPSILON = 1e-6 @@ -210,7 +146,7 @@ def INPUT_TYPES(cls) -> dict: Lexicon.CONTROL: ("STRING", {"default": ""}), Lexicon.NOTE: ("STRING", {"default": ""}), Lexicon.VALUE: ("STRING", {"default": ""}), - Lexicon.NORMALIZE: ("STRING", {"default": ""}) + Lexicon.NORMALIZE: ("STRING", {"default": ""}), } }) return Lexicon._parse(d, cls) @@ -246,33 +182,78 @@ def __filter(self, data:int, value:str) -> bool: return False def run(self, **kw) -> Tuple[bool]: - message: MIDIMessage = parse_param(kw, Lexicon.MIDI, EnumConvertType.ANY, None) - note_on = parse_param(kw, Lexicon.ON, EnumConvertType.STRING, MIDINoteOnFilter.IGNORE.name) - chan = parse_param(kw, Lexicon.CHANNEL, EnumConvertType.STRING, "") - ctrl = parse_param(kw, Lexicon.CONTROL, EnumConvertType.STRING, "") - note = parse_param(kw, Lexicon.NOTE, EnumConvertType.STRING, "") - value = parse_param(kw, Lexicon.VALUE, EnumConvertType.STRING, "") - normal = parse_param(kw, Lexicon.NORMALIZE, EnumConvertType.STRING, "") - params = list(zip_longest_fill(message, note_on, chan, ctrl, note, value, normal)) - results = [] - pbar = ProgressBar(len(params)) - for idx, (message, note_on, chan, ctrl, note, value, normal) in enumerate(params): - note_on = MIDINoteOnFilter[note_on] - if note_on != MIDINoteOnFilter.IGNORE: - if note_on == "TRUE" and message.note_on != True: - results.append((message, False, )) - if note_on == "FALSE" and message.note_on != False: - results.append((message, False, )) - elif self.__filter(message.channel, chan) == False: - results.append((message, False, )) - elif self.__filter(message.control, ctrl) == False: - results.append((message, False, )) - elif self.__filter(message.note, note) == False: - results.append((message, False, )) - elif self.__filter(message.value, value) == False: - results.append((message, False, )) - elif self.__filter(message.normal, normal) == False: - results.append((message, False, )) - results.append((message, True, )) - pbar.update_absolute(idx) - return [list(x) for x in (zip(*results))] + message: MIDIMessage = kw.get(Lexicon.MIDI, None) + note_on: str = parse_param(kw, Lexicon.ON, EnumConvertType.STRING, MIDINoteOnFilter.IGNORE.name)[0] + chan: str = parse_param(kw, Lexicon.CHANNEL, EnumConvertType.STRING, "")[0] + ctrl: str = parse_param(kw, Lexicon.CONTROL, EnumConvertType.STRING, "")[0] + note: str = parse_param(kw, Lexicon.NOTE, EnumConvertType.STRING, "")[0] + value: str = parse_param(kw, Lexicon.VALUE, EnumConvertType.STRING, "")[0] + normal: str = parse_param(kw, Lexicon.NORMALIZE, EnumConvertType.STRING, "")[0] + + note_on = MIDINoteOnFilter[note_on] + if note_on != MIDINoteOnFilter.IGNORE: + if note_on == MIDINoteOnFilter.NOTE_ON and message.note_on != True: + return message, False, + if note_on == MIDINoteOnFilter.NOTE_OFF and message.note_on != False: + return message, False, + elif self.__filter(message.channel, chan) == False: + return message, False, + elif self.__filter(message.control, ctrl) == False: + return message, False, + elif self.__filter(message.note, note) == False: + return message, False, + elif self.__filter(message.value, value) == False: + return message, False, + elif self.__filter(message.normal, normal) == False: + return message, False, + return message, True, + +class MIDIFilterEZNode(JOVBaseNode): + NAME = "MIDI FILTER EZ (JOV) ❇️" + CATEGORY = f"JOVIMETRIX πŸ”ΊπŸŸ©πŸ”΅/{JOV_CATEGORY}" + RETURN_TYPES = ("JMIDIMSG", "BOOLEAN", ) + RETURN_NAMES = (Lexicon.MIDI, Lexicon.TRIGGER,) + SORT = 25 + DESCRIPTION = """ +Filter MIDI messages based on various criteria, including MIDI mode (such as note on or note off), MIDI channel, control number, note number, value, and normalized value. This node is useful for processing MIDI input and selectively passing through only the desired messages. It helps simplify MIDI data handling by allowing you to focus on specific types of MIDI events. +""" + + @classmethod + def INPUT_TYPES(cls) -> dict: + d = super().INPUT_TYPES() + d = deep_merge(d, { + "optional": { + Lexicon.MIDI: ('JMIDIMSG', {"default": None}), + Lexicon.MODE: (MIDINoteOnFilter._member_names_, {"default": MIDINoteOnFilter.IGNORE.name}), + Lexicon.CHANNEL: ("INT", {"default": -1, "mij": -1, "maj": 127}), + Lexicon.CONTROL: ("INT", {"default": -1, "mij": -1, "maj": 127}), + Lexicon.NOTE: ("INT", {"default": -1, "mij": -1, "maj": 127}), + Lexicon.VALUE: ("INT", {"default": -1, "mij": -1, "maj": 127}), + } + }) + return Lexicon._parse(d, cls) + + def run(self, **kw) -> Tuple[MIDIMessage, bool]: + + message: MIDIMessage = parse_param(kw, Lexicon.MIDI, EnumConvertType.ANY, None)[0] + note_on = parse_param(kw, Lexicon.MODE, EnumConvertType.STRING, MIDINoteOnFilter.IGNORE.name)[0] + chan = parse_param(kw, Lexicon.CHANNEL, EnumConvertType.INT, -1)[0] + ctrl = parse_param(kw, Lexicon.CONTROL, EnumConvertType.INT, -1)[0] + note = parse_param(kw, Lexicon.NOTE, EnumConvertType.INT, -1)[0] + value = parse_param(kw, Lexicon.VALUE, EnumConvertType.INT, -1)[0] + + note_on = MIDINoteOnFilter[note_on] + if note_on != MIDINoteOnFilter.IGNORE: + if note_on == MIDINoteOnFilter.NOTE_ON and message.note_on != True: + return message, False, + if note_on == MIDINoteOnFilter.NOTE_OFF and message.note_on != False: + return message, False, + elif chan > -1 and message.channel != chan: + return message, False, + elif ctrl > -1 and message.control != ctrl: + return message, False, + elif note > -1 and message.note != note: + return message, False, + elif value > -1 and message.value != value: + return message, False, + return message, True, diff --git a/node_list.json b/node_list.json index a04b98a..edde3a7 100644 --- a/node_list.json +++ b/node_list.json @@ -26,10 +26,10 @@ "GLSL NORMAL (JOV) \ud83e\uddd9\ud83c\udffd": "Convert input into a Normal map", "GLSL NORMAL BLEND (JOV) \ud83e\uddd9\ud83c\udffd": "Blend two Normal maps", "GLSL POSTERIZE (JOV) \ud83e\uddd9\ud83c\udffd": "Reduce the pixel color data range", - "GLSL REPATILE (JOV) \ud83e\uddd9\ud83c\udffd": "REPATILE", "GLSL RGB-2-HSV (JOV) \ud83e\uddd9\ud83c\udffd": "Convert RGB(A) input into HSV color space", "GLSL RGB-2-LAB (JOV) \ud83e\uddd9\ud83c\udffd": "Convert RGB(A) image into LAB color space", "GLSL RGB-2-XYZ (JOV) \ud83e\uddd9\ud83c\udffd": "Convert RGB(A) input into XYZ color space", + "GLSL TRANSFORM (JOV) \ud83e\uddd9\ud83c\udffd": "TRANSFORM", "GLSL XYZ-2-LAB (JOV) \ud83e\uddd9\ud83c\udffd": "Convert XYZ(W) image into LAB color space", "GLSL XYZ-2-RGB (JOV) \ud83e\uddd9\ud83c\udffd": "Convert XYZ(W) image into RGB color space", "GRADIENT MAP (JOV) \ud83c\uddf2\ud83c\uddfa": "Remaps an input image using a gradient lookup table (LUT)", diff --git a/res/glsl/modify/modify-repatile.frag b/res/glsl/modify/modify-repatile.frag deleted file mode 100644 index e43f402..0000000 --- a/res/glsl/modify/modify-repatile.frag +++ /dev/null @@ -1,15 +0,0 @@ -// name: REPATILE -// desc: Generate a square grid -// category: MODIFY - -uniform vec2 grid_xy; // 16,16;0;512;1 | grid squares per width x height - -void mainImage( out vec4 fragColor, in vec2 fragCoord ) -{ - vec2 uv = fragCoord / iResolution.xy; - vec2 grid_pixel_space = iResolution.xy / grid_xy; - vec2 grid_uv = fract(uv / grid_pixel_space) * grid_pixel_space; - vec2 line = step(grid_uv, vec2(1.0)); - float val = max(line.x, line.y); - fragColor = vec4(vec3(val), 1.0); -} \ No newline at end of file diff --git a/res/glsl/modify/modify-transform.frag b/res/glsl/modify/modify-transform.frag new file mode 100644 index 0000000..0b582c1 --- /dev/null +++ b/res/glsl/modify/modify-transform.frag @@ -0,0 +1,27 @@ +// name: TRANSFORM +// desc: Abuse UV space to create repetitions. Maximums are row: width/4; column: height/4 +// category: MODIFY + +uniform sampler2D image; // | RGB(A) input to repeat +uniform vec2 offset; // 0.0,0.0;-0.5;0.5;0.001 | positional offset (-0.5..0.5) +uniform float rotate; // 0;0;1;0.001 | rotation from 0..2pi +uniform vec2 tile; // 1.0,1.0;1;2048;1 | repetitions on X and Y + +#define TAU 6.283185307179586 + +void mainImage( out vec4 fragColor, in vec2 fragCoord ) +{ + // normalize + offset + vec2 uv = (fragCoord - offset * iResolution.xy) / iResolution.xy; + + // rotation matrix + float cosAngle = cos(rotate * TAU); + float sinAngle = sin(rotate * TAU); + mat2 rotationMatrix = mat2(cosAngle, -sinAngle, sinAngle, cosAngle); + + // center rotate, scale + uv = rotationMatrix * (uv - 0.5) + 0.5; + vec2 repeat = vec2(min(iResolution.x / 4., tile.x), min(iResolution.y / 4., tile.y)); + uv *= repeat; + fragColor = texture(image, uv); +} \ No newline at end of file diff --git a/sup/shader.py b/sup/shader.py index 4907824..36cc099 100644 --- a/sup/shader.py +++ b/sup/shader.py @@ -6,6 +6,7 @@ """ import re +from enum import Enum from typing import Any, Dict, Tuple import cv2 @@ -49,6 +50,11 @@ 'sampler2D': EnumConvertType.IMAGE } +class EnumEdgeGLSL(Enum): + CLAMP = 10 + WRAP = 20 + MIRROR = 30 + RE_VARIABLE = re.compile(r"uniform\s+(\w+)\s+(\w+);(?:\s*\/\/\s*([0-9.,\s]*))?\s*(?:;\s*([0-9.-]+))?\s*(?:;\s*([0-9.-]+))?\s*(?:;\s*([0-9.-]+))?\s*(?:\|\s*(.*))?$", re.MULTILINE) RE_SHADER_META = re.compile(r"^\/\/\s?([A-Za-z_]{3,}):\s?([A-Za-z_0-9 \-\(\)\[\]\/\.]+)$", re.MULTILINE) @@ -349,7 +355,10 @@ def bgcolor(self) -> Tuple[int, ...]: def bgcolor(self, color:Tuple[int, ...]) -> None: self.__bgcolor = tuple(float(x) / 255. for x in color) - def render(self, time_delta:float=0., tile_edge:Tuple[bool,...]=(False,False), **kw) -> np.ndarray: + def render(self, time_delta:float=0., + tile_edge:Tuple[EnumEdgeGLSL,...]=(EnumEdgeGLSL.CLAMP, EnumEdgeGLSL.CLAMP), + **kw) -> np.ndarray: + glfw.make_context_current(self.__window) gl.glUseProgram(self.__program) @@ -395,15 +404,14 @@ def render(self, time_delta:float=0., tile_edge:Tuple[bool,...]=(False,False), * gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MIN_FILTER, gl.GL_LINEAR) gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MAG_FILTER, gl.GL_LINEAR) - if tile_edge[0]: - gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_WRAP_S, gl.GL_REPEAT) - else: - gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_WRAP_S, gl.GL_CLAMP_TO_EDGE) - - if tile_edge[1]: - gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_WRAP_T, gl.GL_REPEAT) - else: - gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_WRAP_T, gl.GL_CLAMP_TO_EDGE) + for idx, text_wrap in enumerate([gl.GL_TEXTURE_WRAP_S, gl.GL_TEXTURE_WRAP_T]): + match EnumEdgeGLSL[tile_edge[idx]]: + case EnumEdgeGLSL.WRAP: + gl.glTexParameteri(gl.GL_TEXTURE_2D, text_wrap, gl.GL_REPEAT) + case EnumEdgeGLSL.MIRROR: + gl.glTexParameteri(gl.GL_TEXTURE_2D, text_wrap, gl.GL_MIRRORED_REPEAT) + case _: + gl.glTexParameteri(gl.GL_TEXTURE_2D, text_wrap, gl.GL_CLAMP_TO_EDGE) gl.glUniform1i(p_loc, texture_index) texture_index += 1 diff --git a/web/widget/widget_vector.js b/web/widget/widget_vector.js index 8f0bc4c..90bed5a 100644 --- a/web/widget/widget_vector.js +++ b/web/widget/widget_vector.js @@ -24,8 +24,8 @@ const VectorWidget = (app, inputName, options, initial, desc='') => { widget.options.precision = 0; widget.options.step = 1; if (widget.options?.rgb || false) { - widget.options.max = 255; - widget.options.min = 0; + widget.options.maj = 255; + widget.options.mij = 0; // add the label for being an RGB(A) field? widget.options.label = ['πŸŸ₯', '🟩', '🟦', 'ALPHA']; } @@ -109,22 +109,18 @@ const VectorWidget = (app, inputName, options, initial, desc='') => { ctx.restore() } - function clamp(w, v, idx) { - if (w.options?.max !== undefined) { - v = Math.min(v, w.options.max) - } - if (w.options?.min !== undefined) { - v = Math.max(v, w.options.min) - } - const precision = widget.options?.precision !== undefined ? widget.options.precision : 0; - w.value[idx] = (precision == 0) ? Number(v) : parseFloat(v).toFixed(precision) + function clamp(widget, v, idx) { + v = Math.min(v, widget.options?.maj !== undefined ? widget.options.maj : v); + v = Math.max(v, widget.options?.mij !== undefined ? widget.options.mij : v); + const precision = widget.options?.precision || 0; + widget.value[idx] = (precision == 0) ? Number(v) : parseFloat(v).toFixed(precision); } widget.mouse = function (e, pos, node) { let delta = 0; if (e.type === 'pointerdown' && isDragging === undefined) { const x = pos[0] - label_full; - const size = Object.keys(this.value).length;; + const size = Object.keys(this.value).length; const element_width = (node.size[0] - label_full - widget_padding * 1.25) / size; const index = Math.floor(x / element_width); if (index >= 0 && index < size) { @@ -182,8 +178,8 @@ const VectorWidget = (app, inputName, options, initial, desc='') => { if (this.value[idx] != v) { setTimeout( function () { - clamp(this, v, idx) - domInnerValueChange(node, pos, this, this.value, e) + clamp(this, v, idx); + domInnerValueChange(node, pos, this, this.value, e); }.bind(this), 20) } }.bind(this), e);