diff --git a/engine/animation.py b/engine/animation.py deleted file mode 100644 index 6b6cd7b..0000000 --- a/engine/animation.py +++ /dev/null @@ -1,340 +0,0 @@ -""" -Animation system - Clock, events, triggers, durations, and animation controller. -""" - -import time -from collections.abc import Callable -from dataclasses import dataclass, field -from enum import Enum, auto -from typing import Any - - -class Clock: - """High-resolution clock for animation timing.""" - - def __init__(self): - self._start_time = time.perf_counter() - self._paused = False - self._pause_offset = 0.0 - self._pause_start = 0.0 - - def reset(self) -> None: - self._start_time = time.perf_counter() - self._paused = False - self._pause_offset = 0.0 - self._pause_start = 0.0 - - def elapsed(self) -> float: - if self._paused: - return self._pause_start - self._start_time - self._pause_offset - return time.perf_counter() - self._start_time - self._pause_offset - - def elapsed_ms(self) -> float: - return self.elapsed() * 1000 - - def elapsed_frames(self, fps: float = 60.0) -> int: - return int(self.elapsed() * fps) - - def pause(self) -> None: - if not self._paused: - self._paused = True - self._pause_start = time.perf_counter() - - def resume(self) -> None: - if self._paused: - self._pause_offset += time.perf_counter() - self._pause_start - self._paused = False - - -class TriggerType(Enum): - TIME = auto() # Trigger after elapsed time - FRAME = auto() # Trigger after N frames - CYCLE = auto() # Trigger on cycle repeat - CONDITION = auto() # Trigger when condition is met - MANUAL = auto() # Trigger manually - - -@dataclass -class Trigger: - """Event trigger configuration.""" - - type: TriggerType - value: float | int = 0 - condition: Callable[["AnimationController"], bool] | None = None - repeat: bool = False - repeat_interval: float = 0.0 - - -@dataclass -class Event: - """An event with trigger, duration, and action.""" - - name: str - trigger: Trigger - action: Callable[["AnimationController", float], None] - duration: float = 0.0 - ease: Callable[[float], float] | None = None - - def __post_init__(self): - if self.ease is None: - self.ease = linear_ease - - -def linear_ease(t: float) -> float: - return t - - -def ease_in_out(t: float) -> float: - return t * t * (3 - 2 * t) - - -def ease_out_bounce(t: float) -> float: - if t < 1 / 2.75: - return 7.5625 * t * t - elif t < 2 / 2.75: - t -= 1.5 / 2.75 - return 7.5625 * t * t + 0.75 - elif t < 2.5 / 2.75: - t -= 2.25 / 2.75 - return 7.5625 * t * t + 0.9375 - else: - t -= 2.625 / 2.75 - return 7.5625 * t * t + 0.984375 - - -class AnimationController: - """Controls animation parameters with clock and events.""" - - def __init__(self, fps: float = 60.0): - self.clock = Clock() - self.fps = fps - self.frame = 0 - self._events: list[Event] = [] - self._active_events: dict[str, float] = {} - self._params: dict[str, Any] = {} - self._cycled = 0 - - def add_event(self, event: Event) -> "AnimationController": - self._events.append(event) - return self - - def set_param(self, key: str, value: Any) -> None: - self._params[key] = value - - def get_param(self, key: str, default: Any = None) -> Any: - return self._params.get(key, default) - - def update(self) -> dict[str, Any]: - """Update animation state, return current params.""" - elapsed = self.clock.elapsed() - - for event in self._events: - triggered = False - - if event.trigger.type == TriggerType.TIME: - if self.clock.elapsed() >= event.trigger.value: - triggered = True - elif event.trigger.type == TriggerType.FRAME: - if self.frame >= event.trigger.value: - triggered = True - elif event.trigger.type == TriggerType.CYCLE: - cycle_duration = event.trigger.value - if cycle_duration > 0: - current_cycle = int(elapsed / cycle_duration) - if current_cycle > self._cycled: - self._cycled = current_cycle - triggered = True - elif event.trigger.type == TriggerType.CONDITION: - if event.trigger.condition and event.trigger.condition(self): - triggered = True - elif event.trigger.type == TriggerType.MANUAL: - pass - - if triggered: - if event.name not in self._active_events: - self._active_events[event.name] = 0.0 - - progress = 0.0 - if event.duration > 0: - self._active_events[event.name] += 1 / self.fps - progress = min( - 1.0, self._active_events[event.name] / event.duration - ) - eased_progress = event.ease(progress) - event.action(self, eased_progress) - - if progress >= 1.0: - if event.trigger.repeat: - self._active_events[event.name] = 0.0 - else: - del self._active_events[event.name] - else: - event.action(self, 1.0) - if not event.trigger.repeat: - del self._active_events[event.name] - else: - self._active_events[event.name] = 0.0 - - self.frame += 1 - return dict(self._params) - - -@dataclass -class PipelineParams: - """Snapshot of pipeline parameters for animation.""" - - effect_enabled: dict[str, bool] = field(default_factory=dict) - effect_intensity: dict[str, float] = field(default_factory=dict) - camera_mode: str = "vertical" - camera_speed: float = 1.0 - camera_x: int = 0 - camera_y: int = 0 - display_backend: str = "terminal" - scroll_speed: float = 1.0 - - -class Preset: - """Packages a starting pipeline config + Animation controller.""" - - def __init__( - self, - name: str, - description: str = "", - initial_params: PipelineParams | None = None, - animation: AnimationController | None = None, - ): - self.name = name - self.description = description - self.initial_params = initial_params or PipelineParams() - self.animation = animation or AnimationController() - - def create_controller(self) -> AnimationController: - controller = AnimationController() - for key, value in self.initial_params.__dict__.items(): - controller.set_param(key, value) - for event in self.animation._events: - controller.add_event(event) - return controller - - -def create_demo_preset() -> Preset: - """Create the demo preset with effect cycling and camera modes.""" - animation = AnimationController(fps=60) - - effects = ["noise", "fade", "glitch", "firehose"] - camera_modes = ["vertical", "horizontal", "omni", "floating", "trace"] - - def make_effect_action(eff): - def action(ctrl, t): - ctrl.set_param("current_effect", eff) - ctrl.set_param("effect_intensity", t) - - return action - - def make_camera_action(cam_mode): - def action(ctrl, t): - ctrl.set_param("camera_mode", cam_mode) - - return action - - for i, effect in enumerate(effects): - effect_duration = 5.0 - - animation.add_event( - Event( - name=f"effect_{effect}", - trigger=Trigger( - type=TriggerType.TIME, - value=i * effect_duration, - repeat=True, - repeat_interval=len(effects) * effect_duration, - ), - duration=effect_duration, - action=make_effect_action(effect), - ease=ease_in_out, - ) - ) - - for i, mode in enumerate(camera_modes): - camera_duration = 10.0 - animation.add_event( - Event( - name=f"camera_{mode}", - trigger=Trigger( - type=TriggerType.TIME, - value=i * camera_duration, - repeat=True, - repeat_interval=len(camera_modes) * camera_duration, - ), - duration=0.5, - action=make_camera_action(mode), - ) - ) - - animation.add_event( - Event( - name="pulse", - trigger=Trigger(type=TriggerType.CYCLE, value=2.0, repeat=True), - duration=1.0, - action=lambda ctrl, t: ctrl.set_param("pulse", t), - ease=ease_out_bounce, - ) - ) - - return Preset( - name="demo", - description="Demo mode with effect cycling and camera modes", - initial_params=PipelineParams( - effect_enabled={ - "noise": False, - "fade": False, - "glitch": False, - "firehose": False, - "hud": True, - }, - effect_intensity={ - "noise": 0.0, - "fade": 0.0, - "glitch": 0.0, - "firehose": 0.0, - }, - camera_mode="vertical", - camera_speed=1.0, - display_backend="pygame", - ), - animation=animation, - ) - - -def create_pipeline_preset() -> Preset: - """Create preset for pipeline visualization.""" - animation = AnimationController(fps=60) - - animation.add_event( - Event( - name="camera_trace", - trigger=Trigger(type=TriggerType.CYCLE, value=8.0, repeat=True), - duration=8.0, - action=lambda ctrl, t: ctrl.set_param("camera_mode", "trace"), - ) - ) - - animation.add_event( - Event( - name="highlight_path", - trigger=Trigger(type=TriggerType.CYCLE, value=4.0, repeat=True), - duration=4.0, - action=lambda ctrl, t: ctrl.set_param("path_progress", t), - ) - ) - - return Preset( - name="pipeline", - description="Pipeline visualization with trace camera", - initial_params=PipelineParams( - camera_mode="trace", - camera_speed=1.0, - display_backend="pygame", - ), - animation=animation, - )