Skip to content

Spin, step 5: Tumbling in and settling

The sheet has always started upright. In this step it starts at a random orientation and unwinds the tilt while it spins, so it tumbles in and still lands exactly on the text.

3D chapters used: Rotation.

Update the module

Replace myspin.py with this version:

myspin.py
"""MySpin, tutorial step 5: tumbling in and settling.

The sheet starts in a random orientation and unwinds its tilt as it spins, landing exactly on the input cells. The
next step turns the constants into options.
"""

from __future__ import annotations

from dataclasses import dataclass

from terminaltexteffects import Color, ColorPair, EffectCharacter, Gradient, Scene, easing
from terminaltexteffects.engine.base_config import BaseConfig
from terminaltexteffects.engine.base_effect import BaseEffect, BaseEffectIterator
from terminaltexteffects.engine.space3d import Body, Space3D
from terminaltexteffects.utils import argutils

START_DISTANCE = 200
"""How far behind the canvas the sheet starts, in columns."""

SPIN_FRAMES = 240
"""Frames from the start until the sheet lands."""

SHADOW_COLOR = Color("#303030")
"""Color of the text when it faces away from the light."""

FINAL_GRADIENT = Gradient(Color("#8A008A"), Color("#00D1FF"), Color("#FFFFFF"), steps=12)
"""Colors of the text facing the light, top to bottom."""

HIDE_BACK = False
"""Whether to hide the text while its back faces the viewer, instead of showing it mirrored in shadow."""


def get_effect_resources() -> tuple[str, type[BaseEffect], type[BaseConfig]]:
    """Return the command name, effect class, and configuration class TTE registers."""
    return "myspin", MySpin, MySpinConfig


@dataclass
class MySpinConfig(BaseConfig):
    """Configuration for the MySpin effect. It has no options yet."""

    parser_spec = argutils.ParserSpec(
        name="myspin",
        help="The text tumbles in from the distance as a flat sheet, spins, and settles into place.",
        description="myspin | The text tumbles in from the distance as a flat sheet, spins, and settles into place.",
        epilog="Example: tte myspin",
    )


class MySpinIterator(BaseEffectIterator[MySpinConfig]):
    """Produce the frames of the MySpin effect."""

    def __init__(self, effect: MySpin) -> None:
        """Mount the text on a sheet far behind the canvas and queue its approach."""
        super().__init__(effect)
        self.final_colors: dict[EffectCharacter, ColorPair] = {}
        self.space = Space3D(self.terminal, perspective=1, light="front")
        self.build()

    def build(self) -> None:
        """Shade and mount every visible character, move the sheet back, tip it, and queue the settle and approach."""
        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,
            Gradient.Direction.VERTICAL,
        )
        characters = [character for character in self.terminal.get_characters() if character.input_symbol != " "]
        for character in characters:
            final_color = final_gradient_mapping[character.input_coord]
            self.final_colors[character] = ColorPair(fg=final_color)
            # The mount's light reading picks the frame: shadow when the text faces away, the final color facing
            # the light.
            scene = character.animation.new_scene(sync=Scene.SyncMetric.LIGHT)
            scene.apply_gradient_to_symbols(
                character.input_symbol, 1, fg_gradient=Gradient(SHADOW_COLOR, final_color, steps=10)
            )
            character.animation.activate_scene(scene)
            self.terminal.set_character_visibility(character, is_visible=True)
            self.active_characters.add(character)
        self.sheet = self.space.body()
        self.sheet.add_chars(characters, self.sheet.text_plane(characters), hide_when_facing_away=HIDE_BACK)
        # The anchors are placed for the sheet's pose at rest, so move the sheet after mounting.
        self.sheet.set_position(0, 0, -START_DISTANCE)
        self.sheet.random_pose()
        self.sheet.settle("y", turns=2, frames=SPIN_FRAMES, ease=easing.out_cubic)
        self.sheet.move_to(0, 0, 0, frames=SPIN_FRAMES, ease=easing.out_cubic, together=True)
        self.sheet.on_complete(self.finish)

    def finish(self, sheet: Body) -> None:
        """Hand the characters back, on their input cells, and show them in their final colors."""
        sheet.release_all()
        for character, colors in self.final_colors.items():
            character.animation.deactivate_scene()
            character.animation.set_appearance(character.input_symbol, colors)

    def __next__(self) -> str:
        """Move the sheet one frame, then the characters with it."""
        if self.space.animating or self.active_characters:
            self.space.step()
            self.update()
            return self.frame
        raise StopIteration


class MySpin(BaseEffect[MySpinConfig]):
    """The text tumbles in from the distance as a flat sheet, spins, and settles into place."""

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

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

What changed

Two lines replace the spin call:

self.sheet.random_pose()
self.sheet.settle("y", turns=2, frames=SPIN_FRAMES, ease=easing.out_cubic)

random_pose() turns the sheet to a random orientation right away, drawn from the space's random number generator.

settle spins about an axis while it unwinds every other part of the rotation, and lands exactly at rest. A plain spin from a tilted pose would keep the tilt: it turns about the world's vertical axis, so the sheet would wobble around it and finish tilted.

Run it

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

MySpin after step 5: the text tumbles in at an angle and straightens as it spins

The starting pose is random, so each run tumbles in differently.

Next step

Step 6 turns the constants into options and handles colored input.