Animation
Motion decides where a character is. Animation decides what it looks like: its symbol, its colors, and text styles
such as bold. Each character has its own Animation object,
character.animation, which plays scenes. A scene is a list of frames, and each frame shows one appearance for
a number of ticks.
A character's appearance
At any moment a character has one appearance, a CharacterVisual: a
symbol, a foreground and background color, and style flags. A character that has never played a scene shows its input
symbol with no colors, so the terminal's default colors apply.
There are two ways to change the appearance:
- Play a scene, which changes the appearance frame by frame.
- Call
character.animation.set_appearance(symbol, colors)to change it once. With no arguments, it restores the input symbol with no colors.
A playing scene replaces the appearance on every tick, so set_appearance() only lasts while no scene is active.
A first scene
from terminaltexteffects import Color, ColorPair
scene = character.animation.new_scene()
scene.add_frame(".", duration=5, colors=ColorPair(fg=Color("#ff2000")))
scene.add_frame("o", duration=5, colors=ColorPair(fg=Color("#ffc000")))
scene.add_frame(character.input_symbol, duration=5, colors=ColorPair(fg=Color("#ffffff")))
character.animation.activate_scene(scene)
new_scene()creates an emptySceneowned by this character.add_frame()adds a frame: a symbol, how many ticks to show it, and optional colors and styles.activate_scene()makes the scene the character's active scene and shows its first frame straight away.
Like a path, the scene plays once the character is in your iterator's active_characters. Each tick shows the current
frame and counts down its duration. When the last frame finishes, the scene completes and the character keeps the
last frame's appearance. A character stays active while it has an unfinished scene, even after its path is complete.
A frame's symbol must be a single printable character. Wide characters, such as most emoji, are allowed.
Frame styles
add_frame() takes these keyword arguments besides the symbol and duration:
| Argument | Effect |
|---|---|
colors |
A ColorPair(fg=..., bg=...). Either color can be None to use the terminal's default. |
bold, dim, italic, underline, blink, reverse, hidden, strike |
Text styles. All default to False. Terminals differ in which styles they support. |
Colors are covered in detail in the Color chapter.
Gradients across symbols
Many effects show a short sequence of symbols while the color changes smoothly. apply_gradient_to_symbols() builds
those frames for you:
heat = Gradient(Color("#ff2000"), Color("#ffc000"), Color("#ffffff"), steps=6)
scene.apply_gradient_to_symbols((".", "o", "O", character.input_symbol), 3, fg_gradient=heat)
It adds one frame for each color in the gradient, each lasting the given duration, and spreads the symbols evenly
across those frames. The heat gradient has 13 colors, so this scene has 13 frames of 3 ticks each: about three frames
of each symbol, from a red . to the white input symbol. When there are more symbols than colors, it adds one frame per
symbol and spreads the colors instead. Pass bg_gradient to change the background as well.
The Glow example
The examples in this chapter use the Glow effect. Every character slides home from the canvas's left edge, as in the
Travel example from the Motion chapter, while a scene heats it from a red spark to its white input symbol. Save it as
glow.py in your community effects directory:
"""Glow: characters slide home while a scene heats them from a red spark to white text.
An Effect Guide example for the Animation chapter.
"""
from __future__ import annotations
import typing
from dataclasses import dataclass
from terminaltexteffects import Color, Coord, Gradient, Scene, easing
from terminaltexteffects.engine.base_config import BaseConfig
from terminaltexteffects.engine.base_effect import BaseEffect, BaseEffectIterator
from terminaltexteffects.utils import argutils
SYNC_METRICS = {"none": None, "distance": Scene.SyncMetric.DISTANCE, "step": Scene.SyncMetric.STEP}
def get_effect_resources() -> tuple[str, type[BaseEffect], type[BaseConfig]]:
"""Return the command name, effect class, and configuration class TTE registers."""
return "glow", Glow, GlowConfig
@dataclass
class GlowConfig(BaseConfig):
"""Configuration for the Glow effect."""
parser_spec = argutils.ParserSpec(
name="glow",
help="Characters slide home, heating from a red spark to white text.",
description="glow | Characters slide home, heating from a red spark to white text.",
epilog="Example: tte glow --sync distance",
)
frame_duration: int = argutils.ArgSpec(
name="--frame-duration",
type=argutils.PositiveInt.type_parser,
default=3,
metavar=argutils.PositiveInt.METAVAR,
help="Ticks to show each frame of the scene. Ignored when the scene is synced.",
) # pyright: ignore[reportAssignmentType]
"int : Ticks to show each frame of the scene. Ignored when the scene is synced."
sync: typing.Literal["none", "distance", "step"] = argutils.ArgSpec(
name="--sync",
default="none",
choices=["none", "distance", "step"],
help="Sync the scene to the character's path.",
) # pyright: ignore[reportAssignmentType]
"typing.Literal['none', 'distance', 'step'] : Sync the scene to the character's path."
class GlowIterator(BaseEffectIterator[GlowConfig]):
"""Produce the frames of the Glow effect."""
def __init__(self, effect: Glow) -> None:
"""Give every character a path home and a scene, and start both."""
super().__init__(effect)
heat = Gradient(Color("#ff2000"), Color("#ffc000"), Color("#ffffff"), steps=6)
for character in self.terminal.get_characters():
character.motion.set_coordinate(Coord(self.terminal.canvas.left, character.input_coord.row))
path = character.motion.new_path(speed=0.5, ease=easing.out_expo)
path.new_waypoint(character.input_coord)
character.motion.activate_path(path)
scene = character.animation.new_scene(sync=SYNC_METRICS[self.config.sync])
scene.apply_gradient_to_symbols(
(".", "o", "O", character.input_symbol),
self.config.frame_duration,
fg_gradient=heat,
)
character.animation.activate_scene(scene)
self.terminal.set_character_visibility(character, is_visible=True)
self.active_characters.add(character)
def __next__(self) -> str:
"""Advance every character one tick and return the frame."""
if self.active_characters:
self.update()
return self.frame
raise StopIteration
class Glow(BaseEffect[GlowConfig]):
"""Characters slide home, heating from a red spark to white text."""
@property
def _config_cls(self) -> type[GlowConfig]:
return GlowConfig
@property
def _iterator_cls(self) -> type[GlowIterator]:
return GlowIterator

