Files

297 lines
12 KiB
Python

""" Jovimetrix - Animation """
import sys
import numpy as np
from comfy.utils import ProgressBar
from cozy_comfyui import \
InputType, EnumConvertType, \
deep_merge, parse_param, zip_longest_fill
from cozy_comfyui.lexicon import \
Lexicon
from cozy_comfyui.node import \
CozyBaseNode
from cozy_comfyui.maths.ease import \
EnumEase, \
ease_op
from cozy_comfyui.maths.norm import \
EnumNormalize, \
norm_op
from cozy_comfyui.maths.wave import \
EnumWave, \
wave_op
from cozy_comfyui.maths.series import \
seriesLinear
# ==============================================================================
# === GLOBAL ===
# ==============================================================================
JOV_CATEGORY = "ANIMATION"
# ==============================================================================
# === CLASS ===
# ==============================================================================
class ResultObject(object):
def __init__(self, *arg, **kw) -> None:
self.frame = []
self.lin = []
self.fixed = []
self.trigger = []
self.batch = []
class TickNode(CozyBaseNode):
NAME = "TICK (JOV) ⏱"
CATEGORY = JOV_CATEGORY
RETURN_TYPES = ("FLOAT", "FLOAT", "FLOAT", "FLOAT", "FLOAT")
RETURN_NAMES = ("VALUE", "LINEAR", "EASED", "SCALAR_LIN", "SCALAR_EASE")
OUTPUT_IS_LIST = (True, True, True, True, True,)
OUTPUT_TOOLTIPS = (
"List of values",
"Normalized values",
"Eased values",
"Scalar normalized values",
"Scalar eased values",
)
DESCRIPTION = """
Value generator with normalized values based on based on time interval.
"""
@classmethod
def INPUT_TYPES(cls) -> InputType:
d = super().INPUT_TYPES()
d = deep_merge(d, {
"optional": {
# forces a MOD on CYCLE
Lexicon.START: ("FLOAT", {
"default": 0, "min": -sys.maxsize, "max": sys.maxsize
}),
# interval between frames
Lexicon.STEP: ("FLOAT", {
"default": 0, "min": -sys.float_info.max, "max": sys.float_info.max, "precision": 3,
"tooltip": "Amount to add to each frame per tick"
}),
# how many frames to dump....
Lexicon.COUNT: ("INT", {
"default": 1, "min": 1, "max": 1500
}),
Lexicon.LOOP: ("INT", {
"default": 0, "min": 0, "max": sys.maxsize,
"tooltip": "What value before looping starts. 0 means linear playback (no loop point)"
}),
Lexicon.PINGPONG: ("BOOLEAN", {
"default": False
}),
Lexicon.EASE: (EnumEase._member_names_, {
"default": EnumEase.LINEAR.name}),
Lexicon.NORMALIZE: (EnumNormalize._member_names_, {
"default": EnumNormalize.MINMAX2.name}),
Lexicon.SCALAR: ("FLOAT", {
"default": 1, "min": 0, "max": sys.float_info.max
})
}
})
return Lexicon._parse(d)
def run(self, **kw) -> tuple[float, ...]:
"""
Generates a series of numbers with various options including:
- Custom start value (supporting floating point and negative numbers)
- Custom step value (supporting floating point and negative numbers)
- Fixed number of frames
- Custom loop point (series restarts after reaching this many steps)
- Ping-pong option (reverses direction at end points)
- Support for easing functions
- Normalized output 0..1, -1..1, L2 or ZScore
"""
start = parse_param(kw, Lexicon.START, EnumConvertType.FLOAT, 0)[0]
step = parse_param(kw, Lexicon.STEP, EnumConvertType.FLOAT, 0)[0]
count = parse_param(kw, Lexicon.COUNT, EnumConvertType.INT, 1, 1, 1500)[0]
loop = parse_param(kw, Lexicon.LOOP, EnumConvertType.INT, 0, 0)[0]
pingpong = parse_param(kw, Lexicon.PINGPONG, EnumConvertType.BOOLEAN, False)[0]
ease = parse_param(kw, Lexicon.EASE, EnumEase, EnumEase.LINEAR.name)[0]
normalize = parse_param(kw, Lexicon.NORMALIZE, EnumNormalize, EnumNormalize.MINMAX1.name)[0]
scalar = parse_param(kw, Lexicon.SCALAR, EnumConvertType.FLOAT, 1, 0)[0]
if step == 0:
step = 1
cycle = seriesLinear(start, step, count, loop, pingpong)
linear = norm_op(normalize, np.array(cycle))
eased = ease_op(ease, linear, len(linear))
scalar_linear = linear * scalar
scalar_eased = eased * scalar
return (
cycle,
linear.tolist(),
eased.tolist(),
scalar_linear.tolist(),
scalar_eased.tolist(),
)
class WaveGeneratorNode(CozyBaseNode):
NAME = "WAVE GEN (JOV) 🌊"
NAME_PRETTY = "WAVE GEN (JOV) 🌊"
CATEGORY = JOV_CATEGORY
RETURN_TYPES = ("FLOAT", "INT", )
RETURN_NAMES = ("FLOAT", "INT", )
DESCRIPTION = """
Produce waveforms like sine, square, or sawtooth with adjustable frequency, amplitude, phase, and offset. It's handy for creating oscillating patterns or controlling animation dynamics. This node emits both continuous floating-point values and integer representations of the generated waves.
"""
@classmethod
def INPUT_TYPES(cls) -> InputType:
d = super().INPUT_TYPES()
d = deep_merge(d, {
"optional": {
Lexicon.WAVE: (EnumWave._member_names_, {
"default": EnumWave.SIN.name}),
Lexicon.FREQ: ("FLOAT", {
"default": 1, "min": 0, "max": sys.float_info.max, "step": 0.01,}),
Lexicon.AMP: ("FLOAT", {
"default": 1, "min": 0, "max": sys.float_info.max, "step": 0.01,}),
Lexicon.PHASE: ("FLOAT", {
"default": 0, "min": 0, "max": 1, "step": 0.01}),
Lexicon.OFFSET: ("FLOAT", {
"default": 0, "min": 0, "max": 1, "step": 0.001}),
Lexicon.TIME: ("FLOAT", {
"default": 0, "min": 0, "max": sys.float_info.max, "step": 0.0001}),
Lexicon.INVERT: ("BOOLEAN", {
"default": False}),
Lexicon.ABSOLUTE: ("BOOLEAN", {
"default": False,}),
}
})
return Lexicon._parse(d)
def run(self, **kw) -> tuple[float, int]:
op = parse_param(kw, Lexicon.WAVE, EnumWave, EnumWave.SIN.name)
freq = parse_param(kw, Lexicon.FREQ, EnumConvertType.FLOAT, 1, 0)
amp = parse_param(kw, Lexicon.AMP, EnumConvertType.FLOAT, 1, 0)
phase = parse_param(kw, Lexicon.PHASE, EnumConvertType.FLOAT, 0, 0)
shift = parse_param(kw, Lexicon.OFFSET, EnumConvertType.FLOAT, 0, 0)
delta_time = parse_param(kw, Lexicon.TIME, EnumConvertType.FLOAT, 0, 0)
invert = parse_param(kw, Lexicon.INVERT, EnumConvertType.BOOLEAN, False)
absolute = parse_param(kw, Lexicon.ABSOLUTE, EnumConvertType.BOOLEAN, False)
results = []
params = list(zip_longest_fill(op, freq, amp, phase, shift, delta_time, invert, absolute))
pbar = ProgressBar(len(params))
for idx, (op, freq, amp, phase, shift, delta_time, invert, absolute) in enumerate(params):
# freq = 1. / freq
if invert:
amp = 1. / val
val = wave_op(op, phase, freq, amp, shift, delta_time)
if absolute:
val = np.abs(val)
val = max(-sys.float_info.max, min(val, sys.float_info.max))
results.append([val, int(val)])
pbar.update_absolute(idx)
return *list(zip(*results)),
'''
class TickOldNode(CozyBaseNode):
NAME = "TICK OLD (JOV) ⏱"
CATEGORY = JOV_CATEGORY
RETURN_TYPES = ("INT", "FLOAT", "FLOAT", COZY_TYPE_ANY, COZY_TYPE_ANY,)
RETURN_NAMES = ("VAL", "LINEAR", "FPS", "TRIGGER", "BATCH",)
OUTPUT_IS_LIST = (True, False, False, False, False,)
OUTPUT_TOOLTIPS = (
"Current value for the configured tick as ComfyUI List",
"Normalized tick value (0..1) based on BPM and Loop",
"Current 'frame' in the tick based on FPS setting",
"Based on the BPM settings, on beat hit, output the input at '⚡'",
"Current batch of values for the configured tick as standard list which works in other Jovimetrix nodes",
)
DESCRIPTION = """
A timer and frame counter, emitting pulses or signals based on time intervals. It allows precise synchronization and control over animation sequences, with options to adjust FPS, BPM, and loop points. This node is useful for generating time-based events or driving animations with rhythmic precision.
"""
@classmethod
def INPUT_TYPES(cls) -> InputType:
d = super().INPUT_TYPES()
d = deep_merge(d, {
"optional": {
# data to pass on a pulse of the loop
Lexicon.TRIGGER: (COZY_TYPE_ANY, {
"default": None,
"tooltip": "Output to send when beat (BPM setting) is hit"
}),
# forces a MOD on CYCLE
Lexicon.START: ("INT", {
"default": 0, "min": 0, "max": sys.maxsize,
}),
Lexicon.LOOP: ("INT", {
"default": 0, "min": 0, "max": sys.maxsize,
"tooltip": "Number of frames before looping starts. 0 means continuous playback (no loop point)"
}),
Lexicon.FPS: ("INT", {
"default": 24, "min": 1
}),
Lexicon.BPM: ("INT", {
"default": 120, "min": 1, "max": 60000,
"tooltip": "BPM trigger rate to send the input. If input is empty, TRUE is sent on trigger"
}),
Lexicon.NOTE: ("INT", {
"default": 4, "min": 1, "max": 256,
"tooltip": "Number of beats per measure. Quarter note is 4, Eighth is 8, 16 is 16, etc."}),
# how many frames to dump....
Lexicon.BATCH: ("INT", {
"default": 1, "min": 1, "max": 32767,
"tooltip": "Number of frames wanted"
}),
Lexicon.STEP: ("INT", {
"default": 0, "min": 0, "max": sys.maxsize
}),
}
})
return Lexicon._parse(d)
def run(self, ident, **kw) -> tuple[int, float, float, Any]:
passthru = parse_param(kw, Lexicon.TRIGGER, EnumConvertType.ANY, None)[0]
stride = parse_param(kw, Lexicon.STEP, EnumConvertType.INT, 0)[0]
loop = parse_param(kw, Lexicon.LOOP, EnumConvertType.INT, 0)[0]
start = parse_param(kw, Lexicon.START, EnumConvertType.INT, self.__frame)[0]
if loop != 0:
self.__frame %= loop
fps = parse_param(kw, Lexicon.FPS, EnumConvertType.INT, 24, 1)[0]
bpm = parse_param(kw, Lexicon.BPM, EnumConvertType.INT, 120, 1)[0]
divisor = parse_param(kw, Lexicon.NOTE, EnumConvertType.INT, 4, 1)[0]
beat = 60. / max(1., bpm) / divisor
batch = parse_param(kw, Lexicon.BATCH, EnumConvertType.INT, 1, 1)[0]
step_fps = 1. / max(1., float(fps))
trigger = None
results = ResultObject()
pbar = ProgressBar(batch)
step = stride if stride != 0 else max(1, loop / batch)
for idx in range(batch):
trigger = False
lin = start if loop == 0 else start / loop
fixed_step = math.fmod(start * step_fps, fps)
if (math.fmod(fixed_step, beat) == 0):
trigger = [passthru]
if loop != 0:
start %= loop
results.frame.append(start)
results.lin.append(float(lin))
results.fixed.append(float(fixed_step))
results.trigger.append(trigger)
results.batch.append(start)
start += step
pbar.update_absolute(idx)
return (results.frame, results.lin, results.fixed, results.trigger, results.batch,)
'''