Skip to content

Scene

Module: terminaltexteffects.engine.animation

Opt-in Caches

New in 0.16.0

cache_easing and cache_appearance are available on development main and are not in the published 0.15.0 package.

Both options default to False. Opt in only after measuring an effect; each cache has overhead that can outweigh its benefit. See Performance Testing for the benchmark harness and a summary of the measurements behind each cache.

Eased-Scene Schedules

Set cache_easing=True when constructing a scene (directly or through Animation.new_scene) to reuse frame-index schedules across scenes with the same built-in easing function and frame durations. Each scene keeps its own frames, visuals, playback cursor, and lifecycle events.

The shared cache retains at most 32 schedules, with at most 4096 ticks per schedule. Scenes hold weak references so eviction can release a schedule; playback reacquires it when needed. Assigning a different ease or adding frames selects a new schedule. Resetting or looping reuses the existing layout. Custom easing functions, longer scenes, and motion-synced scenes keep their ordinary playback behavior. Distinct layouts, short playback, and frequent eviction can cost more than schedule reuse saves.

Encoded Appearance Reuse

Set cache_appearance=True on Scene or Animation.new_scene to reuse immutable ANSI strings when constructing frames with repeated symbols, styles, and resolved foreground/background colors. Frames, visuals, colors, and playback state retain their usual independent ownership; editing a visual's formatted string still invalidates terminal output. Explicit format_symbol() calls retain ordinary formatting.

The shared LRU cache holds at most 2048 encodings. Existing visuals retain their strings after eviction. Subclass/formatter overrides and custom style or color-code values use the ordinary path. Unicode symbol validation and width calculation remain unchanged. Color policy is applied before selecting an encoding, including preexisting colors/bold, XTerm conversion, and no-color output. Lookup overhead can outweigh reuse when appearances vary.

Waves enables both options for its repeated wave scene.

Scene Reference

A Scene is a collection of Frames that can be played in sequence. Scenes can be looped and synced to movement.

Methods:

Name Description
add_frame

Adds a Frame to the Scene.

activate

Activates the Scene.

get_next_visual

Gets the next CharacterVisual in the Scene.

apply_gradient_to_symbols

Applies a gradient effect to a sequence of symbols.

reset_scene

Resets the Scene.

Attributes:

Name Type Description
scene_id str

the ID of the Scene

is_looping bool

Whether the Scene should loop

sync SyncMetric | None

The type of sync to use for the Scene

ease EasingFunction | None

The easing function to use for the Scene

cache_easing bool

Opt into shared frame schedules for built-in easing functions.

cache_appearance bool

Reuse immutable encoded strings during frame construction.

no_color bool

Whether to ignore colors

use_xterm_colors bool

Whether to convert all colors to XTerm-256 colors

frames list[Frame]

The list of Frames in the Scene

played_frames list[Frame]

The list of Frames that have been played

_frame_index int

The current frame index for ordinary playback.

_frame_end_steps list[int]

Cumulative frame-duration boundaries for eased playback.

easing_total_steps int

The total number of steps in the easing function

easing_current_step int

The current step in the easing function

preexisting_colors ColorPair | None

The preexisting colors parsed from the input.

preexisting_bold bool

Whether parsed input bold styling should override frame bold styling.

