Skip to content

Motion

Module: terminaltexteffects.engine.motion

Motion class for managing the movement of a character.

Attributes:

Name Type Description
paths dict[str, Path]

dictionary of paths

character EffectCharacter

The EffectCharacter to move.

current_coord Coord

current coordinate

previous_coord Coord

previous coordinate

active_path Path | None

active path

Methods:

Name Description
set_coordinate

Coord) -> None: Sets the current coordinate to the given coordinate.

new_path

float = 1, ease: easing.EasingFunction | None = None, layer: int | None = None, hold_time: int = 0, loop: bool = False, id: str = "") -> Path: Creates a new Path and adds it to the Motion.paths dictionary with the path_id as key.

query_path

str) -> Path: Returns the path with the given path_id.

movement_is_complete

Returns whether the character has an active path.

chain_paths

list[Path], loop=False): Creates a chain of paths by registering activation events for each path such that paths[n] activates paths[n+1] when reached. If loop is True, paths[-1] activates paths[0] when reached.

activate_path

Path) -> None: Activates the first waypoint in the path.

deactivate_path

Path) -> None: Unsets the current path if the current path is path.

move

Moves the character one step closer to the target position based on an easing function if present, otherwise linearly.

Source code in terminaltexteffects/engine/motion.py
class Motion:
    """Motion class for managing the movement of a character.

    Attributes:
        paths (dict[str, Path]): dictionary of paths
        character (base_character.EffectCharacter): The EffectCharacter to move.
        current_coord (Coord): current coordinate
        previous_coord (Coord): previous coordinate
        active_path (Path | None): active path

    Methods:
        set_coordinate(coord: Coord) -> None:
            Sets the current coordinate to the given coordinate.
        new_path(speed: float = 1, ease: easing.EasingFunction | None = None, layer: int | None = None, hold_time: int = 0, loop: bool = False, id: str = "") -> Path:
            Creates a new Path and adds it to the Motion.paths dictionary with the path_id as key.
        query_path(path_id: str) -> Path:
            Returns the path with the given path_id.
        movement_is_complete() -> bool:
            Returns whether the character has an active path.
        chain_paths(paths: list[Path], loop=False):
            Creates a chain of paths by registering activation events for each path such
            that paths[n] activates paths[n+1] when reached. If loop is True, paths[-1] activates
            paths[0] when reached.
        activate_path(path: Path) -> None:
            Activates the first waypoint in the path.
        deactivate_path(path: Path) -> None:
            Unsets the current path if the current path is path.
        move() -> None:
            Moves the character one step closer to the target position based on an easing function if present, otherwise linearly."""

    def __init__(self, character: "base_character.EffectCharacter"):
        """Initializes the Motion object with the given EffectCharacter.

        Args:
            character (base_character.EffectCharacter): The EffectCharacter to move.
        """
        self.paths: dict[str, Path] = {}
        self.character = character
        self.current_coord: Coord = Coord(character.input_coord.column, character.input_coord.row)
        self.previous_coord: Coord = Coord(-1, -1)
        self.active_path: Path | None = None

    def set_coordinate(self, coord: Coord) -> None:
        """Sets the current coordinate to the given coordinate.

        Args:
            coord (Coord): coordinate
        """
        self.current_coord = coord

    def new_path(
        self,
        *,
        speed: float = 1,
        ease: easing.EasingFunction | None = None,
        layer: int | None = None,
        hold_time: int = 0,
        loop: bool = False,
        id: str = "",
    ) -> Path:
        """Creates a new Path and adds it to the Motion.paths dictionary with the path_id as key.

        Args:
            speed (float, optional): speed > 0. Defaults to 1.
            ease (easing.EasingFunction | None, optional): easing function for character movement. Defaults to None.
            layer (int | None, optional): layer to move the character to, if None, layer is unchanged. Defaults to None.
            hold_time (int, optional): number of frames to hold the character at the end of the path. Defaults to 0.
            loop (bool, optional): Whether the path should loop back to the beginning. Default is False.
            id (str, optional): Unique identifier for the path. Used to query for the path. Defaults to "".

        Raises:
            ValueError: If a path with the provided id already exists.

        Returns:
            Path: The new path.
        """
        if not id:
            found_unique = False
            current_id = len(self.paths)
            while not found_unique:
                id = f"{len(self.paths)}"
                if id not in self.paths:
                    found_unique = True
                else:
                    current_id += 1
        if id in self.paths:
            raise ValueError(f"Path with id {id} already exists.")
        new_path = Path(id, speed, ease, layer, hold_time, loop)
        self.paths[id] = new_path
        return new_path

    def query_path(self, path_id: str) -> Path:
        """Returns the path with the given path_id.

        Args:
            path_id (str): path_id

        Returns:
            Path: The path with the given path_id.
        """
        path = self.paths.get(path_id, None)
        if not path:
            raise ValueError(f"Path with id {path_id} not found.")
        return path

    def movement_is_complete(self) -> bool:
        """Returns whether the character has an active path.

        Returns:
            bool: True if the character has no active path, False otherwise.
        """
        if self.active_path is None:
            return True
        return False

    def _get_easing_factor(self, easing_func: easing.EasingFunction) -> float:
        """Returns the percentage of total distance that should be moved based on the easing function.

        Args:
            easing_func (easing.EasingFunction): The easing function to use.

        Returns:
            float: The percentage of total distance to move.
        """
        if not self.active_path:
            raise ValueError("No active path.")
        elapsed_step_ratio = self.active_path.current_step / self.active_path.max_steps
        return easing_func(elapsed_step_ratio)

    def chain_paths(self, paths: list[Path], loop=False):
        """Creates a chain of paths by registering activation events for each path such
        that paths[n] activates paths[n+1] when reached. If loop is True, paths[-1] activates
        paths[0] when reached.

        Args:
            paths (list[Path]): list of paths to chain
            loop (bool, optional): Whether the chain should loop. Defaults to False.
        """
        if len(paths) < 2:
            return
        for i, path in enumerate(paths):
            if i == 0:
                continue
            self.character.event_handler.register_event(
                self.character.event_handler.Event.PATH_COMPLETE,
                paths[i - 1],
                self.character.event_handler.Action.ACTIVATE_PATH,
                path,
            )
        if loop:
            self.character.event_handler.register_event(
                self.character.event_handler.Event.PATH_COMPLETE,
                paths[-1],
                self.character.event_handler.Action.ACTIVATE_PATH,
                paths[0],
            )

    def activate_path(self, path: Path) -> None:
        """
        Activates the first waypoint in the given path and updates the path's properties accordingly.

        This method sets the active path to the given path, calculates the distance to the first waypoint,
        and updates the total distance of the path. If the path has an origin segment, it removes it from
        the segments list and subtracts its distance from the total distance. Then, it creates a new origin
        segment from the current coordinate to the first waypoint and inserts it at the beginning of the segments list.

        The method also resets the current step, hold time remaining, and max steps of the path based on the total distance
        and speed. It ensures that the enter and exit events for each segment are not triggered. If the path has a layer,
        it sets the character's layer to it. Finally, it triggers the PATH_ACTIVATED event for the character.

        Args:
            path (Path): The path to activate.
        """
        self.active_path = path
        first_waypoint = self.active_path.waypoints[0]
        if first_waypoint.bezier_control:
            distance_to_first_waypoint = geometry.find_length_of_bezier_curve(
                self.current_coord, first_waypoint.bezier_control, first_waypoint.coord
            )
        else:
            distance_to_first_waypoint = geometry.find_length_of_line(
                self.current_coord,
                first_waypoint.coord,
            )
        self.active_path.total_distance += distance_to_first_waypoint
        if self.active_path.origin_segment:
            self.active_path.segments.pop(0)
            self.active_path.total_distance -= self.active_path.origin_segment.distance
        self.active_path.origin_segment = Segment(
            Waypoint("origin", self.current_coord), first_waypoint, distance_to_first_waypoint
        )
        self.active_path.segments.insert(0, self.active_path.origin_segment)
        self.active_path.current_step = 0
        self.active_path.hold_time_remaining = self.active_path.hold_time
        self.active_path.max_steps = round(self.active_path.total_distance / self.active_path.speed)
        for segment in self.active_path.segments:
            segment.enter_event_triggered = False
            segment.exit_event_triggered = False
        if self.active_path.layer is not None:
            self.character.layer = self.active_path.layer
        self.character.event_handler._handle_event(self.character.event_handler.Event.PATH_ACTIVATED, self.active_path)

    def deactivate_path(self, path: Path) -> None:
        """Set the active path to None if the active path is the given path.

        Args:
            path (Path): the Path to deactivate
        """
        if self.active_path and self.active_path is path:
            self.active_path = None

    def move(self) -> None:
        """
        Moves the character along the active path.

        The character's current coordinate is updated to the next step in the active path. If the active path is completed,
        an event is triggered based on whether the path is set to loop or not. If the path is set to loop, the path is
        deactivated and then reactivated to start from the beginning. If not, the path is simply deactivated and a
        PATH_COMPLETE event is triggered.

        If the path has a hold time, the character will pause at the end of the path for the specified duration. During
        this hold time, a PATH_HOLDING event is triggered on the first frame, and the hold time is decremented on each
        subsequent frame until it reaches zero.

        If there is no active path or if the active path has no segments, the character does not move.

        The character's previous coordinate is preserved before moving to allow for clearing the location in the terminal.
        """
        # preserve previous coordinate to allow for clearing the location in the terminal
        self.previous_coord = Coord(self.current_coord.column, self.current_coord.row)

        if not self.active_path or not self.active_path.segments:
            return
        self.current_coord = self.active_path.step(self.character.event_handler)
        if self.active_path.current_step == self.active_path.max_steps:
            if self.active_path.hold_time and self.active_path.hold_time_remaining == self.active_path.hold_time:
                self.character.event_handler._handle_event(
                    self.character.event_handler.Event.PATH_HOLDING, self.active_path
                )
                self.active_path.hold_time_remaining -= 1
                return
            elif self.active_path.hold_time_remaining:
                self.active_path.hold_time_remaining -= 1
                return
            if self.active_path.loop and len(self.active_path.segments) > 1:
                looping_path = self.active_path
                self.deactivate_path(self.active_path)
                self.activate_path(looping_path)
            else:
                self.completed_path = self.active_path
                self.deactivate_path(self.active_path)
                self.character.event_handler._handle_event(
                    self.character.event_handler.Event.PATH_COMPLETE, self.completed_path
                )

