Skip to content

Step 2: Release

In step 1 the whole text appeared in a single frame. Spray builds the text up instead, releasing characters in a random order, a few per frame. In this step you add that timing, before any character moves.

Engine chapters used: Frame loop, Characters.

Update the module

Replace myspray.py with this version:

myspray.py
"""MySpray, tutorial step 2: release the characters a few at a time.

Characters appear in a random order, one or more per frame, until the whole text is shown.
"""

from __future__ import annotations

import random
from dataclasses import dataclass

from terminaltexteffects import EffectCharacter
from terminaltexteffects.engine.base_config import BaseConfig
from terminaltexteffects.engine.base_effect import BaseEffect, BaseEffectIterator
from terminaltexteffects.utils import argutils


def get_effect_resources() -> tuple[str, type[BaseEffect], type[BaseConfig]]:
    """Return the command name, effect class, and configuration class TTE registers."""
    return "myspray", MySpray, MySprayConfig


@dataclass
class MySprayConfig(BaseConfig):
    """Configuration for the MySpray effect. It has no options yet."""

    parser_spec = argutils.ParserSpec(
        name="myspray",
        help="Sprays the characters from a single point.",
        description="myspray | Sprays the characters from a single point.",
        epilog="Example: tte myspray",
    )


class MySprayIterator(BaseEffectIterator[MySprayConfig]):
    """Produce the frames of the MySpray effect."""

    def __init__(self, effect: MySpray) -> None:
        """Set up every character before the first frame."""
        super().__init__(effect)
        self.pending_chars: list[EffectCharacter] = []
        self.build()

    def build(self) -> None:
        """Queue every character for release in a random order."""
        self.pending_chars.extend(self.terminal.get_characters())
        random.shuffle(self.pending_chars)
        # Release up to 0.5% of the characters per frame, and at least one.
        self._volume = max(int(len(self.pending_chars) * 0.005), 1)

    def __next__(self) -> str:
        """Release a few characters, advance the animation, and return the frame."""
        if self.pending_chars or self.active_characters:
            for _ in range(random.randint(1, self._volume)):
                if self.pending_chars:
                    next_character = self.pending_chars.pop()
                    self.terminal.set_character_visibility(next_character, is_visible=True)
                    self.active_characters.add(next_character)
            self.update()
            return self.frame
        raise StopIteration


class MySpray(BaseEffect[MySprayConfig]):
    """Sprays the characters from a single point."""

    @property
    def _config_cls(self) -> type[MySprayConfig]:
        return MySprayConfig

    @property
    def _iterator_cls(self) -> type[MySprayIterator]:
        return MySprayIterator

What changed

A queue of characters

build() puts every character in self.pending_chars and shuffles it, so the release order is random. Spray keeps setup in a build() method called from __init__; the setup grows over the next steps.

self._volume is the most characters to release in one frame: 0.5% of the text, and at least one. The tutorial input has 121 characters, so its volume is 1; text with 2,000 characters would release up to 10 per frame.

Releasing characters each frame

Each call to __next__ now does three things:

  1. It pops between 1 and self._volume characters off the queue, makes each one visible, and adds it to self.active_characters.
  2. It calls self.update(), which ticks the active characters.
  3. It returns the frame.

The effect ends when the queue is empty and no character is active.

Released characters have no path or scene yet, so they finish on their first tick and update() removes them from active_characters straight away. They stay visible, so they stay on screen. Adding them to active_characters now means they start moving once step 3 gives them somewhere to go.

Run it

tte --input-file tutorial_input.txt myspray

The characters appear one at a time in a random order until the text is complete:

MySpray after step 2: characters appear one at a time in random order

The order is random, so each run fills in differently.

Next step

Step 3 starts every character at the edge of the canvas and sends it to its place in the text.