Skip to content

LaserEtch

Demo

Quick Start

laseretch.py
from terminaltexteffects import Gradient
from terminaltexteffects.effects.effect_laseretch import LaserEtch

effect = LaserEtch("YourTextHere")
effect.effect_config.final_gradient_direction = Gradient.Direction.HORIZONTAL
with effect.terminal_output() as terminal:
    for frame in effect:
        terminal.print(frame)

Etch Patterns

New in 0.16.0

CharacterOrder patterns and --reverse-etch-pattern are available on development main and are not in the published 0.15.0 package.

--etch-pattern accepts algorithm and every CharacterOrder. The default algorithm follows a recursive backtracker. Group patterns reverse alternate groups for serpentine traversal; sort patterns follow the exact order returned by Terminal.get_characters().

See Spiral Sorting for the six spiral orders, their starting corners, and how multiple arms interleave.

The laser moves to each scheduled character; etch_speed controls how many characters are etched per emission and etch_delay controls the frames between emissions. Uncolored spaces retain the existing skip behavior.

tte laseretch --etch-pattern spiral_clockwise
tte laseretch --etch-pattern spiral_counter_clockwise_double --etch-speed 1

Use --reverse-etch-pattern to reverse the complete traversal while preserving group membership. The flag defaults to off. Reversed spirals travel outward along the selected path; changing clockwise to counterclockwise instead selects a different inward path. Reversal applies after serpentine traversal or after the default algorithm has built its path.

tte laseretch --etch-pattern spiral_clockwise --reverse-etch-pattern

Configurations normalize to CharacterOrder. Existing CharacterGroup and CharacterSort values and their CLI spellings remain accepted as compatibility inputs.

For library use, assign a native enum or its CLI spelling:

from terminaltexteffects.effects.effect_laseretch import LaserEtch
from terminaltexteffects import CharacterOrder

effect = LaserEtch("YourTextHere")
effect.effect_config.etch_pattern = CharacterOrder.SPIRAL_CLOCKWISE_QUAD

A laser etches characters onto the terminal.

Classes:

Name Description
LaserEtch

A laser etches characters onto the terminal.

LaserEtchConfig

Configuration for the LaserEtch effect.

LaserEtchIterator

Iterator for the LaserEtch effect.

LaserEtch

Bases: BaseEffect[LaserEtchConfig]

A laser etches characters onto the terminal.

Source code in terminaltexteffects/effects/effect_laseretch.py
class LaserEtch(BaseEffect[LaserEtchConfig]):
    """A laser etches characters onto the terminal."""

    @property
    def _config_cls(self) -> type:
        return LaserEtchConfig

    @property
    def _iterator_cls(self) -> type:
        return LaserEtchIterator

LaserEtchConfig dataclass

Bases: BaseConfig

LaserEtch effect configuration dataclass.

Attributes:

Name Type Description
reverse_etch_pattern bool

Reverse the complete traversal while preserving group membership.

etch_pattern `CharacterOrder` | `Literal['algorithm']`

Character order used to etch the text. algorithm follows a recursive backtracker. Spatial patterns reverse alternate groups for serpentine traversal; individual patterns use the exact sorted order, including clockwise and counterclockwise single, double, and quad spirals.

etch_speed int

Along with etch_delay, determines the speed at which the characters are etched onto the terminal. This value specifies the number of characters to etch simultaneously.

etch_delay int

Along with etch_speed, determines the speed at which the characters are etched onto the terminal. This values specifies the number of frames to wait before etching the next group of characters.

cool_gradient_stops tuple[Color, ...]

Space separated, unquoted, list of colors for the gradient used to cool the characters after etching. If only one color is provided, the characters will be displayed in that color.

laser_gradient_stops tuple[Color, ...]

Space separated, unquoted, list of colors for the laser gradient. If only one color is provided, the characters will be displayed in that color.

spark_gradient_stops tuple[Color, ...]

Space separated, unquoted, list of colors for the spark cooling gradient. If only one color is provided, the characters will be displayed in that color.

spark_cooling_frames int

Number of frames to display each spark cooling gradient step. Increase to slow down the rate of cooling.

final_gradient_stops tuple[Color, ...]

Space separated, unquoted, list of colors for the character gradient (applied across the canvas). If only one color is provided, the characters will be displayed in that color.

final_gradient_steps tuple[int, ...] | int

Space separated, unquoted, list of the number of gradient steps to use. More steps will create a smoother and longer gradient animation.

final_gradient_frames int

Number of frames to display each gradient step. Increase to slow down the gradient animation.

final_gradient_direction Direction

Direction of the final gradient.