Source code in terminaltexteffects/engine/animation.py
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
class Scene:
    """A Scene is a collection of Frames that can be played in sequence. Scenes can be looped and synced to movement.

    Methods:
        add_frame: Adds a Frame to the Scene.
        activate: Activates the Scene.
        get_next_visual: Gets the next CharacterVisual in the Scene.
        apply_gradient_to_symbols: Applies a gradient effect to a sequence of symbols.
        reset_scene: Resets the Scene.

    Attributes:
        scene_id (str): the ID of the Scene
        is_looping (bool): Whether the Scene should loop
        sync (Scene.SyncMetric | None): The type of sync to use for the Scene
        ease (easing.EasingFunction | None): The easing function to use for the Scene
        cache_easing (bool): Opt into shared frame schedules for built-in easing functions.
        cache_appearance (bool): Reuse immutable encoded strings during frame construction.
        no_color (bool): Whether to ignore colors
        use_xterm_colors (bool): Whether to convert all colors to XTerm-256 colors
        frames (list[Frame]): The list of Frames in the Scene
        played_frames (list[Frame]): The list of Frames that have been played
        _frame_index (int): The current frame index for ordinary playback.
        _frame_end_steps (list[int]): Cumulative frame-duration boundaries for eased playback.
        easing_total_steps (int): The total number of steps in the easing function
        easing_current_step (int): The current step in the easing function
        preexisting_colors (graphics.ColorPair | None): The preexisting colors parsed from the input.
        preexisting_bold (bool): Whether parsed input bold styling should override frame bold styling.

    """

    xterm_color_map: typing.ClassVar[dict[str, int]] = {}

    class SyncMetric(Enum):
        """Enum for specifying how a Scene synchronizes to motion progress.

        Attributes:
            DISTANCE (int): Sync scene progress to the active path's traveled distance.
            STEP (int): Sync scene progress to the active path's current step count.
            LIGHT (int): Sync scene frames to the active 3D mount's `light`, from the first frame in shadow to the
                last facing the light.
            DEPTH (int): Sync scene frames to the active 3D mount's `nearness`, from the first frame at the far side
                of its body to the last at the near side.

        """

        DISTANCE = auto()
        STEP = auto()
        LIGHT = auto()
        DEPTH = auto()

    def __init__(
        self,
        scene_id: str,
        *,
        is_looping: bool = False,
        sync: SyncMetric | None = None,
        ease: easing.EasingFunction | None = None,
        cache_easing: bool = False,
        cache_appearance: bool = False,
        no_color: bool = False,
        use_xterm_colors: bool = False,
    ) -> None:
        """Initialize a Scene.

        Args:
            scene_id (str): the ID of the Scene
            is_looping (bool, optional): Whether the Scene should loop. Defaults to False.
            sync (Scene.SyncMetric | None, optional): The type of sync to use for the Scene. Defaults to None.
            ease (easing.EasingFunction | None, optional): The easing function to use for the Scene. Defaults to None.
            cache_easing (bool, optional): Reuse built-in easing schedules up to 4096 ticks. Defaults to False.
                Custom callables retain per-tick evaluation. Frames and playback state remain independent.
            cache_appearance (bool, optional): Share bounded immutable encodings during construction.
                Defaults to False. Mutable visuals and frames remain independently owned.
            no_color (bool, optional): Whether to colors should be ignored. Defaults to False.
            use_xterm_colors (bool, optional): Whether to convert all colors to XTerm-256 colors. Defaults to False.

        """
        self.scene_id = scene_id
        self.is_looping = is_looping
        self.sync: Scene.SyncMetric | None = sync
        self.ease: easing.EasingFunction | None = ease
        self.cache_easing = cache_easing
        self.cache_appearance = cache_appearance
        self._easing_schedule: (
            tuple[
                easing.EasingFunction,
                int,
                ReferenceType[_EasingSchedule] | None,
            ]
            | None
        ) = None
        self.no_color = no_color
        self.use_xterm_colors = use_xterm_colors
        self.frames: list[Frame] = []
        self.played_frames: list[Frame] = []
        self._frame_index: int = 0
        self._frame_end_steps: list[int] = []
        self.easing_total_steps: int = 0
        self.easing_current_step: int = 0
        self.preexisting_colors: graphics.ColorPair | None = None
        self.preexisting_bold: bool = False
        self._frame_added_callback: typing.Callable[[CharacterVisual], None] | None = None
        self._visual_width_mask: int = 0

    def _get_easing_schedule(self) -> _EasingSchedule | None:
        """Reuse a schedule while easing and the duration layout remain unchanged.

        `add_frame` changes `easing_total_steps`, invalidating the memo. Resetting
        playback preserves the layout and schedule. Weak references let the shared
        cache release evicted schedules even while scenes remain alive.
        """
        ease = self.ease
        assert ease is not None
        total_steps = self.easing_total_steps
        memo = self._easing_schedule
        valid = memo is not None and memo[0] is ease and memo[1] == total_steps
        if valid and memo is not None:
            if memo[2] is None:
                return None
            schedule = memo[2]()
            if schedule is not None:
                return schedule
        if not (
            isinstance(total_steps, int)
            and 0 < total_steps <= _MAX_EASING_SCHEDULE_STEPS
            and any(ease is f for f in _CACHEABLE_EASING_FUNCTIONS)
        ):
            self._easing_schedule = (ease, total_steps, None)
            return None
        schedule = _get_easing_schedule(ease, total_steps, tuple(self._frame_end_steps))
        self._easing_schedule = (ease, total_steps, ref(schedule))
        return schedule

    def _get_color_code(self, color: graphics.Color | None) -> str | int | None:
        """Get the color code for the given color.

        RGB colors are converted to XTerm-256 colors if use_xterm_colors is True.
        If no_color is True, returns None. Otherwise, returns the RGB color.

        Args:
            color (graphics.Color | None): the color to get the code for

        Returns:
            str | int | None: the color code

        """
        return _get_color_code(
            color,
            no_color=self.no_color,
            use_xterm_colors=self.use_xterm_colors,
            xterm_color_map=self.xterm_color_map,
        )

    def add_frame(
        self,
        symbol: str,
        duration: int,
        *,
        colors: graphics.ColorPair | None = None,
        bold: bool = False,
        dim: bool = False,
        italic: bool = False,
        underline: bool = False,
        blink: bool = False,
        reverse: bool = False,
        hidden: bool = False,
        strike: bool = False,
    ) -> None:
        """Add a Frame to the Scene with the given symbol, duration, color, and graphical modes.

        If `preexisting_colors` is set on the Scene, those colors override the `colors`
        argument for every frame added to the Scene.

        Args:
            symbol (str): the symbol to show
            duration (int): the number of frames to use the Frame
            colors (graphics.ColorPair | None, optional): the colors to use. Defaults to None.
            bold (bool, optional): bold mode. Defaults to False.
            dim (bool, optional): dim mode. Defaults to False.
            italic (bool, optional): italic mode. Defaults to False.
            underline (bool, optional): underline mode. Defaults to False.
            blink (bool, optional): blink mode. Defaults to False.
            reverse (bool, optional): reverse mode. Defaults to False.
            hidden (bool, optional): hidden mode. Defaults to False.
            strike (bool, optional): strike mode. Defaults to False.

        Raises:
            FrameDurationError: if the frame duration is less than 1
            InvalidSymbolError: if `symbol` does not contain one independently printable Unicode code point

        """
        # override fg and bg colors if they are set in the Scene due to existing color handling = always
        if self.preexisting_colors:
            colors = self.preexisting_colors
        if self.preexisting_bold:
            bold = True

        # get the color code for the fg and bg colors
        if colors:
            char_vis_fg_color = self._get_color_code(colors.fg)
            char_vis_bg_color = self._get_color_code(colors.bg)
        else:
            char_vis_fg_color = None
            char_vis_bg_color = None

        if duration < 1:
            raise FrameDurationError(duration)
        char_vis = CharacterVisual(
            symbol,
            bold=bold,
            dim=dim,
            italic=italic,
            underline=underline,
            blink=blink,
            reverse=reverse,
            hidden=hidden,
            strike=strike,
            colors=colors,
            _fg_color_code=char_vis_fg_color,
            _bg_color_code=char_vis_bg_color,
            _cache_appearance=self.cache_appearance,
        )
        width_bit = 1 << (char_vis.cell_width - 1)
        if not self._visual_width_mask & width_bit:
            self._visual_width_mask |= width_bit
            if self._frame_added_callback is not None:
                self._frame_added_callback(char_vis)
        frame = Frame(char_vis, duration)
        self.frames.append(frame)
        self.easing_total_steps += frame.duration
        self._frame_end_steps.append(self.easing_total_steps)

    def activate(self) -> CharacterVisual:
        """Activate the Scene by returning the current frame's `CharacterVisual`.

        Called by the `Animation` object when the Scene is activated.

        Raises:
            ActivateEmptySceneError: if the Scene has no frames

        Returns:
            CharacterVisual: the first frame's visual.

        """
        if self.frames:
            return self.frames[self._frame_index].character_visual
        raise ActivateEmptySceneError(self)

    def get_next_visual(self) -> CharacterVisual:
        """Get the next CharacterVisual in the Scene.

        Retrieve the current frame from `frames`, then increment the frame's `ticks_elapsed`.
        If `ticks_elapsed` reaches the frame duration, reset that frame's `ticks_elapsed` to `0`
        and advance the playback index. When a non-looping Scene completes, move all frames
        to `played_frames`; looping Scenes reset their playback index to begin again.
        Return the current frame's `CharacterVisual`.

        Returns:
            CharacterVisual: The visual of the current frame in the Scene.

        """
        current_frame = self.frames[self._frame_index]
        next_visual = current_frame.character_visual
        current_frame.ticks_elapsed += 1
        if current_frame.ticks_elapsed == current_frame.duration:
            current_frame.ticks_elapsed = 0
            self._frame_index += 1
            if self._frame_index == len(self.frames):
                if self.is_looping:
                    self._frame_index = 0
                else:
                    self.played_frames.extend(self.frames)
                    self.frames.clear()
                    self._frame_index = 0
        return next_visual

    def apply_gradient_to_symbols(
        self,
        symbols: typing.Sequence[str],
        duration: int,
        *,
        fg_gradient: graphics.Gradient | None = None,
        bg_gradient: graphics.Gradient | None = None,
    ) -> None:
        """Apply a gradient effect to a sequence of symbols and add each symbol as a frame to the Scene.

        Args:
            symbols (Sequence[str]): The sequence of symbols to apply the gradient to.
            duration (int): The duration to show each frame.
            fg_gradient (graphics.Gradient | None): The foreground gradient to apply. Defaults to None.
            bg_gradient (graphics.Gradient | None): The background gradient to apply. Defaults to None.

        Returns:
            None

        Raises:
            AnimationSceneError: if gradients are invalid
            InvalidSymbolError: if a symbol does not contain one independently printable Unicode code point

        """
        T = typing.TypeVar("T")
        R = typing.TypeVar("R")

        def cyclic_distribution(
            larger_seq: typing.Sequence[T],
            smaller_seq: typing.Sequence[R],
        ) -> typing.Generator[tuple[T, R], None, None]:
            """Distributes the elements of a smaller sequence cyclically across a larger sequence with overflow.

            Example:
                cyclic_distribution([1, 2, 3, 4, 5], [a, b]) -> [(1, a), (2, a), (3, a), (4, b), (5, b)]

            Args:
                larger_seq (typing.Sequence[T]): the larger sequence
                smaller_seq (typing.Sequence[R]): the smaller sequence

            Yields:
                typing.Generator[tuple[T, R], None, None]: a generator yielding tuples of elements from the
                    larger and smaller sequences

            """
            repeat_factor = len(larger_seq) // len(smaller_seq)
            overflow_count = len(larger_seq) % len(smaller_seq)
            overflow_used = False
            smaller_index = 0
            current_repeat_factor = 0
            for larger_seq_element in larger_seq:
                if current_repeat_factor >= repeat_factor:
                    if overflow_count:
                        if overflow_used:
                            smaller_index += 1
                            current_repeat_factor = 0
                            overflow_used = False
                        else:
                            overflow_used = True
                            overflow_count -= 1
                    else:
                        smaller_index += 1
                        current_repeat_factor = 0

                current_repeat_factor += 1
                yield larger_seq_element, smaller_seq[smaller_index]

        if fg_gradient is None and bg_gradient is None:
            message = "Foreground and background gradient are None. At least one gradient must be provided."
            raise AnimationSceneError(
                message,
            )
        if not ((fg_gradient and fg_gradient.spectrum) or (bg_gradient and bg_gradient.spectrum)):
            message = (
                "Foreground and background gradient are empty. At least one gradient must have at least one color."
            )
            raise AnimationSceneError(message)
        if not symbols:
            message = "At least one symbol must be provided."
            raise AnimationSceneError(message)
        for symbol in symbols:
            get_symbol_cell_width(symbol)
        color_pairs: list[graphics.ColorPair] = []
        if fg_gradient and fg_gradient.spectrum and bg_gradient and bg_gradient.spectrum:
            if len(fg_gradient.spectrum) >= len(bg_gradient.spectrum):
                color_pairs = [
                    graphics.ColorPair(fg=fg_color, bg=bg_color)
                    for fg_color, bg_color in cyclic_distribution(fg_gradient.spectrum, bg_gradient.spectrum)
                ]
            else:
                color_pairs = [
                    graphics.ColorPair(fg=fg_color, bg=bg_color)
                    for bg_color, fg_color in cyclic_distribution(bg_gradient.spectrum, fg_gradient.spectrum)
                ]
        elif fg_gradient and fg_gradient.spectrum:
            color_pairs = [graphics.ColorPair(fg=color, bg=None) for color in fg_gradient.spectrum]
        elif bg_gradient and bg_gradient.spectrum:
            color_pairs = [graphics.ColorPair(fg=None, bg=color) for color in bg_gradient.spectrum]

        if len(symbols) >= len(color_pairs):
            for symbol, colors in cyclic_distribution(symbols, color_pairs):
                self.add_frame(symbol, duration, colors=colors)
        else:
            for colors, symbol in cyclic_distribution(color_pairs, symbols):
                self.add_frame(symbol, duration, colors=colors)

    def reset_scene(self) -> None:
        """Reset the Scene to its initial playback state.

        All remaining frames are moved back into the full frame sequence, each frame's
        `ticks_elapsed` is reset to `0`, `played_frames` is cleared, and
        `easing_current_step` is reset to `0`.
        """
        for sequence in self.frames:
            sequence.ticks_elapsed = 0
            self.played_frames.append(sequence)
        self.frames.clear()
        self.frames.extend(self.played_frames)
        self.played_frames.clear()
        self._frame_index = 0
        self.easing_current_step = 0

    def __eq__(self, other: object) -> bool:
        """Check if two Scene objects are equal based on their scene_id."""
        if not isinstance(other, Scene):
            return NotImplemented
        return self.scene_id == other.scene_id

    def __hash__(self) -> int:
        """Return the hash value of the Scene based on its scene_id."""
        return hash(self.scene_id)

