Files
2026-01-28 19:33:35 +13:00

224 lines
8.1 KiB
Python

"""
Frame evaluation and interpolation logic
"""
import copy
from typing import Dict, List
def get_interpolated_point_types(curve_a: Dict, curve_b: Dict, total_points: int) -> List[str]:
"""
Get interpolated pointTypes array from two curves.
Since pointTypes should be consistent across all keyframes, prefer curve_a.
Args:
curve_a: First curve (earlier keyframe)
curve_b: Second curve (later keyframe)
total_points: Total point count in the interpolated result
Returns:
List of point types ("smooth" or "hard")
"""
types_a = curve_a.get("pointTypes", []) if isinstance(curve_a.get("pointTypes"), list) else []
types_b = curve_b.get("pointTypes", []) if isinstance(curve_b.get("pointTypes"), list) else []
result = []
for i in range(total_points):
if i < len(types_a) and types_a[i] in ("smooth", "hard"):
result.append(types_a[i])
elif i < len(types_b) and types_b[i] in ("smooth", "hard"):
result.append(types_b[i])
else:
result.append("hard") # Default for any missing entries
return result
def evaluate_frame(shape: Dict, frame_index: int) -> List[Dict]:
"""
Evaluate curves for a given frame.
Each curve is interpolated independently based on its own keyframes.
Args:
shape: Shape dictionary
frame_index: Frame to evaluate
Returns:
List of curve dictionaries for this frame
"""
frames = shape.get("frames", {})
frame_key = str(frame_index)
# Collect all unique curve IDs and their keyframes
all_curve_ids = set()
curve_keyframes: Dict[str, List[int]] = {} # curveId -> sorted list of frame numbers
for frame_str, curves in frames.items():
try:
frame_num = int(frame_str)
except ValueError:
continue
for curve in (curves or []):
if curve and "id" in curve:
curve_id = curve["id"]
all_curve_ids.add(curve_id)
if curve_id not in curve_keyframes:
curve_keyframes[curve_id] = []
curve_keyframes[curve_id].append(frame_num)
# Sort keyframes for each curve
for curve_id in curve_keyframes:
curve_keyframes[curve_id].sort()
if not all_curve_ids:
return []
result = []
# Evaluate each curve independently based on its own keyframes
for curve_id in all_curve_ids:
keyframes = curve_keyframes.get(curve_id, [])
if not keyframes:
continue
# Check for exact keyframe for this curve
if frame_index in keyframes:
frame_curves = frames.get(frame_key, [])
curve = next((c for c in frame_curves if c["id"] == curve_id), None)
if curve:
result.append(copy.deepcopy(curve))
continue
# Find prev and next keyframes FOR THIS CURVE
prev_frame = None
next_frame = None
for fn in keyframes:
if fn < frame_index:
prev_frame = fn
elif fn > frame_index:
next_frame = fn
break
# Interpolate or hold based on this curve's keyframes
if prev_frame is not None and next_frame is not None:
# Interpolate between this curve's keyframes
curve_a = next((c for c in frames.get(str(prev_frame), []) if c["id"] == curve_id), None)
curve_b = next((c for c in frames.get(str(next_frame), []) if c["id"] == curve_id), None)
if curve_a and curve_b:
t = (frame_index - prev_frame) / (next_frame - prev_frame)
interpolated_points = []
# Handle different point counts - use the smaller count
min_points = min(len(curve_a["points"]), len(curve_b["points"]))
for i in range(min_points):
pa = curve_a["points"][i]
pb = curve_b["points"][i]
x = pa[0] + (pb[0] - pa[0]) * t
y = pa[1] + (pb[1] - pa[1]) * t
interpolated_points.append([x, y])
# If one curve has more points, add them at the end (hold from that curve)
if len(curve_a["points"]) > min_points:
for i in range(min_points, len(curve_a["points"])):
interpolated_points.append([curve_a["points"][i][0], curve_a["points"][i][1]])
elif len(curve_b["points"]) > min_points:
for i in range(min_points, len(curve_b["points"])):
interpolated_points.append([curve_b["points"][i][0], curve_b["points"][i][1]])
result.append({
"id": curve_a["id"],
"closed": curve_a["closed"],
"points": interpolated_points,
"pointTypes": get_interpolated_point_types(curve_a, curve_b, len(interpolated_points))
})
else:
# Hold from nearest keyframe for this curve
hold_frame = prev_frame if prev_frame is not None else next_frame
if hold_frame is not None:
curve = next((c for c in frames.get(str(hold_frame), []) if c["id"] == curve_id), None)
if curve:
result.append(copy.deepcopy(curve))
return result
def interpolate_curves(
curves_a: List[Dict],
curves_b: List[Dict],
frame_a: int,
frame_b: int,
target_frame: int
) -> List[Dict]:
"""
Linearly interpolate curves between two keyframes.
Curves that only exist in one keyframe are held (included as-is).
Args:
curves_a: Curves at earlier keyframe
curves_b: Curves at later keyframe
frame_a: Earlier frame number
frame_b: Later frame number
target_frame: Frame to interpolate to
Returns:
List of interpolated curves
"""
# Calculate interpolation factor
t = (target_frame - frame_a) / (frame_b - frame_a)
# Track which curve IDs we've processed
processed_ids = set()
result = []
# Process curves from A - interpolate if in both, hold if only in A
for curve_a in curves_a:
processed_ids.add(curve_a["id"])
# Find matching curve in curves_b
curve_b = None
for cb in curves_b:
if cb["id"] == curve_a["id"]:
curve_b = cb
break
if curve_b is not None:
# Curve exists in both keyframes - interpolate
points_a = curve_a["points"]
points_b = curve_b["points"]
# Handle different point counts - use the smaller count
min_points = min(len(points_a), len(points_b))
interpolated_points = []
for i in range(min_points):
pa = points_a[i]
pb = points_b[i]
x = pa[0] + (pb[0] - pa[0]) * t
y = pa[1] + (pb[1] - pa[1]) * t
interpolated_points.append([x, y])
# If one curve has more points, add them at the end (hold from that curve)
if len(points_a) > min_points:
for i in range(min_points, len(points_a)):
interpolated_points.append([points_a[i][0], points_a[i][1]])
elif len(points_b) > min_points:
for i in range(min_points, len(points_b)):
interpolated_points.append([points_b[i][0], points_b[i][1]])
result.append({
"id": curve_a["id"],
"closed": curve_a["closed"],
"points": interpolated_points,
"pointTypes": get_interpolated_point_types(curve_a, curve_b, len(interpolated_points))
})
else:
# Curve only in A - hold it
result.append(copy.deepcopy(curve_a))
# Process curves only in B (not in A) - hold them
for curve_b in curves_b:
if curve_b["id"] not in processed_ids:
result.append(copy.deepcopy(curve_b))
return result