Source code in terminaltexteffects/effects/effect_laseretch.py
@dataclass
class LaserEtchConfig(BaseConfig):
    """LaserEtch effect configuration dataclass.

    Attributes:
        reverse_etch_pattern (bool): Reverse the complete traversal while preserving group membership.
        etch_pattern (`CharacterOrder` | `Literal['algorithm']`): Character order used
            to etch the text. `algorithm` follows a recursive backtracker. Spatial patterns reverse
            alternate groups for serpentine traversal; individual patterns use the exact sorted order,
            including clockwise and counterclockwise single, double, and quad spirals.
        etch_speed (int): Along with etch_delay, determines the speed at which the characters are etched onto the terminal.
            This value specifies the number of characters to etch simultaneously.
        etch_delay (int): Along with etch_speed, determines the speed at which the characters are etched onto the terminal.
            This values specifies the number of frames to wait before etching the next group of characters.
        cool_gradient_stops (tuple[tte.Color, ...]): Space separated, unquoted, list of colors for the gradient used to
            cool the characters after etching. If only one color is provided, the characters will be displayed in that color.
        laser_gradient_stops (tuple[tte.Color, ...]): Space separated, unquoted, list of colors for the laser gradient.
            If only one color is provided, the characters will be displayed in that color.
        spark_gradient_stops (tuple[tte.Color, ...]): Space separated, unquoted, list of colors for the spark cooling gradient.
            If only one color is provided, the characters will be displayed in that color.
        spark_cooling_frames (int): Number of frames to display each spark cooling gradient step. Increase to slow down the
            rate of cooling.
        final_gradient_stops (tuple[tte.Color, ...]): Space separated, unquoted, list of colors for the character gradient
            (applied across the canvas). If only one color is provided, the characters will be displayed in that color.
        final_gradient_steps (tuple[int, ...] | int): Space separated, unquoted, list of the number of gradient steps to use.
            More steps will create a smoother and longer gradient animation.
        final_gradient_frames (int): Number of frames to display each gradient step.
            Increase to slow down the gradient animation.
        final_gradient_direction (tte.Gradient.Direction): Direction of the final gradient.

    """  # noqa: E501

    parser_spec: argutils.ParserSpec = argutils.ParserSpec(
        name="laseretch",
        help="A laser etches characters onto the terminal.",
        description="A laser etches characters onto the terminal.",
        epilog=(
            "Example: terminaltexteffects laseretch --etch-pattern algorithm --etch-speed 1 --etch-delay 1 "
            "--cool-gradient-stops ffe680 ff7b00 --laser-gradient-stops ffffff 376cff "
            "--spark-gradient-stops ffffff ffe680 ff7b00 1a0900 --spark-cooling-frames 7 --final-gradient-stops "
            "8A008A 00D1FF ffffff --final-gradient-steps 8 --final-gradient-frames 4 "
            "--final-gradient-direction vertical"
        ),
    )

    etch_pattern: argutils.CharacterOrder | argutils.CharacterGroup | argutils.CharacterSort | Literal["algorithm"] = (
        argutils.ArgSpec(
            name="--etch-pattern",
            default="algorithm",
            type=_etch_pattern_type_parser,
            metavar="algorithm " + " ".join(argutils.CharacterOrderArg.METAVAR),
            help="Pattern used to etch the text: algorithm, serpentine character groups, or exact character sorts.",
        )
    )  # pyright: ignore[reportAssignmentType]
    "CharacterOrder | Literal['algorithm']: Pattern used to etch the text."

    reverse_etch_pattern: bool = argutils.ArgSpec(
        name="--reverse-etch-pattern",
        default=False,
        action="store_true",
        help="Reverse the complete etch pattern traversal.",
    )  # pyright: ignore[reportAssignmentType]
    "bool : Reverse the complete traversal, preserving group membership."

    etch_speed: int = argutils.ArgSpec(
        name="--etch-speed",
        type=argutils.PositiveInt.type_parser,
        default=1,
        metavar=argutils.PositiveInt.METAVAR,
        help="Along with etch_delay, determines the speed at which the characters are etched onto the terminal. "
        "This value specifies the number of characters to etch simultaneously.",
    )  # pyright: ignore[reportAssignmentType]
    (
        "int: Along with etch_delay, determines the speed at which the characters are etched onto the terminal. "
        "This value specifies the number of characters to etch simultaneously."
    )

    etch_delay: int = argutils.ArgSpec(
        name="--etch-delay",
        type=argutils.NonNegativeInt.type_parser,
        default=1,
        metavar=argutils.NonNegativeInt.METAVAR,
        help="Along with etch_speed, determines the speed at which the characters are etched onto the terminal. "
        "This values specifies the number of frames to wait before etching the next set of characters.",
    )  # pyright: ignore[reportAssignmentType]
    (
        "int: Along with etch_speed, determines the speed at which the characters are etched onto the terminal. "
        "This values specifies the number of frames to wait before etching the next set of characters."
    )

    cool_gradient_stops: tuple[tte.Color, ...] = argutils.ArgSpec(
        name="--cool-gradient-stops",
        type=argutils.ColorArg.type_parser,
        nargs="+",
        action=argutils.TupleAction,
        default=(tte.Color("#ffe680"), tte.Color("#ff7b00")),
        metavar=argutils.ColorArg.METAVAR,
        help="Space separated, unquoted, list of colors for the gradient used to cool the characters after etching. "
        "If only one color is provided, the characters will be displayed in that color.",
    )  # pyright: ignore[reportAssignmentType]
    "tuple[Color, ...]: Space separated, unquoted, list of colors for the cooling gradient "
    "If only one color is provided, the characters will be displayed in that color."

    laser_gradient_stops: tuple[tte.Color, ...] = argutils.ArgSpec(
        name="--laser-gradient-stops",
        type=argutils.ColorArg.type_parser,
        nargs="+",
        action=argutils.TupleAction,
        default=(tte.Color("#ffffff"), tte.Color("#376cff")),
        metavar=argutils.ColorArg.METAVAR,
        help="Space separated, unquoted, list of colors for the laser gradient. "
        "If only one color is provided, the characters will be displayed in that color.",
    )  # pyright: ignore[reportAssignmentType]
    "tuple[Color, ...]: Space separated, unquoted, list of colors for the laser gradient. "
    "If only one color is provided, the characters will be displayed in that color."

    spark_gradient_stops: tuple[tte.Color, ...] = argutils.ArgSpec(
        name="--spark-gradient-stops",
        type=argutils.ColorArg.type_parser,
        nargs="+",
        action=argutils.TupleAction,
        default=(tte.Color("#ffffff"), tte.Color("#ffe680"), tte.Color("#ff7b00"), tte.Color("#1a0900")),
        metavar=argutils.ColorArg.METAVAR,
        help="Space separated, unquoted, list of colors for the spark cooling gradient. "
        "If only one color is provided, the characters will be displayed in that color.",
    )  # pyright: ignore[reportAssignmentType]
    "tuple[Color, ...]: Space separated, unquoted, list of colors for the spark cooling gradient. "
    "If only one color is provided, the characters will be displayed in that color."

    spark_cooling_frames: int = argutils.ArgSpec(
        name="--spark-cooling-frames",
        type=argutils.PositiveInt.type_parser,
        default=7,
        metavar=argutils.PositiveInt.METAVAR,
        help="Number of frames to display each spark cooling gradient step. Increase to slow down the rate of cooling.",
    )  # pyright: ignore[reportAssignmentType]
    "int: Number of frames to display each spark cooling gradient step. Increase to slow down the rate of cooling."

    final_gradient_stops: tuple[tte.Color, ...] = FinalGradientStopsArg(
        default=(tte.Color("#8A008A"), tte.Color("#00D1FF"), tte.Color("#ffffff")),
    )  # pyright: ignore[reportAssignmentType]
    "tuple[Color, ...]: Space separated, unquoted, list of colors for the character gradient "
    "(applied across the canvas). If only one color is provided, the characters will be displayed in that color."

    final_gradient_steps: tuple[int, ...] | int = FinalGradientStepsArg(
        default=8,
    )  # pyright: ignore[reportAssignmentType]
    "tuple[int, ...] | int: Space separated, unquoted, list of the number of gradient steps to use. More steps will "
    "create a smoother and longer gradient animation."

    final_gradient_frames: int = FinalGradientFramesArg(
        default=4,
    )  # pyright: ignore[reportAssignmentType]
    "int: Number of frames to display each gradient step. Increase to slow down the gradient animation."

    final_gradient_direction: tte.Gradient.Direction = FinalGradientDirectionArg(
        default=tte.Gradient.Direction.VERTICAL,
    )  # pyright: ignore[reportAssignmentType]
    "Gradient.Direction : Direction of the final gradient."