SyncMetric

Bases: Enum

Enum for specifying how a Scene synchronizes to motion progress.

Attributes:

Name Type Description
DISTANCE int

Sync scene progress to the active path's traveled distance.

STEP int

Sync scene progress to the active path's current step count.

LIGHT int

Sync scene frames to the active 3D mount's light, from the first frame in shadow to the last facing the light.

DEPTH int

Sync scene frames to the active 3D mount's nearness, from the first frame at the far side of its body to the last at the near side.

Source code in terminaltexteffects/engine/animation.py
class SyncMetric(Enum):
    """Enum for specifying how a Scene synchronizes to motion progress.

    Attributes:
        DISTANCE (int): Sync scene progress to the active path's traveled distance.
        STEP (int): Sync scene progress to the active path's current step count.
        LIGHT (int): Sync scene frames to the active 3D mount's `light`, from the first frame in shadow to the
            last facing the light.
        DEPTH (int): Sync scene frames to the active 3D mount's `nearness`, from the first frame at the far side
            of its body to the last at the near side.

    """

    DISTANCE = auto()
    STEP = auto()
    LIGHT = auto()
    DEPTH = auto()

__eq__(other)

Check if two Scene objects are equal based on their scene_id.

Source code in terminaltexteffects/engine/animation.py
def __eq__(self, other: object) -> bool:
    """Check if two Scene objects are equal based on their scene_id."""
    if not isinstance(other, Scene):
        return NotImplemented
    return self.scene_id == other.scene_id

