Player and composition

Use one player for simulations, drawing callbacks, and layered scenes.

from bruhanimate import ImageEffect, Scene, effect_registry, play

def build(buffer):
    snow = effect_registry.create("snow", buffer, " ", preset="light")
    return Scene(snow, ImageEffect("Hello!"))

play(effect_factory=build, fps=60, updates_per_second=30)

The factory receives a screen-sized Buffer. Named effects can be played directly with settings or a preset. Player(screen, animation) borrows an existing Screen; play opens/restores its own. Both return the number of presented frames.

Animation contract

  • start acquires resources; construction must be inert.

  • update(dt) advances simulation; dt is seconds.

  • draw(buffer) reads current state without advancing or consuming solver input.

  • close releases resources and must be safe before startup and on repeats.

BaseEffect requires update and supplies a draw implementation for its persistent private raster. CallbackEffect accepts explicit draw and optional update functions. Player registers cleanup before startup; Scene closes started children in reverse order, including partial startup failure. Do not share one instance across owners.

Timing and composition

FPS controls presentations. An explicit updates_per_second schedules fixed ticks, independent of FPS, with bounded catch-up. Without it, update runs once per draw. Built-ins advance once per tick; their probabilities and speed settings use ticks. frames counts presentations, and None runs until quit. fps=None disables pacing.

duration=10 limits wall-clock seconds after startup, including pauses and completion hold. It must be finite and non-negative, or None for no limit. Zero starts no effect; play still opens/restores its Screen. The first stop condition wins. Sleeps end at the deadline, but a running callback cannot be interrupted and may overrun. Owned effects close normally on duration expiry. See Discover effects, presets, and settings for typed effect/preset selection and CLI playback.

Scene paints children from back to front. None is transparent; spaces paint unless Layer(animation, transparent=True) opts into space transparency. ImageEffect centers text/image rows by default, accepts x/y including zero/negative positions, and moves at velocity=(columns_per_second, rows_per_second). Content clips.

For local animation surfaces with borders, titles, movement, and optional edge bouncing, use Movable panels. Panel owns its child, translates local drawing into parent coordinates, and can pace the child independently of panel movement.

Default quit keys are q/Q, Escape, and Ctrl+C. Player’s event_handler replaces this routing; True means quit. play also accepts an event_handler. Pass TerminalEffect.handle_event for shell input. For typed bindings and panel navigation, pass FocusGroup.handle_event; see Keyboard bindings and focus. Its dispatch result distinguishes handled, unhandled, and quit; the handle_event adapter returns True only for quit. Existing Boolean handlers keep their contract. Resize raises ScreenResizedError after cleanup by default. Set on_resize="resize" for state-preserving reflow; see Terminal resize. Other errors also propagate. manage_effect=False borrows an already started instance without start/close.

See the full player guide for lifecycle, buffer ownership, transparency, and limitations.

on_complete=”hold” keeps presenting completed content; “exit” stops after the final draw. Playback exposes a finished flag, and Scene completes when all finite children complete while ignoring endless backgrounds. See Solver playback and Grid viewport for solver controls and board drawing.

API

One playback loop for simulations, drawings, and composed scenes.

class bruhanimate.player.Animation(*args, **kwargs)[source]

Structural content contract; inheritance and registry membership are optional.

Constructors are inert. Owners register cleanup before start and always close their content. An optional finished property (True/False/None) reports finite completion; an input handler is passed to Player explicitly.

start() → None[source]

Acquire playback resources; tolerate repeated calls.

update(dt: float) → None[source]

Advance simulation; dt is non-negative elapsed/fixed-tick seconds.

draw(buffer: Buffer) → None[source]

Paint current state without advancing it or retaining the target buffer.

close() → None[source]

Release owned resources, safely before start or after partial failure.

__init__(*args, **kwargs)
class bruhanimate.player.ResizableAnimation(*args, **kwargs)[source]

An Animation with a discoverable resize(width_height) viewport hook.

class bruhanimate.player.CallbackEffect(draw: Callable[[Buffer], None], *, update: Callable[[float], None] | None = None)[source]

Adapt drawing and optional simulation callbacks without a subclass.

Resourceful content should implement Animation with explicit start/close. A bare callable is never guessed to be a drawing callback or a factory.

