ASCII images, transitions, masks, and styling¶
An image here is a rectangular arrangement of terminal cells: ASCII artwork,
plain text, or rows returned by text_to_image (pyfiglet). It is not a bitmap
file loader. ImageEffect and the image transitions implement the ordinary
Animation protocol. Masks and styling belong to Layer and therefore also
work with labels, callbacks, solver drawings, and other animations. No additional
public base-image hierarchy or legacy renderer is required.
Try python -m bruhanimate.demos.image_demo for six resizable panels, or
python -m bruhanimate.demos.tv_demo for alternating entrances over plasma.
Typed image effects¶
All helpers are exported from bruhanimate. Editor completion shows constructor
options; DissolveDirection offers "in" and "out".
WipeDirection offers "left", "right", "up", and "down".
Effect |
Behavior |
Main controls |
|---|---|---|
|
Display/pan text; optional typewriter reveal |
|
|
Fly in from the left with changing symbols and jitter |
|
|
Assemble scattered glyphs into their final positions |
|
|
Reveal or remove glyphs in random order |
|
|
Reveal a straight edge across the image bounds |
|
from bruhanimate import (
FocusImageEffect, Layer, Scene, TextStyle, effect_registry, play, text_to_image,
)
def build(buffer):
plasma = effect_registry.create("plasma", buffer, " ", seed=7)
title = FocusImageEffect(
text_to_image("bruh", font="small"), duration=2.5,
scatter=(8, 3), seed=42, repeat=True, reverse=True,
hold_duration=3, repeat_delay=1,
)
return Scene(
Layer(plasma, updates_per_second=30),
Layer(title, style=TextStyle(foreground=231, bold=True),
preserve_background=True),
)
play(effect_factory=build, duration=10, on_complete="hold", on_resize="resize")
The four transitions share x/y (None centers each axis),
repeat, reverse, hold_duration, and repeat_delay. Options are set at construction.
duration reports transition seconds, excluding hold/gap; Glitch derives this
from its glyph count, launch rate, and travel time. Glitch, Focus, and Dissolve
also accept seed; Wipe is deterministic and needs no seed. Non-space glyphs alone animate;
spaces and unseen glyphs are transparent. Input must be plain single-cell text,
without ANSI escapes, wide glyphs, or combining marks. Use Layer styling for color.
Focus scatter is a pair of non-negative integer offsets relative to each glyph’s
final position. It is independent of terminal size. Glyphs interpolate linearly,
and overlapping particles paint in original row order. On completion the image
is exact. Dissolve "in" starts blank and ends with the image; "out" starts
with the image and ends blank. Wipe direction names describe the edge motion:
right reveals from the left; left from the right; down from the top;
up from the bottom. Its reverse leg retracts the same edge.
Timing, completion, and ownership¶
Only update(dt) advances time, in simulation seconds. Drawing does not consume
randomness or advance state. Seeds make initial scatter/order/glitches reproducible;
repeated cycles reuse them. Duration and speeds must be positive and finite;
hold/gap times must be finite and non-negative. Empty images finish immediately.
Without repetition, finished becomes True after the transition and the final
state remains visible. Use on_complete="hold" to keep a background running.
With repeat=True, the transition completes, holds its final state, paints
nothing during the gap, and starts again. For a dissolve exit the final state is
already blank. Non-empty repeated transitions report finished=False throughout.
Large updates can cross multiple cycles without performing work per skipped cycle.
Set reverse=True to add a return leg after the hold. The same transition is
sampled backwards, so Glitch retraces its flight and symbols, Focus scatters back
along its original paths, and Dissolve removes the last-revealed glyphs first.
Both legs take duration seconds. A repeating cycle now lasts
2 * duration + hold_duration + repeat_delay; the gap starts after the exit.
This is the behavior used by the TV and image showcase demos.
For a single round trip (reverse=True, repeat=False), completion waits until
2 * duration + hold_duration and retains the initial state: blank for Glitch
or Wipe or dissolve-in, scattered glyphs for Focus, and the full image for dissolve-out.
Repeating cycles hide all output during their gap. The default reverse=False
preserves the existing one-way behavior. Resizing during the hold or reverse leg
does not reset time or change paths/order.
Resize preserves time, scatter/order, and completion. Centered images recenter; explicit coordinates stay fixed. Drawing clips; offscreen glyphs still progress. Focus particles and Glitch jitter can extend beyond the final image rectangle; use a mask, Layer region, or Panel when those movements need a boundary.
Scene owns each animation once. These image effects own no children or external resources and consume no input. Startup and cleanup are idempotent and do not restart progress. Configure keyboard routing separately through Player.
For a timed playlist of different animations, use Animation sequences. The TV demo uses AnimationSequence to own and replace each transition while plasma runs.
Masks for any animation¶
Layer(mask=...) accepts a CellMask callable with signature
(x: int, y: int, size: tuple[int, int]) -> bool. Coordinates are destination
cells; size is (width, height). True permits painting. Masks intersect region
and transparency rules without translating the child or changing its clock.
They run during draw and must be pure: do not read input, consume randomness,
or advance simulation there. Derive geometry from size to follow resizing.
ImageMask snapshots ASCII rows, allowing non-space positions. x and y
are integer positions or None for centering. invert=True allows the complement,
including cells outside the image. Ragged rows and empty masks are supported.
Masks are immutable and own no lifecycle resources.
from bruhanimate import ImageMask, Layer, Scene, effect_registry, play, text_to_image
def build(buffer):
plasma = effect_registry.create("plasma", buffer, " ", seed=7)
return Scene(Layer(plasma, mask=ImageMask(text_to_image("bruh", font="small"))))
play(effect_factory=build, duration=5, on_resize="resize")
This paints plasma only through the lettering. The TV uses a custom CellMask for its sloping monitor opening; the same mask applies to plasma and animated text.
Styling and background preservation¶
Layer(style=TextStyle(foreground=231, background=None, bold=True)) styles any
visible single-cell child output. Explicit palette colors (0..255) override child
colors; None leaves them alone. True enables bold; False leaves child bold alone.
The widget API still accepts the same TextStyle class and imports.
preserve_background=True uses the already composed lower cell’s background
color, while retaining the upper glyph and its foreground attributes. An explicit
style.background wins. Basic/bright ANSI colors, 256-color palettes, RGB colors,
and resets are supported. An unstyled/transparent lower cell supplies the terminal
default background. This is cell-background composition, not alpha blending.
Foreground and other lower-layer attributes are not inherited.
Plain spaces still paint unless transparent=True; ANSI-styled spaces remain
visible. Transparency is evaluated before Layer styling. None cells never paint.
Only layers using these options parse cell styles; ordinary composition retains
its existing path. Children still receive isolated buffers; lower animations are
not read directly or borrowed by upper animations.
API reference¶
Pure, viewport-aware masks usable with any animation Layer.
- bruhanimate.masks.CellMask¶
Predicate (x, y, (width, height)) in destination cells; True allows paint.
Called during draw. It must be pure and must not advance animation or consume input/randomness. Region clipping and transparency still apply independently.
alias of
Callable[[int,int,tuple[int,int]],bool]
- class bruhanimate.masks.ImageMask(image: str | Sequence[str], *, x: int | None = None, y: int | None = None, invert: bool = False)[source]¶
Immutable mask from plain ASCII rows: non-space cells allow painting.
x/y locate the mask in destination coordinates; None centers that axis on every draw, including after resize. Ragged/empty rows are allowed. invert selects the complement, including cells outside the image. The mask owns no animation or resources and works with any Layer child, not just images.
- rows: tuple[str, ...]¶
- x: int | None¶
- y: int | None¶
- invert: bool¶
- width: int¶
ASCII image transitions; masks and styling belong to generic Scene layers.
- class bruhanimate.image._CycleClock(duration: float, repeat: bool, hold: float, gap: float, reverse: bool = False)[source]¶
Shared simulation clock, with bounded work across arbitrarily many cycles.
- property visible: bool¶
- property transition_time: float¶
forwards, held, then optionally backwards.
- Type:
Time sampled by the effect
- class bruhanimate.image._ImageTransition(image: str | Sequence[str], *, x: float | None, y: float | None, duration: float, seed: int | None, repeat: bool, hold_duration: float, repeat_delay: float, reverse: bool)[source]¶
Implementation sharing, not an additional public content hierarchy.
- __init__(image: str | Sequence[str], *, x: float | None, y: float | None, duration: float, seed: int | None, repeat: bool, hold_duration: float, repeat_delay: float, reverse: bool) None[source]¶
- property duration: float¶
Transition time in seconds, excluding the optional hold and gap.
- property finished: bool¶
Empty images finish immediately; non-empty repeating images never do.
- class bruhanimate.image.GlitchImageEffect(image: str | Sequence[str], *, x: float | None = None, y: float | None = None, characters_per_second: float = 18, travel_time: float = 0.65, seed: int | None = None, repeat: bool = False, hold_duration: float = 3, repeat_delay: float = 1, reverse: bool = False)[source]¶
Fly glyphs from the image’s left edge, flicker, then settle.
Non-space glyphs launch in column order (top to bottom within a column), at characters_per_second. Each travels from one column before the image’s left edge over travel_time seconds, jittering by at most one row and changing symbols at 20 Hz. Settled glyphs paint above flying ones. Spaces are transparent.
x/y locate the final image; None centers each axis on the draw viewport. All timing uses update(dt) simulation seconds. Draw never advances state or randomness. Seeded glitches repeat independently of draw frequency. Resize recenters without restarting. duration is the time until the last arrival. finished becomes True after one entrance and the final image stays visible. repeat=True instead flies in, holds for hold_duration seconds, hides for repeat_delay seconds and repeats forever. Empty images finish immediately.
reverse=True adds a backwards leg after the hold, retracing the same seeded frames over the same duration. With repeat=True the hidden gap follows that exit. Without repetition, finished waits for the return to the initial state. duration always describes one leg, excluding hold/gap.
Supply plain single-cell text or text_to_image() rows. Rate and travel_time must be positive/finite; hold/gap must be non-negative/finite. Options are configured at construction. No children, external resources, or input are owned. start/close are safe to repeat without resetting progress. Use on_complete=”hold” to keep a Scene background running after a single entrance.
- class bruhanimate.image.FocusImageEffect(image: str | Sequence[str], *, x: float | None = None, y: float | None = None, duration: float = 2, scatter: tuple[int, int] = (12, 4), seed: int | None = None, repeat: bool = False, hold_duration: float = 3, repeat_delay: float = 1, reverse: bool = False)[source]¶
Assemble ASCII glyphs from seeded scattered positions into their image.
scatter=(columns, rows) bounds random offsets from each glyph’s final cell. Coordinates interpolate linearly over duration seconds. Colliding glyphs paint in original row order; the final image is exact. Scatter must be a pair of non-negative integers. It is relative to the image, not terminal dimensions, so resizing only recenters; clipped particles continue their trajectories.
x/y default to centering. Spaces are transparent. Seed, timing, completion, repetition, lifecycle and plain single-cell input follow GlitchImageEffect: update advances simulation, draw is pure, repeat=True holds then hides before replaying the same scatter. duration is positive/finite; hold/gap are non-negative/finite seconds. The final image remains after a single entrance. No child ownership, input handling or external resources are involved.
reverse=True retraces the same scatter after the hold. Repeating cycles then hide during the gap; a single round trip finishes at the scattered start state.
- class bruhanimate.image.DissolveImageEffect(image: str | Sequence[str], *, x: float | None = None, y: float | None = None, duration: float = 2, direction: Literal['in', 'out'] = 'in', seed: int | None = None, repeat: bool = False, hold_duration: float = 3, repeat_delay: float = 1, reverse: bool = False)[source]¶
Reveal or remove ASCII glyphs in a seeded random order.
direction=”in” begins blank and ends with the image; “out” begins with the image and ends blank. duration is positive, finite simulation seconds. Spaces are transparent; x/y default to centering on the draw viewport. Resize preserves progress and the random order. Seeded cycles replay the same order. repeat=True holds the final state for hold_duration seconds, paints nothing for repeat_delay seconds, then repeats. For an exit, the held final state is blank. Non-empty repeats never finish; one transition finishes at duration and retains its final state. Empty images finish at once.
Plain single-cell input, lifecycle and timing follow GlitchImageEffect. Only update advances time; draw is pure. Configure options at construction. No child ownership, external resources or input routing are involved.
reverse=True retraces the random order after the hold: an entrance dissolves out, while an exit dissolves back in. A single round trip finishes in its initial state; repeating cycles hide for repeat_delay before starting again.
- class bruhanimate.image.WipeImageEffect(image: str | Sequence[str], *, x: float | None = None, y: float | None = None, duration: float = 2, direction: Literal['left', 'right', 'up', 'down'] = 'right', repeat: bool = False, hold_duration: float = 3, repeat_delay: float = 1, reverse: bool = False)[source]¶
Reveal text with a straight edge moving in direction across its bounds.
right reveals from the left, left from the right, down from the top, and up from the bottom. Spaces remain transparent. duration is positive simulation seconds for one leg. reverse retraces the wipe after hold_duration; repeat adds a blank repeat_delay before a new cycle. A non-repeating reverse ends blank; a normal one-shot retains the full image. Empty images finish at once. x/y=None centers on the current viewport, retaining progress across resize. Construction is inert; draw is pure. No resources, children or input are owned.