Skip to content

Motion

Each character moves with its own Motion object, character.motion. You give it paths, each a list of waypoints, and activate one; the character then moves a little along it every tick until it reaches the end. This chapter covers building paths and controlling how characters travel along them.

A first path

path = character.motion.new_path(speed=0.5)
path.new_waypoint(character.input_coord)
character.motion.activate_path(path)
  • new_path() creates an empty Path owned by this character.
  • new_waypoint() adds a Waypoint, a coordinate to travel to. A path can have any number of waypoints; the character visits them in order.
  • activate_path() starts the path from wherever the character is now. The engine adds a segment from the character's current position to the first waypoint, so a path doesn't need a starting waypoint.

To start somewhere other than the character's home, move it first with character.motion.set_coordinate(coord). The character's position is character.motion.current_coord.

The path runs once the character is in your iterator's active_characters. When the character reaches the last waypoint, the path completes and the character becomes inactive, unless it also has a scene playing.

The Travel example

The examples in this chapter use the Travel effect. It moves every character from the canvas's left edge to its home, with options for speed, easing, and a curved path. Save it as travel.py in your community effects directory:

travel.py
"""Travel: move every character home along a path.

An Effect Guide example for the Motion chapter.
"""

from __future__ import annotations

from dataclasses import dataclass

from terminaltexteffects import Coord, 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 "travel", Travel, TravelConfig


@dataclass
class TravelConfig(BaseConfig):
    """Configuration for the Travel effect."""

    parser_spec = argutils.ParserSpec(
        name="travel",
        help="Moves every character home along a path.",
        description="travel | Moves every character home along a path.",
        epilog=f"{argutils.EASING_EPILOG}Example: tte travel --ease out_bounce",
    )
    speed: float = argutils.ArgSpec(
        name="--speed",
        type=argutils.PositiveFloat.type_parser,
        default=0.5,
        metavar=argutils.PositiveFloat.METAVAR,
        help="Distance each character moves per frame.",
    )  # pyright: ignore[reportAssignmentType]
    "float : Distance each character moves per frame."

    ease: easing.EasingFunction = argutils.ArgSpec(
        name="--ease",
        type=argutils.Ease.type_parser,
        default=easing.linear,
        metavar=argutils.Ease.METAVAR,
        help="Easing function applied to each path.",
    )  # pyright: ignore[reportAssignmentType]
    "easing.EasingFunction : Easing function applied to each path."

    curve: bool = argutils.ArgSpec(
        name="--curve",
        default=False,
        action="store_true",
        help="Start in the bottom left corner and curve home through the bottom right corner.",
    )  # pyright: ignore[reportAssignmentType]
    "bool : Start in the bottom left corner and curve home through the bottom right corner."


class TravelIterator(BaseEffectIterator[TravelConfig]):
    """Produce the frames of the Travel effect."""

    def __init__(self, effect: Travel) -> None:
        """Give every character a path home and start it moving."""
        super().__init__(effect)
        canvas = self.terminal.canvas
        for character in self.terminal.get_characters():
            path = character.motion.new_path(speed=self.config.speed, ease=self.config.ease)
            if self.config.curve:
                character.motion.set_coordinate(Coord(canvas.left, canvas.bottom))
                path.new_waypoint(character.input_coord, bezier_control=Coord(canvas.right, canvas.bottom))
            else:
                character.motion.set_coordinate(Coord(canvas.left, character.input_coord.row))
                path.new_waypoint(character.input_coord)
            character.motion.activate_path(path)
            self.terminal.set_character_visibility(character, is_visible=True)
            self.active_characters.add(character)

    def __next__(self) -> str:
        """Move the characters one step and return the frame."""
        if self.active_characters:
            self.update()
            return self.frame
        raise StopIteration


class Travel(BaseEffect[TravelConfig]):
    """Moves every character home along a path."""

    @property
    def _config_cls(self) -> type[TravelConfig]:
        return TravelConfig

    @property
    def _iterator_cls(self) -> type[TravelIterator]:
        return TravelIterator

With the default options, every character moves at a constant speed, so characters that start farther from home arrive later:

tte --input-file concept_input.txt travel

Every row slides in from the left at a constant speed

Speed and duration

speed is the distance a character covers per tick. The number of ticks a path takes is its length divided by its speed, rounded: a path 20 cells long at speed 0.5 takes 40 frames, or two-thirds of a second at 60 frames per second.

Lengths are measured on screen, so one row counts as two columns (see Distances are measured on screen). Because the length includes the segment from the character's starting position, characters with different distances to travel take different times at the same speed. To make characters arrive together, give each a speed proportional to its distance.

Easing

An easing function changes how progress along the path is spread over its duration. With ease=None, the default, the character moves the same distance every tick. With an easing function, it can start slowly, finish slowly, or overshoot and settle. The path's duration doesn't change.

from terminaltexteffects import easing

path = character.motion.new_path(speed=0.5, ease=easing.out_expo)
tte --input-file concept_input.txt travel --ease out_expo

Rows rush in and slow down sharply near home

tte --input-file concept_input.txt travel --ease in_out_quart

Rows start slowly, speed up, and slow down again

tte --input-file concept_input.txt travel --ease out_bounce

Rows overshoot and bounce back before settling

The easing module has in_, out_, and in_out_ variants of the sine, quad, cubic, quart, quint, expo, circ, back, elastic, and bounce curves, plus linear. easing.make_easing(x1, y1, x2, y2) builds a custom cubic bezier curve, like CSS cubic-bezier().

Easing applies to the whole path, not each segment. On a path with several waypoints, an out_ easing slows the character down near the last waypoint only.

Curves

Give a waypoint bezier_control to curve the segment that ends at it. One control point makes a quadratic curve; a tuple of two makes a cubic curve. The character doesn't pass through the control point; the curve bends toward it.

path.new_waypoint(character.input_coord, bezier_control=Coord(canvas.right, canvas.bottom))

With --curve, Travel starts every character in the bottom left corner and bends its path toward the bottom right corner:

tte --input-file concept_input.txt travel --curve

Characters sweep along the bottom and curve up into place

More path options

new_path() takes these keyword arguments:

Argument Effect
speed Distance per tick. Must be greater than 0.
ease Easing function for the whole path.
layer Layer to put the character on when the path is activated. None leaves the layer unchanged.
hold_time Number of ticks to stay at the last waypoint before the path completes.
loop After reaching the last waypoint, travel back to the first and repeat forever. A path needs at least two waypoints to loop.
path_id A name for the path, so you can find it with motion.query_path(path_id). One is generated if you don't give one.

A character has at most one active path. Activating another path replaces the current one, starting from wherever the character is. motion.deactivate_path() stops the active path without completing it, and motion.movement_is_complete() reports whether no path is active.

Sequences of paths

To run paths one after another, chain them:

character.motion.chain_paths([rise, drift, land])

chain_paths() registers events so that each path activates the next one when it completes; pass loop=True to return from the last to the first. Chaining is a shortcut for the event handling described in the Events chapter, which can also start scenes, change layers, or run your own code when a path starts, completes, or reaches a waypoint.

Where to go next