193 lines
6.9 KiB
Python
193 lines
6.9 KiB
Python
import numpy as np
|
|
import re
|
|
|
|
# Easing
|
|
def ease_in(t):
|
|
return np.power(t, 3)
|
|
|
|
def ease_out(t):
|
|
return 1 - np.power(1 - t, 3)
|
|
|
|
def ease_in_out(t):
|
|
return np.where(t < 0.5, 4 * np.power(t, 3), 1 - np.power(-2 * t + 2, 3) / 2)
|
|
|
|
def bounce_out(t):
|
|
n1 = 7.5625
|
|
d1 = 2.75
|
|
conditions = [
|
|
t < 1 / d1,
|
|
(t >= 1 / d1) & (t < 2 / d1),
|
|
(t >= 2 / d1) & (t < 2.5 / d1),
|
|
t >= 2.5 / d1
|
|
]
|
|
functions = [
|
|
lambda t: n1 * t * t,
|
|
lambda t: n1 * (t - 1.5 / d1) ** 2 + 0.75,
|
|
lambda t: n1 * (t - 2.25 / d1) ** 2 + 0.9375,
|
|
lambda t: n1 * (t - 2.625 / d1) ** 2 + 0.984375
|
|
]
|
|
return np.piecewise(t, conditions, functions)
|
|
|
|
def square(t):
|
|
return np.where(t < 0.5, 0, 1)
|
|
|
|
def sawtooth(t, repetitions=4):
|
|
return (t * repetitions) % 1
|
|
|
|
def bump_dip(t):
|
|
return np.where(
|
|
t < 0.3, t**2,
|
|
np.where(
|
|
t < 0.6, np.abs(t - 0.45) * 4,
|
|
1 - ((t - 0.6) / 0.4)**2
|
|
)
|
|
)
|
|
|
|
def exponential_in_out(t):
|
|
return np.where(
|
|
t < 0.5,
|
|
np.power(2, 20 * t - 10) / 2,
|
|
(2 - np.power(2, -20 * t + 10)) / 2
|
|
)
|
|
|
|
# Easing functions dictionary
|
|
easing_functions = {
|
|
'linear': lambda t: t,
|
|
'ease-in': ease_in,
|
|
'ease-out': ease_out,
|
|
'ease-in-out': ease_in_out,
|
|
'bounce-in': lambda t: 1 - bounce_out(1 - t),
|
|
'bounce-out': bounce_out,
|
|
'bounce-in-out': lambda t: np.where(t < 0.5, (1 - bounce_out(1 - 2 * t)) / 2, (1 + bounce_out(2 * t - 1)) / 2),
|
|
'sinusoidal-in': lambda t: 1 - np.cos((t * np.pi) / 2),
|
|
'sinusoidal-out': lambda t: np.sin((t * np.pi) / 2),
|
|
'sinusoidal-in-out': lambda t: -(np.cos(np.pi * t) - 1) / 2,
|
|
'cubic': lambda t: t ** 4,
|
|
'square': square,
|
|
'sawtooth': lambda t: sawtooth(t),
|
|
'triangle': lambda t: 2 * np.abs(t - 0.5),
|
|
'bump-dip': bump_dip,
|
|
'exponential-in': lambda t: np.power(2, 10 * (t - 1)),
|
|
'exponential-out': lambda t: 1 - np.power(2, -10 * t),
|
|
'exponential-in-out': exponential_in_out
|
|
}
|
|
|
|
def apply_easing(schedule, mode='linear'):
|
|
if mode not in easing_functions:
|
|
raise ValueError(f"Easing mode '{mode}' is not supported.")
|
|
|
|
schedule_arr = np.array(schedule, dtype=float)
|
|
|
|
if not np.all((schedule_arr >= -1) & (schedule_arr <= 1)):
|
|
min_val = schedule_arr.min()
|
|
max_val = schedule_arr.max()
|
|
normalized_numbers = (schedule_arr - min_val) / (max_val - min_val)
|
|
schedule_arr = easing_functions[mode](normalized_numbers)
|
|
schedule_arr = schedule_arr * (max_val - min_val) + min_val
|
|
else:
|
|
schedule_arr = easing_functions[mode](schedule_arr)
|
|
|
|
return schedule_arr
|
|
|
|
def safe_eval(expr, t_val=1, end_frame=1, custom_vars={}):
|
|
allowed_funcs = ['where', 'invert', 'put', 'sin', 'cos', 'tan', 'exp', 'log', 'sqrt', 'abs', 'arcsin', 'arccos', 'arctan', 'power', 'pi', 'arctan2']
|
|
allowed_names = {name: getattr(np, name) for name in allowed_funcs}
|
|
allowed_names.update({
|
|
"np": np,
|
|
"t": t_val,
|
|
"z": end_frame,
|
|
"end_frame": end_frame,
|
|
"len": len,
|
|
})
|
|
|
|
if custom_vars and isinstance(custom_vars, dict):
|
|
allowed_names.update(custom_vars)
|
|
|
|
try:
|
|
return eval(expr, {"__builtins__": None}, allowed_names)
|
|
except Exception as e:
|
|
raise ValueError(f"Error evaluating expression '{expr}': {str(e)}")
|
|
|
|
class KeyframeScheduler:
|
|
def __init__(self, end_frame=0, custom_vars={}):
|
|
self.keyframes = []
|
|
self.end_frame = end_frame
|
|
self.custom_vars = custom_vars
|
|
|
|
def parse_keyframes(self, schedule_str):
|
|
self.keyframes = []
|
|
pattern = re.compile(r'\[(.*?)\]')
|
|
|
|
schedule_str = schedule_str.replace('\n', ' ').replace('\r', ' ').strip()
|
|
segments = [segment.strip() for segment in schedule_str.split(",")]
|
|
for segment in segments:
|
|
if segment.strip():
|
|
index_expr, value_expr = [expr.strip() for expr in segment.split(":")]
|
|
|
|
if pattern.match(index_expr):
|
|
expr = pattern.search(index_expr).group(1)
|
|
try:
|
|
index = int(safe_eval(expr, 0, self.end_frame, self.custom_vars))
|
|
except Exception as e:
|
|
raise ValueError(f"Error evaluating index expression '{expr}': {str(e)}")
|
|
elif index_expr == "end_frame" or index_expr == "z":
|
|
if self.end_frame != 0:
|
|
index = self.end_frame - 1
|
|
else:
|
|
raise ValueError("`end_frame` must be specified and greater than 0 to use 'z'.")
|
|
else:
|
|
index = int(index_expr)
|
|
|
|
if value_expr.startswith("(") and value_expr.endswith(")"):
|
|
value_expr = value_expr[1:-1]
|
|
|
|
self.keyframes.append((index, value_expr))
|
|
|
|
def is_numeric(self, val):
|
|
if isinstance(val, (int, float)):
|
|
return True
|
|
try:
|
|
float(val)
|
|
return True
|
|
except (ValueError, TypeError):
|
|
return False
|
|
|
|
def generate_schedule(self, schedule_str, easing_mode='None', ndigits=2):
|
|
self.parse_keyframes(schedule_str)
|
|
if not self.keyframes:
|
|
return []
|
|
|
|
max_index = self.end_frame if self.end_frame != 0 else max(self.keyframes, key=lambda kf: kf[0])[0] + 1
|
|
schedule = np.zeros(max_index)
|
|
|
|
for i in range(len(self.keyframes)):
|
|
start_index, start_expr = self.keyframes[i]
|
|
end_index = self.keyframes[i+1][0] if i+1 < len(self.keyframes) else max_index
|
|
|
|
start_val = safe_eval(start_expr, start_index, self.end_frame, self.custom_vars)
|
|
end_val = safe_eval(self.keyframes[i+1][1], end_index, self.end_frame, self.custom_vars) if i+1 < len(self.keyframes) else start_val
|
|
|
|
start_numeric = self.is_numeric(start_expr)
|
|
end_numeric = self.is_numeric(self.keyframes[i+1][1]) if i+1 < len(self.keyframes) else True
|
|
|
|
if start_index == end_index:
|
|
schedule[start_index] = start_val
|
|
elif start_numeric and end_numeric:
|
|
start_val = float(start_val)
|
|
end_val = float(end_val)
|
|
for j in range(start_index, end_index):
|
|
t = j
|
|
progress = (j - start_index) / (end_index - start_index) if end_index != start_index else 0
|
|
schedule[j] = start_val + (end_val - start_val) * progress
|
|
else:
|
|
for j in range(start_index, end_index):
|
|
t = j
|
|
schedule[j] = safe_eval(start_expr, t, self.end_frame, self.custom_vars)
|
|
|
|
if easing_mode != "None":
|
|
schedule = apply_easing(schedule, easing_mode)
|
|
|
|
schedule = np.round(schedule, ndigits)
|
|
|
|
return schedule.tolist()
|