Skip to content

Step 3: Move

So far, released characters appear in place. In Spray they fly out from a single point on the edge of the canvas and land in their place in the text. In this step each character gets a path from that point to its home.

Engine chapters used: Canvas and coordinates, Motion.

Update the module

Replace myspray.py with this version:

myspray.py
"""MySpray, tutorial step 3: move the characters from the spray origin to their homes.

Released characters fly from the middle of the canvas's right edge to their place in the text.
"""

from __future__ import annotations

import random
from dataclasses import dataclass

from terminaltexteffects import Coord, EffectCharacter, easing
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:
        """Give every character a path home from the spray origin, and queue it for release."""
        canvas = self.terminal.canvas
        spray_origin_map = {
            "center": canvas.center,
            "n": Coord(canvas.center_column, canvas.top),
            "nw": Coord(canvas.left, canvas.top),
            "w": Coord(canvas.left, canvas.center_row),
            "sw": Coord(canvas.left, canvas.bottom),
            "s": Coord(canvas.center_column, canvas.bottom),
            "se": Coord(canvas.right - 1, canvas.bottom),
            "e": Coord(canvas.right - 1, canvas.center_row),
            "ne": Coord(canvas.right - 1, canvas.top),
        }
        spray_origin = spray_origin_map["e"]
        for character in self.terminal.get_characters():
            character.motion.set_coordinate(spray_origin)
            input_coord_path = character.motion.new_path(
                speed=random.uniform(0.6, 1.4),
                ease=easing.out_expo,
            )
            input_coord_path.new_waypoint(character.input_coord)
            character.motion.activate_path(input_coord_path)
            self.pending_chars.append(character)
        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

The spray origin

spray_origin_map names nine positions on the canvas: its center, the middle of each edge, and each corner. They use canvas bounds rather than text bounds, so the spray starts from the edge of the canvas even when the canvas is larger than the text. Positions on the right edge use canvas.right - 1, one column in from the edge.

MySpray always uses "e", the middle of the right edge, for now. Step 6 turns the position into an option, which is why all nine are listed already.

A path for every character

For each character, build() now:

  1. Moves the character to the spray origin with set_coordinate().
  2. Creates a path with a random speed between 0.6 and 1.4 and the out_expo easing, so characters leave the origin fast and slow down as they arrive.
  3. Adds one waypoint, the character's home, input_coord.
  4. Activates the path. Because the character isn't in active_characters yet, it doesn't move until it is released.

The random speeds spread out the arrival times, so the text doesn't land all at once.

__next__ is unchanged. Releasing a character now adds a character with an unfinished path to active_characters, so it stays active and moves each tick until it reaches home. The effect ends when the last character lands.

Run it

tte --input-file tutorial_input.txt myspray

Characters stream out from the right edge, spread across the text, and settle into place:

MySpray after step 3: characters fly from the right edge to their places

Moving characters cross characters that have already landed. Every character is on layer 0, so where two overlap, the one with the higher character_id is drawn on top, and a flying character can disappear behind a settled one.

Next step

Step 4 uses events to put moving characters on a higher layer, so they always pass in front.