__init__(character)

Initializes the Motion object with the given EffectCharacter.

Parameters:

Name Type Description Default
character EffectCharacter

The EffectCharacter to move.

required
Source code in terminaltexteffects/engine/motion.py
def __init__(self, character: "base_character.EffectCharacter"):
    """Initializes the Motion object with the given EffectCharacter.

    Args:
        character (base_character.EffectCharacter): The EffectCharacter to move.
    """
    self.paths: dict[str, Path] = {}
    self.character = character
    self.current_coord: Coord = Coord(character.input_coord.column, character.input_coord.row)
    self.previous_coord: Coord = Coord(-1, -1)
    self.active_path: Path | None = None

activate_path(path)

Activates the first waypoint in the given path and updates the path's properties accordingly.

This method sets the active path to the given path, calculates the distance to the first waypoint, and updates the total distance of the path. If the path has an origin segment, it removes it from the segments list and subtracts its distance from the total distance. Then, it creates a new origin segment from the current coordinate to the first waypoint and inserts it at the beginning of the segments list.

The method also resets the current step, hold time remaining, and max steps of the path based on the total distance and speed. It ensures that the enter and exit events for each segment are not triggered. If the path has a layer, it sets the character's layer to it. Finally, it triggers the PATH_ACTIVATED event for the character.