__hash__()

Return the hash value of the Scene based on its scene_id.

Source code in terminaltexteffects/engine/animation.py
def __hash__(self) -> int:
    """Return the hash value of the Scene based on its scene_id."""
    return hash(self.scene_id)

__init__(scene_id, *, is_looping=False, sync=None, ease=None, cache_easing=False, cache_appearance=False, no_color=False, use_xterm_colors=False)

Initialize a Scene.

Parameters:

Name Type Description Default
scene_id str

the ID of the Scene

required
is_looping bool

Whether the Scene should loop. Defaults to False.

False
sync SyncMetric | None

The type of sync to use for the Scene. Defaults to None.

None
ease EasingFunction | None

The easing function to use for the Scene. Defaults to None.

None
cache_easing bool

Reuse built-in easing schedules up to 4096 ticks. Defaults to False. Custom callables retain per-tick evaluation. Frames and playback state remain independent.

False
cache_appearance bool

Share bounded immutable encodings during construction. Defaults to False. Mutable visuals and frames remain independently owned.

False
no_color bool

Whether to colors should be ignored. Defaults to False.

False
use_xterm_colors bool

Whether to convert all colors to XTerm-256 colors. Defaults to False.

False
Source code in terminaltexteffects/engine/animation.py
def __init__(
    self,
    scene_id: str,
    *,
    is_looping: bool = False,
    sync: SyncMetric | None = None,
    ease: easing.EasingFunction | None = None,
    cache_easing: bool = False,
    cache_appearance: bool = False,
    no_color: bool = False,
    use_xterm_colors: bool = False,
) -> None:
    """Initialize a Scene.

    Args:
        scene_id (str): the ID of the Scene
        is_looping (bool, optional): Whether the Scene should loop. Defaults to False.
        sync (Scene.SyncMetric | None, optional): The type of sync to use for the Scene. Defaults to None.
        ease (easing.EasingFunction | None, optional): The easing function to use for the Scene. Defaults to None.
        cache_easing (bool, optional): Reuse built-in easing schedules up to 4096 ticks. Defaults to False.
            Custom callables retain per-tick evaluation. Frames and playback state remain independent.
        cache_appearance (bool, optional): Share bounded immutable encodings during construction.
            Defaults to False. Mutable visuals and frames remain independently owned.
        no_color (bool, optional): Whether to colors should be ignored. Defaults to False.
        use_xterm_colors (bool, optional): Whether to convert all colors to XTerm-256 colors. Defaults to False.

    """
    self.scene_id = scene_id
    self.is_looping = is_looping
    self.sync: Scene.SyncMetric | None = sync
    self.ease: easing.EasingFunction | None = ease
    self.cache_easing = cache_easing
    self.cache_appearance = cache_appearance
    self._easing_schedule: (
        tuple[
            easing.EasingFunction,
            int,
            ReferenceType[_EasingSchedule] | None,
        ]
        | None
    ) = None
    self.no_color = no_color
    self.use_xterm_colors = use_xterm_colors
    self.frames: list[Frame] = []
    self.played_frames: list[Frame] = []
    self._frame_index: int = 0
    self._frame_end_steps: list[int] = []
    self.easing_total_steps: int = 0
    self.easing_current_step: int = 0
    self.preexisting_colors: graphics.ColorPair | None = None
    self.preexisting_bold: bool = False
    self._frame_added_callback: typing.Callable[[CharacterVisual], None] | None = None
    self._visual_width_mask: int = 0