__init__(draw: Callable[[Buffer], None], *, update: Callable[[float], None] | None = None)[source]
start() → None[source]
update(dt: float) → None[source]
draw(buffer: Buffer) → None[source]
close() → None[source]
class bruhanimate.player.FramePresenter(screen: Screen)[source]

Present changed cells in a batch and recycle the previous frame buffer.

After present(), the supplied frame belongs to the presenter. Draw into the returned buffer instead; mutating the displayed frame would invalidate diffing.

__init__(screen: Screen)[source]
resize() → None[source]

Invalidate the previous frame after Screen.refresh_size().

present(frame: Buffer) → Buffer[source]
class bruhanimate.player.Player(screen: ~bruhanimate.bruhutil.bruhscreen.Screen, effect: ~typing.Literal['static', 'offset', 'noise', 'stars', 'plasma', 'gol', 'rain', 'matrix', 'drawlines', 'snow', 'twinkle', 'audio', 'firework', 'fire', 'julia', 'boids', 'sand', 'diffusion', 'water', 'smoke', 'metaballs', 'cloth', 'terrain', 'automaton', 'voronoi', 'perlin'] | str | ~bruhanimate.player.Animation | None = None, *, effect_factory: ~collections.abc.Callable[[~bruhanimate.bruhutil.bruhffer.Buffer], ~bruhanimate.player.Animation] | None = None, fps: float | None = 30, updates_per_second: float | None = None, frames: int | None = None, duration: float | None = None, on_complete: ~typing.Literal['hold', 'exit'] = 'hold', on_resize: ~typing.Literal['raise', 'resize'] = 'raise', background: str = ' ', settings: object | None = None, preset: str | None = None, seed: int | None = None, manage_effect: bool = True, event_handler: ~collections.abc.Callable[[int], bool] | None = None, presenter: ~bruhanimate.player.FramePresenter | None = None, max_catch_up: int = 5, clock: ~collections.abc.Callable[[], float] = <built-in function perf_counter>, sleep: ~collections.abc.Callable[[float], None] = <built-in function sleep>)[source]

Play an animation on a borrowed Screen.

By default playback starts and closes the supplied effect, including on failure. manage_effect=False borrows an already started effect; factories always create owned effects. Player never closes the supplied Screen.

Without updates_per_second, one update occurs per presentation. An explicit update rate uses fixed ticks independent of FPS, with bounded catch-up after stalls. frames counts presentations; duration limits elapsed wall-clock seconds after startup (including pauses/hold); None leaves each limit unset. A zero limit starts no content. Callbacks are synchronous and may overrun a deadline. fps=None disables pacing; uncapped updates use elapsed time when no fixed rate is supplied. Drawing receives a cleared buffer each presentation. BaseEffect retains its own simulation raster; draw() never advances it.

on_complete=’exit’ stops after drawing an animation whose optional finished property is true. The default ‘hold’ keeps drawing and polling input. Content without this property (or reporting None) has no automatic completion. on_resize=’raise’ retains ScreenResizedError behavior. ‘resize’ refreshes the viewport and notifies optional Resizable content without restarting it. Snow, plasma, fire, rain and Matrix adapt their simulation area, retaining state. Other built-in rasters retain their original size and clip on shrink; extra cells on growth show the composition background. Factories are never recalled. Duration and frame limits continue across resize. Named effects accept seed to override their selected settings on a copy; None preserves the settings seed. Configure instance/factory seeds directly.

__init__(screen: ~bruhanimate.bruhutil.bruhscreen.Screen, effect: ~typing.Literal['static', 'offset', 'noise', 'stars', 'plasma', 'gol', 'rain', 'matrix', 'drawlines', 'snow', 'twinkle', 'audio', 'firework', 'fire', 'julia', 'boids', 'sand', 'diffusion', 'water', 'smoke', 'metaballs', 'cloth', 'terrain', 'automaton', 'voronoi', 'perlin'] | str | ~bruhanimate.player.Animation | None = None, *, effect_factory: ~collections.abc.Callable[[~bruhanimate.bruhutil.bruhffer.Buffer], ~bruhanimate.player.Animation] | None = None, fps: float | None = 30, updates_per_second: float | None = None, frames: int | None = None, duration: float | None = None, on_complete: ~typing.Literal['hold', 'exit'] = 'hold', on_resize: ~typing.Literal['raise', 'resize'] = 'raise', background: str = ' ', settings: object | None = None, preset: str | None = None, seed: int | None = None, manage_effect: bool = True, event_handler: ~collections.abc.Callable[[int], bool] | None = None, presenter: ~bruhanimate.player.FramePresenter | None = None, max_catch_up: int = 5, clock: ~collections.abc.Callable[[], float] = <built-in function perf_counter>, sleep: ~collections.abc.Callable[[float], None] = <built-in function sleep>)[source]
run() → int[source]

