From 3c3b5a184e6f579b294f8f02beb9c805a0051c35 Mon Sep 17 00:00:00 2001 From: RyanOnTheInside <7623207+ryanontheinside@users.noreply.github.com> Date: Thu, 19 Dec 2024 09:56:11 -0500 Subject: [PATCH] abstracted state management, added sequence controls, updated examples --- .gitignore | 34 +- README.MD | 27 +- __init__.py | 16 +- base/control_base.py | 63 ++ control_nodes.py | 283 --------- controls/sequence_controls.py | 131 ++++ controls/utility_controls.py | 123 ++++ controls/value_controls.py | 185 ++++++ examples/FPS_Monitor_API.json | 61 ++ examples/FPS_Monitor_WORKFLOW.json | 217 +++++++ examples/pixelate_example_API.json | 75 --- examples/pixelate_example_WORKFLOW.json | 263 -------- examples/prompt_updating_WORKFLOW.json | 658 +++++--------------- examples/shape_resize_example_API.json | 66 +- examples/shape_resize_example_WORKFLOW.json | 302 +++++---- patterns/movement_patterns.py | 119 ++++ requirements.txt | 1 + 17 files changed, 1311 insertions(+), 1313 deletions(-) create mode 100644 base/control_base.py delete mode 100644 control_nodes.py create mode 100644 controls/sequence_controls.py create mode 100644 controls/utility_controls.py create mode 100644 controls/value_controls.py create mode 100644 examples/FPS_Monitor_API.json create mode 100644 examples/FPS_Monitor_WORKFLOW.json delete mode 100644 examples/pixelate_example_API.json delete mode 100644 examples/pixelate_example_WORKFLOW.json create mode 100644 patterns/movement_patterns.py create mode 100644 requirements.txt diff --git a/.gitignore b/.gitignore index 25a43fa..e1251c7 100644 --- a/.gitignore +++ b/.gitignore @@ -2,19 +2,37 @@ __pycache__/ *.py[cod] *$py.class - -# Distribution / packaging -dist/ +*.so +.Python build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ *.egg-info/ +.installed.cfg +*.egg # Virtual environments venv/ -env/ -.env/ +ENV/ + +# IDE files +.idea/ +.vscode/ +*.swp +*.swo + +# Ignore assets folder but keep the folder structure +assets/* +!assets/.gitkeep # IDE specific files -.vscode/ -.idea/ -*.swp .DS_Store \ No newline at end of file diff --git a/README.MD b/README.MD index 4c8dea0..fbe781d 100644 --- a/README.MD +++ b/README.MD @@ -2,6 +2,7 @@ Simple control nodes that generate changing values over separate executions of a workflow. Each time the workflow is executed, the value will be updated. This is useful for animating parameters in systems like ComfyStream that execute the workflow once per frame frame. + ## Nodes ### FloatControl @@ -22,4 +23,28 @@ More soon! ## Demo In this demo we are controlling the width and height of a shape mask with an Int Control node. Imagine controlling the denoise on a KSampler with a Float Control, though! -![Value Control Demo](assets/ValueControl.gif) \ No newline at end of file +![Value Control Demo](assets/ValueControl.gif) + + + +## Installation + +### Option 1: ComfyUI Manager (Recommended) +The easiest way to install is through [ComfyUI Manager](https://github.com/ltdrdata/ComfyUI-Manager): +1. Install ComfyUI Manager if you haven't already +2. Open ComfyUI +3. Navigate to the Manager tab +4. Search for "Control Nodes" +5. Click Install + +### Option 2: Manual Installation +1. Clone this repository into your ComfyUI custom_nodes folder: +```bash +cd ComfyUI/custom_nodes +git clone https://github.com/your-repo/comfyui-control-nodes.git +``` + +2. Install the required dependencies: +```bash +pip install -r requirements.txt +``` \ No newline at end of file diff --git a/__init__.py b/__init__.py index b04565b..056ae55 100644 --- a/__init__.py +++ b/__init__.py @@ -1,10 +1,18 @@ -from .control_nodes import FloatControl, IntControl, StringControl +from .controls.value_controls import FloatControl, IntControl, StringControl +from .controls.sequence_controls import FloatSequence, IntSequence, StringSequence +from .controls.utility_controls import FPSMonitor from .quick_shape_mask import QuickShapeMask NODE_CLASS_MAPPINGS = { "FloatControl": FloatControl, "IntControl": IntControl, "StringControl": StringControl, + "FloatSequence": FloatSequence, + "IntSequence": IntSequence, + "StringSequence": StringSequence, + "FPSMonitor": FPSMonitor, + #"IntervalControl": IntervalControl, + #"DeltaControl": DeltaControl, "QuickShapeMask": QuickShapeMask } @@ -12,6 +20,12 @@ NODE_DISPLAY_NAME_MAPPINGS = { "FloatControl": "Float Control (RyanOnTheInside)", "IntControl": "Int Control (RyanOnTheInside)", "StringControl": "String Control (RyanOnTheInside)", + "FloatSequence": "Float Sequence (RyanOnTheInside)", + "IntSequence": "Int Sequence (RyanOnTheInside)", + "StringSequence": "String Sequence (RyanOnTheInside)", + "FPSMonitor": "FPS Monitor (RyanOnTheInside)", + #"IntervalControl": "Interval Control (RyanOnTheInside)", + #"DeltaControl": "Delta Control (RyanOnTheInside)", "QuickShapeMask": "Quick Shape Mask (RyanOnTheInside)" } diff --git a/base/control_base.py b/base/control_base.py new file mode 100644 index 0000000..f14fada --- /dev/null +++ b/base/control_base.py @@ -0,0 +1,63 @@ +from abc import ABC, abstractmethod +import time + +class Pattern(ABC): + """Base class for all patterns (movement, timing, etc)""" + @abstractmethod + def calculate(self, phase: float, min_val: float, max_val: float) -> float: + """Calculate pattern value at given phase (0-1)""" + pass + + @abstractmethod + def get_name(self) -> str: + """Pattern name for UI""" + pass + +class StateManager: + """Handles persistent state between executions""" + def __init__(self): + self._states = {} + + def get_state(self, node_id: str, default=None): + return self._states.get(node_id, default) + + def set_state(self, node_id: str, state): + self._states[node_id] = state + +class ControlNodeBase(ABC): + """Base class for all control nodes""" + + # Shared state manager + state_manager = StateManager() + + @classmethod + def INPUT_TYPES(cls): + """Base input types common to all control nodes""" + return { + "required": { + "always_execute": ("BOOLEAN", { + "default": True, + "tooltip": "When enabled, the node updates every execution" + }), + }, + } + + @classmethod + def IS_CHANGED(cls, **kwargs): + if kwargs.get('always_execute', True): + return float(time.time()) + return False + + def __init__(self): + self.node_id = str(id(self)) + + def get_state(self, default=None): + return self.state_manager.get_state(self.node_id, default) + + def set_state(self, state): + self.state_manager.set_state(self.node_id, state) + + @abstractmethod + def update(self, *args, **kwargs): + """Main update method to be implemented by control nodes""" + pass \ No newline at end of file diff --git a/control_nodes.py b/control_nodes.py deleted file mode 100644 index f6c1444..0000000 --- a/control_nodes.py +++ /dev/null @@ -1,283 +0,0 @@ -import math -import time - -class ValueControlBase: - """Base class for float and integer control nodes""" - # Class level storage for persistent values between executions - instances = {} - @classmethod - def INPUT_TYPES(cls): - """Base input types that are common to both Float and Int controls""" - return { - "required": { - "steps_per_cycle": ("INT", { - "default": 30, - "min": 1, - "max": 1000, - "step": 1, - "tooltip": "Number of steps to complete one full cycle of the movement pattern" - }), - "movement_type": ([ - "static", - "sine_wave", - "triangle_wave", - "sawtooth", - "square_wave", - "bounce", - "exponential", - "logarithmic", - "pulse", - "random_walk", - "smooth_noise" - ], { - "default": "sine_wave", - "tooltip": "Pattern of value changes over time:\nstatic: No change\nsine_wave: Smooth sinusoidal pattern\ntriangle_wave: Linear up and down\nsawtooth: Linear up, instant reset\nsquare_wave: Instant switch between min/max\nbounce: Bouncing ball effect\nexponential: Accelerating increase\nlogarithmic: Decelerating increase\npulse: Single peak per cycle\nrandom_walk: Brownian motion\nsmooth_noise: Smooth random pattern" - }), - "always_execute": ("BOOLEAN", { - "default": True, - "tooltip": "When enabled, the node updates every execution. When disabled, only updates when inputs change" - }), - }, - } - - @classmethod - def IS_CHANGED(cls, **kwargs): - if kwargs.get('always_execute', True): - return float(time.time()) # Return unique timestamp when always_execute is True - return False # Otherwise return consistent value - - def __init__(self): - self.instance_id = str(id(self)) - if self.instance_id not in self.__class__.instances: - self.__class__.instances[self.instance_id] = { - 'current_value': None, - 'iteration': 0 - } - - def update_value_base(self, maximum_value, minimum_value, starting_value, steps_per_cycle, movement_type, always_execute=True): - instance = self.__class__.instances[self.instance_id] - # Initialize if this is the first run - if instance['current_value'] is None: - instance['current_value'] = starting_value - instance['iteration'] = 0 - return (starting_value,) - - # Increment iteration counter - instance['iteration'] += 1 - current_iteration = instance['iteration'] - - # Calculate phase (0 to 1) - phase = (current_iteration % steps_per_cycle) / steps_per_cycle - - # Update value based on movement type - if movement_type == "static": - new_value = instance['current_value'] - - elif movement_type == "sine_wave": - amplitude = (maximum_value - minimum_value) / 2 - center = minimum_value + amplitude - new_value = center + amplitude * math.sin(2 * math.pi * phase) - - elif movement_type == "triangle_wave": - if phase < 0.5: - new_value = minimum_value + (maximum_value - minimum_value) * (2 * phase) - else: - new_value = maximum_value - (maximum_value - minimum_value) * (2 * (phase - 0.5)) - - elif movement_type == "sawtooth": - new_value = minimum_value + (maximum_value - minimum_value) * phase - - elif movement_type == "square_wave": - new_value = maximum_value if phase < 0.5 else minimum_value - - elif movement_type == "bounce": - bounce = abs(math.sin(math.pi * phase)) - new_value = minimum_value + (maximum_value - minimum_value) * bounce - - elif movement_type == "exponential": - exp_phase = math.exp(4 * phase) - 1 - exp_max = math.exp(4) - 1 - new_value = minimum_value + (maximum_value - minimum_value) * (exp_phase / exp_max) - - elif movement_type == "logarithmic": - log_phase = math.log(1 + 99 * phase) / math.log(100) - new_value = minimum_value + (maximum_value - minimum_value) * log_phase - - elif movement_type == "pulse": - pulse = math.exp(-10 * ((phase - 0.5) ** 2)) - new_value = minimum_value + (maximum_value - minimum_value) * pulse - - elif movement_type == "random_walk": - if not hasattr(instance, 'last_delta'): - instance['last_delta'] = 0 - - momentum = 0.7 - random_component = (2 * (hash(str(current_iteration)) / 2**32) - 1) * 0.1 - instance['last_delta'] = momentum * instance['last_delta'] + random_component - - current_pos = (instance['current_value'] - minimum_value) / (maximum_value - minimum_value) - new_pos = current_pos + instance['last_delta'] - new_pos = max(0, min(1, new_pos)) # Clamp between 0 and 1 - new_value = minimum_value + (maximum_value - minimum_value) * new_pos - - elif movement_type == "smooth_noise": - t = current_iteration * 0.1 - noise = (math.sin(t) + math.sin(2.2*t + 5.52) + math.sin(3.6*t + 4.12)) / 3 - new_value = minimum_value + (maximum_value - minimum_value) * (noise + 1) / 2 - - # Clamp value to min/max range - new_value = max(minimum_value, min(maximum_value, new_value)) - instance['current_value'] = new_value - - return (new_value,) - -class FloatControl(ValueControlBase): - """A control node that outputs a floating point value that can change over time.""" - - DESCRIPTION = "Generates a floating point value that changes over time according to various patterns. Useful for animating parameters or creating dynamic effects." - - @classmethod - def INPUT_TYPES(cls): - input_types = super().INPUT_TYPES() - input_types["required"].update({ - "maximum_value": ("FLOAT", { - "default": 1.0, - "min": -10000.0, - "max": 10000.0, - "step": 0.1, - "tooltip": "Maximum value that can be output" - }), - "minimum_value": ("FLOAT", { - "default": 0.0, - "min": -10000.0, - "max": 10000.0, - "step": 0.1, - "tooltip": "Minimum value that can be output" - }), - "starting_value": ("FLOAT", { - "default": 0.5, - "min": -10000.0, - "max": 10000.0, - "step": 0.1, - "tooltip": "Initial value when the node first executes" - }), - }) - return input_types - - RETURN_TYPES = ("FLOAT",) - FUNCTION = "update_value" - CATEGORY = "control" - - def update_value(self, maximum_value, minimum_value, starting_value, steps_per_cycle, movement_type, always_execute=True): - return self.update_value_base(maximum_value, minimum_value, starting_value, steps_per_cycle, movement_type, always_execute) - -class IntControl(ValueControlBase): - """A control node that outputs an integer value that can change over time.""" - - DESCRIPTION = "Generates an integer value that changes over time according to various patterns. Useful for animating discrete parameters or creating stepped animations." - - @classmethod - def INPUT_TYPES(cls): - input_types = super().INPUT_TYPES() - input_types["required"].update({ - "maximum_value": ("INT", { - "default": 100, - "min": -10000, - "max": 10000, - "step": 1, - "tooltip": "Maximum value that can be output" - }), - "minimum_value": ("INT", { - "default": 0, - "min": -10000, - "max": 10000, - "step": 1, - "tooltip": "Minimum value that can be output" - }), - "starting_value": ("INT", { - "default": 50, - "min": -10000, - "max": 10000, - "step": 1, - "tooltip": "Initial value when the node first executes" - }), - }) - return input_types - - RETURN_TYPES = ("INT",) - FUNCTION = "update_value" - CATEGORY = "control" - - def update_value(self, maximum_value, minimum_value, starting_value, steps_per_cycle, movement_type, always_execute=True): - result = self.update_value_base(maximum_value, minimum_value, starting_value, steps_per_cycle, movement_type, always_execute) - return (int(round(result[0])),) - -class StringControl(ValueControlBase): - """A control node that outputs different strings based on patterns over time.""" - - DESCRIPTION = "Generates string outputs that change over time according to various patterns. Useful for cycling through text options or creating pattern-based text animations." - - @classmethod - def INPUT_TYPES(cls): - input_types = super().INPUT_TYPES() - input_types["required"].update({ - "strings": ("STRING", { - "multiline": True, - "default": "first string\nsecond string\nthird string", - "tooltip": "List of strings to cycle through (one per line)" - }), - }) - return input_types - - RETURN_TYPES = ("STRING",) - FUNCTION = "update_value" - CATEGORY = "control" - - def update_value(self, strings, steps_per_cycle, movement_type, always_execute=True): - # Split the input strings into a list - string_list = [s.strip() for s in strings.split('\n') if s.strip()] - if not string_list: - return ("",) # Return empty string if no valid strings provided - - instance = self.__class__.instances[self.instance_id] - # Initialize if this is the first run - if instance['current_value'] is None: - instance['current_value'] = string_list[0] - instance['iteration'] = 0 - return (string_list[0],) - - # Increment iteration counter - instance['iteration'] += 1 - current_iteration = instance['iteration'] - - # Calculate phase (0 to 1) - phase = (current_iteration % steps_per_cycle) / steps_per_cycle - - # Get the number of strings - num_strings = len(string_list) - - # Update value based on movement type - if movement_type == "static": - index = 0 - - elif movement_type in ["sine_wave", "triangle_wave", "sawtooth"]: - # Map phase to string index - index = int(phase * num_strings) - - elif movement_type == "square_wave": - index = 0 if phase < 0.5 else num_strings // 2 - - elif movement_type in ["bounce", "exponential", "logarithmic", "pulse"]: - index = int(phase * (num_strings - 1)) - - elif movement_type in ["random_walk", "smooth_noise"]: - # Use hash of current iteration for reproducible randomness - index = hash(str(current_iteration)) % num_strings - - # Ensure index is within bounds - index = max(0, min(num_strings - 1, index)) - - # Update and return new value - new_value = string_list[index] - instance['current_value'] = new_value - return (new_value,) \ No newline at end of file diff --git a/controls/sequence_controls.py b/controls/sequence_controls.py new file mode 100644 index 0000000..351c4a5 --- /dev/null +++ b/controls/sequence_controls.py @@ -0,0 +1,131 @@ +from ..base.control_base import ControlNodeBase + +class SequenceControlBase(ControlNodeBase): + """Base class for sequence-based controls""" + + @classmethod + def INPUT_TYPES(cls): + inputs = super().INPUT_TYPES() + inputs["required"].update({ + "steps_per_item": ("INT", { + "default": 30, + "min": 1, + "max": 1000, + "step": 1, + "tooltip": "Number of frames to show each item" + }), + "sequence_mode": (["forward", "reverse", "pingpong", "random"], { + "default": "forward", + "tooltip": "How to move through the sequence" + }) + }) + return inputs + + def update_sequence_base(self, values: list, steps_per_item: int, sequence_mode: str, always_execute: bool = True): + # Initialize or get state + state = self.get_state({ + "current_index": 0, + "step_counter": 0, + "direction": 1 # 1 for forward, -1 for reverse (used in pingpong) + }) + + if not values: + return (values[0] if values else None,) + + # Update step counter + state["step_counter"] += 1 + + # Check if we should move to next item + if state["step_counter"] >= steps_per_item: + state["step_counter"] = 0 + + # Update index based on sequence mode + if sequence_mode == "forward": + state["current_index"] = (state["current_index"] + 1) % len(values) + elif sequence_mode == "reverse": + state["current_index"] = (state["current_index"] - 1) % len(values) + elif sequence_mode == "pingpong": + next_index = state["current_index"] + state["direction"] + if next_index >= len(values) or next_index < 0: + state["direction"] *= -1 # Reverse direction + next_index = state["current_index"] + state["direction"] + state["current_index"] = next_index + elif sequence_mode == "random": + import random + state["current_index"] = random.randint(0, len(values) - 1) + + # Save state + self.set_state(state) + + # Return current value + return (values[state["current_index"]],) + +class FloatSequence(SequenceControlBase): + """Cycles through a sequence of float values""" + + RETURN_TYPES = ("FLOAT",) + FUNCTION = "update" + CATEGORY = "real-time/control/sequence" + + @classmethod + def INPUT_TYPES(cls): + inputs = super().INPUT_TYPES() + inputs["required"].update({ + "values": ("STRING", { + "default": "0.0, 0.5, 1.0", + "tooltip": "List of float values (comma separated)" + }) + }) + return inputs + + def update(self, values: str, steps_per_item: int, sequence_mode: str, always_execute: bool = True): + # Parse comma-separated float values + float_values = [float(v.strip()) for v in values.split(',') if v.strip()] + return self.update_sequence_base(float_values, steps_per_item, sequence_mode, always_execute) + +class IntSequence(SequenceControlBase): + """Cycles through a sequence of integer values""" + + RETURN_TYPES = ("INT",) + FUNCTION = "update" + CATEGORY = "real-time/control/sequence" + + @classmethod + def INPUT_TYPES(cls): + inputs = super().INPUT_TYPES() + inputs["required"].update({ + "values": ("STRING", { + "default": "0, 5, 10", + "tooltip": "List of integer values (comma separated)" + }) + }) + return inputs + + def update(self, values: str, steps_per_item: int, sequence_mode: str, always_execute: bool = True): + # Parse comma-separated integer values + int_values = [int(v.strip()) for v in values.split(',') if v.strip()] + return self.update_sequence_base(int_values, steps_per_item, sequence_mode, always_execute) + +class StringSequence(SequenceControlBase): + """Cycles through a sequence of strings""" + + RETURN_TYPES = ("STRING",) + FUNCTION = "update" + CATEGORY = "real-time/control/sequence" + + @classmethod + def INPUT_TYPES(cls): + inputs = super().INPUT_TYPES() + inputs["required"].update({ + "values": ("STRING", { + "multiline": True, + "default": "first\nsecond\nthird", + "tooltip": "List of strings (one per line)" + }) + }) + return inputs + + def update(self, values: str, steps_per_item: int, sequence_mode: str, always_execute: bool = True): + # Split into string values + string_values = [v.strip() for v in values.split('\n') if v.strip()] + return self.update_sequence_base(string_values, steps_per_item, sequence_mode, always_execute) \ No newline at end of file diff --git a/controls/utility_controls.py b/controls/utility_controls.py new file mode 100644 index 0000000..927d07d --- /dev/null +++ b/controls/utility_controls.py @@ -0,0 +1,123 @@ +from ..base.control_base import ControlNodeBase +import time +import numpy as np +import cv2 +import torch + +class FPSMonitor(ControlNodeBase): + """Generates an FPS overlay as an image and mask""" + + RETURN_TYPES = ("IMAGE", "MASK") + FUNCTION = "update" + CATEGORY = "real-time/control/utility" + + @classmethod + def INPUT_TYPES(cls): + inputs = super().INPUT_TYPES() + inputs["required"].update({ + "width": ("INT", { + "default": 512, + "min": 64, + "max": 4096, + "step": 1 + }), + "height": ("INT", { + "default": 512, + "min": 64, + "max": 4096, + "step": 1 + }), + "text_color": ("INT", { + "default": 255, + "min": 0, + "max": 255, + "step": 1 + }), + "text_size": ("FLOAT", { + "default": 1.0, + "min": 0.1, + "max": 5.0, + "step": 0.1 + }), + "window_size": ("INT", { + "default": 60, + "min": 1, + "max": 300, + "step": 1, + "tooltip": "Number of frames to average over" + }) + }) + return inputs + + def update(self, width: int, height: int, text_color: int, text_size: float, window_size: int, always_execute=True): + current_time = time.time() + state = self.get_state({ + "last_time": current_time, + "frame_times": [], + "last_fps": 0, + "last_avg_fps": 0, + "cached_image": None, + "cached_mask": None, + "last_dims": (0, 0), + "last_color": 0, + "last_size": 0 + }) + + # Calculate FPS + delta_time = current_time - state["last_time"] + current_fps = 1.0 / delta_time if delta_time > 0 else 0 + + # Update rolling window + state["frame_times"].append(delta_time) + if len(state["frame_times"]) > window_size: + state["frame_times"].pop(0) + + # Calculate average FPS + avg_delta = sum(state["frame_times"]) / len(state["frame_times"]) + average_fps = 1.0 / avg_delta if avg_delta > 0 else 0 + + # Check if we need to redraw + needs_redraw = ( + abs(current_fps - state["last_fps"]) > 1.0 or # FPS changed significantly + abs(average_fps - state["last_avg_fps"]) > 0.5 or # Avg FPS changed significantly + state["last_dims"] != (width, height) or # Dimensions changed + state["last_color"] != text_color or # Color changed + state["last_size"] != text_size or # Text size changed + state["cached_image"] is None # No cache exists + ) + + if needs_redraw: + # Create text + fps_text = f"FPS: {current_fps:.1f} (avg: {average_fps:.1f})" + + # Create image and mask using numpy/cv2 + image = np.zeros((height, width, 3), dtype=np.uint8) + mask = np.zeros((height, width), dtype=np.uint8) + + # Position text in top-left with padding + padding = 10 + pos = (padding, padding + 20) # Add vertical offset for cv2's text positioning + + # Draw text - much simpler with cv2! + cv2.putText(image, fps_text, pos, cv2.FONT_HERSHEY_SIMPLEX, text_size, (text_color, text_color, text_color), 1) + cv2.putText(mask, fps_text, pos, cv2.FONT_HERSHEY_SIMPLEX, text_size, 255, 1) + + # Convert to float32 and normalize + image = image.astype(np.float32) / 255.0 + mask = mask.astype(np.float32) / 255.0 + + # Convert to torch tensors with correct shapes + state["cached_image"] = torch.from_numpy(image[None, ...]) # BHWC + state["cached_mask"] = torch.from_numpy(mask[None, ...]) # BHW + + # Update cache parameters + state["last_fps"] = current_fps + state["last_avg_fps"] = average_fps + state["last_dims"] = (width, height) + state["last_color"] = text_color + state["last_size"] = text_size + + state["last_time"] = current_time + self.set_state(state) + + return (state["cached_image"], state["cached_mask"]) diff --git a/controls/value_controls.py b/controls/value_controls.py new file mode 100644 index 0000000..dc7ac43 --- /dev/null +++ b/controls/value_controls.py @@ -0,0 +1,185 @@ +from ..base.control_base import ControlNodeBase +from ..patterns.movement_patterns import MOVEMENT_PATTERNS + +class ValueControlBase(ControlNodeBase): + """Base class for float and integer control nodes""" + + @classmethod + def INPUT_TYPES(cls): + inputs = super().INPUT_TYPES() + inputs["required"].update({ + "steps_per_cycle": ("INT", { + "default": 30, + "min": 1, + "max": 1000, + "step": 1, + "tooltip": "Number of steps to complete one full cycle" + }), + "movement_type": (list(MOVEMENT_PATTERNS.keys()), { + "default": "sine", + "tooltip": "Pattern of value changes over time" + }), + }) + return inputs + + def update(self, *args, **kwargs): + """Implement abstract method from ControlNodeBase""" + return self.update_value_base(*args, **kwargs) + + def update_value_base(self, maximum_value, minimum_value, starting_value, steps_per_cycle, movement_type, always_execute=True): + state = self.get_state({ + "current_value": None, + "phase": 0.0 + }) + + # Initialize if this is the first run + if state["current_value"] is None: + state["current_value"] = starting_value + self.set_state(state) + return (starting_value,) + + # Update phase + state["phase"] = (state["phase"] + 1/steps_per_cycle) % 1.0 + + # Calculate new value using pattern + pattern = MOVEMENT_PATTERNS[movement_type] + new_value = pattern.calculate(state["phase"], minimum_value, maximum_value) + + # Update state + state["current_value"] = new_value + self.set_state(state) + + return (new_value,) + +class FloatControl(ValueControlBase): + + DESCRIPTION = "Generates a floating point value that changes over time according to various patterns." + + @classmethod + def INPUT_TYPES(cls): + inputs = super().INPUT_TYPES() + inputs["required"].update({ + "maximum_value": ("FLOAT", { + "default": 1.0, + "min": -10000.0, + "max": 10000.0, + "step": 0.1, + "tooltip": "Maximum value that can be output" + }), + "minimum_value": ("FLOAT", { + "default": 0.0, + "min": -10000.0, + "max": 10000.0, + "step": 0.1, + "tooltip": "Minimum value that can be output" + }), + "starting_value": ("FLOAT", { + "default": 0.5, + "min": -10000.0, + "max": 10000.0, + "step": 0.1, + "tooltip": "Initial value when the node first executes" + }), + }) + return inputs + + RETURN_TYPES = ("FLOAT",) + FUNCTION = "update_value" + CATEGORY = "real-time/control/value" + + def update_value(self, maximum_value, minimum_value, starting_value, steps_per_cycle, movement_type, always_execute=True): + return self.update_value_base(maximum_value, minimum_value, starting_value, steps_per_cycle, movement_type, always_execute) + +class IntControl(ValueControlBase): + + DESCRIPTION = "Generates an integer value that changes over time according to various patterns." + + @classmethod + def INPUT_TYPES(cls): + inputs = super().INPUT_TYPES() + inputs["required"].update({ + "maximum_value": ("INT", { + "default": 100, + "min": -10000, + "max": 10000, + "step": 1, + "tooltip": "Maximum value that can be output" + }), + "minimum_value": ("INT", { + "default": 0, + "min": -10000, + "max": 10000, + "step": 1, + "tooltip": "Minimum value that can be output" + }), + "starting_value": ("INT", { + "default": 50, + "min": -10000, + "max": 10000, + "step": 1, + "tooltip": "Initial value when the node first executes" + }), + }) + return inputs + + RETURN_TYPES = ("INT",) + FUNCTION = "update_value" + CATEGORY = "real-time/control/value" + + def update_value(self, maximum_value, minimum_value, starting_value, steps_per_cycle, movement_type, always_execute=True): + result = self.update_value_base(maximum_value, minimum_value, starting_value, steps_per_cycle, movement_type, always_execute) + return (int(round(result[0])),) + +class StringControl(ControlNodeBase): + + DESCRIPTION = "Generates string outputs that change over time according to various patterns." + + @classmethod + def INPUT_TYPES(cls): + inputs = super().INPUT_TYPES() + inputs["required"].update({ + "strings": ("STRING", { + "multiline": True, + "default": "first string\nsecond string\nthird string", + "tooltip": "List of strings to cycle through (one per line)" + }), + "steps_per_cycle": ("INT", { + "default": 30, + "min": 1, + "max": 1000, + "step": 1, + "tooltip": "Number of steps to complete one full cycle" + }), + "movement_type": (list(MOVEMENT_PATTERNS.keys()), { + "default": "sine", + "tooltip": "Pattern of value changes over time" + }), + }) + return inputs + + RETURN_TYPES = ("STRING",) + FUNCTION = "update" + CATEGORY = "real-time/control/value" + + def update(self, strings, steps_per_cycle, movement_type, always_execute=True): + # Split the input strings into a list + string_list = [s.strip() for s in strings.split('\n') if s.strip()] + if not string_list: + return ("",) + + state = self.get_state({ + "phase": 0.0 + }) + + # Update phase + state["phase"] = (state["phase"] + 1/steps_per_cycle) % 1.0 + + # Calculate index using pattern + pattern = MOVEMENT_PATTERNS[movement_type] + index_float = pattern.calculate(state["phase"], 0, len(string_list) - 1) + index = int(round(index_float)) % len(string_list) + + # Update state + self.set_state(state) + + return (string_list[index],) \ No newline at end of file diff --git a/examples/FPS_Monitor_API.json b/examples/FPS_Monitor_API.json new file mode 100644 index 0000000..5a3f5c9 --- /dev/null +++ b/examples/FPS_Monitor_API.json @@ -0,0 +1,61 @@ +{ + "257": { + "inputs": { + "images": [ + "260", + 0 + ] + }, + "class_type": "PreviewImage", + "_meta": { + "title": "Preview Image" + } + }, + "258": { + "inputs": { + "always_execute": true, + "width": 512, + "height": 512, + "text_color": 0, + "text_size": 1.3, + "window_size": 60 + }, + "class_type": "FPSMonitor", + "_meta": { + "title": "FPS Monitor (RyanOnTheInside)" + } + }, + "259": { + "inputs": { + "image": "example.png", + "upload": "image" + }, + "class_type": "LoadImage", + "_meta": { + "title": "Load Image" + } + }, + "260": { + "inputs": { + "x": 0, + "y": 0, + "resize_source": false, + "destination": [ + "259", + 0 + ], + "source": [ + "258", + 0 + ], + "mask": [ + "258", + 1 + ] + }, + "class_type": "ImageCompositeMasked", + "_meta": { + "title": "ImageCompositeMasked" + } + } +} \ No newline at end of file diff --git a/examples/FPS_Monitor_WORKFLOW.json b/examples/FPS_Monitor_WORKFLOW.json new file mode 100644 index 0000000..f4b521a --- /dev/null +++ b/examples/FPS_Monitor_WORKFLOW.json @@ -0,0 +1,217 @@ +{ + "last_node_id": 260, + "last_link_id": 79, + "nodes": [ + { + "id": 259, + "type": "LoadImage", + "pos": { + "0": 61, + "1": 383 + }, + "size": [ + 315, + 314 + ], + "flags": {}, + "order": 0, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "IMAGE", + "type": "IMAGE", + "links": [ + 76 + ], + "shape": 3, + "slot_index": 0 + }, + { + "name": "MASK", + "type": "MASK", + "links": null, + "shape": 3 + } + ], + "properties": { + "Node name for S&R": "LoadImage" + }, + "widgets_values": [ + "example.png", + "image" + ] + }, + { + "id": 260, + "type": "ImageCompositeMasked", + "pos": { + "0": 640.8989868164062, + "1": 505.1637268066406 + }, + "size": { + "0": 315, + "1": 146 + }, + "flags": {}, + "order": 2, + "mode": 0, + "inputs": [ + { + "name": "destination", + "type": "IMAGE", + "link": 76 + }, + { + "name": "source", + "type": "IMAGE", + "link": 77 + }, + { + "name": "mask", + "type": "MASK", + "link": 78 + } + ], + "outputs": [ + { + "name": "IMAGE", + "type": "IMAGE", + "links": [ + 79 + ], + "shape": 3, + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "ImageCompositeMasked" + }, + "widgets_values": [ + 0, + 0, + false + ] + }, + { + "id": 257, + "type": "PreviewImage", + "pos": { + "0": 1073, + "1": 496 + }, + "size": { + "0": 471.7497863769531, + "1": 372.4176940917969 + }, + "flags": {}, + "order": 3, + "mode": 0, + "inputs": [ + { + "name": "images", + "type": "IMAGE", + "link": 79 + } + ], + "outputs": [], + "properties": { + "Node name for S&R": "PreviewImage" + }, + "widgets_values": [] + }, + { + "id": 258, + "type": "FPSMonitor", + "pos": { + "0": 51, + "1": 775 + }, + "size": { + "0": 365.4000244140625, + "1": 198 + }, + "flags": {}, + "order": 1, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "IMAGE", + "type": "IMAGE", + "links": [ + 77 + ], + "shape": 3, + "slot_index": 0 + }, + { + "name": "MASK", + "type": "MASK", + "links": [ + 78 + ], + "shape": 3, + "slot_index": 1 + } + ], + "properties": { + "Node name for S&R": "FPSMonitor" + }, + "widgets_values": [ + true, + 512, + 512, + 0, + 1.3, + 60 + ] + } + ], + "links": [ + [ + 76, + 259, + 0, + 260, + 0, + "IMAGE" + ], + [ + 77, + 258, + 0, + 260, + 1, + "IMAGE" + ], + [ + 78, + 258, + 1, + 260, + 2, + "MASK" + ], + [ + 79, + 260, + 0, + 257, + 0, + "IMAGE" + ] + ], + "groups": [], + "config": {}, + "extra": { + "ds": { + "scale": 1.083470594338915, + "offset": [ + 90.70709082773429, + -247.79249615524978 + ] + } + }, + "version": 0.4 +} \ No newline at end of file diff --git a/examples/pixelate_example_API.json b/examples/pixelate_example_API.json deleted file mode 100644 index c8dd169..0000000 --- a/examples/pixelate_example_API.json +++ /dev/null @@ -1,75 +0,0 @@ -{ - "206": { - "inputs": { - "image": "example.png", - "upload": "image" - }, - "class_type": "LoadImage", - "_meta": { - "title": "Load Image" - } - }, - "218": { - "inputs": { - "steps_per_cycle": 30, - "movement_type": "sine_wave", - "always_execute": true, - "maximum_value": 1, - "minimum_value": 1, - "starting_value": 1 - }, - "class_type": "FloatControl", - "_meta": { - "title": "Float Control (RyanOnTheInside)" - } - }, - "223": { - "inputs": { - "strength": [ - "218", - 0 - ], - "feature_threshold": 0, - "feature_param": "pixel_size", - "feature_mode": "relative", - "pixel_size": [ - "224", - 0 - ], - "images": [ - "206", - 0 - ] - }, - "class_type": "FlexImagePixelate", - "_meta": { - "title": "Flex Image Pixelate | RyanOnTheInside" - } - }, - "224": { - "inputs": { - "steps_per_cycle": 30, - "movement_type": "sine_wave", - "always_execute": true, - "maximum_value": 25, - "minimum_value": 1, - "starting_value": 1 - }, - "class_type": "IntControl", - "_meta": { - "title": "Int Control (RyanOnTheInside)" - } - }, - "225": { - "inputs": { - "images": [ - "223", - 0 - ] - }, - "class_type": "PreviewImage", - "_meta": { - "title": "Preview Image" - } - } -} \ No newline at end of file diff --git a/examples/pixelate_example_WORKFLOW.json b/examples/pixelate_example_WORKFLOW.json deleted file mode 100644 index 7fb66a1..0000000 --- a/examples/pixelate_example_WORKFLOW.json +++ /dev/null @@ -1,263 +0,0 @@ -{ - "last_node_id": 225, - "last_link_id": 44, - "nodes": [ - { - "id": 224, - "type": "IntControl", - "pos": { - "0": 573, - "1": 468 - }, - "size": { - "0": 315, - "1": 178 - }, - "flags": {}, - "order": 0, - "mode": 0, - "inputs": [], - "outputs": [ - { - "name": "INT", - "type": "INT", - "links": [ - 43 - ], - "shape": 3, - "slot_index": 0 - } - ], - "properties": { - "Node name for S&R": "IntControl" - }, - "widgets_values": [ - 30, - "sine_wave", - true, - 25, - 1, - 1 - ] - }, - { - "id": 206, - "type": "LoadImage", - "pos": { - "0": 169, - "1": 270 - }, - "size": { - "0": 315, - "1": 314 - }, - "flags": {}, - "order": 1, - "mode": 0, - "inputs": [], - "outputs": [ - { - "name": "IMAGE", - "type": "IMAGE", - "links": [ - 41 - ], - "slot_index": 0, - "shape": 3 - }, - { - "name": "MASK", - "type": "MASK", - "links": null, - "shape": 3 - } - ], - "properties": { - "Node name for S&R": "LoadImage" - }, - "widgets_values": [ - "example.png", - "image" - ] - }, - { - "id": 218, - "type": "FloatControl", - "pos": { - "0": 501, - "1": 705 - }, - "size": { - "0": 390.5999755859375, - "1": 178 - }, - "flags": {}, - "order": 2, - "mode": 0, - "inputs": [], - "outputs": [ - { - "name": "FLOAT", - "type": "FLOAT", - "links": [ - 42 - ], - "shape": 3, - "slot_index": 0 - } - ], - "properties": { - "Node name for S&R": "FloatControl" - }, - "widgets_values": [ - 30, - "sine_wave", - true, - 1, - 1, - 1 - ] - }, - { - "id": 223, - "type": "FlexImagePixelate", - "pos": { - "0": 1046, - "1": 277 - }, - "size": [ - 466.1999816894531, - 194 - ], - "flags": {}, - "order": 3, - "mode": 0, - "inputs": [ - { - "name": "images", - "type": "IMAGE", - "link": 41 - }, - { - "name": "opt_feature", - "type": "FEATURE", - "link": null - }, - { - "name": "opt_feature_pipe", - "type": "FEATURE_PIPE", - "link": null - }, - { - "name": "pixel_size", - "type": "INT", - "link": 43, - "widget": { - "name": "pixel_size" - } - }, - { - "name": "strength", - "type": "FLOAT", - "link": 42, - "widget": { - "name": "strength" - } - } - ], - "outputs": [ - { - "name": "IMAGE", - "type": "IMAGE", - "links": [ - 44 - ], - "shape": 3, - "slot_index": 0 - } - ], - "properties": { - "Node name for S&R": "FlexImagePixelate" - }, - "widgets_values": [ - 1, - 0, - "pixel_size", - "relative", - 10 - ] - }, - { - "id": 225, - "type": "PreviewImage", - "pos": { - "0": 1613, - "1": 279 - }, - "size": { - "0": 210, - "1": 246 - }, - "flags": {}, - "order": 4, - "mode": 0, - "inputs": [ - { - "name": "images", - "type": "IMAGE", - "link": 44 - } - ], - "outputs": [], - "properties": { - "Node name for S&R": "PreviewImage" - } - } - ], - "links": [ - [ - 41, - 206, - 0, - 223, - 0, - "IMAGE" - ], - [ - 42, - 218, - 0, - 223, - 4, - "FLOAT" - ], - [ - 43, - 224, - 0, - 223, - 3, - "INT" - ], - [ - 44, - 223, - 0, - 225, - 0, - "IMAGE" - ] - ], - "groups": [], - "config": {}, - "extra": { - "ds": { - "scale": 0.8954302432552945, - "offset": [ - 55.025652300823474, - -11.200829568935536 - ] - } - }, - "version": 0.4 -} \ No newline at end of file diff --git a/examples/prompt_updating_WORKFLOW.json b/examples/prompt_updating_WORKFLOW.json index dd9047d..c99e788 100644 --- a/examples/prompt_updating_WORKFLOW.json +++ b/examples/prompt_updating_WORKFLOW.json @@ -1,280 +1,20 @@ { - "last_node_id": 246, - "last_link_id": 79, + "last_node_id": 250, + "last_link_id": 84, "nodes": [ - { - "id": 206, - "type": "LoadImage", - "pos": [ - -130, - -30 - ], - "size": [ - 315, - 314 - ], - "flags": {}, - "order": 0, - "mode": 4, - "inputs": [], - "outputs": [ - { - "name": "IMAGE", - "type": "IMAGE", - "links": [ - 47 - ], - "slot_index": 0, - "shape": 3 - }, - { - "name": "MASK", - "type": "MASK", - "links": null, - "shape": 3 - } - ], - "properties": { - "Node name for S&R": "LoadImage" - }, - "widgets_values": [ - "example.png", - "image" - ] - }, - { - "id": 234, - "type": "QuickShapeMask", - "pos": [ - -130, - 340 - ], - "size": [ - 315, - 226 - ], - "flags": {}, - "order": 8, - "mode": 4, - "inputs": [ - { - "name": "width", - "type": "INT", - "link": 57, - "widget": { - "name": "width" - } - }, - { - "name": "height", - "type": "INT", - "link": 58, - "widget": { - "name": "height" - } - } - ], - "outputs": [ - { - "name": "MASK", - "type": "MASK", - "links": [ - 59 - ], - "shape": 3 - } - ], - "properties": { - "Node name for S&R": "QuickShapeMask" - }, - "widgets_values": [ - "circle", - 64, - 64, - 256, - 256, - 512, - 512, - 1 - ] - }, - { - "id": 231, - "type": "IntControl", - "pos": [ - -720, - 420 - ], - "size": [ - 315, - 178 - ], - "flags": {}, - "order": 1, - "mode": 4, - "inputs": [], - "outputs": [ - { - "name": "INT", - "type": "INT", - "links": [ - 58 - ], - "slot_index": 0, - "shape": 3 - } - ], - "properties": { - "Node name for S&R": "IntControl" - }, - "widgets_values": [ - 29, - "sine_wave", - true, - 502, - 1, - 474 - ] - }, - { - "id": 224, - "type": "IntControl", - "pos": [ - -730, - 180 - ], - "size": [ - 315, - 178 - ], - "flags": {}, - "order": 2, - "mode": 4, - "inputs": [], - "outputs": [ - { - "name": "INT", - "type": "INT", - "links": [ - 57 - ], - "slot_index": 0, - "shape": 3 - } - ], - "properties": { - "Node name for S&R": "IntControl" - }, - "widgets_values": [ - 30, - "sine_wave", - true, - 512, - 1, - 1 - ] - }, - { - "id": 227, - "type": "ImageCompositeMasked", - "pos": [ - 180, - 190 - ], - "size": [ - 315, - 146 - ], - "flags": {}, - "order": 12, - "mode": 4, - "inputs": [ - { - "name": "destination", - "type": "IMAGE", - "link": 46 - }, - { - "name": "source", - "type": "IMAGE", - "link": 47 - }, - { - "name": "mask", - "type": "MASK", - "link": 59, - "shape": 7 - } - ], - "outputs": [ - { - "name": "IMAGE", - "type": "IMAGE", - "links": [ - 56 - ], - "slot_index": 0, - "shape": 3 - } - ], - "properties": { - "Node name for S&R": "ImageCompositeMasked" - }, - "widgets_values": [ - 0, - 0, - false - ] - }, - { - "id": 228, - "type": "EmptyImage", - "pos": [ - -130, - -200 - ], - "size": [ - 315, - 130 - ], - "flags": {}, - "order": 3, - "mode": 4, - "inputs": [], - "outputs": [ - { - "name": "IMAGE", - "type": "IMAGE", - "links": [ - 46 - ], - "slot_index": 0, - "shape": 3 - } - ], - "properties": { - "Node name for S&R": "EmptyImage" - }, - "widgets_values": [ - 512, - 512, - 1, - 0 - ] - }, { "id": 242, "type": "VAEDecode", - "pos": [ - 2888.253662109375, - 143.8224334716797 - ], - "size": [ - 210, - 46 - ], + "pos": { + "0": 2888.253662109375, + "1": 143.8224334716797 + }, + "size": { + "0": 210, + "1": 46 + }, "flags": {}, - "order": 15, + "order": 8, "mode": 0, "inputs": [ { @@ -306,16 +46,16 @@ { "id": 243, "type": "PreviewImage", - "pos": [ - 3244.5068359375, - 165.66177368164062 - ], - "size": [ - 210, - 26 - ], + "pos": { + "0": 3244.5068359375, + "1": 165.66177368164062 + }, + "size": { + "0": 210, + "1": 26 + }, "flags": {}, - "order": 16, + "order": 9, "mode": 0, "inputs": [ { @@ -333,16 +73,16 @@ { "id": 244, "type": "KSampler", - "pos": [ - 2463.904296875, - 167.01150512695312 - ], - "size": [ - 315, - 262 - ], + "pos": { + "0": 2463.904296875, + "1": 167.01150512695312 + }, + "size": { + "0": 315, + "1": 262 + }, "flags": {}, - "order": 13, + "order": 7, "mode": 0, "inputs": [ { @@ -368,7 +108,7 @@ { "name": "denoise", "type": "FLOAT", - "link": 79, + "link": 84, "widget": { "name": "denoise" } @@ -396,46 +136,19 @@ 1 ] }, - { - "id": 233, - "type": "SaveImage", - "pos": [ - -282.7172546386719, - 503.5550842285156 - ], - "size": [ - 315, - 270 - ], - "flags": {}, - "order": 14, - "mode": 4, - "inputs": [ - { - "name": "images", - "type": "IMAGE", - "link": 56 - } - ], - "outputs": [], - "properties": {}, - "widgets_values": [ - "ComfyUI" - ] - }, { "id": 246, "type": "VAEEncode", - "pos": [ - 2072.603759765625, - 400.1223449707031 - ], - "size": [ - 210, - 46 - ], + "pos": { + "0": 2072.603759765625, + "1": 400.1223449707031 + }, + "size": { + "0": 210, + "1": 46 + }, "flags": {}, - "order": 10, + "order": 5, "mode": 0, "inputs": [ { @@ -467,16 +180,16 @@ { "id": 245, "type": "LoadImage", - "pos": [ - 1451.361572265625, - 390.3949890136719 - ], - "size": [ - 315, - 314 - ], + "pos": { + "0": 1451.361572265625, + "1": 390.3949890136719 + }, + "size": { + "0": 315, + "1": 314 + }, "flags": {}, - "order": 4, + "order": 0, "mode": 0, "inputs": [], "outputs": [ @@ -504,57 +217,19 @@ "image" ] }, - { - "id": 218, - "type": "FloatControl", - "pos": [ - 1936.280517578125, - 532.5691528320312 - ], - "size": [ - 390.5999755859375, - 178 - ], - "flags": {}, - "order": 5, - "mode": 0, - "inputs": [], - "outputs": [ - { - "name": "FLOAT", - "type": "FLOAT", - "links": [ - 79 - ], - "slot_index": 0, - "shape": 3 - } - ], - "properties": { - "Node name for S&R": "FloatControl" - }, - "widgets_values": [ - 30, - "sine_wave", - true, - 1, - 0.5, - 1 - ] - }, { "id": 240, "type": "CLIPTextEncode", - "pos": [ - 1942.4326171875, - 74.73815155029297 - ], - "size": [ - 400, - 200 - ], + "pos": { + "0": 1942.4326171875, + "1": 74.73815155029297 + }, + "size": { + "0": 400, + "1": 200 + }, "flags": {}, - "order": 9, + "order": 4, "mode": 0, "inputs": [ { @@ -583,16 +258,16 @@ { "id": 237, "type": "CheckpointLoaderSimple", - "pos": [ - 1428.05126953125, - 80.11500549316406 - ], - "size": [ - 315, - 98 - ], + "pos": { + "0": 1428.05126953125, + "1": 80.11500549316406 + }, + "size": { + "0": 315, + "1": 98 + }, "flags": {}, - "order": 6, + "order": 1, "mode": 0, "inputs": [], "outputs": [ @@ -630,54 +305,19 @@ "sd_xl_turbo_1.0_fp16.safetensors" ] }, - { - "id": 235, - "type": "StringControl", - "pos": [ - 1386.592041015625, - -191.31919860839844 - ], - "size": [ - 403.1999816894531, - 200 - ], - "flags": {}, - "order": 7, - "mode": 0, - "inputs": [], - "outputs": [ - { - "name": "STRING", - "type": "STRING", - "links": [ - 67 - ], - "slot_index": 0 - } - ], - "properties": { - "Node name for S&R": "StringControl" - }, - "widgets_values": [ - 30, - "sine_wave", - true, - "a man\na fish\na dog\na boat" - ] - }, { "id": 239, "type": "CLIPTextEncode", - "pos": [ - 1944.341552734375, - -184.06251525878906 - ], - "size": [ - 400, - 200 - ], + "pos": { + "0": 1944.341552734375, + "1": -184.06251525878906 + }, + "size": { + "0": 400, + "1": 200 + }, "flags": {}, - "order": 11, + "order": 6, "mode": 0, "inputs": [ { @@ -688,7 +328,7 @@ { "name": "text", "type": "STRING", - "link": 67, + "link": 83, "widget": { "name": "text" } @@ -710,57 +350,81 @@ "widgets_values": [ "" ] + }, + { + "id": 249, + "type": "StringControl", + "pos": { + "0": 1387, + "1": -191 + }, + "size": { + "0": 403.1999816894531, + "1": 200 + }, + "flags": {}, + "order": 2, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "STRING", + "type": "STRING", + "links": [ + 83 + ], + "shape": 3 + } + ], + "properties": { + "Node name for S&R": "StringControl" + }, + "widgets_values": [ + true, + "first string\nsecond string\nthird string", + 30, + "sine" + ] + }, + { + "id": 250, + "type": "FloatControl", + "pos": { + "0": 1936, + "1": 533 + }, + "size": { + "0": 390.5999755859375, + "1": 178 + }, + "flags": {}, + "order": 3, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "FLOAT", + "type": "FLOAT", + "links": [ + 84 + ], + "shape": 3 + } + ], + "properties": { + "Node name for S&R": "FloatControl" + }, + "widgets_values": [ + true, + 30, + "sine", + 1, + 0, + 0.5 + ] } ], "links": [ - [ - 46, - 228, - 0, - 227, - 0, - "IMAGE" - ], - [ - 47, - 206, - 0, - 227, - 1, - "IMAGE" - ], - [ - 56, - 227, - 0, - 233, - 0, - "IMAGE" - ], - [ - 57, - 224, - 0, - 234, - 0, - "INT" - ], - [ - 58, - 231, - 0, - 234, - 1, - "INT" - ], - [ - 59, - 234, - 0, - 227, - 2, - "MASK" - ], [ 62, 237, @@ -777,14 +441,6 @@ 0, "CLIP" ], - [ - 67, - 235, - 0, - 239, - 1, - "STRING" - ], [ 68, 237, @@ -858,8 +514,16 @@ "LATENT" ], [ - 79, - 218, + 83, + 249, + 0, + 239, + 1, + "STRING" + ], + [ + 84, + 250, 0, 244, 4, @@ -870,10 +534,10 @@ "config": {}, "extra": { "ds": { - "scale": 0.8954302432552947, + "scale": 0.74002499442587, "offset": [ - -1250.6542800886016, - 289.2155026351817 + -1025.94614868967, + 339.03945879560297 ] } }, diff --git a/examples/shape_resize_example_API.json b/examples/shape_resize_example_API.json index 2e03f73..dc942d7 100644 --- a/examples/shape_resize_example_API.json +++ b/examples/shape_resize_example_API.json @@ -9,20 +9,6 @@ "title": "Load Image" } }, - "224": { - "inputs": { - "steps_per_cycle": 30, - "movement_type": "sine_wave", - "always_execute": true, - "maximum_value": 512, - "minimum_value": 1, - "starting_value": 1 - }, - "class_type": "IntControl", - "_meta": { - "title": "Int Control (RyanOnTheInside)" - } - }, "227": { "inputs": { "x": 0, @@ -37,7 +23,7 @@ 0 ], "mask": [ - "234", + "237", 0 ] }, @@ -58,20 +44,6 @@ "title": "EmptyImage" } }, - "231": { - "inputs": { - "steps_per_cycle": 29, - "movement_type": "sine_wave", - "always_execute": true, - "maximum_value": 502, - "minimum_value": 1, - "starting_value": 474 - }, - "class_type": "IntControl", - "_meta": { - "title": "Int Control (RyanOnTheInside)" - } - }, "233": { "inputs": { "filename_prefix": "ComfyUI", @@ -85,15 +57,43 @@ "title": "Save Image" } }, - "234": { + "235": { + "inputs": { + "always_execute": true, + "steps_per_cycle": 30, + "movement_type": "sine", + "maximum_value": 100, + "minimum_value": 0, + "starting_value": 50 + }, + "class_type": "IntControl", + "_meta": { + "title": "Int Control (RyanOnTheInside)" + } + }, + "236": { + "inputs": { + "always_execute": true, + "steps_per_cycle": 30, + "movement_type": "sine", + "maximum_value": 100, + "minimum_value": 0, + "starting_value": 50 + }, + "class_type": "IntControl", + "_meta": { + "title": "Int Control (RyanOnTheInside)" + } + }, + "237": { "inputs": { "shape": "circle", "width": [ - "224", + "235", 0 ], "height": [ - "231", + "236", 0 ], "x": 256, @@ -104,7 +104,7 @@ }, "class_type": "QuickShapeMask", "_meta": { - "title": "QuickShapeMask" + "title": "Quick Shape Mask (RyanOnTheInside)" } } } \ No newline at end of file diff --git a/examples/shape_resize_example_WORKFLOW.json b/examples/shape_resize_example_WORKFLOW.json index bc3c641..83331fa 100644 --- a/examples/shape_resize_example_WORKFLOW.json +++ b/examples/shape_resize_example_WORKFLOW.json @@ -1,83 +1,7 @@ { - "last_node_id": 234, - "last_link_id": 59, + "last_node_id": 237, + "last_link_id": 64, "nodes": [ - { - "id": 224, - "type": "IntControl", - "pos": { - "0": 270, - "1": 350 - }, - "size": { - "0": 315, - "1": 178 - }, - "flags": {}, - "order": 0, - "mode": 0, - "inputs": [], - "outputs": [ - { - "name": "INT", - "type": "INT", - "links": [ - 57 - ], - "slot_index": 0, - "shape": 3 - } - ], - "properties": { - "Node name for S&R": "IntControl" - }, - "widgets_values": [ - 30, - "sine_wave", - true, - 512, - 1, - 1 - ] - }, - { - "id": 231, - "type": "IntControl", - "pos": { - "0": 270, - "1": 590 - }, - "size": { - "0": 315, - "1": 178 - }, - "flags": {}, - "order": 1, - "mode": 0, - "inputs": [], - "outputs": [ - { - "name": "INT", - "type": "INT", - "links": [ - 58 - ], - "slot_index": 0, - "shape": 3 - } - ], - "properties": { - "Node name for S&R": "IntControl" - }, - "widgets_values": [ - 29, - "sine_wave", - true, - 502, - 1, - 474 - ] - }, { "id": 218, "type": "FloatControl", @@ -90,7 +14,7 @@ "1": 178 }, "flags": {}, - "order": 2, + "order": 0, "mode": 4, "inputs": [], "outputs": [ @@ -126,7 +50,7 @@ "1": 314 }, "flags": {}, - "order": 3, + "order": 1, "mode": 0, "inputs": [], "outputs": [ @@ -182,7 +106,7 @@ { "name": "mask", "type": "MASK", - "link": 59 + "link": 64 } ], "outputs": [ @@ -212,10 +136,10 @@ "0": 1684, "1": 317 }, - "size": [ - 315, - 270 - ], + "size": { + "0": 315, + "1": 270 + }, "flags": {}, "order": 7, "mode": 0, @@ -232,6 +156,136 @@ "ComfyUI" ] }, + { + "id": 235, + "type": "IntControl", + "pos": { + "0": 270, + "1": 350 + }, + "size": { + "0": 315, + "1": 178 + }, + "flags": {}, + "order": 2, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "INT", + "type": "INT", + "links": [ + 62 + ], + "shape": 3 + } + ], + "properties": { + "Node name for S&R": "IntControl" + }, + "widgets_values": [ + true, + 30, + "sine", + 100, + 0, + 50 + ] + }, + { + "id": 236, + "type": "IntControl", + "pos": { + "0": 274, + "1": 593 + }, + "size": { + "0": 315, + "1": 178 + }, + "flags": {}, + "order": 3, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "INT", + "type": "INT", + "links": [ + 63 + ], + "shape": 3 + } + ], + "properties": { + "Node name for S&R": "IntControl" + }, + "widgets_values": [ + true, + 30, + "sine", + 100, + 0, + 50 + ] + }, + { + "id": 237, + "type": "QuickShapeMask", + "pos": { + "0": 770, + "1": 642 + }, + "size": { + "0": 315, + "1": 226 + }, + "flags": {}, + "order": 5, + "mode": 0, + "inputs": [ + { + "name": "width", + "type": "INT", + "link": 62, + "widget": { + "name": "width" + } + }, + { + "name": "height", + "type": "INT", + "link": 63, + "widget": { + "name": "height" + } + } + ], + "outputs": [ + { + "name": "MASK", + "type": "MASK", + "links": [ + 64 + ], + "shape": 3 + } + ], + "properties": { + "Node name for S&R": "QuickShapeMask" + }, + "widgets_values": [ + "circle", + 64, + 64, + 256, + 256, + 512, + 512, + 1 + ] + }, { "id": 228, "type": "EmptyImage", @@ -267,62 +321,6 @@ 1, 0 ] - }, - { - "id": 234, - "type": "QuickShapeMask", - "pos": { - "0": 770, - "1": 642 - }, - "size": [ - 315, - 226 - ], - "flags": {}, - "order": 5, - "mode": 0, - "inputs": [ - { - "name": "width", - "type": "INT", - "link": 57, - "widget": { - "name": "width" - } - }, - { - "name": "height", - "type": "INT", - "link": 58, - "widget": { - "name": "height" - } - } - ], - "outputs": [ - { - "name": "MASK", - "type": "MASK", - "links": [ - 59 - ], - "shape": 3 - } - ], - "properties": { - "Node name for S&R": "QuickShapeMask" - }, - "widgets_values": [ - "circle", - 64, - 64, - 256, - 256, - 512, - 512, - 1 - ] } ], "links": [ @@ -351,24 +349,24 @@ "IMAGE" ], [ - 57, - 224, + 62, + 235, 0, - 234, + 237, 0, "INT" ], [ - 58, - 231, + 63, + 236, 0, - 234, + 237, 1, "INT" ], [ - 59, - 234, + 64, + 237, 0, 227, 2, @@ -379,10 +377,10 @@ "config": {}, "extra": { "ds": { - "scale": 1.0834705943389067, + "scale": 0.9849732675808334, "offset": [ - -150.8890623486404, - -198.28032954985966 + -76.72280722620647, + 57.18529404821425 ] } }, diff --git a/patterns/movement_patterns.py b/patterns/movement_patterns.py new file mode 100644 index 0000000..4449467 --- /dev/null +++ b/patterns/movement_patterns.py @@ -0,0 +1,119 @@ +from ..base.control_base import Pattern +import math +import random + +class StaticPattern(Pattern): + def get_name(self) -> str: + return "static" + + def calculate(self, phase, min_val, max_val): + return min_val + +class SinePattern(Pattern): + def get_name(self) -> str: + return "sine" + + def calculate(self, phase, min_val, max_val): + amplitude = (max_val - min_val) / 2 + center = min_val + amplitude + return center + amplitude * math.sin(2 * math.pi * phase) + +class TrianglePattern(Pattern): + def get_name(self) -> str: + return "triangle" + + def calculate(self, phase, min_val, max_val): + if phase < 0.5: + return min_val + (max_val - min_val) * (2 * phase) + return max_val - (max_val - min_val) * (2 * (phase - 0.5)) + +class SawtoothPattern(Pattern): + def get_name(self) -> str: + return "sawtooth" + + def calculate(self, phase, min_val, max_val): + return min_val + (max_val - min_val) * phase + +class SquarePattern(Pattern): + def get_name(self) -> str: + return "square" + + def calculate(self, phase, min_val, max_val): + return max_val if phase < 0.5 else min_val + +class BouncePattern(Pattern): + def get_name(self) -> str: + return "bounce" + + def calculate(self, phase, min_val, max_val): + bounce = abs(math.sin(math.pi * phase)) + return min_val + (max_val - min_val) * bounce + +class ExponentialPattern(Pattern): + def get_name(self) -> str: + return "exponential" + + def calculate(self, phase, min_val, max_val): + exp_phase = math.exp(4 * phase) - 1 + exp_max = math.exp(4) - 1 + return min_val + (max_val - min_val) * (exp_phase / exp_max) + +class LogarithmicPattern(Pattern): + def get_name(self) -> str: + return "logarithmic" + + def calculate(self, phase, min_val, max_val): + log_phase = math.log(1 + 99 * phase) / math.log(100) + return min_val + (max_val - min_val) * log_phase + +class PulsePattern(Pattern): + def get_name(self) -> str: + return "pulse" + + def calculate(self, phase, min_val, max_val): + pulse = math.exp(-10 * ((phase - 0.5) ** 2)) + return min_val + (max_val - min_val) * pulse + +class RandomWalkPattern(Pattern): + def get_name(self) -> str: + return "random_walk" + + def __init__(self): + self.last_delta = 0 + self.current_pos = 0.5 + + def calculate(self, phase, min_val, max_val): + momentum = 0.7 + random_component = (2 * random.random() - 1) * 0.1 + self.last_delta = momentum * self.last_delta + random_component + + self.current_pos += self.last_delta + self.current_pos = max(0, min(1, self.current_pos)) + + return min_val + (max_val - min_val) * self.current_pos + +class SmoothNoisePattern(Pattern): + def get_name(self) -> str: + return "smooth_noise" + + def calculate(self, phase, min_val, max_val): + t = phase * 10 # Scale phase for more interesting variation + noise = (math.sin(t) + math.sin(2.2*t + 5.52) + math.sin(3.6*t + 4.12)) / 3 + return min_val + (max_val - min_val) * (noise + 1) / 2 + +# Pattern registry +MOVEMENT_PATTERNS = { + pattern().get_name(): pattern() for pattern in [ + StaticPattern, + SinePattern, + TrianglePattern, + SawtoothPattern, + SquarePattern, + BouncePattern, + ExponentialPattern, + LogarithmicPattern, + PulsePattern, + RandomWalkPattern, + SmoothNoisePattern + ] +} \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..1db7aea --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +opencv-python \ No newline at end of file