cool_gradient_stops = argutils.ArgSpec(name='--cool-gradient-stops', type=argutils.ColorArg.type_parser, nargs='+', action=argutils.TupleAction, default=(tte.Color('#ffe680'), tte.Color('#ff7b00')), metavar=argutils.ColorArg.METAVAR, help='Space separated, unquoted, list of colors for the gradient used to cool the characters after etching. If only one color is provided, the characters will be displayed in that color.') class-attribute instance-attribute

tuple[Color, ...]: Space separated, unquoted, list of colors for the cooling gradient

etch_delay = argutils.ArgSpec(name='--etch-delay', type=argutils.NonNegativeInt.type_parser, default=1, metavar=argutils.NonNegativeInt.METAVAR, help='Along with etch_speed, determines the speed at which the characters are etched onto the terminal. This values specifies the number of frames to wait before etching the next set of characters.') class-attribute instance-attribute

int: Along with etch_speed, determines the speed at which the characters are etched onto the terminal. This values specifies the number of frames to wait before etching the next set of characters.

etch_pattern = argutils.ArgSpec(name='--etch-pattern', default='algorithm', type=_etch_pattern_type_parser, metavar='algorithm ' + ' '.join(argutils.CharacterOrderArg.METAVAR), help='Pattern used to etch the text: algorithm, serpentine character groups, or exact character sorts.') class-attribute instance-attribute

CharacterOrder | Literal['algorithm']: Pattern used to etch the text.

etch_speed = argutils.ArgSpec(name='--etch-speed', type=argutils.PositiveInt.type_parser, default=1, metavar=argutils.PositiveInt.METAVAR, help='Along with etch_delay, determines the speed at which the characters are etched onto the terminal. This value specifies the number of characters to etch simultaneously.') class-attribute instance-attribute

int: Along with etch_delay, determines the speed at which the characters are etched onto the terminal. This value specifies the number of characters to etch simultaneously.

final_gradient_direction = FinalGradientDirectionArg(default=tte.Gradient.Direction.VERTICAL) class-attribute instance-attribute

Gradient.Direction : Direction of the final gradient.

final_gradient_frames = FinalGradientFramesArg(default=4) class-attribute instance-attribute

int: Number of frames to display each gradient step. Increase to slow down the gradient animation.

final_gradient_steps = FinalGradientStepsArg(default=8) class-attribute instance-attribute

tuple[int, ...] | int: Space separated, unquoted, list of the number of gradient steps to use. More steps will

final_gradient_stops = FinalGradientStopsArg(default=(tte.Color('#8A008A'), tte.Color('#00D1FF'), tte.Color('#ffffff'))) class-attribute instance-attribute

tuple[Color, ...]: Space separated, unquoted, list of colors for the character gradient

