Files
siraxe-ComfyUI-WanVideoWrap…/utility/draw_utils.py
T
2025-11-23 05:12:32 +00:00

408 lines
18 KiB
Python

import math
class InterpMath:
@staticmethod
def _linear_interp(p0, p1, t):
result = {
'x': p0['x'] + (p1['x'] - p0['x']) * t,
'y': p0['y'] + (p1['y'] - p0['y']) * t
}
# Interpolate box rotation field if present
if 'boxR' in p0 or 'boxR' in p1:
br0 = p0.get('boxR', 0.0)
br1 = p1.get('boxR', 0.0)
result['boxR'] = br0 + (br1 - br0) * t
# Preserve other fields from p0
for key in p0:
if key not in result:
result[key] = p0[key]
return result
@staticmethod
def _ease_in(t, strength=1.0):
return t ** (2.0 * strength)
@staticmethod
def _ease_out(t, strength=1.0):
return 1 - (1 - t) ** (2.0 * strength)
@staticmethod
def _ease_in_out(t, strength=1.0):
if t < 0.5:
return (2 ** (2.0 * strength - 1)) * (t ** (2.0 * strength))
else:
return 1 - (2 ** (2.0 * strength - 1)) * ((1 - t) ** (2.0 * strength))
@staticmethod
def _ease_out_in(t, strength=1.0):
if t < 0.5:
return 0.5 * (1 - (1 - 2*t) ** (2.0 * strength))
else:
return 0.5 + 0.5 * ((2*t - 1) ** (2.0 * strength))
@staticmethod
def interpolate_or_downsample_path(points, target_frames, method='linear', easing_path='full', bounce_between=0.0, easing_strength=1.0, interpolation='linear'):
n_points = len(points)
if n_points == target_frames:
return points
if n_points < 2:
return [points[0]] * target_frames if n_points == 1 else []
output_points = []
easing_map = {
"linear": lambda t, s=1.0: t,
"in": lambda t, s=easing_strength: InterpMath._ease_in(t, s),
"out": lambda t, s=easing_strength: InterpMath._ease_out(t, s),
"in_out": lambda t, s=easing_strength: InterpMath._ease_in_out(t, s),
"out_in": lambda t, s=easing_strength: InterpMath._ease_out_in(t, s)
}
apply_easing = easing_map.get(method, lambda t, s=1.0: t)
# Create inverted easing functions for "alternate" mode
inverted_easing_map = {
"linear": lambda t, s=1.0: t,
"in": lambda t, s=easing_strength: InterpMath._ease_out(t, s),
"out": lambda t, s=easing_strength: InterpMath._ease_in(t, s),
"in_out": lambda t, s=easing_strength: InterpMath._ease_out_in(t, s),
"out_in": lambda t, s=easing_strength: InterpMath._ease_in_out(t, s)
}
apply_inverted_easing = inverted_easing_map.get(method, lambda t, s=1.0: t)
total_path_length = 0
segment_lengths = []
for i in range(n_points - 1):
dx = points[i+1]['x'] - points[i]['x']
dy = points[i+1]['y'] - points[i]['y']
length = (dx**2 + dy**2)**0.5
segment_lengths.append(length)
for i in range(len(segment_lengths)):
if segment_lengths[i] == 0:
segment_lengths[i] = 1e-9
total_path_length = sum(segment_lengths)
if total_path_length == 0:
return [points[0]] * target_frames
major_segments = []
control_point_indices = []
if easing_path == 'each':
if interpolation == 'basis':
control_point_indices = [i for i, p in enumerate(points) if p.get('highlighted')]
# If no highlighted points for basis+each, we let control_point_indices be empty,
# which causes a fallback to 'full' path easing later.
else:
control_point_indices = [i for i, p in enumerate(points) if p.get('is_control')]
if not control_point_indices:
# Fallback for non-basis interpolations
control_point_indices = list(range(n_points))
elif easing_path == 'full':
if interpolation != 'linear':
control_point_indices = [i for i, p in enumerate(points) if p.get('highlighted')]
elif easing_path == 'alternate':
# For alternate mode, use the same segment boundaries as 'each' mode
if interpolation == 'basis':
control_point_indices = [i for i, p in enumerate(points) if p.get('highlighted')]
# If no highlighted points for basis+each, we let control_point_indices be empty,
# which causes a fallback to 'full' path easing later.
else:
control_point_indices = [i for i, p in enumerate(points) if p.get('is_control')]
if not control_point_indices:
# Fallback for non-basis interpolations
control_point_indices = list(range(n_points))
if len(control_point_indices) > 1:
if control_point_indices[0] != 0:
control_point_indices.insert(0, 0)
if control_point_indices[-1] != n_points - 1:
control_point_indices.append(n_points - 1)
control_point_indices = sorted(list(set(control_point_indices)))
for i in range(len(control_point_indices) - 1):
start_idx = control_point_indices[i]
end_idx = control_point_indices[i+1]
length = sum(segment_lengths[start_idx:end_idx])
major_segments.append({'start_idx': start_idx, 'end_idx': end_idx, 'length': length})
segment_frame_ranges = []
if major_segments:
frames_allocated = 0
segment_lengths_sum = sum(seg['length'] for seg in major_segments)
if segment_lengths_sum > 0:
frames_per_segment_float = [(seg['length'] / segment_lengths_sum) * target_frames for seg in major_segments]
else:
frames_per_segment_float = [target_frames / len(major_segments)] * len(major_segments) if len(major_segments) > 0 else []
frames_per_segment_int = [int(f) for f in frames_per_segment_float]
remainders = [(f - i, idx) for idx, (f, i) in enumerate(zip(frames_per_segment_float, frames_per_segment_int))]
remainders.sort(key=lambda x: x[0], reverse=True)
missing_frames = target_frames - sum(frames_per_segment_int)
if missing_frames > 0:
for i in range(missing_frames):
if not remainders: break
frames_per_segment_int[remainders[i % len(remainders)][1]] += 1
for idx, seg in enumerate(major_segments):
num_frames = frames_per_segment_int[idx]
segment_frame_ranges.append({
'segment': seg,
'start_frame': frames_allocated,
'frame_count': num_frames,
'segment_idx': idx # Track the segment index for alternating easing
})
frames_allocated += num_frames
for i in range(target_frames):
t_linear = i / (target_frames - 1) if target_frames > 1 else 0
eased_target_dist = 0
if segment_frame_ranges:
target_segment_info = None
for seg_info in segment_frame_ranges:
if i >= seg_info['start_frame'] and i < seg_info['start_frame'] + seg_info['frame_count']:
target_segment_info = seg_info
break
if target_segment_info is None:
target_segment_info = segment_frame_ranges[-1]
frame_index_in_segment = i - target_segment_info['start_frame']
if target_segment_info['frame_count'] > 1:
t_segment_local = frame_index_in_segment / (target_segment_info['frame_count'] - 1)
elif target_segment_info['frame_count'] == 1:
t_segment_local = 1.0
else:
t_segment_local = 0.0
# Apply normal easing or inverted easing based on the segment index for 'alternate' mode
if easing_path == 'alternate':
# Alternate between normal and inverted easing based on segment index
if target_segment_info['segment_idx'] % 2 == 0: # Even index (0, 2, 4, ...)
t_segment_eased = apply_easing(t_segment_local, easing_strength)
else: # Odd index (1, 3, 5, ...)
t_segment_eased = apply_inverted_easing(t_segment_local, easing_strength)
else:
# For 'each' and 'full' modes, use the normal easing
t_segment_eased = apply_easing(t_segment_local, easing_strength)
dist_before_segment = 0
for seg in major_segments:
if seg == target_segment_info['segment']:
break
dist_before_segment += seg['length']
eased_target_dist = dist_before_segment + t_segment_eased * target_segment_info['segment']['length']
else:
t_eased = apply_easing(t_linear, easing_strength)
eased_target_dist = t_eased * total_path_length
current_dist = 0.0
segment_index = 0
while segment_index < len(segment_lengths) and current_dist + segment_lengths[segment_index] < eased_target_dist:
current_dist += segment_lengths[segment_index]
segment_index += 1
segment_index = min(segment_index, n_points - 2)
dist_into_segment = eased_target_dist - current_dist
micro_segment_len = segment_lengths[segment_index]
t_micro_segment = dist_into_segment / micro_segment_len if micro_segment_len > 0 else 0
p0 = points[segment_index]
p1 = points[segment_index + 1]
interpolated_point = InterpMath._linear_interp(p0, p1, t_micro_segment)
output_points.append(interpolated_point)
if target_frames > 0 and output_points:
output_points[0] = points[0].copy()
if target_frames > 1:
output_points[-1] = points[-1].copy()
return output_points
@staticmethod
def apply_acceleration_remapping(points, acceleration):
"""
Apply acceleration-based timing remapping to a sequence of points.
For positive acceleration (> 0.00):
- Movement is faster at the end by squishing end coordinates and stretching starting ones
For negative acceleration (< 0.00):
- Movement is slower at the end by stretching end coordinates (and compressing starting ones)
For zero acceleration (0.00):
- Skip the function (return original points)
Args:
points: List of points with x, y coordinates
acceleration: Acceleration value from -1.0 to 1.0
Returns:
List of remapped points
"""
if abs(acceleration) < 0.001: # Close to zero, skip remapping
return points
if len(points) <= 2:
return points
n_points = len(points)
output_points = []
for i in range(n_points):
# Calculate normalized position along the path (0 to 1)
t = i / (n_points - 1) if n_points > 1 else 0
# Apply acceleration remapping with clamped values to prevent math errors
# Convert acceleration to proper easing behavior:
# Positive acceleration should create ease-out (start fast, end slow)
# Negative acceleration should create ease-in (start slow, end fast)
clamped_acceleration = max(-0.99, min(0.99, acceleration))
if clamped_acceleration > 0:
# For positive acceleration, create ease-out behavior (start fast, end slow)
# Use exponent < 1 to make time progress faster at the start and slower at the end
exponent = max(0.01, 1.0 - clamped_acceleration)
else:
# For negative acceleration, create ease-in behavior (start slow, end fast)
# Use exponent > 1 to make time progress slower at the start and faster at the end
# abs(negative_acceleration) is positive, so 1.0 + positive_value > 1
exponent = 1.0 + abs(clamped_acceleration)
# Handle edge cases to avoid math errors
if t == 0.0:
remap_t = 0.0
elif t == 1.0:
remap_t = 1.0
else:
remap_t = pow(t, exponent)
# Ensure remap_t stays within bounds
remap_t = max(0.0, min(1.0, remap_t))
# Map remap_t back to original point index
original_index = remap_t * (n_points - 1)
index_low = int(original_index)
index_high = min(index_low + 1, n_points - 1)
t_local = original_index - index_low
# Linear interpolation between the two nearest points
if index_low == index_high:
output_points.append(points[index_low].copy())
else:
p1 = points[index_low]
p2 = points[index_high]
x = p1['x'] * (1.0 - t_local) + p2['x'] * t_local
y = p1['y'] * (1.0 - t_local) + p2['y'] * t_local
new_point = {'x': x, 'y': y}
# Preserve any additional properties from the original points
for key in p1:
if key not in ['x', 'y']:
new_point[key] = p1[key]
output_points.append(new_point)
# Ensure the first and last points match the original to maintain path integrity
if output_points and len(output_points) > 1:
output_points[0] = points[0].copy()
output_points[-1] = points[-1].copy()
return output_points
def interpolate_points(points, interpolation, easing_path='full', steps_per_segment=3):
if not points or len(points) < 2:
return points
if interpolation in ['cardinal', 'basis']:
highlighted_indices = [i for i, p in enumerate(points) if p.get('highlighted')]
if highlighted_indices:
boundaries = sorted(list(set([0] + highlighted_indices + [len(points) - 1])))
if len(boundaries) > 2:
final_path = []
for i in range(len(boundaries) - 1):
start_idx = boundaries[i]
end_idx = boundaries[i+1]
segment = points[start_idx : end_idx + 1]
if len(segment) > 1:
interpolated_segment = interpolate_points(segment, interpolation, easing_path, steps_per_segment)
if i > 0:
final_path.extend(interpolated_segment[1:])
else:
final_path.extend(interpolated_segment)
return final_path
interpolated_points = []
if interpolation == 'linear':
interpolated_points = [p.copy() for p in points]
elif interpolation == 'cardinal':
num_points = len(points)
padded_points = [points[0]] + points + [points[-1]]
interpolated_points.append(points[0].copy())
for i in range(num_points - 1):
p0, p1, p2, p3 = padded_points[i:i+4]
for t_step in range(1, steps_per_segment + 1):
t = t_step / float(steps_per_segment)
t2, t3 = t * t, t * t * t
c0 = -0.5 * t3 + 1.0 * t2 - 0.5 * t
c1 = 1.5 * t3 - 2.5 * t2 + 1.0
c2 = -1.5 * t3 + 2.0 * t2 + 0.5 * t
c3 = 0.5 * t3 - 0.5 * t2
x = p0['x'] * c0 + p1['x'] * c1 + p2['x'] * c2 + p3['x'] * c3
y = p0['y'] * c0 + p1['y'] * c1 + p2['y'] * c2 + p3['y'] * c3
interpolated_points.append({'x': x, 'y': y})
elif interpolation == 'basis':
num_points = len(points)
padded_points = [points[0]] * 2 + points + [points[-1]] * 2
for i in range(num_points + 1):
p0, p1, p2, p3 = padded_points[i:i+4]
for t_step in range(steps_per_segment + 1):
if i > 0 and t_step == 0:
continue
t = t_step / float(steps_per_segment)
t2, t3 = t * t, t * t * t
b0 = (1 - t)**3 / 6
b1 = (3 * t3 - 6 * t2 + 4) / 6
b2 = (-3 * t3 + 3 * t2 + 3 * t + 1) / 6
b3 = t3 / 6
x = p0['x'] * b0 + p1['x'] * b1 + p2['x'] * b2 + p3['x'] * b3
y = p0['y'] * b0 + p1['y'] * b1 + p2['y'] * b2 + p3['y'] * b3
interpolated_points.append({'x': x, 'y': y})
# The basis spline doesn't naturally pass through the last control point, so we add it explicitly.
if points:
interpolated_points.append(points[-1].copy())
if not interpolated_points:
return points
control_points_to_process = [p.copy() for p in points]
for cp in control_points_to_process:
min_dist_sq = float('inf')
closest_point_in_path = None
for ip in interpolated_points:
dist_sq = (ip['x'] - cp['x'])**2 + (ip['y'] - cp['y'])**2
if dist_sq < min_dist_sq:
min_dist_sq = dist_sq
closest_point_in_path = ip
if closest_point_in_path is not None:
for key, value in cp.items():
if key not in ['x', 'y']:
closest_point_in_path[key] = value
return interpolated_points