Step 5: Color
So far every character has been drawn in the terminal's default color. In Spray, the text ends in a gradient that runs from purple at the bottom, through cyan, to white at the top, and each character fades into its color from a random color of that gradient while it flies. In this step each character gets a scene that does this.
Engine chapters used: Animation, Color.
Update the module
Replace myspray.py with this version:
"""MySpray, tutorial step 5: color the characters.
Each character fades from a random color of the final gradient to its own place in that gradient.
"""
from __future__ import annotations
import random
from dataclasses import dataclass
from terminaltexteffects import Color, Coord, EffectCharacter, EventHandler, Gradient, 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 and a droplet scene, and queue it for release."""
canvas = self.terminal.canvas
final_gradient = Gradient(Color("#8A008A"), Color("#00D1FF"), Color("#FFFFFF"), steps=12)
final_gradient_mapping = final_gradient.build_coordinate_color_mapping(
canvas.text_bottom,
canvas.text_top,
canvas.text_left,
canvas.text_right,
Gradient.Direction.VERTICAL,
)
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,
)
droplet_scn = character.animation.new_scene()
spray_gradient = Gradient(
random.choice(final_gradient.spectrum),
final_gradient_mapping[character.input_coord],
steps=7,
)
droplet_scn.apply_gradient_to_symbols(character.input_symbol, 20, fg_gradient=spray_gradient)
character.animation.activate_scene(droplet_scn)
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 final gradient
build() starts by creating the final gradient, from purple through cyan to white with 12 steps between each pair of
stops. build_coordinate_color_mapping() then gives every coordinate in the text a color from it. With
Gradient.Direction.VERTICAL, every character in a row gets the same color, from the first stop on the bottom row to
the last stop on the top row.
The mapping uses the text bounds, not the canvas bounds, so the gradient spans the text even when the canvas is larger.
A droplet scene for every character
For each character, build() also:
- Creates a scene with
new_scene(). - Builds a short gradient from a random color in the final gradient to the character's own color, with 7 steps, so 8 colors.
- Adds a frame for each of those colors with
apply_gradient_to_symbols(). Every frame shows the input symbol and lasts 20 ticks, so the scene lasts 160 ticks. - Activates the scene.
The random starting colors make the flying characters a mix of the gradient's colors, and each one settles to its place in the gradient.
The scene isn't synced to the path, so it plays at its own pace. Every path here takes under 100 ticks, so each character lands before its scene ends and keeps fading in place. A character stays active until both its path and its scene are complete, so the effect now ends when the last character finishes fading rather than when it lands.
Run it
The characters fly in colored and fade to the final gradient, purple at the bottom and white at the top:

MySpray now looks like the built-in Spray effect with its default options.
Next step
Step 6 adds command-line options for the spray position, speed, colors, and more, in place of the values written into the code.