laser_gradient_stops = argutils.ArgSpec(name='--laser-gradient-stops', type=argutils.ColorArg.type_parser, nargs='+', action=argutils.TupleAction, default=(tte.Color('#ffffff'), tte.Color('#376cff')), metavar=argutils.ColorArg.METAVAR, help='Space separated, unquoted, list of colors for the laser gradient. If only one color is provided, the characters will be displayed in that color.') class-attribute instance-attribute

tuple[Color, ...]: Space separated, unquoted, list of colors for the laser gradient.

reverse_etch_pattern = argutils.ArgSpec(name='--reverse-etch-pattern', default=False, action='store_true', help='Reverse the complete etch pattern traversal.') class-attribute instance-attribute

bool : Reverse the complete traversal, preserving group membership.

spark_cooling_frames = argutils.ArgSpec(name='--spark-cooling-frames', type=argutils.PositiveInt.type_parser, default=7, metavar=argutils.PositiveInt.METAVAR, help='Number of frames to display each spark cooling gradient step. Increase to slow down the rate of cooling.') class-attribute instance-attribute

int: Number of frames to display each spark cooling gradient step. Increase to slow down the rate of cooling.

spark_gradient_stops = argutils.ArgSpec(name='--spark-gradient-stops', type=argutils.ColorArg.type_parser, nargs='+', action=argutils.TupleAction, default=(tte.Color('#ffffff'), tte.Color('#ffe680'), tte.Color('#ff7b00'), tte.Color('#1a0900')), metavar=argutils.ColorArg.METAVAR, help='Space separated, unquoted, list of colors for the spark cooling gradient. If only one color is provided, the characters will be displayed in that color.') class-attribute instance-attribute

tuple[Color, ...]: Space separated, unquoted, list of colors for the spark cooling gradient.

LaserEtchIterator

Bases: BaseEffectIterator[LaserEtchConfig]

Iterator for the LaserEtch effect.

