Spin, step 3: Perspective
The flat view from steps 1 and 2 only squeezes the text as it turns. In this step the camera gets perspective, so near things look bigger, and the sheet comes from far behind the canvas.
3D chapters used: Camera and projection, Animating in 3D.
Update the module
Replace myspin.py with this version:
"""MySpin, tutorial step 3: perspective.
The sheet starts far behind the canvas and spins as it comes forward, growing as it nears. Later steps build it up
into the Spin effect.
"""
from __future__ import annotations
from dataclasses import dataclass
from terminaltexteffects import Vec3, easing
from terminaltexteffects.engine.base_config import BaseConfig
from terminaltexteffects.engine.base_effect import BaseEffect, BaseEffectIterator
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."""
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:
"""Move the text plane far behind the canvas and queue its approach."""
super().__init__(effect)
self.world = self.terminal.world
self.build()
def build(self) -> None:
"""Show every visible character, move the text plane back, and queue the spin and approach."""
world = self.world
world.camera.set_perspective(1)
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)
self.sheet = world.text_plane
self.sheet.position = Vec3(0, 0, -START_DISTANCE)
self.sheet.spin("y", turns=2, frames=SPIN_FRAMES, ease=easing.out_cubic)
# A body moves along paths like a character: here straight back to the canvas, in step with the spin.
approach = self.sheet.motion.new_path(speed=START_DISTANCE / SPIN_FRAMES, ease=easing.out_cubic)
approach.new_waypoint(Vec3(0, 0, 0))
self.sheet.motion.activate_path(approach)
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
What changed
Two constants
START_DISTANCE and SPIN_FRAMES name the numbers the new code shares. Step 6 turns them into options.
Perspective
world.camera.set_perspective(1) gives the camera its default perspective. With perspective, a point's view-axis depth
controls its scale on screen: far points crowd toward the center, and near points spread out. The edge of the sheet that
turns toward you now grows, and the far edge shrinks.
Starting far away
self.sheet.position = Vec3(0, 0, -START_DISTANCE) moves the sheet 200 columns away from the viewer, along z.
It's still centered, so it appears as a small block in the middle of the canvas.
The sheet comes back with a path, exactly as a character would travel. A body's path is in its parent's frame, which for the text plane is the world, so the waypoint is the world origin:
self.sheet.spin("y", turns=2, frames=SPIN_FRAMES, ease=easing.out_cubic)
approach = self.sheet.motion.new_path(speed=START_DISTANCE / SPIN_FRAMES, ease=easing.out_cubic)
approach.new_waypoint(Vec3(0, 0, 0))
self.sheet.motion.activate_path(approach)
A path's speed is units per frame, so a speed of START_DISTANCE / SPIN_FRAMES takes exactly SPIN_FRAMES frames,
and the approach and the spin finish on the same frame. Both also use out_cubic, so the sheet arrives fast and slows
as it lands, and the spin and the approach stay in step all the way: halfway through the turn, it's also halfway
home. world.animating covers body paths as well as tweens, so the effect runs until both are done.
Run it

While the sheet is far away, many characters land on the same cells, so the text reads as a jumble until it nears.
This tutorial animates the sheet while keeping the camera at its default pose. For the complementary approach, leave the sheet in place and try moving the camera.
Next step
Step 4 colors each character by how directly it faces the light, and decides what to show when the back faces you.