activate()

Activate the Scene by returning the current frame's CharacterVisual.

Called by the Animation object when the Scene is activated.

Raises:

Type Description
ActivateEmptySceneError

if the Scene has no frames

Returns:

Name Type Description
CharacterVisual CharacterVisual

the first frame's visual.

Source code in terminaltexteffects/engine/animation.py
def activate(self) -> CharacterVisual:
    """Activate the Scene by returning the current frame's `CharacterVisual`.

    Called by the `Animation` object when the Scene is activated.

    Raises:
        ActivateEmptySceneError: if the Scene has no frames

    Returns:
        CharacterVisual: the first frame's visual.

    """
    if self.frames:
        return self.frames[self._frame_index].character_visual
    raise ActivateEmptySceneError(self)

add_frame(symbol, duration, *, colors=None, bold=False, dim=False, italic=False, underline=False, blink=False, reverse=False, hidden=False, strike=False)

Add a Frame to the Scene with the given symbol, duration, color, and graphical modes.

If preexisting_colors is set on the Scene, those colors override the colors argument for every frame added to the Scene.

Parameters:

Name Type Description Default
symbol str

the symbol to show

required
duration int

the number of frames to use the Frame

required
colors ColorPair | None

the colors to use. Defaults to None.

None
bold bool

bold mode. Defaults to False.

