"""Timing primitives from classical animation (the A1 layer). These compile a sparse sequence of keyframe-cursor states into a dense 24fps frame stream by interpolating between pinned states with classical-animation rules: eases, stride (on-twos), and moving-hold micro-vibration. This is what makes the cursor's temporal layer load-bearing for animation specifically rather than decoration around a video-generation model. """ from __future__ import annotations from dataclasses import dataclass from typing import Callable, Optional import math Curve = Callable[[float], float] def linear(t: float) -> float: return t def ease_in_out_cubic(t: float) -> float: return 4 * t * t * t if t < 0.5 else 1 - (-2 * t + 2) ** 3 / 2 def ease_in_quad(t: float) -> float: return t * t def ease_out_quad(t: float) -> float: return 1 - (1 - t) ** 2 def spring(t: float, tension: float = 0.3) -> float: return 1 - math.cos(t * math.pi * (1 + tension)) * math.exp(-3 * t) CURVES: dict[str, Curve] = { "linear": linear, "ease": ease_in_out_cubic, "ease-in": ease_in_quad, "ease-out": ease_out_quad, "spring": lambda t: spring(t), } @dataclass(frozen=True, slots=True) class AnimationSegment: """A compiled keyframe→keyframe segment. Intent, not rendered pixels.""" start_frame: int end_frame: int curve_name: str stride: int = 2 micro_profile: Optional[str] = None def ease_between( start_frame: int, end_frame: int, curve: str = "ease", stride: int = 2, ) -> AnimationSegment: if curve not in CURVES: raise ValueError(f"Unknown curve {curve!r}; choices: {sorted(CURVES)}") if stride < 1: raise ValueError(f"stride must be >= 1; got {stride}") return AnimationSegment(start_frame, end_frame, curve, stride) def step_on_stride(start_frame: int, end_frame: int, stride: int = 2) -> list[int]: """Return the per-frame *sample frame index* under stride-N discipline. On-twos: every 2 frames advance; intermediate frame holds the prior sample. """ out = [] for f in range(start_frame, end_frame): rel = f - start_frame sample_rel = (rel // stride) * stride out.append(start_frame + sample_rel) return out def hold_with_micro( start_frame: int, duration: int, micro: str = "breath" ) -> AnimationSegment: """A moving-hold: spatial pin + micro-vibration to signal life over long holds.""" return AnimationSegment(start_frame, start_frame + duration, "linear", 1, micro) def interp_t(curve_name: str, t: float) -> float: return CURVES[curve_name](max(0.0, min(1.0, t)))