Source code in terminaltexteffects/effects/effect_laseretch.py
class LaserEtchIterator(BaseEffectIterator[LaserEtchConfig]):
    """Iterator for the LaserEtch effect."""

    class Laser:
        """A class to represent a laser beam effect in a terminal.

        The Laser class is responsible for creating and managing a laser beam effect
        in a terminal. It handles the initialization of the laser beam, the creation
        of spark effects, repositioning of the laser beam, emitting sparks, and
        disabling the laser beam.

        Methods:
            reposition(target: Coord) -> None:
                Repositions the laser beam to the target coordinate.
            emit(spark_count: int = 1) -> None:
                Emits a specified number of sparks from the laser beam.
            disable() -> None:
                Disables the laser beam by setting the visibility of the beam characters to False.

        """

        def __init__(
            self,
            terminal: tte.Terminal,
            config: LaserEtchConfig,
            active_chars: set[tte.EffectCharacter],
        ) -> None:
            """Initialize the laser beam.

            Args:
                terminal (Terminal): The effect terminal.
                config (LaserEtchConfig): The effect configuration.
                active_chars (set[EffectCharacter]): The set of active characters in the effect.

            """
            self.terminal = terminal
            self.config = config
            self.active_chars = active_chars
            self.position: tte.Coord = tte.Coord(0, 0)
            row = 0
            col = 0
            self.beam_chars: list[tte.EffectCharacter] = []
            laser_gradient = deque(tte.Gradient(*config.laser_gradient_stops, steps=6, loop=True))
            self.spark_gradient = tte.Gradient(
                *config.spark_gradient_stops,
                steps=(3, 8),
            )
            self.sparks_pool = self._make_sparks_pool()
            while row <= self.terminal.canvas.top:
                symbol = "*" if not self.beam_chars else "/"
                char = self.terminal.add_character(symbol, tte.Coord(col, row))
                char.layer = 2
                self.terminal.set_character_visibility(char, is_visible=True)
                row += 1
                col += 1
                self.beam_chars.append(char)
                laser_scn = char.animation.new_scene(scene_id="laser", is_looping=True)
                for color in laser_gradient:
                    laser_scn.add_frame(char.input_symbol, 3, colors=tte.ColorPair(fg=color))
                laser_gradient.rotate(-1)
                char.animation.activate_scene(laser_scn)

        def _make_sparks_pool(self) -> tte.ParticlePool:
            def initialize_sparks(spark_char: tte.EffectCharacter) -> None:
                spark_char.layer = 2
                spark_scn = spark_char.animation.new_scene(scene_id="spark")
                for color in self.spark_gradient:
                    spark_scn.add_frame(
                        spark_char.input_symbol,
                        self.config.spark_cooling_frames,
                        colors=tte.ColorPair(fg=color),
                    )

            sparks_pool = tte.ParticlePool(
                self.terminal,
                self.active_chars,
                symbols=(".", ",", "*"),
                initial_count=2000,
                initializer=initialize_sparks,
            )
            for spark in sparks_pool.particles:
                sparks_pool.reclaim_on_event(spark, spark.animation.query_scene("spark"))
            return sparks_pool

        def reposition(self, target: tte.Coord) -> None:
            """Reposition the laser beam to the target coordinate.

            Set the coordinate of the laser beam characters based on the target coordinate to
            create the appearance of a laser beam.

            Args:
                target (Coord): The target coordinate for the laser beam.

            """
            self.position = target
            row = target.row
            col = target.column
            for char in self.beam_chars:
                char.motion.set_coordinate(tte.Coord(col, row))
                row += 1
                col += 1
            self.emit_sparks()

        def emit_sparks(self, spark_count: int = 1) -> None:
            """Emit sparks from the laser beam.

            Sets up the spark character Path and activates the Path and Scene for each spark character.

            Args:
                spark_count (int, optional): Number of spark characters to emit. Defaults to 1.

            """

            def setup_spark_path(spark_char: tte.EffectCharacter) -> None:
                spark_char.motion.set_coordinate(self.position)
                spark_path = spark_char.motion.new_path(ease=tte.easing.out_sine, speed=0.3)
                fall_target_coord = tte.Coord(
                    random.randint(self.position.column - 20, self.position.column + 20),
                    self.terminal.canvas.bottom,
                )
                spark_path.new_waypoint(
                    fall_target_coord,
                    bezier_control=tte.Coord(fall_target_coord.column, self.position.row + random.randint(-10, 20)),
                )
                spark_char.motion.activate_path(spark_path)
                spark_char.animation.activate_scene("spark")

            for _ in range(spark_count):
                self.sparks_pool.emit(self.position, on_emit=setup_spark_path)

        def disable(self) -> None:
            """Disable the laser beam by setting the visibility of the beam characters to False."""
            for char in self.beam_chars:
                char.animation.deactivate_scene()
                self.terminal.set_character_visibility(char, is_visible=False)

    def __init__(self, effect: LaserEtch) -> None:
        """Initialize the effect iterator."""
        super().__init__(effect)
        self.character_final_color_map: dict[tte.EffectCharacter, tte.ColorPair] = {}
        self.pending_chars: list[tte.EffectCharacter] = []
        self.build()
        self.char_delay = 0
        self.laser = LaserEtchIterator.Laser(self.terminal, self.config, self.active_characters)
        self.active_characters.update(self.laser.beam_chars)
        self.color_shifted_chars: set[tte.EffectCharacter] = set()

    @staticmethod
    def _has_input_colors(character: tte.EffectCharacter) -> bool:
        return any((character.animation.input_fg_color, character.animation.input_bg_color))

    def build(self) -> None:
        """Build the effect."""
        final_fg_gradient = tte.Gradient(*self.config.final_gradient_stops, steps=self.config.final_gradient_steps)
        final_gradient_mapping = final_fg_gradient.build_coordinate_color_mapping(
            self.terminal.canvas.text_bottom,
            self.terminal.canvas.text_top,
            self.terminal.canvas.text_left,
            self.terminal.canvas.text_right,
            self.config.final_gradient_direction,
        )
        for character in self.terminal.get_characters():
            final_fg_color: tte.Color | None
            final_bg_color: tte.Color | None
            if self.terminal.config.existing_color_handling == "dynamic":
                self.character_final_color_map[character] = tte.ColorPair(
                    fg=character.animation.input_fg_color,
                    bg=character.animation.input_bg_color,
                )
                final_fg_color = self.character_final_color_map[character].fg
                final_bg_color = self.character_final_color_map[character].bg
                cool_gradient = tte.Gradient(
                    *self.config.cool_gradient_stops,
                    steps=8,
                )
            else:
                self.character_final_color_map[character] = tte.ColorPair(
                    fg=final_gradient_mapping[character.input_coord],
                )
                final_fg_color = self.character_final_color_map[character].fg
                final_bg_color = self.character_final_color_map[character].bg
                cool_gradient = tte.Gradient(
                    *self.config.cool_gradient_stops,
                    final_gradient_mapping[character.input_coord],
                    steps=8,
                )
            spawn_scn = character.animation.new_scene(scene_id="spawn")
            spawn_scn.add_frame("^", duration=3, colors=tte.ColorPair("#ffe680"))
            for color in cool_gradient:
                spawn_scn.add_frame(character.input_symbol, 3, colors=tte.ColorPair(fg=color))
            if self.terminal.config.existing_color_handling == "dynamic":
                if final_fg_color or final_bg_color:
                    fg_gradient = (
                        tte.Gradient(cool_gradient.spectrum[-1], final_fg_color, steps=8) if final_fg_color else None
                    )
                    bg_gradient = (
                        tte.Gradient(cool_gradient.spectrum[-1], final_bg_color, steps=8) if final_bg_color else None
                    )
                    spawn_scn.apply_gradient_to_symbols(
                        character.input_symbol,
                        3,
                        fg_gradient=fg_gradient,
                        bg_gradient=bg_gradient,
                    )
                else:
                    white_cooldown = tte.Gradient(cool_gradient.spectrum[-1], tte.Color("#ffffff"), steps=8)
                    spawn_scn.apply_gradient_to_symbols(
                        character.input_symbol,
                        3,
                        fg_gradient=white_cooldown,
                    )
                    spawn_scn.add_frame(character.input_symbol, 3, colors=tte.ColorPair())
            character.animation.activate_scene(spawn_scn)
        if self.config.etch_pattern == "algorithm":
            algo = RecursiveBacktracker(self.terminal, limit_to_text_boundary=True)
            while not algo.complete:
                algo.step()
            self.pending_chars = algo.char_link_order
            if self.config.reverse_etch_pattern:
                self.pending_chars.reverse()
        else:
            self.pending_chars = self.terminal.get_characters(
                order=self.config.etch_pattern,
                serpentine=True,
                reverse=self.config.reverse_etch_pattern,
            )

    def __next__(self) -> str:
        """Return the next frame in the effect."""
        while self.pending_chars or self.active_characters:
            if not self.char_delay:
                for _ in range(self.config.etch_speed):
                    if not self.pending_chars:
                        break
                    next_char = self.pending_chars.pop(0)
                    while next_char.input_symbol == " " and not self._has_input_colors(next_char):
                        if self.pending_chars:
                            next_char = self.pending_chars.pop(0)
                        else:
                            break

                    self.terminal.set_character_visibility(next_char, is_visible=True)
                    self.active_characters.add(next_char)
                    self.laser.reposition(next_char.input_coord)

                self.char_delay = self.config.etch_delay
            else:
                self.char_delay -= 1
            if self.pending_chars:
                self.active_characters.update(self.laser.beam_chars)
            else:
                self.laser.disable()
            self.update()
            return self.frame
        raise StopIteration