False
dim bool

dim mode. Defaults to False.

False
italic bool

italic mode. Defaults to False.

False
underline bool

underline mode. Defaults to False.

False
blink bool

blink mode. Defaults to False.

False
reverse bool

reverse mode. Defaults to False.

False
hidden bool

hidden mode. Defaults to False.

False
strike bool

strike mode. Defaults to False.

False

Raises:

Type Description
FrameDurationError

if the frame duration is less than 1

InvalidSymbolError

if symbol does not contain one independently printable Unicode code point

Source code in terminaltexteffects/engine/animation.py
def add_frame(
    self,
    symbol: str,
    duration: int,
    *,
    colors: graphics.ColorPair | None = None,
    bold: bool = False,
    dim: bool = False,
    italic: bool = False,
    underline: bool = False,
    blink: bool = False,
    reverse: bool = False,
    hidden: bool = False,
    strike: bool = False,
) -> None:
    """Add a Frame to the Scene with the given symbol, duration, color, and graphical modes.

    If `preexisting_colors` is set on the Scene, those colors override the `colors`
    argument for every frame added to the Scene.

    Args:
        symbol (str): the symbol to show
        duration (int): the number of frames to use the Frame
        colors (graphics.ColorPair | None, optional): the colors to use. Defaults to None.
        bold (bool, optional): bold mode. Defaults to False.
        dim (bool, optional): dim mode. Defaults to False.
        italic (bool, optional): italic mode. Defaults to False.
        underline (bool, optional): underline mode. Defaults to False.
        blink (bool, optional): blink mode. Defaults to False.
        reverse (bool, optional): reverse mode. Defaults to False.
        hidden (bool, optional): hidden mode. Defaults to False.
        strike (bool, optional): strike mode. Defaults to False.

    Raises:
        FrameDurationError: if the frame duration is less than 1
        InvalidSymbolError: if `symbol` does not contain one independently printable Unicode code point

    """
    # override fg and bg colors if they are set in the Scene due to existing color handling = always
    if self.preexisting_colors:
        colors = self.preexisting_colors
    if self.preexisting_bold:
        bold = True

    # get the color code for the fg and bg colors
    if colors:
        char_vis_fg_color = self._get_color_code(colors.fg)
        char_vis_bg_color = self._get_color_code(colors.bg)
    else:
        char_vis_fg_color = None
        char_vis_bg_color = None

    if duration < 1:
        raise FrameDurationError(duration)
    char_vis = CharacterVisual(
        symbol,
        bold=bold,
        dim=dim,
        italic=italic,
        underline=underline,
        blink=blink,
        reverse=reverse,
        hidden=hidden,
        strike=strike,
        colors=colors,
        _fg_color_code=char_vis_fg_color,
        _bg_color_code=char_vis_bg_color,
        _cache_appearance=self.cache_appearance,
    )
    width_bit = 1 << (char_vis.cell_width - 1)
    if not self._visual_width_mask & width_bit:
        self._visual_width_mask |= width_bit
        if self._frame_added_callback is not None:
            self._frame_added_callback(char_vis)
    frame = Frame(char_vis, duration)
    self.frames.append(frame)
    self.easing_total_steps += frame.duration
    self._frame_end_steps.append(self.easing_total_steps)