Parameters:

Name Type Description Default
path Path

The path to activate.

required
Source code in terminaltexteffects/engine/motion.py
def activate_path(self, path: Path) -> None:
    """
    Activates the first waypoint in the given path and updates the path's properties accordingly.

    This method sets the active path to the given path, calculates the distance to the first waypoint,
    and updates the total distance of the path. If the path has an origin segment, it removes it from
    the segments list and subtracts its distance from the total distance. Then, it creates a new origin
    segment from the current coordinate to the first waypoint and inserts it at the beginning of the segments list.

    The method also resets the current step, hold time remaining, and max steps of the path based on the total distance
    and speed. It ensures that the enter and exit events for each segment are not triggered. If the path has a layer,
    it sets the character's layer to it. Finally, it triggers the PATH_ACTIVATED event for the character.

    Args:
        path (Path): The path to activate.
    """
    self.active_path = path
    first_waypoint = self.active_path.waypoints[0]
    if first_waypoint.bezier_control:
        distance_to_first_waypoint = geometry.find_length_of_bezier_curve(
            self.current_coord, first_waypoint.bezier_control, first_waypoint.coord
        )
    else:
        distance_to_first_waypoint = geometry.find_length_of_line(
            self.current_coord,
            first_waypoint.coord,
        )
    self.active_path.total_distance += distance_to_first_waypoint
    if self.active_path.origin_segment:
        self.active_path.segments.pop(0)
        self.active_path.total_distance -= self.active_path.origin_segment.distance
    self.active_path.origin_segment = Segment(
        Waypoint("origin", self.current_coord), first_waypoint, distance_to_first_waypoint
    )
    self.active_path.segments.insert(0, self.active_path.origin_segment)
    self.active_path.current_step = 0
    self.active_path.hold_time_remaining = self.active_path.hold_time
    self.active_path.max_steps = round(self.active_path.total_distance / self.active_path.speed)
    for segment in self.active_path.segments:
        segment.enter_event_triggered = False
        segment.exit_event_triggered = False
    if self.active_path.layer is not None:
        self.character.layer = self.active_path.layer
    self.character.event_handler._handle_event(self.character.event_handler.Event.PATH_ACTIVATED, self.active_path)