Laser

A class to represent a laser beam effect in a terminal.

The Laser class is responsible for creating and managing a laser beam effect in a terminal. It handles the initialization of the laser beam, the creation of spark effects, repositioning of the laser beam, emitting sparks, and disabling the laser beam.

Methods:

Name Description
reposition

Coord) -> None: Repositions the laser beam to the target coordinate.

emit

int = 1) -> None: Emits a specified number of sparks from the laser beam.

disable

Disables the laser beam by setting the visibility of the beam characters to False.

Source code in terminaltexteffects/effects/effect_laseretch.py
class Laser:
    """A class to represent a laser beam effect in a terminal.

    The Laser class is responsible for creating and managing a laser beam effect
    in a terminal. It handles the initialization of the laser beam, the creation
    of spark effects, repositioning of the laser beam, emitting sparks, and
    disabling the laser beam.

    Methods:
        reposition(target: Coord) -> None:
            Repositions the laser beam to the target coordinate.
        emit(spark_count: int = 1) -> None:
            Emits a specified number of sparks from the laser beam.
        disable() -> None:
            Disables the laser beam by setting the visibility of the beam characters to False.

    """

    def __init__(
        self,
        terminal: tte.Terminal,
        config: LaserEtchConfig,
        active_chars: set[tte.EffectCharacter],
    ) -> None:
        """Initialize the laser beam.

        Args:
            terminal (Terminal): The effect terminal.
            config (LaserEtchConfig): The effect configuration.
            active_chars (set[EffectCharacter]): The set of active characters in the effect.

        """
        self.terminal = terminal
        self.config = config
        self.active_chars = active_chars
        self.position: tte.Coord = tte.Coord(0, 0)
        row = 0
        col = 0
        self.beam_chars: list[tte.EffectCharacter] = []
        laser_gradient = deque(tte.Gradient(*config.laser_gradient_stops, steps=6, loop=True))
        self.spark_gradient = tte.Gradient(
            *config.spark_gradient_stops,
            steps=(3, 8),
        )
        self.sparks_pool = self._make_sparks_pool()
        while row <= self.terminal.canvas.top:
            symbol = "*" if not self.beam_chars else "/"
            char = self.terminal.add_character(symbol, tte.Coord(col, row))
            char.layer = 2
            self.terminal.set_character_visibility(char, is_visible=True)
            row += 1
            col += 1
            self.beam_chars.append(char)
            laser_scn = char.animation.new_scene(scene_id="laser", is_looping=True)
            for color in laser_gradient:
                laser_scn.add_frame(char.input_symbol, 3, colors=tte.ColorPair(fg=color))
            laser_gradient.rotate(-1)
            char.animation.activate_scene(laser_scn)

    def _make_sparks_pool(self) -> tte.ParticlePool:
        def initialize_sparks(spark_char: tte.EffectCharacter) -> None:
            spark_char.layer = 2
            spark_scn = spark_char.animation.new_scene(scene_id="spark")
            for color in self.spark_gradient:
                spark_scn.add_frame(
                    spark_char.input_symbol,
                    self.config.spark_cooling_frames,
                    colors=tte.ColorPair(fg=color),
                )

        sparks_pool = tte.ParticlePool(
            self.terminal,
            self.active_chars,
            symbols=(".", ",", "*"),
            initial_count=2000,
            initializer=initialize_sparks,
        )
        for spark in sparks_pool.particles:
            sparks_pool.reclaim_on_event(spark, spark.animation.query_scene("spark"))
        return sparks_pool

    def reposition(self, target: tte.Coord) -> None:
        """Reposition the laser beam to the target coordinate.

        Set the coordinate of the laser beam characters based on the target coordinate to
        create the appearance of a laser beam.

        Args:
            target (Coord): The target coordinate for the laser beam.

        """
        self.position = target
        row = target.row
        col = target.column
        for char in self.beam_chars:
            char.motion.set_coordinate(tte.Coord(col, row))
            row += 1
            col += 1
        self.emit_sparks()

    def emit_sparks(self, spark_count: int = 1) -> None:
        """Emit sparks from the laser beam.

        Sets up the spark character Path and activates the Path and Scene for each spark character.

        Args:
            spark_count (int, optional): Number of spark characters to emit. Defaults to 1.

        """

        def setup_spark_path(spark_char: tte.EffectCharacter) -> None:
            spark_char.motion.set_coordinate(self.position)
            spark_path = spark_char.motion.new_path(ease=tte.easing.out_sine, speed=0.3)
            fall_target_coord = tte.Coord(
                random.randint(self.position.column - 20, self.position.column + 20),
                self.terminal.canvas.bottom,
            )
            spark_path.new_waypoint(
                fall_target_coord,
                bezier_control=tte.Coord(fall_target_coord.column, self.position.row + random.randint(-10, 20)),
            )
            spark_char.motion.activate_path(spark_path)
            spark_char.animation.activate_scene("spark")

        for _ in range(spark_count):
            self.sparks_pool.emit(self.position, on_emit=setup_spark_path)

    def disable(self) -> None:
        """Disable the laser beam by setting the visibility of the beam characters to False."""
        for char in self.beam_chars:
            char.animation.deactivate_scene()
            self.terminal.set_character_visibility(char, is_visible=False)