apply_gradient_to_symbols(symbols, duration, *, fg_gradient=None, bg_gradient=None)

Apply a gradient effect to a sequence of symbols and add each symbol as a frame to the Scene.

Parameters:

Name Type Description Default
symbols Sequence[str]

The sequence of symbols to apply the gradient to.

required
duration int

The duration to show each frame.

required
fg_gradient Gradient | None

The foreground gradient to apply. Defaults to None.

None
bg_gradient Gradient | None

The background gradient to apply. Defaults to None.

None

Returns:

Type Description
None

None

Raises:

Type Description
AnimationSceneError

if gradients are invalid

InvalidSymbolError

if a symbol does not contain one independently printable Unicode code point

Source code in terminaltexteffects/engine/animation.py
def apply_gradient_to_symbols(
    self,
    symbols: typing.Sequence[str],
    duration: int,
    *,
    fg_gradient: graphics.Gradient | None = None,
    bg_gradient: graphics.Gradient | None = None,
) -> None:
    """Apply a gradient effect to a sequence of symbols and add each symbol as a frame to the Scene.

    Args:
        symbols (Sequence[str]): The sequence of symbols to apply the gradient to.
        duration (int): The duration to show each frame.
        fg_gradient (graphics.Gradient | None): The foreground gradient to apply. Defaults to None.
        bg_gradient (graphics.Gradient | None): The background gradient to apply. Defaults to None.

    Returns:
        None

    Raises:
        AnimationSceneError: if gradients are invalid
        InvalidSymbolError: if a symbol does not contain one independently printable Unicode code point

    """
    T = typing.TypeVar("T")
    R = typing.TypeVar("R")

    def cyclic_distribution(
        larger_seq: typing.Sequence[T],
        smaller_seq: typing.Sequence[R],
    ) -> typing.Generator[tuple[T, R], None, None]:
        """Distributes the elements of a smaller sequence cyclically across a larger sequence with overflow.

        Example:
            cyclic_distribution([1, 2, 3, 4, 5], [a, b]) -> [(1, a), (2, a), (3, a), (4, b), (5, b)]

        Args:
            larger_seq (typing.Sequence[T]): the larger sequence
            smaller_seq (typing.Sequence[R]): the smaller sequence

        Yields:
            typing.Generator[tuple[T, R], None, None]: a generator yielding tuples of elements from the
                larger and smaller sequences

        """
        repeat_factor = len(larger_seq) // len(smaller_seq)
        overflow_count = len(larger_seq) % len(smaller_seq)
        overflow_used = False
        smaller_index = 0
        current_repeat_factor = 0
        for larger_seq_element in larger_seq:
            if current_repeat_factor >= repeat_factor:
                if overflow_count:
                    if overflow_used:
                        smaller_index += 1
                        current_repeat_factor = 0
                        overflow_used = False
                    else:
                        overflow_used = True
                        overflow_count -= 1
                else:
                    smaller_index += 1
                    current_repeat_factor = 0

            current_repeat_factor += 1
            yield larger_seq_element, smaller_seq[smaller_index]

    if fg_gradient is None and bg_gradient is None:
        message = "Foreground and background gradient are None. At least one gradient must be provided."
        raise AnimationSceneError(
            message,
        )
    if not ((fg_gradient and fg_gradient.spectrum) or (bg_gradient and bg_gradient.spectrum)):
        message = (
            "Foreground and background gradient are empty. At least one gradient must have at least one color."
        )
        raise AnimationSceneError(message)
    if not symbols:
        message = "At least one symbol must be provided."
        raise AnimationSceneError(message)
    for symbol in symbols:
        get_symbol_cell_width(symbol)
    color_pairs: list[graphics.ColorPair] = []
    if fg_gradient and fg_gradient.spectrum and bg_gradient and bg_gradient.spectrum:
        if len(fg_gradient.spectrum) >= len(bg_gradient.spectrum):
            color_pairs = [
                graphics.ColorPair(fg=fg_color, bg=bg_color)
                for fg_color, bg_color in cyclic_distribution(fg_gradient.spectrum, bg_gradient.spectrum)
            ]
        else:
            color_pairs = [
                graphics.ColorPair(fg=fg_color, bg=bg_color)
                for bg_color, fg_color in cyclic_distribution(bg_gradient.spectrum, fg_gradient.spectrum)
            ]
    elif fg_gradient and fg_gradient.spectrum:
        color_pairs = [graphics.ColorPair(fg=color, bg=None) for color in fg_gradient.spectrum]
    elif bg_gradient and bg_gradient.spectrum:
        color_pairs = [graphics.ColorPair(fg=None, bg=color) for color in bg_gradient.spectrum]

    if len(symbols) >= len(color_pairs):
        for symbol, colors in cyclic_distribution(symbols, color_pairs):
            self.add_frame(symbol, duration, colors=colors)
    else:
        for colors, symbol in cyclic_distribution(color_pairs, symbols):
            self.add_frame(symbol, duration, colors=colors)

