297 lines
12 KiB
Python
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,)
|
|
|
|
''' |