chain_paths(paths, loop=False)

Creates a chain of paths by registering activation events for each path such that paths[n] activates paths[n+1] when reached. If loop is True, paths[-1] activates paths[0] when reached.

Parameters:

Name Type Description Default
paths list[Path]

list of paths to chain

required
loop bool

Whether the chain should loop. Defaults to False.

False
Source code in terminaltexteffects/engine/motion.py
def chain_paths(self, paths: list[Path], loop=False):
    """Creates a chain of paths by registering activation events for each path such
    that paths[n] activates paths[n+1] when reached. If loop is True, paths[-1] activates
    paths[0] when reached.

    Args:
        paths (list[Path]): list of paths to chain
        loop (bool, optional): Whether the chain should loop. Defaults to False.
    """
    if len(paths) < 2:
        return
    for i, path in enumerate(paths):
        if i == 0:
            continue
        self.character.event_handler.register_event(
            self.character.event_handler.Event.PATH_COMPLETE,
            paths[i - 1],
            self.character.event_handler.Action.ACTIVATE_PATH,
            path,
        )
    if loop:
        self.character.event_handler.register_event(
            self.character.event_handler.Event.PATH_COMPLETE,
            paths[-1],
            self.character.event_handler.Action.ACTIVATE_PATH,
            paths[0],
        )

deactivate_path(path)

Set the active path to None if the active path is the given path.

Parameters:

Name Type Description Default
path Path

the Path to deactivate

required
Source code in terminaltexteffects/engine/motion.py
def deactivate_path(self, path: Path) -> None:
    """Set the active path to None if the active path is the given path.

    Args:
        path (Path): the Path to deactivate
    """
    if self.active_path and self.active_path is path:
        self.active_path = None

move()

Moves the character along the active path.

The character's current coordinate is updated to the next step in the active path. If the active path is completed, an event is triggered based on whether the path is set to loop or not. If the path is set to loop, the path is deactivated and then reactivated to start from the beginning. If not, the path is simply deactivated and a PATH_COMPLETE event is triggered.

If the path has a hold time, the character will pause at the end of the path for the specified duration. During this hold time, a PATH_HOLDING event is triggered on the first frame, and the hold time is decremented on each subsequent frame until it reaches zero.

If there is no active path or if the active path has no segments, the character does not move.

The character's previous coordinate is preserved before moving to allow for clearing the location in the terminal.

Source code in terminaltexteffects/engine/motion.py
def move(self) -> None:
    """
    Moves the character along the active path.

    The character's current coordinate is updated to the next step in the active path. If the active path is completed,
    an event is triggered based on whether the path is set to loop or not. If the path is set to loop, the path is
    deactivated and then reactivated to start from the beginning. If not, the path is simply deactivated and a
    PATH_COMPLETE event is triggered.

    If the path has a hold time, the character will pause at the end of the path for the specified duration. During
    this hold time, a PATH_HOLDING event is triggered on the first frame, and the hold time is decremented on each
    subsequent frame until it reaches zero.

    If there is no active path or if the active path has no segments, the character does not move.

    The character's previous coordinate is preserved before moving to allow for clearing the location in the terminal.
    """
    # preserve previous coordinate to allow for clearing the location in the terminal
    self.previous_coord = Coord(self.current_coord.column, self.current_coord.row)

    if not self.active_path or not self.active_path.segments:
        return
    self.current_coord = self.active_path.step(self.character.event_handler)
    if self.active_path.current_step == self.active_path.max_steps:
        if self.active_path.hold_time and self.active_path.hold_time_remaining == self.active_path.hold_time:
            self.character.event_handler._handle_event(
                self.character.event_handler.Event.PATH_HOLDING, self.active_path
            )
            self.active_path.hold_time_remaining -= 1
            return
        elif self.active_path.hold_time_remaining:
            self.active_path.hold_time_remaining -= 1
            return
        if self.active_path.loop and len(self.active_path.segments) > 1:
            looping_path = self.active_path
            self.deactivate_path(self.active_path)
            self.activate_path(looping_path)
        else:
            self.completed_path = self.active_path
            self.deactivate_path(self.active_path)
            self.character.event_handler._handle_event(
                self.character.event_handler.Event.PATH_COMPLETE, self.completed_path
            )