get_next_visual()

Get the next CharacterVisual in the Scene.

Retrieve the current frame from frames, then increment the frame's ticks_elapsed. If ticks_elapsed reaches the frame duration, reset that frame's ticks_elapsed to 0 and advance the playback index. When a non-looping Scene completes, move all frames to played_frames; looping Scenes reset their playback index to begin again. Return the current frame's CharacterVisual.

Returns:

Name Type Description
CharacterVisual CharacterVisual

The visual of the current frame in the Scene.

Source code in terminaltexteffects/engine/animation.py
def get_next_visual(self) -> CharacterVisual:
    """Get the next CharacterVisual in the Scene.

    Retrieve the current frame from `frames`, then increment the frame's `ticks_elapsed`.
    If `ticks_elapsed` reaches the frame duration, reset that frame's `ticks_elapsed` to `0`
    and advance the playback index. When a non-looping Scene completes, move all frames
    to `played_frames`; looping Scenes reset their playback index to begin again.
    Return the current frame's `CharacterVisual`.

    Returns:
        CharacterVisual: The visual of the current frame in the Scene.

    """
    current_frame = self.frames[self._frame_index]
    next_visual = current_frame.character_visual
    current_frame.ticks_elapsed += 1
    if current_frame.ticks_elapsed == current_frame.duration:
        current_frame.ticks_elapsed = 0
        self._frame_index += 1
        if self._frame_index == len(self.frames):
            if self.is_looping:
                self._frame_index = 0
            else:
                self.played_frames.extend(self.frames)
                self.frames.clear()
                self._frame_index = 0
    return next_visual

reset_scene()

Reset the Scene to its initial playback state.

All remaining frames are moved back into the full frame sequence, each frame's ticks_elapsed is reset to 0, played_frames is cleared, and easing_current_step is reset to 0.

Source code in terminaltexteffects/engine/animation.py
def reset_scene(self) -> None:
    """Reset the Scene to its initial playback state.

    All remaining frames are moved back into the full frame sequence, each frame's
    `ticks_elapsed` is reset to `0`, `played_frames` is cleared, and
    `easing_current_step` is reset to `0`.
    """
    for sequence in self.frames:
        sequence.ticks_elapsed = 0
        self.played_frames.append(sequence)
    self.frames.clear()
    self.frames.extend(self.played_frames)
    self.played_frames.clear()
    self._frame_index = 0
    self.easing_current_step = 0