Skip to content

Blackhole

Demo

Quick Start

blackhole.py
from terminaltexteffects.effects.effect_blackhole import Blackhole

effect = Blackhole("YourTextHere")
with effect.terminal_output() as terminal:
    for frame in effect:
        terminal.print(frame)

Creates a blackhole in a starfield, consumes the stars, explodes the input data back into position.

Classes:

Name Description
BlackholeConfig

Configuration for the Blackhole effect.

Blackhole

Creates a blackhole in a starfield, consumes the stars, explodes the input data back into position.

BlackholeIterator

Iterator for the Blackhole effect. Does not normally need to be called directly.

Blackhole

Bases: BaseEffect[BlackholeConfig]

Creates a blackhole in a starfield, consumes the stars, explodes the input data back into position.

Attributes:

Name Type Description
effect_config BlackholeConfig

Configuration for the Blackhole effect.

terminal_config TerminalConfig

Configuration for the terminal.

Source code in terminaltexteffects/effects/effect_blackhole.py
class Blackhole(BaseEffect[BlackholeConfig]):
    """Creates a blackhole in a starfield, consumes the stars, explodes the input data back into position.

    Attributes:
        effect_config (BlackholeConfig): Configuration for the Blackhole effect.
        terminal_config (TerminalConfig): Configuration for the terminal.

    """

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

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

BlackholeConfig dataclass

Bases: BaseConfig

Configuration for the Blackhole effect.

Attributes:

Name Type Description
blackhole_color Color

Color for the stars that comprise the blackhole border.

star_colors tuple[Color, ...]

Colors used for the collapse point and to color characters during the explosion before they cool to their final color.

final_gradient_stops tuple[Color, ...]

Tuple of colors for the character gradient. If only one color is provided, the characters will be displayed in that color.

final_gradient_steps tuple[int, ...] | int

Number of transitions used between final gradient stops to create color levels across the canvas. More transitions add spatial color detail but do not change animation duration. Valid values are n > 0.

final_gradient_direction Direction

Direction of the final gradient.

Source code in terminaltexteffects/effects/effect_blackhole.py
@dataclass
class BlackholeConfig(BaseConfig):
    """Configuration for the Blackhole effect.

    Attributes:
        blackhole_color (Color): Color for the stars that comprise the blackhole border.
        star_colors (tuple[Color, ...]): Colors used for the collapse point and to color characters during the
            explosion before they cool to their final color.
        final_gradient_stops (tuple[Color, ...]): Tuple of colors for the character gradient. If only one color is
            provided, the characters will be displayed in that color.
        final_gradient_steps (tuple[int, ...] | int): Number of transitions used between final gradient stops to
            create color levels across the canvas. More transitions add spatial color detail but do not change
            animation duration. Valid values are n > 0.
        final_gradient_direction (Gradient.Direction): Direction of the final gradient.

    """

    parser_spec: argutils.ParserSpec = argutils.ParserSpec(
        name="blackhole",
        help="Characters are consumed by a black hole and explode outwards.",
        description="blackhole | Characters are consumed by a black hole and explode outwards.",
        epilog=(
            "Example: terminaltexteffects blackhole --blackhole-color ffffff "
            "--star-colors ffcc0d ff7326 ff194d bf2669 702a8c 049dbf "
            "--final-gradient-stops 8A008A 00D1FF ffffff --final-gradient-steps 9 --final-gradient-direction diagonal"
        ),
    )
    blackhole_color: Color = argutils.ArgSpec(
        name="--blackhole-color",
        type=argutils.ColorArg.type_parser,
        default=Color("#ffffff"),
        metavar=argutils.ColorArg.METAVAR,
        help="Color for the stars that comprise the blackhole border.",
    )  # pyright: ignore[reportAssignmentType]
    "Color : Color for the stars that comprise the blackhole border."

    star_colors: tuple[Color, ...] = argutils.ArgSpec(
        name="--star-colors",
        type=argutils.ColorArg.type_parser,
        nargs="+",
        action=argutils.TupleAction,
        default=(
            Color("#ffcc0d"),
            Color("#ff7326"),
            Color("#ff194d"),
            Color("#bf2669"),
            Color("#702a8c"),
            Color("#049dbf"),
        ),
        metavar=argutils.ColorArg.METAVAR,
        help="Colors used for the collapse point and to color characters during the explosion before they cool to "
        "their final color.",
    )  # pyright: ignore[reportAssignmentType]
    (
        "tuple[Color, ...] : Colors used for the collapse point and to color characters during the explosion "
        "before they cool to their final color."
    )

    final_gradient_stops: tuple[Color, ...] = FinalGradientStopsArg(
        default=(Color("#8A008A"), Color("#00D1FF"), Color("#ffffff")),
    )  # pyright: ignore[reportAssignmentType]
    (
        "tuple[Color, ...] : Tuple of colors for the final color gradient. If only one color is provided, the "
        "characters will be displayed in that color."
    )

    final_gradient_steps: tuple[int, ...] | int = FinalGradientStepsArg(
        default=9,
        help=(
            "Number of transitions used between final gradient stops to create color levels across the canvas. More "
            "transitions add spatial color detail but do not change animation duration."
        ),
    )  # pyright: ignore[reportAssignmentType]
    (
        "tuple[int, ...] | int : Number of transitions used between final gradient stops to create color levels "
        "across the canvas. More transitions add spatial color detail but do not change animation duration."
    )

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

