Recipes
Each recipe is a complete effect that combines ideas from several chapters. They're short enough to read in one sitting, and each one is a good starting point for an effect of your own. Run them like the concept examples:
Spinning logo
The input text spins like a sign on a post, brightening as it faces the light, then settles back into place and hands the characters back to 2D.
"""Logo: the input text spins like a sign, shaded by the light, and settles back into place.
An Effect Guide recipe.
"""
from __future__ import annotations
from dataclasses import dataclass
from terminaltexteffects import Color, ColorPair, Gradient, Scene, easing
from terminaltexteffects.engine.base_config import BaseConfig
from terminaltexteffects.engine.base_effect import BaseEffect, BaseEffectIterator
from terminaltexteffects.engine.space3d import Body, Space3D
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 "logo", Logo, LogoConfig
@dataclass
class LogoConfig(BaseConfig):
"""Configuration for the Logo effect."""
parser_spec = argutils.ParserSpec(
name="logo",
help="The input text spins like a sign and settles back into place.",
description="logo | The input text spins like a sign and settles back into place.",
epilog="Example: tte --canvas-width 80 --canvas-height 24 --anchor-text c logo",
)
class LogoIterator(BaseEffectIterator[LogoConfig]):
"""Produce the frames of the Logo effect."""
def __init__(self, effect: Logo) -> None:
"""Mount the text on a plane, color it by the light, and queue the spin."""
super().__init__(effect)
self.space = Space3D(self.terminal, light="front")
self.sign = self.space.body()
characters = [c for c in self.terminal.get_characters() if c.input_symbol != " "]
shade = Gradient(Color("#3a003a"), Color("#8A008A"), Color("#00D1FF"), Color("#ffffff"), steps=10)
for character in characters:
# The mount's light reading picks the frame, so the text brightens as it turns toward the light.
scene = character.animation.new_scene(sync=Scene.SyncMetric.LIGHT)
scene.apply_gradient_to_symbols(character.input_symbol, 1, fg_gradient=shade)
character.animation.activate_scene(scene)
self.terminal.set_character_visibility(character, is_visible=True)
self.active_characters.add(character)
self.sign.add_chars(characters, self.sign.text_plane(characters), hide_when_facing_away=True)
self.sign.spin("y", turns=2, frames=200, ease=easing.in_sine)
self.sign.settle("y", turns=1, frames=160, ease=easing.out_back)
self.sign.on_complete(self.finish)
def finish(self, sign: Body) -> None:
"""Release the text and show it in its input colors."""
sign.release_all()
# "always" reapplies input colors by itself. "dynamic" needs them passed, and "ignore" leaves them off.
dynamic = self.terminal.config.existing_color_handling == "dynamic"
for character in self.terminal.get_characters():
colors = None
if dynamic:
colors = ColorPair(fg=character.animation.input_fg_color, bg=character.animation.input_bg_color)
character.animation.deactivate_scene()
character.animation.set_appearance(character.input_symbol, colors)
def __next__(self) -> str:
"""Spin the sign until it settles."""
if self.space.animating or self.active_characters:
self.space.step()
self.update()
return self.frame
raise StopIteration
class Logo(BaseEffect[LogoConfig]):
"""The input text spins like a sign and settles back into place."""
@property
def _config_cls(self) -> type[LogoConfig]:
return LogoConfig
@property
def _iterator_cls(self) -> type[LogoIterator]:
return LogoIterator