Run until quit, presentation limit, or configured finite completion.

Returns:

Frames presented, which may differ from simulation update count.

Return type:

int

Raises:
  • ScreenResizedError – After closing owned content when size changes.

  • Exception – Startup/update/draw/presentation errors after cleanup. KeyboardInterrupt instead ends normally. The borrowed Screen is never closed here; Screen.show/play owns terminal restoration.

bruhanimate.player.play(effect: Literal['static', 'offset', 'noise', 'stars', 'plasma', 'gol', 'rain', 'matrix', 'drawlines', 'snow', 'twinkle', 'audio', 'firework', 'fire', 'julia', 'boids', 'sand', 'diffusion', 'water', 'smoke', 'metaballs', 'cloth', 'terrain', 'automaton', 'voronoi', 'perlin'] | str | Animation | None = None, *, effect_factory: Callable[[Buffer], Animation] | None = None, fps: float | None = 30, updates_per_second: float | None = None, frames: int | None = None, duration: float | None = None, on_complete: Literal['hold', 'exit'] = 'hold', on_resize: Literal['raise', 'resize'] = 'raise', background: str = ' ', settings: object | None = None, preset: str | None = None, seed: int | None = None, event_handler: Callable[[int], bool] | None = None) → int[source]

Open a screen, play owned content, and restore the terminal without a prompt.

Discover names with effects.names() and configurations with effects.snow.presets(). Use effects.snow.play() for effect-specific preset and settings completion. Custom registered names remain accepted here.

Supply an effect name, an Animation object, or effect_factory(buffer), with exactly one source. Named effects accept settings/preset; explicit settings take precedence. Factories receive a correctly sized private Buffer. seed overrides named-effect settings on an independent copy; None preserves their seed. Seeds are non-negative integers and do not change global RNGs.

fps paces presentations (None is uncapped), updates_per_second optionally sets fixed simulation ticks, and frames limits presentations (None is endless). duration limits wall-clock seconds after startup, including paused/held time. None is unlimited; zero creates/starts no content. The first stop condition wins. A running callback cannot be interrupted, so a slow frame may overrun. on_complete selects hold/exit for content reporting a finished status. on_resize=’resize’ opts into live reflow; ‘raise’ is the compatibility default. Resizable content receives (width, height); other content must draw into any viewport. Factories are called once and solver state remains live. event_handler replaces the default quit bindings; returning True exits. Use Player directly for a borrowed Screen, injectable clock, or presenter.

Returns:

Number of presented frames. Rendering errors propagate after the terminal and owned content have been restored/closed.

Return type:

int

Composition independent of terminal ownership, pacing, and presentation.

class bruhanimate.scene.Layer(animation: Animation, transparent: bool = False, *, region: tuple[int, int, int, int] | None = None, updates_per_second: float | None = None, max_catch_up: int = 5, mask: Callable[[int, int, tuple[int, int]], bool] | None = None, style: TextStyle | None = None, preserve_background: bool = False)[source]

One animation in paint order. None cells always reveal lower layers.

transparent=True also treats plain spaces as transparent. ANSI-colored spaces remain visible, so colored backgrounds can be composed deliberately.

region=(x, y, width, height) clips painting in destination coordinates; it does not translate or resize the child’s full-scene drawing buffer. Negative origins and empty regions are allowed. Clipping never pauses updates. updates_per_second=None forwards every Scene update unchanged. A positive rate accumulates supplied dt and sends fixed 1/rate ticks, beginning after one full interval. max_catch_up bounds ticks per Scene update; reaching the bound discards remaining accumulated time. Presentation still draws current state every frame. These clocks use simulation seconds, not wall time.