blackhole_color = argutils.ArgSpec(name='--blackhole-color', type=argutils.ColorArg.type_parser, default=Color('#ffffff'), metavar=argutils.ColorArg.METAVAR, help='Color for the stars that comprise the blackhole border.') class-attribute instance-attribute

Color : Color for the stars that comprise the blackhole border.

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

Gradient.Direction : Direction of the final gradient.

final_gradient_steps = FinalGradientStepsArg(default=9, help='Number of transitions used between final gradient stops to create color levels across the canvas. More transitions add spatial color detail but do not change animation duration.') class-attribute instance-attribute

tuple[int, ...] | int : Number of transitions used between final gradient stops to create color levels across the canvas. More transitions add spatial color detail but do not change animation duration.

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

tuple[Color, ...] : Tuple of colors for the final color gradient. If only one color is provided, the characters will be displayed in that color.

star_colors = argutils.ArgSpec(name='--star-colors', type=argutils.ColorArg.type_parser, nargs='+', action=argutils.TupleAction, default=(Color('#ffcc0d'), Color('#ff7326'), Color('#ff194d'), Color('#bf2669'), Color('#702a8c'), Color('#049dbf')), metavar=argutils.ColorArg.METAVAR, help='Colors used for the collapse point and to color characters during the explosion before they cool to their final color.') class-attribute instance-attribute

tuple[Color, ...] : Colors used for the collapse point and to color characters during the explosion before they cool to their final color.

BlackholeIterator

Bases: BaseEffectIterator[BlackholeConfig]

Iterator for the Blackhole effect.

