""" 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, )