Step 4: Layer
At the end of step 3, every character was on layer 0, so a flying character could be hidden behind one that had already landed. In this step two events keep moving characters in front: one raises a character to layer 1 when its path starts, and the other lowers it back to layer 0 when it lands.
Engine chapters used: Characters, Events.
Update the module
Replace myspray.py with this version:
"""MySpray, tutorial step 4: draw moving characters in front of settled ones.
Events move each character to layer 1 while it flies and back to layer 0 when it lands.
"""
from __future__ import annotations
import random
from dataclasses import dataclass
from terminaltexteffects import Coord, EffectCharacter, EventHandler, 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.event_handler.register_event(
EventHandler.Event.PATH_ACTIVATED,
input_coord_path,
EventHandler.Action.SET_LAYER,
1,
)
character.event_handler.register_event(
EventHandler.Event.PATH_COMPLETE,
input_coord_path,
EventHandler.Action.SET_LAYER,
0,
)
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
build() registers two events on each character's path, between creating the path and activating it:
PATH_ACTIVATEDwithSET_LAYERand target1: when the path starts, the character moves to layer 1.PATH_COMPLETEwithSET_LAYERand target0: when the character reaches home, it returns to layer 0.
Characters on a higher layer are drawn on top, so a flying character on layer 1 always shows in front of the settled characters on layer 0.
The order matters. PATH_ACTIVATED fires the moment activate_path() is called, here during build(), long before
the character is released. Registering the event after activate_path() would miss it, and the character would stay
on layer 0. Because the layer is set during build(), every queued character is already on layer 1 when it is released.
Path also has a layer argument that does the same as the first event. MySpray uses events to match the built-in
Spray effect, and because the second event, lowering the layer on landing, has no shortcut.
Run it
The animation looks like step 3, except that flying characters now always pass in front of characters that have already landed:

Next step
Step 5 colors the characters: each one fades from a random color into its place in a gradient across the text.