mask(x, y, (width, height)) is a pure destination-coordinate predicate; False clips the cell without changing updates or child ownership. ImageMask builds one from ASCII rows. style overrides cell colors/bold using TextStyle. preserve_background=True retains the already composed lower cell’s background unless style.background explicitly overrides it. These options work with any Animation. Spaces remain opaque unless transparent=True, before styling.

animation: Animation
transparent: bool = False
region: tuple[int, int, int, int] | None = None
updates_per_second: float | None = None
max_catch_up: int = 5
mask: Callable[[int, int, tuple[int, int]], bool] | None = None
style: TextStyle | None = None
preserve_background: bool = False
__init__(animation: Animation, transparent: bool = False, *, region: tuple[int, int, int, int] | None = None, updates_per_second: float | None = None, max_catch_up: int = 5, mask: Callable[[int, int, tuple[int, int]], bool] | None = None, style: TextStyle | None = None, preserve_background: bool = False) → None
class bruhanimate.scene.Scene(*layers: Animation | Layer)[source]

Own and paint animations from back to front through the shared Player.

Each child gets an isolated drawing buffer. A scene owns each child once; close runs in reverse start order, including after partial startup failure. Build a new scene to change its membership during playback.

__init__(*layers: Animation | Layer)[source]
property finished: bool | None

Finish when all finite children finish, ignoring endless backgrounds.

A finite child exposes a boolean finished property. A scene containing only endless content reports None. Nested scenes use the same rule.

start() → None[source]
update(dt: float) → None[source]

Advance layer clocks in seconds; no terminal polling or sleeping.

set_visible(visible: bool) → None[source]

Propagate a parent’s visibility to focusable descendants.

resize(size: tuple[int, int]) → None[source]

Notify children without changing layer clocks or fixed region clips.

draw(buffer: Buffer) → None[source]
close() → None[source]
class bruhanimate.scene.TimedOverlay(animation: Animation, *, duration: float, delay: float = 0)[source]

Own an animation visible for duration seconds after an optional delay.

Time advances only in update(dt), including when a sibling Playback is paused. The child starts when the delay elapses and closes on expiry or owner cleanup. duration=0 never starts it. Active time is [delay, delay + duration); a large update that skips this entire window never starts the child. Draw is read-only and paints nothing outside the window, revealing freshly composed lower layers.

Child updates receive only the active part of dt. The child may finish early; its last state remains eligible for drawing until the duration expires. This wrapper reports finished on expiry and participates in Scene completion. It is single-use and does not forward input: route events explicitly through Player. Never give its child to another lifecycle owner.

__init__(animation: Animation, *, duration: float, delay: float = 0)[source]
property finished: bool

Whether the visibility window has expired; early close is not expiry.

start() → None[source]
update(dt: float) → None[source]
set_visible(visible: bool) → None[source]

Propagate parent visibility without changing the visibility timer.

resize(size: tuple[int, int]) → None[source]

Notify unexpired content without resetting the visibility window.

draw(buffer: Buffer) → None[source]
close() → None[source]
class bruhanimate.scene.ImageEffect(image: str | Sequence[str], *, x: float | None = None, y: float | None = None, velocity: tuple[float, float] = (0, 0), characters_per_second: float | None = None)[source]

Draw text or ASCII image rows, centered unless x/y are supplied.

velocity is (columns/second, rows/second). Movement happens in update(), and out-of-bounds content is clipped. Spaces paint; use Layer transparency to reveal an animated background through them.

characters_per_second enables a row-by-row reveal of non-space glyphs. It must be positive and finite; None displays the whole image immediately. Revealing starts blank, leaves spaces/unrevealed cells transparent, and advances only in update(dt), in simulation seconds. finished becomes True after the last glyph; the image remains visible and movement continues. With no reveal, finished is None. Use on_complete=”hold” to keep a Scene’s background running after reveal completion. Resize/repeated start do not reset progress. Rows must contain plain single-cell text, not ANSI escapes.

__init__(image: str | Sequence[str], *, x: float | None = None, y: float | None = None, velocity: tuple[float, float] = (0, 0), characters_per_second: float | None = None)[source]
property finished: bool | None

Whether all reveal glyphs are visible; None for an immediate image.

start() → None[source]
update(dt: float) → None[source]
draw(buffer: Buffer) → None[source]
close() → None[source]

See ASCII images, transitions, masks, and styling for image transitions and masks.