Panels, layer clocks, and temporary overlays

Scene composes animations in paint order. Each Layer can clip its output to a rectangle and choose an independent simulation rate. TimedOverlay makes a child visible for a limited time, then releases it and reveals the current lower layers. Layer also accepts a pure mask, style=TextStyle(...), and preserve_background=True for generic cell composition. See ASCII images, transitions, masks, and styling for masks, styling, and Focus/Dissolve/Glitch transitions. These features use the existing Player and work with native terminal playback; no additional application or dependency is required.

from bruhanimate import ImageEffect, Layer, Scene, TimedOverlay, effect_registry, play

def build(buffer):
    panel = (1, 2, max(0, buffer.width() - 2), max(0, buffer.height() - 4))
    snow = effect_registry.create("snow", buffer, " ", preset="moderate", seed=42)
    return Scene(
        ImageEffect("Snow panel", x=1, y=0),
        Layer(snow, region=panel, updates_per_second=15),
        Layer(TimedOverlay(ImageEffect("This message disappears"), duration=2),
              region=panel),
    )

play(effect_factory=build, fps=60, updates_per_second=60,
     duration=5, on_complete="hold")

Type Layer( in an editor to discover region, updates_per_second, and max_catch_up. TimedOverlay exposes typed duration and delay fields in its constructor. Configure layers and overlays before starting playback.

Revealing text and ASCII images

ImageEffect accepts a typed characters_per_second option. A positive, finite rate reveals one non-space glyph at a time, left to right and top to bottom. Blank and unrevealed cells stay transparent. The default None keeps the existing immediate image behavior, including painting spaces.

from bruhanimate import ImageEffect, Scene, effect_registry, play, text_to_image

def build(buffer):
    return Scene(
        effect_registry.create("plasma", buffer, " ", seed=7),
        ImageEffect(text_to_image("bruh", font="small"),
                    characters_per_second=18),
    )

play(effect_factory=build, duration=5, on_complete="hold", on_resize="resize")

Only update(dt) advances the reveal; dt is simulation seconds. Drawing, resizing, and repeated startup preserve progress. Offscreen glyphs still consume their reveal time. finished becomes True after the last glyph, which remains visible; it is None when revealing is disabled. Empty/all-space reveals finish immediately. Use on_complete="hold" to continue an animated background after the image finishes. Scene owns the image’s lifecycle; it consumes no input. Supply plain single-cell text without ANSI escapes. The TV demo uses the flying/glitching entrance below with per-cell plasma background colors.

Flying, glitching characters

GlitchImageEffect moves individual characters into place. Non-space glyphs launch from one column before the image’s left edge, in column order (top to bottom within each column). Each flickers at 20 Hz and jitters by up to one row until its flight ends, then becomes its final glyph. Settled glyphs paint above flying ones; blank cells stay transparent.

from bruhanimate import GlitchImageEffect, Scene, effect_registry, play, text_to_image

def build(buffer):
    return Scene(
        effect_registry.create("plasma", buffer, " ", seed=7),
        GlitchImageEffect(text_to_image("bruh", font="small"),
                          characters_per_second=18, travel_time=0.65, seed=42),
    )

play(effect_factory=build, duration=7, on_complete="hold", on_resize="resize")

Type GlitchImageEffect( to discover launch rate, travel_time in simulation seconds, seed, and final x/y coordinates (None centers each axis). Rate and travel time must be positive and finite. finished becomes True only after the last arrival; the final image stays visible. An empty image finishes immediately. Update advances time; draw is read-only. Seeded glitches depend on simulation time, not draw frequency. Resize recenters without restarting flight. Scene owns the animation, which consumes no input and owns no external resources. Use a Layer region or Panel to clip flying glyphs that extend beyond the image. This is a composable animation, not a restored legacy renderer.

To repeat the entrance, set repeat=True, hold_duration=3, repeat_delay=1. Each cycle flies in, holds the fully settled image for three simulation seconds, then paints nothing for one second before starting again. These two durations are finite, non-negative seconds; zero skips that phase. Seeded cycles replay the same glitches. The clock handles updates crossing multiple cycles and preserves its phase during resize. Non-empty repeating images report finished=False throughout; empty images still finish immediately. Sibling effects keep running and revealed backgrounds are freshly composed each frame. With the default repeat=False, the final image stays visible indefinitely, regardless of the hold and delay settings. With reverse=True, the hold is followed by the same effect backwards, before the gap or completion. See ASCII images, transitions, masks, and styling for round-trip endpoints and timing.

Clipped regions

region=(x, y, width, height) uses scene/destination coordinates. Width and height are non-negative integers; zero means nothing is painted. Origins may be negative, and rectangles are intersected with the destination Buffer. None uses the whole destination. Fully clipped layers still start and update, but their draw calls can be skipped.

Clipping does not move the child or change its drawing dimensions. Children receive a full-scene scratch Buffer, so a built-in effect’s private raster still matches that Buffer. An ImageEffect at x=0 is not moved to a region’s left edge. Set its position explicitly, or use GridViewport’s region to lay out a board within a panel. Nested layer clips intersect. This is a visual boundary; it does not create simulation collision walls.

None cells remain transparent. Plain spaces paint unless transparent=True; ANSI-colored spaces still paint. Each presentation draws the latest state into isolated scratch buffers, then copies only visible cells. Keep separate private Buffers for separate raster-based effects.

Independent update rates

With updates_per_second=None, a layer receives every Scene update unchanged. A configured positive, finite rate accumulates Scene dt and delivers fixed 1 / rate ticks. The first tick waits for one complete interval; construction, startup, and drawing do not consume ticks. Drawing still happens every presentation, including between ticks. Each Scene instance owns its layer accumulators.

For example, Layer(snow, updates_per_second=15) runs snow at 15 simulation ticks per supplied second while Player presents at 60 FPS. Player’s scene update rate controls scheduling granularity. If a layer needs more ticks than its parent delivers, Scene performs multiple ticks during one update; it does not create another playback loop or present intermediate frames.

max_catch_up=5 limits work per layer per Scene update. Reaching that cap drops remaining accumulated time, including the fractional remainder, to prevent a long stall from causing unbounded work. Raise it deliberately when a large ratio of child/parent rates requires more ticks. This bound also applies during headless capture. Layer time follows delivered simulation time, not wall time discarded by Player’s own catch-up policy. Rates cannot be edited on a frozen Layer.

Playback already has steps_per_second for solver progression. Usually leave its Layer rate unset so pause/step commands are processed on the next Scene update. Rate-limiting its Layer also limits when those controls take effect. Route input explicitly with event_handler=playback.handle_event. Pausing that solver does not pause snow, other layers, or an independently updated overlay.

Temporary overlays and ownership

TimedOverlay(child, duration=2, delay=1) is visible in the half-open interval [1, 3) of supplied simulation seconds. It starts its child when the delay elapses, forwards only the active portion of an update, and closes the child on expiry. If one update skips an entire delayed window, the child is never started. duration=0 never starts it. Negative/non-finite times are rejected.

Expiry happens in update, so the next draw paints nothing and reveals the latest lower layers. The wrapper never erases or modifies a previously presented frame. A child finishing early does not shorten the visibility window. Delayed startup failures close the failing child; owner cleanup closes active children once. Closing before startup is safe. TimedOverlay is single-use, like Playback. Do not share its child with another owner or add that child separately to Scene.

An overlay reports finished=True on expiry and participates in Scene’s finite completion. A scene with an endless background and one overlay therefore finishes when that overlay expires. Use on_complete="hold" to keep the background playing until another limit or quit. A scene with a finite solver waits for both solver completion and overlay expiry. The overlay does not forward input events.

A complete second example

Run the beam demo in your usual terminal, including Windows Terminal:

python -m bruhanimate.demos.beam_demo
from bruhanimate import beam_demo

beam_demo.run(frames=180, fps=60)

An original 15-by-9 public board sends beams downwards; ^ splits a beam diagonally into the following row and overlapping beams merge. The pure lazy solver keeps immutable snapshots and does not enumerate branch histories. Snow updates at 15/s inside the panel, the solver advances at 5 steps/s, and Player presents at 60 FPS. A one-second introductory message disappears while the board continues evolving underneath. The status display remains outside the snow panel. Space pauses, n/right steps, +/- changes solver speed, and q quits.

The demo uses ASCII geometry with ANSI colors; smaller windows clip the board. The final board stays visible until quit or the frame limit. This complements the existing BFS grid demo and keeps all puzzle rules in application/demo code. Layer masks and styling are described in ASCII images, transitions, masks, and styling. Use Paths, pulses, and ripples for reusable path/pulse/ripple accents and Terminal resize for live viewport changes.

TV composition example

For a scene without a solver, run python -m bruhanimate.demos.tv_demo. Plasma plays inside an opaque retro TV while snow falls behind it. A clipped channel badge expires after two seconds. The effects use separate buffers and update rates; the shared Player presents at 60 FPS. See TV demo for the full layer order and customization points.

Moving windows example

Run python -m bruhanimate.demos.windows_demo for four bouncing windows, each with its own effect, rate, and local buffer. Public Movable panels translate the pixels and border together; panel bounds protect the header and footer. See Moving windows demo for ownership and customization.

Compatibility

Existing Layer(animation) and Layer(animation, transparent=True) retain their behavior. New options are keyword-only. Scene now rejects negative or non-finite dt; layers reject malformed regions, rates, and catch-up bounds before starting content. transparent must be a bool. See Player and composition for the Scene/Layer/TimedOverlay API reference.