Every scene starts during setup and plays at the same rate, so all the characters change together, wherever they are. The scene lasts 39 ticks. Characters near the left edge are home before then and stay active until their scene finishes; characters from the right of the text are still moving when their scene ends, and arrive already showing their white input symbol.
Frame duration
A frame's duration is a number of ticks, so it controls how fast a scene plays. With --frame-duration 10, each of
Glow's 13 frames lasts 10 ticks, and the scene takes 130 ticks, longer than any character's path:

At 60 frames per second, 10 ticks is a sixth of a second. Durations must be at least 1.
Syncing a scene to a path
Often a scene should play exactly as long as a path, so a character finishes changing as it arrives. Pass
sync to new_scene() and the scene follows the progress of the character's active path instead of counting ticks:
sync |
Frame shown |
|---|---|
None |
The default. Frames play for their durations, independent of motion. |
Scene.SyncMetric.DISTANCE |
The frame matching how far along the path the character has traveled. |
Scene.SyncMetric.STEP |
The frame matching how many of the path's ticks have passed. |
Frame durations are ignored in a synced scene. When the path completes, or if no path is active, the scene shows its last frame and completes.
The two metrics differ when the path has an easing function. Glow's paths use out_expo, which covers most of the
distance in the first few ticks:

The scene follows the eased distance, so it races through the early frames with the character, and each character shows nearly white for most of its trip.
Without easing, distance and step progress at the same rate. Use DISTANCE to tie an appearance to position, such as
a trail that changes as a character travels, and STEP to tie it to time.
More scene options
new_scene() takes these keyword arguments:
| Argument | Effect |
|---|---|
is_looping |
Start again from the first frame after the last one, forever. |
sync |
Follow the active path's progress, as above. |
ease |
An easing function for the scene's progress through its frames, as with paths. The total duration doesn't change. |
scene_id |
A name for the scene, so you can find it with animation.query_scene(scene_id). One is generated if you don't give one. |
A looping scene never completes, so a character playing one never becomes inactive. Give a looping scene an end, for
example by activating another scene from an event, or remove the character from active_characters yourself.
A character has at most one active scene. Activating another scene replaces the current one;
animation.deactivate_scene() stops the active scene and leaves the character showing its current appearance.
Activating a scene doesn't reset it, so a scene that was stopped part way resumes where it stopped. Call
scene.reset_scene() first to start it from the beginning.
Scenes and events
Scenes fire SCENE_ACTIVATED and SCENE_COMPLETE events, and event actions can activate and deactivate scenes (see
Events). A common pattern is to start a scene when a path completes:
character.event_handler.register_event(Event.PATH_COMPLETE, path, Action.ACTIVATE_SCENE, landing_scene)
Where to go next
- Tutorial step 5: Color gives MySpray's characters a scene and a color in a gradient across the text.
- Color explains
Color,ColorPair, and gradients.