__init__(terminal, config, active_chars)

Initialize the laser beam.

Parameters:

Name Type Description Default
terminal Terminal

The effect terminal.

required
config LaserEtchConfig

The effect configuration.

required
active_chars set[EffectCharacter]

The set of active characters in the effect.

required
Source code in terminaltexteffects/effects/effect_laseretch.py
def __init__(
    self,
    terminal: tte.Terminal,
    config: LaserEtchConfig,
    active_chars: set[tte.EffectCharacter],
) -> None:
    """Initialize the laser beam.

    Args:
        terminal (Terminal): The effect terminal.
        config (LaserEtchConfig): The effect configuration.
        active_chars (set[EffectCharacter]): The set of active characters in the effect.

    """
    self.terminal = terminal
    self.config = config
    self.active_chars = active_chars
    self.position: tte.Coord = tte.Coord(0, 0)
    row = 0
    col = 0
    self.beam_chars: list[tte.EffectCharacter] = []
    laser_gradient = deque(tte.Gradient(*config.laser_gradient_stops, steps=6, loop=True))
    self.spark_gradient = tte.Gradient(
        *config.spark_gradient_stops,
        steps=(3, 8),
    )
    self.sparks_pool = self._make_sparks_pool()
    while row <= self.terminal.canvas.top:
        symbol = "*" if not self.beam_chars else "/"
        char = self.terminal.add_character(symbol, tte.Coord(col, row))
        char.layer = 2
        self.terminal.set_character_visibility(char, is_visible=True)
        row += 1
        col += 1
        self.beam_chars.append(char)
        laser_scn = char.animation.new_scene(scene_id="laser", is_looping=True)
        for color in laser_gradient:
            laser_scn.add_frame(char.input_symbol, 3, colors=tte.ColorPair(fg=color))
        laser_gradient.rotate(-1)
        char.animation.activate_scene(laser_scn)

disable()

Disable the laser beam by setting the visibility of the beam characters to False.

Source code in terminaltexteffects/effects/effect_laseretch.py
def disable(self) -> None:
    """Disable the laser beam by setting the visibility of the beam characters to False."""
    for char in self.beam_chars:
        char.animation.deactivate_scene()
        self.terminal.set_character_visibility(char, is_visible=False)

emit_sparks(spark_count=1)

Emit sparks from the laser beam.

Sets up the spark character Path and activates the Path and Scene for each spark character.

Parameters:

Name Type Description Default
spark_count int

Number of spark characters to emit. Defaults to 1.

1
Source code in terminaltexteffects/effects/effect_laseretch.py
def emit_sparks(self, spark_count: int = 1) -> None:
    """Emit sparks from the laser beam.

    Sets up the spark character Path and activates the Path and Scene for each spark character.

    Args:
        spark_count (int, optional): Number of spark characters to emit. Defaults to 1.

    """

    def setup_spark_path(spark_char: tte.EffectCharacter) -> None:
        spark_char.motion.set_coordinate(self.position)
        spark_path = spark_char.motion.new_path(ease=tte.easing.out_sine, speed=0.3)
        fall_target_coord = tte.Coord(
            random.randint(self.position.column - 20, self.position.column + 20),
            self.terminal.canvas.bottom,
        )
        spark_path.new_waypoint(
            fall_target_coord,
            bezier_control=tte.Coord(fall_target_coord.column, self.position.row + random.randint(-10, 20)),
        )
        spark_char.motion.activate_path(spark_path)
        spark_char.animation.activate_scene("spark")

    for _ in range(spark_count):
        self.sparks_pool.emit(self.position, on_emit=setup_spark_path)

reposition(target)

Reposition the laser beam to the target coordinate.

Set the coordinate of the laser beam characters based on the target coordinate to create the appearance of a laser beam.

Parameters:

Name Type Description Default
target Coord

The target coordinate for the laser beam.

required
Source code in terminaltexteffects/effects/effect_laseretch.py
def reposition(self, target: tte.Coord) -> None:
    """Reposition the laser beam to the target coordinate.

    Set the coordinate of the laser beam characters based on the target coordinate to
    create the appearance of a laser beam.

    Args:
        target (Coord): The target coordinate for the laser beam.

    """
    self.position = target
    row = target.row
    col = target.column
    for char in self.beam_chars:
        char.motion.set_coordinate(tte.Coord(col, row))
        row += 1
        col += 1
    self.emit_sparks()

__init__(effect)

Initialize the effect iterator.

