Manages keyframes and produces interpolated camera paths.
| 189 | |
| 190 | |
| 191 | class KeyframeAnimator: |
| 192 | """Manages keyframes and produces interpolated camera paths.""" |
| 193 | |
| 194 | def __init__(self): |
| 195 | self.keyframes: List[Keyframe] = [] |
| 196 | |
| 197 | def add(self, camera: OrbitCamera, frame_idx: int = 0): |
| 198 | kf = Keyframe( |
| 199 | eye=camera.eye.copy(), |
| 200 | look_at=camera.look_at.copy(), |
| 201 | up=camera.up.copy(), |
| 202 | fov_deg=camera.fov_deg, |
| 203 | frame_idx=frame_idx, |
| 204 | ) |
| 205 | self.keyframes.append(kf) |
| 206 | |
| 207 | def remove_last(self): |
| 208 | if self.keyframes: |
| 209 | self.keyframes.pop() |
| 210 | |
| 211 | def clear(self): |
| 212 | self.keyframes.clear() |
| 213 | |
| 214 | def interpolate(self, steps_per_segment: int = 60) -> List[Tuple[OrbitCamera, int]]: |
| 215 | """Interpolate keyframes with cubic spline. Returns list of (camera, frame_idx).""" |
| 216 | if len(self.keyframes) < 2: |
| 217 | if self.keyframes: |
| 218 | cam = OrbitCamera( |
| 219 | self.keyframes[0].eye, |
| 220 | self.keyframes[0].look_at, |
| 221 | self.keyframes[0].up, |
| 222 | self.keyframes[0].fov_deg, |
| 223 | ) |
| 224 | return [(cam, self.keyframes[0].frame_idx)] |
| 225 | return [] |
| 226 | |
| 227 | from scipy.interpolate import CubicSpline |
| 228 | |
| 229 | n = len(self.keyframes) |
| 230 | t_knots = np.linspace(0, 1, n) |
| 231 | t_interp = np.linspace(0, 1, (n - 1) * steps_per_segment + 1) |
| 232 | |
| 233 | # Stack all fields |
| 234 | eyes = np.array([kf.eye for kf in self.keyframes]) |
| 235 | look_ats = np.array([kf.look_at for kf in self.keyframes]) |
| 236 | ups = np.array([kf.up for kf in self.keyframes]) |
| 237 | fovs = np.array([kf.fov_deg for kf in self.keyframes]) |
| 238 | frames = np.array([kf.frame_idx for kf in self.keyframes], dtype=np.float64) |
| 239 | |
| 240 | cs_eye = CubicSpline(t_knots, eyes) |
| 241 | cs_look = CubicSpline(t_knots, look_ats) |
| 242 | cs_up = CubicSpline(t_knots, ups) |
| 243 | cs_fov = CubicSpline(t_knots, fovs) |
| 244 | cs_frame = CubicSpline(t_knots, frames) |
| 245 | |
| 246 | result = [] |
| 247 | for t in t_interp: |
| 248 | e = cs_eye(t).astype(np.float32) |