movement_is_complete()

Returns whether the character has an active path.

Returns:

Name Type Description
bool bool

True if the character has no active path, False otherwise.

Source code in terminaltexteffects/engine/motion.py
def movement_is_complete(self) -> bool:
    """Returns whether the character has an active path.

    Returns:
        bool: True if the character has no active path, False otherwise.
    """
    if self.active_path is None:
        return True
    return False

new_path(*, speed=1, ease=None, layer=None, hold_time=0, loop=False, id='')

Creates a new Path and adds it to the Motion.paths dictionary with the path_id as key.

Parameters:

Name Type Description Default
speed float

speed > 0. Defaults to 1.

1
ease EasingFunction | None

easing function for character movement. Defaults to None.

None
layer int | None

layer to move the character to, if None, layer is unchanged. Defaults to None.

None
hold_time int

number of frames to hold the character at the end of the path. Defaults to 0.

0
loop bool

Whether the path should loop back to the beginning. Default is False.

False
id str

Unique identifier for the path. Used to query for the path. Defaults to "".

''

Raises:

Type Description
ValueError

If a path with the provided id already exists.

Returns:

Name Type Description
Path Path

The new path.

Source code in terminaltexteffects/engine/motion.py
def new_path(
    self,
    *,
    speed: float = 1,
    ease: easing.EasingFunction | None = None,
    layer: int | None = None,
    hold_time: int = 0,
    loop: bool = False,
    id: str = "",
) -> Path:
    """Creates a new Path and adds it to the Motion.paths dictionary with the path_id as key.

    Args:
        speed (float, optional): speed > 0. Defaults to 1.
        ease (easing.EasingFunction | None, optional): easing function for character movement. Defaults to None.
        layer (int | None, optional): layer to move the character to, if None, layer is unchanged. Defaults to None.
        hold_time (int, optional): number of frames to hold the character at the end of the path. Defaults to 0.
        loop (bool, optional): Whether the path should loop back to the beginning. Default is False.
        id (str, optional): Unique identifier for the path. Used to query for the path. Defaults to "".

    Raises:
        ValueError: If a path with the provided id already exists.

    Returns:
        Path: The new path.
    """
    if not id:
        found_unique = False
        current_id = len(self.paths)
        while not found_unique:
            id = f"{len(self.paths)}"
            if id not in self.paths:
                found_unique = True
            else:
                current_id += 1
    if id in self.paths:
        raise ValueError(f"Path with id {id} already exists.")
    new_path = Path(id, speed, ease, layer, hold_time, loop)
    self.paths[id] = new_path
    return new_path

query_path(path_id)

Returns the path with the given path_id.

Parameters:

Name Type Description Default
path_id str

path_id

required

Returns:

Name Type Description
Path Path

The path with the given path_id.

Source code in terminaltexteffects/engine/motion.py
def query_path(self, path_id: str) -> Path:
    """Returns the path with the given path_id.

    Args:
        path_id (str): path_id

    Returns:
        Path: The path with the given path_id.
    """
    path = self.paths.get(path_id, None)
    if not path:
        raise ValueError(f"Path with id {path_id} not found.")
    return path

set_coordinate(coord)

Sets the current coordinate to the given coordinate.

Parameters:

Name Type Description Default
coord Coord

coordinate

required
Source code in terminaltexteffects/engine/motion.py
def set_coordinate(self, coord: Coord) -> None:
    """Sets the current coordinate to the given coordinate.

    Args:
        coord (Coord): coordinate
    """
    self.current_coord = coord