Files
ryanontheinside-ComfyUI_Rea…/control_nodes.py
T

283 lines
12 KiB
Python

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,)