Source code in terminaltexteffects/effects/effect_laseretch.py
def __init__(self, effect: LaserEtch) -> None:
    """Initialize the effect iterator."""
    super().__init__(effect)
    self.character_final_color_map: dict[tte.EffectCharacter, tte.ColorPair] = {}
    self.pending_chars: list[tte.EffectCharacter] = []
    self.build()
    self.char_delay = 0
    self.laser = LaserEtchIterator.Laser(self.terminal, self.config, self.active_characters)
    self.active_characters.update(self.laser.beam_chars)
    self.color_shifted_chars: set[tte.EffectCharacter] = set()

__next__()

Return the next frame in the effect.

Source code in terminaltexteffects/effects/effect_laseretch.py
def __next__(self) -> str:
    """Return the next frame in the effect."""
    while self.pending_chars or self.active_characters:
        if not self.char_delay:
            for _ in range(self.config.etch_speed):
                if not self.pending_chars:
                    break
                next_char = self.pending_chars.pop(0)
                while next_char.input_symbol == " " and not self._has_input_colors(next_char):
                    if self.pending_chars:
                        next_char = self.pending_chars.pop(0)
                    else:
                        break

                self.terminal.set_character_visibility(next_char, is_visible=True)
                self.active_characters.add(next_char)
                self.laser.reposition(next_char.input_coord)

            self.char_delay = self.config.etch_delay
        else:
            self.char_delay -= 1
        if self.pending_chars:
            self.active_characters.update(self.laser.beam_chars)
        else:
            self.laser.disable()
        self.update()
        return self.frame
    raise StopIteration

build()

Build the effect.

Source code in terminaltexteffects/effects/effect_laseretch.py
def build(self) -> None:
    """Build the effect."""
    final_fg_gradient = tte.Gradient(*self.config.final_gradient_stops, steps=self.config.final_gradient_steps)
    final_gradient_mapping = final_fg_gradient.build_coordinate_color_mapping(
        self.terminal.canvas.text_bottom,
        self.terminal.canvas.text_top,
        self.terminal.canvas.text_left,
        self.terminal.canvas.text_right,
        self.config.final_gradient_direction,
    )
    for character in self.terminal.get_characters():
        final_fg_color: tte.Color | None
        final_bg_color: tte.Color | None
        if self.terminal.config.existing_color_handling == "dynamic":
            self.character_final_color_map[character] = tte.ColorPair(
                fg=character.animation.input_fg_color,
                bg=character.animation.input_bg_color,
            )
            final_fg_color = self.character_final_color_map[character].fg
            final_bg_color = self.character_final_color_map[character].bg
            cool_gradient = tte.Gradient(
                *self.config.cool_gradient_stops,
                steps=8,
            )
        else:
            self.character_final_color_map[character] = tte.ColorPair(
                fg=final_gradient_mapping[character.input_coord],
            )
            final_fg_color = self.character_final_color_map[character].fg
            final_bg_color = self.character_final_color_map[character].bg
            cool_gradient = tte.Gradient(
                *self.config.cool_gradient_stops,
                final_gradient_mapping[character.input_coord],
                steps=8,
            )
        spawn_scn = character.animation.new_scene(scene_id="spawn")
        spawn_scn.add_frame("^", duration=3, colors=tte.ColorPair("#ffe680"))
        for color in cool_gradient:
            spawn_scn.add_frame(character.input_symbol, 3, colors=tte.ColorPair(fg=color))
        if self.terminal.config.existing_color_handling == "dynamic":
            if final_fg_color or final_bg_color:
                fg_gradient = (
                    tte.Gradient(cool_gradient.spectrum[-1], final_fg_color, steps=8) if final_fg_color else None
                )
                bg_gradient = (
                    tte.Gradient(cool_gradient.spectrum[-1], final_bg_color, steps=8) if final_bg_color else None
                )
                spawn_scn.apply_gradient_to_symbols(
                    character.input_symbol,
                    3,
                    fg_gradient=fg_gradient,
                    bg_gradient=bg_gradient,
                )
            else:
                white_cooldown = tte.Gradient(cool_gradient.spectrum[-1], tte.Color("#ffffff"), steps=8)
                spawn_scn.apply_gradient_to_symbols(
                    character.input_symbol,
                    3,
                    fg_gradient=white_cooldown,
                )
                spawn_scn.add_frame(character.input_symbol, 3, colors=tte.ColorPair())
        character.animation.activate_scene(spawn_scn)
    if self.config.etch_pattern == "algorithm":
        algo = RecursiveBacktracker(self.terminal, limit_to_text_boundary=True)
        while not algo.complete:
            algo.step()
        self.pending_chars = algo.char_link_order
        if self.config.reverse_etch_pattern:
            self.pending_chars.reverse()
    else:
        self.pending_chars = self.terminal.get_characters(
            order=self.config.etch_pattern,
            serpentine=True,
            reverse=self.config.reverse_etch_pattern,
        )

get_effect_resources()

Get the command, effect class, and configuration class for the effect.

Returns:

Type Description
tuple[str, type[BaseEffect], type[BaseConfig]]

tuple[str, type[BaseEffect], type[BaseConfig]]: The command name, effect class, and configuration class.

Source code in terminaltexteffects/effects/effect_laseretch.py
def get_effect_resources() -> tuple[str, type[BaseEffect], type[BaseConfig]]:
    """Get the command, effect class, and configuration class for the effect.

    Returns:
        tuple[str, type[BaseEffect], type[BaseConfig]]: The command name, effect class, and configuration class.

    """
    return "laseretch", LaserEtch, LaserEtchConfig