Spin, step 1: A sheet of text
This tutorial builds a 3D effect from scratch: Spin. The text starts far behind the canvas as a flat sheet, turned at a random angle. It comes forward while spinning, unwinds its tilt as it goes, and lands exactly on the input text. Each character brightens as it turns to face you. This is what you'll have at the end:

Your copy is called MySpin and runs as tte myspin. It uses the same input and canvas as the
3D Effects chapters. If you haven't written an effect before, the
MySpray tutorial covers the parts every effect has, which this one skips over.
In this step you turn the text away on its sheet, the text plane, and turn it back.
3D chapters used: The world, Frames and coordinates, Rotation.
Create the module
Save this as myspin.py in your community effects directory:
"""MySpin, tutorial step 1: the text on its sheet, the text plane.
The text starts on a sheet turned away to one side, and the sheet turns back to face the viewer. Later steps build
it up into the Spin effect.
"""
from __future__ import annotations
from dataclasses import dataclass
from terminaltexteffects import easing
from terminaltexteffects.engine.base_config import BaseConfig
from terminaltexteffects.engine.base_effect import BaseEffect, BaseEffectIterator
from terminaltexteffects.utils import argutils
def get_effect_resources() -> tuple[str, type[BaseEffect], type[BaseConfig]]:
"""Return the command name, effect class, and configuration class TTE registers."""
return "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:
"""Turn the text plane away and queue the turn back."""
super().__init__(effect)
self.world = self.terminal.world
self.build()
def build(self) -> None:
"""Show every visible character, turn the text plane away, and queue the turn back."""
world = self.world
world.camera.set_perspective(0)
characters = [character for character in self.terminal.get_characters() if character.input_symbol != " "]
for character in characters:
self.terminal.set_character_visibility(character, is_visible=True)
self.active_characters.add(character)
# The text plane is the body every character starts on, so turning it turns the text.
self.sheet = world.text_plane
self.sheet.rotate("y", 70)
self.sheet.turn_to_rest(frames=120, ease=easing.out_cubic)
def __next__(self) -> str:
"""Move the text plane one frame; the characters on it move with it."""
if self.world.animating or self.active_characters:
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
How it works
The world and the sheet
Every terminal has a world, self.terminal.world, with a camera looking at the canvas.
world.camera.set_perspective(0) makes the view flat, so nothing grows or shrinks with distance yet. Step 3 turns
perspective on.
The sheet is world.text_plane, the body every character starts on. Each character's position is its input cell on
the text plane's surface, and those positions never change in this tutorial: the effect moves the plane, and the
plane carries the text. The text is in its input cells whenever the plane is at rest.
Showing the text
build makes each character visible and adds it to active_characters, so it is drawn from the first frame.
Characters in this step have nothing of their own to do, so update() drops them from active_characters after the
first tick. They still move: whenever the view changes, the world projects every character on the plane again.
Step 4 gives each character a scene, which does need a tick every frame.
Spaces are left out. They're invisible, so showing them would only cost time.
Turning away and back
rotate("y", 70) turns the sheet 70 degrees about the vertical axis, right away.
turn_to_rest(frames=120, ease=easing.out_cubic) queues a tween that turns it back to rest along the shortest
path, slowing as it arrives. The world plays the tween one frame at a time inside update().
Ending
__next__ keeps going while self.world.animating or self.active_characters is true. animating is true while
any body or camera tween is queued or playing, so the effect ends on the frame after the sheet comes to rest, with the
text in its input cells.
Run it

With no perspective, turning the sheet only squeezes the text from the sides. Characters that land on the same cell hide each other, which is why letters go missing while it's turned.
Next step
Step 2 replaces the single turn with a spin.