Skip to content

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:

tte --input-file concept_input.txt --canvas-width 80 --canvas-height 24 --anchor-text c logo

A queued camera shot

Keep bodies in world space and give the camera a short script: hold, truck and pan together, then zoom. The camera lab includes a complete standalone iterator with this setup:

        self.subject.spin("y", turns=0.25, frames=120, ease=easing.in_out_sine)
        camera.hold(24).truck(10, frames=90, ease=easing.in_out_sine)
        camera.pan(5, frames=45, ease=easing.in_out_sine, together=True).on_complete(self.pan_done)
        camera.on_group_complete(self.group_done)
        camera.hold(24).zoom_to(150, frames=60, ease=easing.in_out_sine).hold(24)

The subject's turn uses its own body queue. The camera's individual and group callbacks label the timing without changing the body's animation. See Camera shots and timing for the recording, the frame loop and callback definitions. The restore lesson then demonstrates a return shot that restores the camera before the text lands back in its input cells.

The input text spins like a sign on a post, brightening as it faces the light, then settles back into place in its input colors.

logo.py
"""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 Body, Color, ColorPair, Gradient, Scene, 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 "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:
        """Color the text by the light on the text plane, and queue the plane's spin."""
        super().__init__(effect)
        self.world = self.terminal.world
        self.world.set_light("front")
        self.sign = self.world.text_plane
        # Hide the text while its back faces the viewer.
        self.sign.hide_facing_away = True
        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 light on its spot 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.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:
        """Put the sign back at rest and show the text in its input colors."""
        sign.reset()
        # "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.world.animating or self.active_characters:
            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

The input text spins twice, brightening toward the front, then settles with a small overshoot

  • The text plane: the characters never leave it. The effect turns world.text_plane itself, so the text is exactly on its input cells whenever the plane is at rest (The world).
  • Light: set_light("front") and scenes built with sync=Scene.SyncMetric.LIGHT pick each character's frame from how squarely it faces the light (Light and shading).
  • Back faces: hide_facing_away = True hides the text while its back is to you, so it never reads mirrored (Depth and hidden surfaces).
  • Timing: spin then settle with ease=easing.out_back gives a fast spin that overshoots and springs back to rest. on_complete(self.finish) resets the plane and restores the 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.py
"""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, Ink, Textured, Vec3, easing, rotation_about
from terminaltexteffects.engine.base_config import BaseConfig
from terminaltexteffects.engine.base_effect import BaseEffect, BaseEffectIterator
from terminaltexteffects.engine.shapes import Cube
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)


# Opposite faces add up to seven.
FACES = {
    "front": pips((0, 0), (0, 2), (1, 1), (2, 0), (2, 2)),
    "right": pips((0, 0), (1, 1), (2, 2)),
    "back": pips((0, 0), (2, 2)),
    "left": pips((0, 0), (0, 2), (2, 0), (2, 2)),
    "top": pips((1, 1)),
    "bottom": 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.world = self.terminal.world
        ivory = Gradient(Color("#4a3f35"), Color("#fff4e0"), steps=10)
        looks = {face: Textured(text, ink=Ink(ivory)) for face, text in FACES.items()}
        die = self.world.body(Cube(SIZE), at=(-60, 0, 0)).draw(faces=looks)
        die.rotation = rotation_about("diagonal", 120)
        # Roll to the center while tumbling, then settle with the five facing the viewer and a slight tilt.
        # A path's speed is units per frame, so the roll takes 150 frames, as long as the spin.
        roll = die.motion.new_path(speed=60 / 150, ease=easing.out_cubic)
        roll.new_waypoint(Vec3(0, 0, 0))
        die.motion.activate_path(roll)
        die.spin("z", turns=-1.5, frames=150, ease=easing.out_cubic)
        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 not self.world.animating:
            if self.text_shown:
                raise StopIteration
            for body in self.world.bodies:
                body.erase()  # The world hides its cells when it next paints.
            for character in self.terminal.get_characters():
                self.terminal.set_character_visibility(character, is_visible=True)
            self.text_shown = True
        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

A die tumbles in from the left and settles facing the viewer, tipped slightly to show its top and side

  • Per-face looks: draw(faces=looks) gives each of the cube's faces, by name, its own Textured look (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).
  • Moving and turning together: a path on die.motion rolls it across the screen while a spin of the same length tumbles it, then turn_to lands it on an exact pose (Transforms, Rotation).
  • Starting pose: die.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.py
"""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 Textured, easing
from terminaltexteffects.engine.base_config import BaseConfig
from terminaltexteffects.engine.base_effect import BaseEffect, BaseEffectIterator
from terminaltexteffects.engine.shapes import Sphere
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.world = self.terminal.world
        # More segments and rings make a rounder outline. back="dim" shows the far side through the text's spaces.
        globe = self.world.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 not self.world.animating:
            if self.text_shown:
                raise StopIteration
            for body in self.world.bodies:
                body.erase()  # The world hides its cells when it next paints.
            for character in self.terminal.get_characters():
                self.terminal.set_character_visibility(character, is_visible=True)
            self.text_shown = True
        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

The input text wrapped around a tilted sphere that turns, with the far side showing dimly between letters

  • 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) then rotate("x", 15) leans the axis like a desk globe, and spin("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.py
"""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, Ink, Textured, rotation_about
from terminaltexteffects.engine.base_config import BaseConfig
from terminaltexteffects.engine.base_effect import BaseEffect, BaseEffectIterator
from terminaltexteffects.engine.shapes import Cylinder
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.world = self.terminal.world
        self.world.camera.set_perspective(0.5)
        self.world.set_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.world.body(drum).draw(Textured(self.config.message, ink=Ink(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.rotation = self.tilt * turn
            self.frame_number += 1
        elif not self.text_shown:
            self.drum.erase()  # The world hides its cells when it next paints.
            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 ·"

A message in amber runs right to left around a slightly tilted drum

  • Shape as a band: a Cylinder only 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: set_perspective(0.5) keeps 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.py
"""Wireframes: the regular solids tumble as edge drawings.

An Effect Guide recipe.
"""

from __future__ import annotations

from dataclasses import dataclass

from terminaltexteffects import Color, Wireframe, easing
from terminaltexteffects.engine.base_config import BaseConfig
from terminaltexteffects.engine.base_effect import BaseEffect, BaseEffectIterator
from terminaltexteffects.engine.shapes import Cube, Icosahedron, Octahedron, Tetrahedron
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.world = self.terminal.world
        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.world.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 not self.world.animating:
            if self.text_shown:
                raise StopIteration
            for body in self.world.bodies:
                body.erase()  # The world hides its cells when it next paints.
            for character in self.terminal.get_characters():
                self.terminal.set_character_visibility(character, is_visible=True)
            self.text_shown = True
        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

A tetrahedron, cube, octahedron and icosahedron tumble side by side as colored edges

  • 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 World reference lists every call used here.
  • The Effect Tutorial covers configuration, testing and packaging, which apply to 3D effects unchanged.