Source code in terminaltexteffects/effects/effect_blackhole.py
class BlackholeIterator(BaseEffectIterator[BlackholeConfig]):
    """Iterator for the Blackhole effect."""

    def __init__(self, effect: Blackhole) -> None:
        """Initialize the Blackhole effect iterator.

        Args:
            effect (Blackhole): The Blackhole effect instance.

        """
        super().__init__(effect)
        self.pending_chars: list[EffectCharacter] = []
        self.blackhole_chars: list[EffectCharacter] = []
        self.blackhole_char_set: set[EffectCharacter] = set()
        self.helper_blackhole_chars: list[EffectCharacter] = []
        self.awaiting_consumption_chars: list[EffectCharacter] = []
        self.blackhole_radius = geometry.TERMINAL_ROW_SCALE * max(
            min(round(self.terminal.canvas.width * 0.3), round(self.terminal.canvas.height * 0.20)),
            3,
        )
        self.blackhole_character_count = self.blackhole_radius * 3 // geometry.TERMINAL_ROW_SCALE
        self.compact_layout = False
        self.blackhole_ring_positions: list[geometry.Coord] = []
        self.character_final_color_map: dict[EffectCharacter, Color] = {}
        self.preexisting_colors_present = any(
            any((character.animation.input_fg_color, character.animation.input_bg_color))
            for character in self.terminal.get_characters()
        )
        self.build()

    def prepare_blackhole(self) -> None:
        """Prepare the blackhole and starfield characters."""
        star_symbols = ["*", "'", "`", "¤", "•", "°", "·"]
        starfield_colors = Gradient(Color("#4a4a4d"), Color("#ffffff"), steps=6).spectrum
        gradient_map = {}
        for color in starfield_colors:
            gradient_map[color] = Gradient(color, Color("#000000"), steps=10)
        canvas = self.terminal.canvas
        radius = min(
            self.blackhole_radius,
            (canvas.width - 1) // 2,
            (canvas.height - 1) // 2 * geometry.TERMINAL_ROW_SCALE,
        )
        self.compact_layout = radius < self.blackhole_radius
        if radius >= geometry.TERMINAL_ROW_SCALE:
            ring_positions = geometry.find_coords_on_circle(
                canvas.center,
                radius=radius,
                coords_limit=self.blackhole_character_count,
            )
        else:
            ring_positions = geometry.find_coords_on_rect(
                canvas.center,
                (canvas.width - 1) // 2,
                (canvas.height - 1) // 2,
            )
        ring_count = min(len(ring_positions), self.blackhole_character_count)
        self.blackhole_ring_positions = [
            ring_positions[index * len(ring_positions) // ring_count] for index in range(ring_count)
        ]
        available_chars = list(self.terminal._input_characters)
        if len(available_chars) <= len(self.blackhole_ring_positions):
            for _ in self.blackhole_ring_positions:
                character = self.terminal.add_character("*", canvas.center)
                self.helper_blackhole_chars.append(character)
                self.blackhole_chars.append(character)
        else:
            while len(self.blackhole_chars) < len(self.blackhole_ring_positions):
                self.blackhole_chars.append(available_chars.pop(random.randrange(len(available_chars))))
        self.blackhole_char_set = set(self.blackhole_chars)
        for position_index, character in enumerate(self.blackhole_chars):
            starting_pos = self.blackhole_ring_positions[position_index]
            blackhole_path = character.motion.new_path(path_id="blackhole", speed=0.7, ease=easing.in_out_sine)
            blackhole_path.new_waypoint(starting_pos)
            blackhole_scn = character.animation.new_scene(scene_id="blackhole")
            blackhole_scn.add_frame("*", 1, colors=ColorPair(fg=self.config.blackhole_color))
            character.event_handler.register_event(
                EventHandler.Event.PATH_ACTIVATED,
                blackhole_path,
                EventHandler.Action.SET_LAYER,
                1,
            )
            # make rotation waypoints
            blackhole_rotation_path = character.motion.new_path(path_id="blackhole_rotation", speed=0.45, loop=True)
            for coord in (
                self.blackhole_ring_positions[position_index:] + self.blackhole_ring_positions[:position_index]
            ):
                blackhole_rotation_path.new_waypoint(coord, waypoint_id=str(len(blackhole_rotation_path.waypoints)))
        for character in self.terminal.get_characters():
            self.terminal.set_character_visibility(character, is_visible=True)
            starting_scn = character.animation.new_scene()
            star_symbol = random.choice(star_symbols)
            star_color = random.choice(starfield_colors)
            starting_scn.add_frame(star_symbol, 1, colors=ColorPair(fg=star_color))
            character.animation.activate_scene(starting_scn)
            if character not in self.blackhole_char_set:
                starfield_coord = self.terminal.canvas.random_coord()
                character.motion.set_coordinate(starfield_coord)

                singularity_path = character.motion.new_path(
                    path_id="singularity",
                    speed=random.uniform(0.17, 0.30),
                    ease=easing.in_expo,
                )
                singularity_path.new_waypoint(self.terminal.canvas.center)
                consumed_scn = character.animation.new_scene()
                for color in gradient_map[star_color]:
                    consumed_scn.add_frame(star_symbol, 1, colors=ColorPair(fg=color))
                consumed_scn.add_frame(" ", 1)
                consumed_scn.sync = Scene.SyncMetric.DISTANCE
                character.event_handler.register_event(
                    EventHandler.Event.PATH_ACTIVATED,
                    singularity_path,
                    EventHandler.Action.SET_LAYER,
                    2,
                )
                character.event_handler.register_event(
                    EventHandler.Event.PATH_ACTIVATED,
                    singularity_path,
                    EventHandler.Action.ACTIVATE_SCENE,
                    consumed_scn,
                )
                self.awaiting_consumption_chars.append(character)
        random.shuffle(self.awaiting_consumption_chars)

    def rotate_blackhole(self) -> None:
        """Rotate the blackhole characters."""
        for character in self.blackhole_chars:
            character.motion.activate_path("blackhole_rotation")
            self.active_characters.add(character)

    def collapse_blackhole(self) -> None:
        """Collapse the blackhole characters."""
        black_hole_ring_positions = (
            self.blackhole_ring_positions
            if self.compact_layout
            else geometry.find_coords_on_circle(
                self.terminal.canvas.center,
                radius=self.blackhole_radius + (3 * geometry.TERMINAL_ROW_SCALE),
                coords_limit=len(self.blackhole_chars),
            )
        )
        unstable_symbols = ["◦", "◎", "◉", "●", "◉", "◎", "◦"]
        point_char_made = False
        for character, next_pos in zip(self.blackhole_chars, black_hole_ring_positions):
            expand_path = character.motion.new_path(speed=0.2, ease=easing.in_expo)
            expand_path.new_waypoint(next_pos)
            collapse_path = character.motion.new_path(speed=0.3, ease=easing.in_expo)
            collapse_path.new_waypoint(self.terminal.canvas.center)
            character.event_handler.register_event(
                EventHandler.Event.PATH_COMPLETE,
                expand_path,
                EventHandler.Action.ACTIVATE_PATH,
                collapse_path,
            )
            if not point_char_made:
                point_scn = character.animation.new_scene()
                for _ in range(3):
                    for symbol in unstable_symbols:
                        point_scn.add_frame(
                            symbol,
                            3,
                            colors=ColorPair(fg=random.choice(self.config.star_colors)),
                        )
                character.event_handler.register_event(
                    EventHandler.Event.PATH_COMPLETE,
                    collapse_path,
                    EventHandler.Action.ACTIVATE_SCENE,
                    point_scn,
                )
                character.event_handler.register_event(
                    EventHandler.Event.PATH_COMPLETE,
                    collapse_path,
                    EventHandler.Action.SET_LAYER,
                    3,
                )
                point_char_made = True

            character.motion.activate_path(expand_path)
            self.active_characters.add(character)

    def explode_singularity(self) -> None:
        """Explode the singularity characters."""
        for character in self.helper_blackhole_chars:
            self.terminal.set_character_visibility(character, is_visible=False)
        for character in self.terminal.get_characters():
            nearby_coord = geometry.find_coords_on_circle(
                character.input_coord,
                radius=3 * geometry.TERMINAL_ROW_SCALE,
                coords_limit=5,
            )[random.randrange(0, 5)]
            nearby_path = character.motion.new_path(speed=random.randint(3, 4) / 10, ease=easing.out_expo)
            nearby_path.new_waypoint(nearby_coord)
            input_path = character.motion.new_path(speed=random.randint(4, 6) / 100, ease=easing.in_cubic)
            input_path.new_waypoint(character.input_coord)
            explode_scn = character.animation.new_scene()
            explode_star_color = random.choice(self.config.star_colors)
            explode_scn.add_frame(character.input_symbol, 1, colors=ColorPair(fg=explode_star_color))
            cooling_scn = character.animation.new_scene()
            if self.terminal.config.existing_color_handling == "dynamic" and self.preexisting_colors_present:
                if not any((character.animation.input_fg_color, character.animation.input_bg_color)):
                    cooling_scn.add_frame(character.input_symbol, 1, colors=ColorPair())
                else:
                    cooling_gradient_fg = None
                    cooling_gradient_bg = None
                    if character.animation.input_fg_color:
                        cooling_gradient_fg = Gradient(
                            explode_star_color,
                            character.animation.input_fg_color,
                            steps=10,
                        )
                    if character.animation.input_bg_color:
                        cooling_gradient_bg = Gradient(
                            explode_star_color,
                            character.animation.input_bg_color,
                            steps=10,
                        )
                    cooling_scn.apply_gradient_to_symbols(
                        character.input_symbol,
                        20,
                        fg_gradient=cooling_gradient_fg,
                        bg_gradient=cooling_gradient_bg,
                    )
            else:
                cooling_gradient = Gradient(explode_star_color, self.character_final_color_map[character], steps=10)
                cooling_scn.apply_gradient_to_symbols(character.input_symbol, 20, fg_gradient=cooling_gradient)
            character.event_handler.register_event(
                EventHandler.Event.PATH_COMPLETE,
                nearby_path,
                EventHandler.Action.ACTIVATE_PATH,
                input_path,
            )
            character.event_handler.register_event(
                EventHandler.Event.PATH_COMPLETE,
                nearby_path,
                EventHandler.Action.ACTIVATE_SCENE,
                cooling_scn,
            )
            character.animation.activate_scene(explode_scn)
            character.motion.activate_path(nearby_path)
            self.active_characters.add(character)

    def build(self) -> None:
        """Build the Blackhole effect."""
        final_gradient = Gradient(*self.config.final_gradient_stops, steps=self.config.final_gradient_steps)
        final_gradient_mapping = final_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():
            self.character_final_color_map[character] = final_gradient_mapping[character.input_coord]
        self.prepare_blackhole()
        self.formation_delay = max(100 // len(self.blackhole_chars), 6)
        self.f_delay = self.formation_delay
        self.phase = "forming"
        self.awaiting_blackhole_chars = list(self.blackhole_chars)

    def __next__(self) -> str:
        """Return the next frame in the Blackhole effect."""
        if self.active_characters or self.phase != "complete":
            if self.phase == "forming":
                if self.awaiting_blackhole_chars:
                    if not self.f_delay:
                        next_char = self.awaiting_blackhole_chars.pop(0)
                        next_char.motion.activate_path("blackhole")
                        next_char.animation.activate_scene("blackhole")
                        self.terminal.set_character_visibility(next_char, is_visible=True)
                        self.active_characters.add(next_char)
                        self.f_delay = self.formation_delay
                    else:
                        self.f_delay -= 1
                elif not self.active_characters:
                    self.rotate_blackhole()
                    self.phase = "consuming"
            elif self.phase == "consuming":
                if self.awaiting_consumption_chars:
                    for char in self.awaiting_consumption_chars:
                        char.motion.activate_path("singularity")

                        self.active_characters.add(char)
                    self.awaiting_consumption_chars.clear()

                elif self.active_characters <= self.blackhole_char_set:
                    self.phase = "collapsing"

            elif self.phase == "collapsing":
                self.collapse_blackhole()
                self.phase = "exploding"
            elif self.phase == "exploding" and all(
                character.motion.active_path is None and character.animation.active_scene is None
                for character in self.blackhole_chars
            ):
                self.explode_singularity()
                self.phase = "complete"

            self.update()
            return self.frame

        raise StopIteration

__init__(effect)

Initialize the Blackhole effect iterator.

Parameters:

Name Type Description Default
effect Blackhole

The Blackhole effect instance.

required
Source code in terminaltexteffects/effects/effect_blackhole.py
def __init__(self, effect: Blackhole) -> None:
    """Initialize the Blackhole effect iterator.

    Args:
        effect (Blackhole): The Blackhole effect instance.

    """
    super().__init__(effect)
    self.pending_chars: list[EffectCharacter] = []
    self.blackhole_chars: list[EffectCharacter] = []
    self.blackhole_char_set: set[EffectCharacter] = set()
    self.helper_blackhole_chars: list[EffectCharacter] = []
    self.awaiting_consumption_chars: list[EffectCharacter] = []
    self.blackhole_radius = geometry.TERMINAL_ROW_SCALE * max(
        min(round(self.terminal.canvas.width * 0.3), round(self.terminal.canvas.height * 0.20)),
        3,
    )
    self.blackhole_character_count = self.blackhole_radius * 3 // geometry.TERMINAL_ROW_SCALE
    self.compact_layout = False
    self.blackhole_ring_positions: list[geometry.Coord] = []
    self.character_final_color_map: dict[EffectCharacter, Color] = {}
    self.preexisting_colors_present = any(
        any((character.animation.input_fg_color, character.animation.input_bg_color))
        for character in self.terminal.get_characters()
    )
    self.build()

__next__()

Return the next frame in the Blackhole effect.

Source code in terminaltexteffects/effects/effect_blackhole.py
def __next__(self) -> str:
    """Return the next frame in the Blackhole effect."""
    if self.active_characters or self.phase != "complete":
        if self.phase == "forming":
            if self.awaiting_blackhole_chars:
                if not self.f_delay:
                    next_char = self.awaiting_blackhole_chars.pop(0)
                    next_char.motion.activate_path("blackhole")
                    next_char.animation.activate_scene("blackhole")
                    self.terminal.set_character_visibility(next_char, is_visible=True)
                    self.active_characters.add(next_char)
                    self.f_delay = self.formation_delay
                else:
                    self.f_delay -= 1
            elif not self.active_characters:
                self.rotate_blackhole()
                self.phase = "consuming"
        elif self.phase == "consuming":
            if self.awaiting_consumption_chars:
                for char in self.awaiting_consumption_chars:
                    char.motion.activate_path("singularity")

                    self.active_characters.add(char)
                self.awaiting_consumption_chars.clear()

            elif self.active_characters <= self.blackhole_char_set:
                self.phase = "collapsing"

        elif self.phase == "collapsing":
            self.collapse_blackhole()
            self.phase = "exploding"
        elif self.phase == "exploding" and all(
            character.motion.active_path is None and character.animation.active_scene is None
            for character in self.blackhole_chars
        ):
            self.explode_singularity()
            self.phase = "complete"

        self.update()
        return self.frame

    raise StopIteration

build()

Build the Blackhole effect.

Source code in terminaltexteffects/effects/effect_blackhole.py
def build(self) -> None:
    """Build the Blackhole effect."""
    final_gradient = Gradient(*self.config.final_gradient_stops, steps=self.config.final_gradient_steps)
    final_gradient_mapping = final_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():
        self.character_final_color_map[character] = final_gradient_mapping[character.input_coord]
    self.prepare_blackhole()
    self.formation_delay = max(100 // len(self.blackhole_chars), 6)
    self.f_delay = self.formation_delay
    self.phase = "forming"
    self.awaiting_blackhole_chars = list(self.blackhole_chars)

collapse_blackhole()

Collapse the blackhole characters.

Source code in terminaltexteffects/effects/effect_blackhole.py
def collapse_blackhole(self) -> None:
    """Collapse the blackhole characters."""
    black_hole_ring_positions = (
        self.blackhole_ring_positions
        if self.compact_layout
        else geometry.find_coords_on_circle(
            self.terminal.canvas.center,
            radius=self.blackhole_radius + (3 * geometry.TERMINAL_ROW_SCALE),
            coords_limit=len(self.blackhole_chars),
        )
    )
    unstable_symbols = ["◦", "◎", "◉", "●", "◉", "◎", "◦"]
    point_char_made = False
    for character, next_pos in zip(self.blackhole_chars, black_hole_ring_positions):
        expand_path = character.motion.new_path(speed=0.2, ease=easing.in_expo)
        expand_path.new_waypoint(next_pos)
        collapse_path = character.motion.new_path(speed=0.3, ease=easing.in_expo)
        collapse_path.new_waypoint(self.terminal.canvas.center)
        character.event_handler.register_event(
            EventHandler.Event.PATH_COMPLETE,
            expand_path,
            EventHandler.Action.ACTIVATE_PATH,
            collapse_path,
        )
        if not point_char_made:
            point_scn = character.animation.new_scene()
            for _ in range(3):
                for symbol in unstable_symbols:
                    point_scn.add_frame(
                        symbol,
                        3,
                        colors=ColorPair(fg=random.choice(self.config.star_colors)),
                    )
            character.event_handler.register_event(
                EventHandler.Event.PATH_COMPLETE,
                collapse_path,
                EventHandler.Action.ACTIVATE_SCENE,
                point_scn,
            )
            character.event_handler.register_event(
                EventHandler.Event.PATH_COMPLETE,
                collapse_path,
                EventHandler.Action.SET_LAYER,
                3,
            )
            point_char_made = True

        character.motion.activate_path(expand_path)
        self.active_characters.add(character)

explode_singularity()

Explode the singularity characters.

Source code in terminaltexteffects/effects/effect_blackhole.py
def explode_singularity(self) -> None:
    """Explode the singularity characters."""
    for character in self.helper_blackhole_chars:
        self.terminal.set_character_visibility(character, is_visible=False)
    for character in self.terminal.get_characters():
        nearby_coord = geometry.find_coords_on_circle(
            character.input_coord,
            radius=3 * geometry.TERMINAL_ROW_SCALE,
            coords_limit=5,
        )[random.randrange(0, 5)]
        nearby_path = character.motion.new_path(speed=random.randint(3, 4) / 10, ease=easing.out_expo)
        nearby_path.new_waypoint(nearby_coord)
        input_path = character.motion.new_path(speed=random.randint(4, 6) / 100, ease=easing.in_cubic)
        input_path.new_waypoint(character.input_coord)
        explode_scn = character.animation.new_scene()
        explode_star_color = random.choice(self.config.star_colors)
        explode_scn.add_frame(character.input_symbol, 1, colors=ColorPair(fg=explode_star_color))
        cooling_scn = character.animation.new_scene()
        if self.terminal.config.existing_color_handling == "dynamic" and self.preexisting_colors_present:
            if not any((character.animation.input_fg_color, character.animation.input_bg_color)):
                cooling_scn.add_frame(character.input_symbol, 1, colors=ColorPair())
            else:
                cooling_gradient_fg = None
                cooling_gradient_bg = None
                if character.animation.input_fg_color:
                    cooling_gradient_fg = Gradient(
                        explode_star_color,
                        character.animation.input_fg_color,
                        steps=10,
                    )
                if character.animation.input_bg_color:
                    cooling_gradient_bg = Gradient(
                        explode_star_color,
                        character.animation.input_bg_color,
                        steps=10,
                    )
                cooling_scn.apply_gradient_to_symbols(
                    character.input_symbol,
                    20,
                    fg_gradient=cooling_gradient_fg,
                    bg_gradient=cooling_gradient_bg,
                )
        else:
            cooling_gradient = Gradient(explode_star_color, self.character_final_color_map[character], steps=10)
            cooling_scn.apply_gradient_to_symbols(character.input_symbol, 20, fg_gradient=cooling_gradient)
        character.event_handler.register_event(
            EventHandler.Event.PATH_COMPLETE,
            nearby_path,
            EventHandler.Action.ACTIVATE_PATH,
            input_path,
        )
        character.event_handler.register_event(
            EventHandler.Event.PATH_COMPLETE,
            nearby_path,
            EventHandler.Action.ACTIVATE_SCENE,
            cooling_scn,
        )
        character.animation.activate_scene(explode_scn)
        character.motion.activate_path(nearby_path)
        self.active_characters.add(character)

prepare_blackhole()

Prepare the blackhole and starfield characters.

Source code in terminaltexteffects/effects/effect_blackhole.py
def prepare_blackhole(self) -> None:
    """Prepare the blackhole and starfield characters."""
    star_symbols = ["*", "'", "`", "¤", "•", "°", "·"]
    starfield_colors = Gradient(Color("#4a4a4d"), Color("#ffffff"), steps=6).spectrum
    gradient_map = {}
    for color in starfield_colors:
        gradient_map[color] = Gradient(color, Color("#000000"), steps=10)
    canvas = self.terminal.canvas
    radius = min(
        self.blackhole_radius,
        (canvas.width - 1) // 2,
        (canvas.height - 1) // 2 * geometry.TERMINAL_ROW_SCALE,
    )
    self.compact_layout = radius < self.blackhole_radius
    if radius >= geometry.TERMINAL_ROW_SCALE:
        ring_positions = geometry.find_coords_on_circle(
            canvas.center,
            radius=radius,
            coords_limit=self.blackhole_character_count,
        )
    else:
        ring_positions = geometry.find_coords_on_rect(
            canvas.center,
            (canvas.width - 1) // 2,
            (canvas.height - 1) // 2,
        )
    ring_count = min(len(ring_positions), self.blackhole_character_count)
    self.blackhole_ring_positions = [
        ring_positions[index * len(ring_positions) // ring_count] for index in range(ring_count)
    ]
    available_chars = list(self.terminal._input_characters)
    if len(available_chars) <= len(self.blackhole_ring_positions):
        for _ in self.blackhole_ring_positions:
            character = self.terminal.add_character("*", canvas.center)
            self.helper_blackhole_chars.append(character)
            self.blackhole_chars.append(character)
    else:
        while len(self.blackhole_chars) < len(self.blackhole_ring_positions):
            self.blackhole_chars.append(available_chars.pop(random.randrange(len(available_chars))))
    self.blackhole_char_set = set(self.blackhole_chars)
    for position_index, character in enumerate(self.blackhole_chars):
        starting_pos = self.blackhole_ring_positions[position_index]
        blackhole_path = character.motion.new_path(path_id="blackhole", speed=0.7, ease=easing.in_out_sine)
        blackhole_path.new_waypoint(starting_pos)
        blackhole_scn = character.animation.new_scene(scene_id="blackhole")
        blackhole_scn.add_frame("*", 1, colors=ColorPair(fg=self.config.blackhole_color))
        character.event_handler.register_event(
            EventHandler.Event.PATH_ACTIVATED,
            blackhole_path,
            EventHandler.Action.SET_LAYER,
            1,
        )
        # make rotation waypoints
        blackhole_rotation_path = character.motion.new_path(path_id="blackhole_rotation", speed=0.45, loop=True)
        for coord in (
            self.blackhole_ring_positions[position_index:] + self.blackhole_ring_positions[:position_index]
        ):
            blackhole_rotation_path.new_waypoint(coord, waypoint_id=str(len(blackhole_rotation_path.waypoints)))
    for character in self.terminal.get_characters():
        self.terminal.set_character_visibility(character, is_visible=True)
        starting_scn = character.animation.new_scene()
        star_symbol = random.choice(star_symbols)
        star_color = random.choice(starfield_colors)
        starting_scn.add_frame(star_symbol, 1, colors=ColorPair(fg=star_color))
        character.animation.activate_scene(starting_scn)
        if character not in self.blackhole_char_set:
            starfield_coord = self.terminal.canvas.random_coord()
            character.motion.set_coordinate(starfield_coord)

            singularity_path = character.motion.new_path(
                path_id="singularity",
                speed=random.uniform(0.17, 0.30),
                ease=easing.in_expo,
            )
            singularity_path.new_waypoint(self.terminal.canvas.center)
            consumed_scn = character.animation.new_scene()
            for color in gradient_map[star_color]:
                consumed_scn.add_frame(star_symbol, 1, colors=ColorPair(fg=color))
            consumed_scn.add_frame(" ", 1)
            consumed_scn.sync = Scene.SyncMetric.DISTANCE
            character.event_handler.register_event(
                EventHandler.Event.PATH_ACTIVATED,
                singularity_path,
                EventHandler.Action.SET_LAYER,
                2,
            )
            character.event_handler.register_event(
                EventHandler.Event.PATH_ACTIVATED,
                singularity_path,
                EventHandler.Action.ACTIVATE_SCENE,
                consumed_scn,
            )
            self.awaiting_consumption_chars.append(character)
    random.shuffle(self.awaiting_consumption_chars)

rotate_blackhole()

Rotate the blackhole characters.

Source code in terminaltexteffects/effects/effect_blackhole.py
def rotate_blackhole(self) -> None:
    """Rotate the blackhole characters."""
    for character in self.blackhole_chars:
        character.motion.activate_path("blackhole_rotation")
        self.active_characters.add(character)

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_blackhole.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 "blackhole", Blackhole, BlackholeConfig