- Mounting: the characters are yours, mounted on a shapeless body with
text_plane, so they land exactly on their input cells at rest (Transforms). - Light:
light="front"and scenes built withsync=Scene.SyncMetric.LIGHTpick each character's frame from how squarely it faces the light (Light and shading). - Back faces:
hide_when_facing_away=Truehides the text while its back is to you, so it never reads mirrored (Depth and hidden surfaces). - Timing:
spinthensettlewithease=easing.out_backgives a fast spin that overshoots and springs back to rest.on_complete(self.finish)releases the characters and restores their input colors when it lands (Animating in 3D).
To make it your own, change the gradient, the number of turns, or spin about "x" for a flip.
Die
A die rolls in from the left, tumbling, and lands with the five facing you.
"""Die: a die rolls in from the left and comes to rest showing a five.
An Effect Guide recipe. Each face of the cube gets its own `Textured` look, a pattern of pips.
"""
from __future__ import annotations
from dataclasses import dataclass
from terminaltexteffects import Color, Gradient, easing
from terminaltexteffects.engine.base_config import BaseConfig
from terminaltexteffects.engine.base_effect import BaseEffect, BaseEffectIterator
from terminaltexteffects.engine.space3d import Cube, Space3D, Textured, rotation_about
from terminaltexteffects.utils import argutils
SIZE = 20
"""The die's width in columns. A face is `SIZE` columns wide and `SIZE // 2` rows tall."""
def pips(*spots: tuple[int, int]) -> str:
"""Return a face's text: a solid fill, with a blank pip at each (row, column) spot of a 3 by 3 grid."""
rows = [["█"] * SIZE for _ in range(SIZE // 2)]
for row, column in spots:
# Each pip is two cells wide, which looks round on cells about twice as tall as they are wide.
rows[2 + 3 * row][3 + 6 * column : 5 + 6 * column] = [" ", " "]
return "\n".join("".join(row) for row in rows)
# Box faces are tagged 0 front, 1 right, 2 back, 3 left, 4 top and 5 bottom. Opposite faces add up to seven.
FACES = {
0: pips((0, 0), (0, 2), (1, 1), (2, 0), (2, 2)),
1: pips((0, 0), (1, 1), (2, 2)),
2: pips((0, 0), (2, 2)),
3: pips((0, 0), (0, 2), (2, 0), (2, 2)),
4: pips((1, 1)),
5: pips((0, 0), (0, 2), (1, 0), (1, 2), (2, 0), (2, 2)),
}
def get_effect_resources() -> tuple[str, type[BaseEffect], type[BaseConfig]]:
"""Return the command name, effect class, and configuration class TTE registers."""
return "die", Die, DieConfig
@dataclass
class DieConfig(BaseConfig):
"""Configuration for the Die effect."""
parser_spec = argutils.ParserSpec(
name="die",
help="A die rolls in and comes to rest.",
description="die | A die rolls in and comes to rest.",
epilog="Example: tte --canvas-width 80 --canvas-height 24 --anchor-text c die",
)
class DieIterator(BaseEffectIterator[DieConfig]):
"""Produce the frames of the Die effect."""
def __init__(self, effect: Die) -> None:
"""Draw the die with a pip pattern per face and roll it to the center."""
super().__init__(effect)
self.space = Space3D(self.terminal)
ivory = Gradient(Color("#4a3f35"), Color("#fff4e0"), steps=10)
looks = {tag: Textured(text, gradient=ivory) for tag, text in FACES.items()}
die = self.space.body(Cube(SIZE), at=(-60, 0, 0)).draw(faces=looks)
die.set_rotation(rotation_about("diagonal", 120))
# Roll to the center while tumbling, then settle with the five facing the viewer and a slight tilt.
die.move_to(0, 0, 0, frames=150, ease=easing.out_cubic)
die.spin("z", turns=-1.5, frames=150, ease=easing.out_cubic, together=True)
die.turn_to(rotation_about("x", 20) * rotation_about("y", -25), frames=60, ease=easing.out_back)
die.hold(60)
self.text_shown = False
def __next__(self) -> str:
"""Roll the die, then swap it for the text."""
if self.space.animating:
self.space.step()
elif not self.text_shown:
for body in self.space.bodies:
body.erase()
self.space.step() # Hides the surface characters once nothing is drawn.
for character in self.terminal.get_characters():
self.terminal.set_character_visibility(character, is_visible=True)
self.text_shown = True
else:
raise StopIteration
self.update()
return self.frame
class Die(BaseEffect[DieConfig]):
"""A die rolls in and comes to rest."""
@property
def _config_cls(self) -> type[DieConfig]:
return DieConfig
@property
def _iterator_cls(self) -> type[DieIterator]:
return DieIterator

- Per-face looks:
draw(faces=looks)gives each of the cube's tagged faces its ownTexturedlook (Meshes). Opposite faces add up to seven, as on a real die. - Textures as pictures: each face's text is a solid block of
█with blank pips cut out. Each pip is two cells wide and one tall, which looks round on tall cells (Tall cells, Texture mapping). - Combining tweens:
move_toandspin(..., together=True)roll it across the screen, thenturn_tolands it on an exact pose (Rotation). - Starting pose:
set_rotation(rotation_about("diagonal", 120))starts it at an angle so no face is square to the camera while it rolls.
Globe
The input text wraps around a sphere that turns once. You see the far side through the gaps in the text.
"""Globe: the input text wrapped around a turning sphere, with the far side showing through.
An Effect Guide recipe.
"""
from __future__ import annotations
from dataclasses import dataclass
from terminaltexteffects import easing
from terminaltexteffects.engine.base_config import BaseConfig
from terminaltexteffects.engine.base_effect import BaseEffect, BaseEffectIterator
from terminaltexteffects.engine.space3d import Space3D, Sphere, Textured
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 "globe", Globe, GlobeConfig
@dataclass
class GlobeConfig(BaseConfig):
"""Configuration for the Globe effect."""
parser_spec = argutils.ParserSpec(
name="globe",
help="The input text wrapped around a turning sphere.",
description="globe | The input text wrapped around a turning sphere.",
epilog="Example: tte --canvas-width 80 --canvas-height 24 --anchor-text c globe",
)
class GlobeIterator(BaseEffectIterator[GlobeConfig]):
"""Produce the frames of the Globe effect."""
def __init__(self, effect: Globe) -> None:
"""Wrap the input text around a sphere and turn it."""
super().__init__(effect)
self.space = Space3D(self.terminal)
# More segments and rings make a rounder outline. back="dim" shows the far side through the text's spaces.
globe = self.space.body(Sphere(radius=20, segments=48, rings=24)).draw(Textured(back="dim"))
globe.rotate("z", -15).rotate("x", 15)
globe.spin("y", turns=1, frames=480, ease=easing.in_out_sine, local=True)
self.text_shown = False
def __next__(self) -> str:
"""Turn the globe, then swap it for the text."""
if self.space.animating:
self.space.step()
elif not self.text_shown:
for body in self.space.bodies:
body.erase()
self.space.step() # Hides the surface characters once nothing is drawn.
for character in self.terminal.get_characters():
self.terminal.set_character_visibility(character, is_visible=True)
self.text_shown = True
else:
raise StopIteration
self.update()
return self.frame
class Globe(BaseEffect[GlobeConfig]):
"""The input text wrapped around a turning sphere."""
@property
def _config_cls(self) -> type[GlobeConfig]:
return GlobeConfig
@property
def _iterator_cls(self) -> type[GlobeIterator]:
return GlobeIterator

- Wrapping:
Textured()with no text uses the input text, wrapped around the sphere by its UV layout, and repeated to fill it (Texture mapping). - See-through:
back="dim"draws the far side's text in dim color wherever the near side has a space (Depth and hidden surfaces). - Tilt:
rotate("z", -15)thenrotate("x", 15)leans the axis like a desk globe, andspin("y", ..., local=True)turns the globe about that leaning axis, which stays put (Rotation).
The globe is the most expensive example in the guide, because a round sphere has many triangles and back="dim"
draws it twice. Performance shows the numbers and what to cut.
Ticker on a cylinder
A message runs around a short, wide drum, like a news ticker on a building.
"""Ticker: a message running around a turning cylinder, like a news ticker on a building.
An Effect Guide recipe.
"""
from __future__ import annotations
from dataclasses import dataclass
from terminaltexteffects import Color, Gradient
from terminaltexteffects.engine.base_config import BaseConfig
from terminaltexteffects.engine.base_effect import BaseEffect, BaseEffectIterator
from terminaltexteffects.engine.space3d import Cylinder, Space3D, Textured, rotation_about
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 "ticker", Ticker, TickerConfig
@dataclass
class TickerConfig(BaseConfig):
"""Configuration for the Ticker effect."""
parser_spec = argutils.ParserSpec(
name="ticker",
help="A message running around a turning cylinder.",
description="ticker | A message running around a turning cylinder.",
epilog='Example: tte --canvas-width 80 --canvas-height 24 --anchor-text c ticker --message "HELLO "',
)
message: str = argutils.ArgSpec(
name="--message",
default=" TERMINAL TEXT EFFECTS ·",
help="The message. It repeats around the cylinder.",
) # pyright: ignore[reportAssignmentType]
"str : The message. It repeats around the cylinder."
class TickerIterator(BaseEffectIterator[TickerConfig]):
"""Produce the frames of the Ticker effect."""
def __init__(self, effect: Ticker) -> None:
"""Wrap the message around a short, wide cylinder."""
super().__init__(effect)
# Gentle perspective keeps the front of the drum from magnifying the letters too much.
self.space = Space3D(self.terminal, perspective=0.5, light="front")
amber = Gradient(Color("#3a2000"), Color("#ffb000"), Color("#fff0c0"), steps=10)
# Two columns tall is one row, so the message runs around the drum once, in a single line.
drum = Cylinder(radius=26, height=2, segments=72)
self.drum = self.space.body(drum).draw(Textured(self.config.message, gradient=amber))
self.tilt = rotation_about("x", 10) # Tip the top toward the viewer, so we look down on the drum a little.
self.frames = 420
self.frame_number = 0
self.text_shown = False
def __next__(self) -> str:
"""Turn the drum a little each frame, then swap it for the text."""
if self.frame_number < self.frames:
# Turn about the drum's own axis first, then tilt. Calling rotate("y") on the tilted drum would turn it
# about the world's y axis instead, and the drum would wobble.
turn = rotation_about("y", -0.6 * self.frame_number) # Right to left, so the message reads forward.
self.drum.set_rotation(self.tilt * turn)
self.space.step()
self.frame_number += 1
elif not self.text_shown:
self.drum.erase()
self.space.step() # Hides the surface characters once nothing is drawn.
for character in self.terminal.get_characters():
self.terminal.set_character_visibility(character, is_visible=True)
self.text_shown = True
else:
raise StopIteration
self.update()
return self.frame
class Ticker(BaseEffect[TickerConfig]):
"""A message running around a turning cylinder."""
@property
def _config_cls(self) -> type[TickerConfig]:
return TickerConfig
@property
def _iterator_cls(self) -> type[TickerIterator]:
return TickerIterator
tte --input-file concept_input.txt --canvas-width 80 --canvas-height 24 --anchor-text c ticker --message " HELLO ·"

- Shape as a band: a
Cylinderonly 2 units tall holds one row of text around its side (Tall cells, Texture mapping). - Turning about a tilted axis: the drum is tipped toward you, so turning it with
rotate("y", ...)would turn it about the world's vertical and make it wobble. Instead, it sets its rotation each frame to the tilt times a turn about its own axis (Rotation). - Perspective:
perspective=0.5keeps the near side from magnifying the letters too much (Camera and projection). - Driving it yourself: there's no tween.
__next__counts frames and sets the angle, which is how to make motion that never ends or follows your own rules.
Wireframe solids
Four solids tumble in a row, each in its own color, with hidden edges dimmed.
"""Wireframes: the regular solids tumble as edge drawings.
An Effect Guide recipe.
"""
from __future__ import annotations
from dataclasses import dataclass
from terminaltexteffects import Color, easing
from terminaltexteffects.engine.base_config import BaseConfig
from terminaltexteffects.engine.base_effect import BaseEffect, BaseEffectIterator
from terminaltexteffects.engine.space3d import Cube, Icosahedron, Octahedron, Space3D, Tetrahedron, Wireframe
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 "wireframes", Wireframes, WireframesConfig
@dataclass
class WireframesConfig(BaseConfig):
"""Configuration for the Wireframes effect."""
parser_spec = argutils.ParserSpec(
name="wireframes",
help="The regular solids tumble as edge drawings.",
description="wireframes | The regular solids tumble as edge drawings.",
epilog="Example: tte --canvas-width 80 --canvas-height 24 --anchor-text c wireframes",
)
class WireframesIterator(BaseEffectIterator[WireframesConfig]):
"""Produce the frames of the Wireframes effect."""
def __init__(self, effect: Wireframes) -> None:
"""Line up four solids and tumble each one differently."""
super().__init__(effect)
self.space = Space3D(self.terminal)
solids = (Tetrahedron(11), Cube(14), Octahedron(10), Icosahedron(10))
colors = ("#ff5fd7", "#00D1FF", "#5fff87", "#ffd75f")
for index, (shape, color) in enumerate(zip(solids, colors)):
look = Wireframe(color=Color(color), hidden_color=Color("#55556a"))
body = self.space.body(shape, at=(-30 + 20 * index, 0, 0)).draw(look)
body.spin("y", turns=1 + index % 2, frames=420, ease=easing.in_out_sine)
body.spin("x", turns=1 - index % 2 * 0.5, frames=420, ease=easing.in_out_sine, together=True)
self.text_shown = False
def __next__(self) -> str:
"""Tumble the solids, then swap them for the text."""
if self.space.animating:
self.space.step()
elif not self.text_shown:
for body in self.space.bodies:
body.erase()
self.space.step() # Hides the surface characters once nothing is drawn.
for character in self.terminal.get_characters():
self.terminal.set_character_visibility(character, is_visible=True)
self.text_shown = True
else:
raise StopIteration
self.update()
return self.frame
class Wireframes(BaseEffect[WireframesConfig]):
"""The regular solids tumble as edge drawings."""
@property
def _config_cls(self) -> type[WireframesConfig]:
return WireframesConfig
@property
def _iterator_cls(self) -> type[WireframesIterator]:
return WireframesIterator

- Wireframe look:
Wireframe(color=..., hidden_color=...)draws edges only, with the edges behind the shape in a dark color (3D in a terminal, Depth and hidden surfaces). - Tumbling: two spins about different axes, run
together, at different rates for each solid (Rotation). - Spacing: each solid sits 20 columns from the next, so their edges never cross on screen. Edges that touch blur into one tangle of line symbols.
Where to go next
- The
Space3Dreference lists every call used here. - The Effect Tutorial covers configuration, testing and packaging, which apply to 3D effects unchanged.