# Technical Documentation: ChrisBuilds/terminaltexteffects

> ℹ️ **Provenance:** Multi-Tier Ingestion · [CodeWiki Reference](https://codewiki.google/github.com/ChrisBuilds/terminaltexteffects)

### Overview (`README.md`)

<br/>
<p align="center">
  <a href="https://github.com/ChrisBuilds/terminaltexteffects">
    <img src="https://github.com/ChrisBuilds/terminaltexteffects/assets/57874186/66388e57-e95e-4619-b804-1d8d7ebd124f" alt="TTE" width="80" height="80">
  </a>

  <h3 align="center">Terminal Text Effects</h3>

  <p align="center">
    Inline Visual Effects in the Terminal
    <br/>
    <br/>
  </p>
</p>

[](https://pypi.org/project/terminaltexteffects/)   [](https://youtu.be/eWnYlxOREu4?t=1549) 

> [!NOTE]
> This README and the [documentation](https://chrisbuilds.github.io/terminaltexteffects/) follow
> development `main`, toward 0.16.0. The latest published release is 0.15.0, so some features
> described here are not yet in the package on PyPI. See [SUPPORT.md](SUPPORT.md) for the
> differences between releases.

## Table Of Contents

* [About](#tte)
* [Quick Start](#quick-start)
* [Requirements](#requirements)
* [Support and Security](#support-and-security)
* [Installation](#installation)
* [Usage](#usage)
  * [Application Quickstart](#application-quickstart)
  * [Library Quickstart](#library-quickstart)
* [Effect Showcase](#effect-showcase)
* [Changelog](#changelog)
* [License](#license)

## TTE



TerminalTextEffects (TTE) is a terminal visual effects engine. TTE can be installed as a system application to produce effects in your terminal, or as a Python library to enable effects within your Python scripts/applications. TTE includes a growing library of built-in effects which showcase the engine's features. These features include:

* Xterm 256 / RGB hex color support.
* Complex character movement via Paths, Waypoints, and
  motion easing, with support for Bézier curves.
* Complex animations via Scenes with symbol/color changes,
  layers, easing, and Path-synced progression.
* Variable stop/step color gradient generation.
* Event handling for Path/Scene state changes with
  custom callback support and many pre-defined actions.
* Typed effect configuration that is automatically exposed as CLI arguments.
* Input color preservation and parsing of common ANSI sequences from fetch-style tools.
* Recording rendered effects to files and replaying them without re-rendering.
* Custom effect plugins that run alongside the built-in effects.
* Runs inline, preserving terminal state and workflow.

## Quick Start

```sh
pipx install terminaltexteffects
```

```sh
echo "Hello, terminal!" | tte beams
```

## Requirements

Development `main` requires Python 3.9.2 or newer. The core package does not require any third-party modules. Terminal interactions use standard ANSI terminal sequences and should work in most modern terminals.

## Support and Security

See [SUPPORT.md](SUPPORT.md) for Python/platform coverage, terminal requirements, shell completion,
and reporting ordinary problems. Report vulnerabilities privately through the
route in [SECURITY.md](SECURITY.md); security fixes target the latest stable release.

## Installation

<details>

<summary>pip / pipx</summary>

Application install:

```sh
pipx install terminaltexteffects
```

Library install:

```sh
pip install terminaltexteffects
```

</details>

<details>

<summary>uv</summary>

Run without installing:

```sh
uv tool run terminaltexteffects -h
```

Application install:

```sh
uv tool install terminaltexteffects
```

Library install:

```sh
uv add terminaltexteffects
```

</details>

<details>

<summary>Nix (flakes)</summary>

Enable `nix-command` and `flakes` in your Nix configuration, for example with
`experimental-features = nix-command flakes` in `~/.config/nix/nix.conf`.

Create a shell with it:

```sh
nix shell github:ChrisBuilds/terminaltexteffects
```

Or run it directly:

```sh
echo 'terminaltexteffects is awesome' | nix run github:ChrisBuilds/terminaltexteffects -- beams
```

Or add it as an input to a flake:

```nix
inputs = {
  terminaltexteffects.url = "github:ChrisBuilds/terminaltexteffects";
};
```

These commands use development `main`. Append `/COMMIT_SHA` to select an exact
revision, or `/release-VERSION` for a published release tag that includes Nix support
(replace the uppercase placeholders). Flake inputs are pinned by the consumer's
`flake.lock`; use `terminaltexteffects.packages.${pkgs.system}.default` in its outputs.

</details>

<details>

<summary>Nix (classic)</summary>

Save this template as `shell.nix`. Replace `COMMIT_SHA` with a full commit or an
existing `release-VERSION` tag. Run `nix-shell` once with the empty hash, then copy
the reported `got: sha256-...` value into `hash` and run `nix-shell` again. Repeat
the hash update when changing `rev`. The selected `<nixpkgs>` must provide the
modern Python build tooling used by `default.nix`; CI tests the repository's locked
nixpkgs input, rather than an arbitrary local channel.

```nix
let
  pkgs = import <nixpkgs> {};

  tte = pkgs.callPackage (pkgs.fetchFromGitHub {
    owner = "ChrisBuilds";
    repo = "terminaltexteffects";
    rev = "COMMIT_SHA";
    hash = ""; # Build first, put proper hash in place
  }) {};
in
  pkgs.mkShell {
    packages = [tte];
  }
```

</details>

## Usage

View the [Documentation](https://chrisbuilds.github.io/terminaltexteffects/) for a full installation and usage guide.

### Application Quickstart

```sh
tte [global_options] <effect> [effect_options] < your_file
```

Input can be piped or redirected through stdin, or passed with `--input-file`/`-i`.
`python -m terminaltexteffects` works in place of `tte`.

* Use `tte -h` to list global options and effects, and `tte <effect> -h` to view options for a
  specific effect, such as color or movement direction.
* Randomly select an effect with `--random-effect`/`-R`. Use `--seed` to make the choice repeatable,
  and `--include-effects` or `--exclude-effects` to limit the pool.
* Enable shell completion for built-in effects in the current shell. Add the line to `~/.bashrc` or
  `~/.zshrc` to keep it. See [Shell Completion](https://chrisbuilds.github.io/terminaltexteffects/appguide/#shell-completion)
  for PowerShell and other details.
  * Bash: `eval "$(tte --print-completion bash)"`
  * Zsh: `eval "$(tte --print-completion zsh)"`
* Add custom effect modules to `${XDG_CONFIG_HOME}/terminaltexteffects/effects`, or
  `~/.config/terminaltexteffects/effects` when `XDG_CONFIG_HOME` is unset, empty, or a relative path.
  See [Community effects](https://chrisbuilds.github.io/terminaltexteffects/appguide/#community-effects)
  for installation steps, and browse or share projects in
  [TTE Effect Show and Tell](https://github.com/ChrisBuilds/terminaltexteffects/discussions/categories/tte-effect-show-and-tell).
* Compose text and preview effects interactively by running `tte` with no effect, after installing the
  optional extra with `pipx install 'terminaltexteffects[tui]'`. See
  [Interactive Interface](https://chrisbuilds.github.io/terminaltexteffects/tui/).
* Record effects and replay them later with `--export` and `--play`. See
  [Recording and playback](https://chrisbuilds.github.io/terminaltexteffects/appguide/#recording-and-playback).
* TTE is not a full terminal emulator, but it parses common fetch-style input including SGR colors,
  cursor movement, and carriage returns. Unsupported control sequences fail with an error rather
  than leaking into the animation.

For all options and more examples, view the [Application Usage Guide](https://chrisbuilds.github.io/terminaltexteffects/appguide/).

### Library Quickstart

All effects are iterators which return a string representing the current frame. Basic usage is as simple as importing the effect, instantiating it with the input text, and iterating over the effect.

```python
from terminaltexteffects.effects import Rain

effect = Rain("your text here")

for frame in effect:
    # do something with the string
    ...
```

To let TTE handle terminal setup/teardown, cursor positioning, and animation frame rate, use the `terminal_output()` context manager.

```python
from terminaltexteffects.effects import Rain

effect = Rain("your text here")
with effect.terminal_output() as terminal:
    for frame in effect:
        terminal.print(frame)
```

For more information, view the [Library Usage Guide](https://chrisbuilds.github.io/terminaltexteffects/libguide/).

## Effect Showcase

Below you'll find a subset of the built-in effects. View all of the effects and related information in the [Effects Showroom](https://chrisbuilds.github.io/terminaltexteffects/showroom/).

<details>

<summary>Effects Demos</summary>

#### Beams



#### Burn



#### Decrypt



#### LaserEtch



#### Matrix



#### Spotlights



#### VHSTape



#### Waves



</details>

## Changelog

See the [CHANGELOG](./CHANGELOG.md) for release history. Unreleased changes on development `main` appear
in the [documentation changelog](https://chrisbuilds.github.io/terminaltexteffects/changelog/).
The [ChangeBlog](https://chrisbuilds.github.io/terminaltexteffects/changeblog/changeblog/) provides optional release
write-ups and demos.

## License

Distributed under the MIT License. See [LICENSE](LICENSE) for more information.

---

### Overview (`changelog.d/README.md`)

# Changelog fragments

Add one short, user-facing note per coherent change. Name it
`<number>.<category>.md` using the issue number, or the PR number when there is no
issue, such as `123.fixed.md`. Use additional fragments when a change needs more
than one category; Towncrier also supports numbered suffixes such as `123.fixed.1.md`.

Categories, in release order: `breaking`, `added`, `changed`, `deprecated`,
`removed`, `fixed`, and `security`. Name affected options or APIs and explain
migration for breaking changes. Explain observable behavior; keep implementation
details, validation results, and benchmark methodology in PRs or documentation.

Internal changes (refactors, CI, development docs) and fixes to bugs introduced since
the last release don't need a fragment: apply the `no-changelog` label to the PR instead.

```bash
./.venv/bin/towncrier create --content "Corrected an existing behavior." 123.fixed.md
./.venv/bin/python tools/changelog.py --preview
```

The second command validates all fragments and prints the Unreleased section. The
documentation site's changelog page renders the same preview; `CHANGELOG.md` itself
only gains notes when a release is assembled (see [RELEASING.md](../RELEASING.md)).

The `+0160-*` orphan fragments migrate work that predates the issue-based process.
They intentionally have no invented issue references.

---

### Overview (`dev_effects/README.md`)

# Development effects

Keep unfinished effects here, outside the shipped package. Issue-branch prototypes may be
committed; `effect_dev.py` remains ignored for personal scratch work. No Python files in this
directory are included in wheels or source distributions.

From the repository root, run the development CLI:

```sh
printf 'Hello' | ./.venv/bin/python -m tools.dev dev
```

Use your effect's command name in place of `dev`. To watch an effect while you edit it, open the
interactive interface with `./.venv/bin/python -m tools.dev --tui`: it imports each module again
when you save it, and lists modules that can't be imported in red. Modules use the normal
`get_effect_resources()` interface and absolute imports from `terminaltexteffects`.
This loader supports flat modules; avoid relative sibling imports.

Parser-based tools, including the benchmark tool, use the same discovery:

```sh
TTE_DEV_EFFECTS_DIR="$PWD/dev_effects" ./.venv/bin/python tools/perf/benchmark_effects.py --effect dev
```

Run focused prototype tests and QA on the issue branch. Development effects are loaded only
when explicitly selected through the launcher or `TTE_DEV_EFFECTS_DIR`. Release completion
generation and built-in-only parser calls exclude them even when that setting exists.

To promote an effect, move its module into `terminaltexteffects/effects/` in a reviewed PR,
add permanent tests and documentation, regenerate completions, and add a changelog fragment.
Never place scratch modules in the shipped effects directory. Release validation injects a
prototype into a temporary source copy and requires its exclusion from all distribution formats.

## Promotion checklist

Promote a finished effect through the normal issue -> branch -> draft PR -> review ->
maintainer merge process. Review these items in the PR; CI cannot approve artistic or
behavioral decisions:

- [ ] Record the effect's intended appearance and behavior, supported input, options, and
  defaults in the issue. Agree on options/defaults/visual changes before implementation.
- [ ] Move the finished `effect_<module>.py` into `terminaltexteffects/effects/` and remove
  the promoted prototype copy. Keep other unfinished experiments in `dev_effects/`.
- [ ] Verify `get_effect_resources()` and config parser metadata use the same unique command.
  Check normal `tte <command> --help` discovery without `TTE_DEV_EFFECTS_DIR` or plugins.
- [ ] Add permanent `tests/effects_tests/test_<command>.py` behavior/configuration tests.
  Cover meaningful edge cases (for example empty/small/sparse input, termination, seeded
  behavior, and supported existing-color handling). Test presence alone is insufficient.
- [ ] Add `docs/effects/<command>.md` with accurate CLI and Python examples, options/defaults,
  and supported limitations; add the page to `mkdocs.yml` navigation. Provide a demo where
  useful or explain why it is deferred; keep any referenced demo assets valid.
- [ ] Regenerate Bash, Zsh, and PowerShell completions using
  `./.venv/bin/python tools/generate_shell_completions.py`, then run its `--check` mode.
- [ ] Add the command, in alphabetical order, to the `effect` drop-down in
  `.github/workflows/profile.yml`; the inventory check fails until it is listed.
- [ ] Add a user-facing changelog fragment (normally `.added.md`).
- [ ] Run focused effect tests, formatting, lint, and Pyright, plus
  `./.venv/bin/python tools/check_effect_inventory.py` and a strict docs build.
- [ ] Verify release artifacts include the promoted module while other development effects
  remain excluded. CI's artifact check protects the distribution boundary; inspect its result.
- [ ] Record human visual/terminal QA: appearance/pacing, representative layouts and Unicode,
  supported color modes, interruption/cursor restoration, and relevant terminal platforms.
  Record limitations or reasoned N/A cases. Measure performance changes with controlled
  before/after inputs/seeds and explain any intended output changes.
- [ ] Address review feedback and require latest CI before the maintainer decides to merge.
  Publication remains a separate release process; promotion approval does not publish.

## Automated inventory gate

CI runs `tools/check_effect_inventory.py`, which checks that every shipped `effect_*.py` module is
registered and has a matching command, a nonempty `docs/effects/<command>.md` page listed in the
MkDocs navigation, and a `tests/effects_tests/test_<command>.py` file with tests. It checks structure
only; it doesn't judge test quality or visual correctness.

---

### Index (`docs/index.md`)

# Intro to TTE



## What is TTE?

TerminalTextEffects (TTE) is a terminal visual effects engine. TTE can be installed as a system application to produce
effects in your terminal, or as a Python library to enable effects within your Python scripts/applications. TTE
includes a growing library of built-in effects which showcase the engine's features. These features include:

* XTerm 256 and RGB hex color support, including colors parsed from the input text.
* Complex character movement via Paths, Waypoints, and motion easing, with support for quadratic/cubic Bezier curves.
* Complex animations via Scenes with symbol/color changes, layers, easing, and Path-synced progression.
* Variable stop/step color gradient generation.
* Event handling for Path/Scene state changes with custom callback support and many predefined actions.
* Effect customization exposed through a typed effect configuration dataclass that is automatically handled as CLI
  arguments.
* Flexible character ordering by rows, columns, diagonals, rings, and spirals.
* Inline output that preserves terminal state and workflow, with an optional alternate-screen mode.
* Random and repeated playback, plus recording effects to files for low-CPU replay.
* Community effects loaded from your configuration directory.

Character ordering, alternate-screen output, repeated playback, and recording are new in 0.16.0 and are not in the
published 0.15.0 package.

## Getting Started

TTE can be used as a system application or as a Python library. To get started, visit the installation and usage guides below.

[Installation Guide](./installation.md){ .md-button } [Application Usage](./appguide.md){ .md-button } [Library Usage](./libguide.md){ .md-button }

## Effects Library

TTE includes a growing library of built-in effects. Visit the showroom to see examples of each effect.

[Effects Showroom](./showroom.md){ .md-button }

## Library Cookbook

Check out the cookbook to see interesting examples using the TTE library.

[Library Cookbook](./cookbook.md){ .md-button }

## Release Write-Ups

Friendly release write-ups can be found in the [ChangeBlog](./changeblog/changeblog.md).

[ChangeBlog](./changeblog/changeblog.md){ .md-button }

---

### Effectguide Index (`docs/effectguide/index.md`)

# Effect Guide

This guide teaches you to write your own TerminalTextEffects (TTE) effects. It explains how the engine works,
then walks you through building a complete effect and sharing it with other TTE users.

!!! note "TTE version"

    This guide describes TTE 0.16.0. Engine APIs can change between releases; check the
    [changelog](../changelog.md) for breaking changes before updating an effect to a newer TTE.

## Who this guide is for

You know Python and want to make an effect: a new way for text to arrive on, move around, or leave the terminal.
You don't need to know anything about TTE's internals. Maintainers adding built-in effects follow the same guide, and
then the [promotion checklist](https://github.com/ChrisBuilds/terminaltexteffects/blob/main/dev_effects/README.md#promotion-checklist).

## How the guide is organized

``` mermaid
flowchart LR
    start[Start here] --> engine[The Engine]
    start --> writing[Writing Effects]
    engine --> threed[3D Effects]
    writing -- each step links --> engine
    engine --> reference[API Reference]
    writing --> reference
```

- **The Engine** explains each part of TTE: what it does, how it is designed, and what it can't do. Read it in order
  or come back to a chapter when you need it.
- **3D Effects** teaches the ideas behind 3D graphics (coordinates, transforms, the camera, depth and light) at the
  level you need to use TTE's 3D tools, with diagrams and an example effect for each idea. Read it after the Engine.
- **Writing Effects** is hands-on. Its tutorial rebuilds the built-in [Spray](../effects/spray.md) effect from an
  empty file, one step per page, and each step links the engine chapter it relies on.
- The **Reference** section, generated from the source, lists every class and function. This guide links to it
  instead of repeating signatures.

The guide only uses public APIs: names exported by the `terminaltexteffects` package and the public modules under
`terminaltexteffects.engine` and `terminaltexteffects.utils`. Anything whose name starts with an underscore is
internal and can change without notice.

## What you need

- Python 3.9.2 or later and TTE installed in the same environment (see [Install](../installation.md)).
- A terminal with color support. Any terminal that can run `tte` works.
- A directory for your effect. TTE loads community effects from
  `${XDG_CONFIG_HOME:-$HOME/.config}/terminaltexteffects/effects`; the
  [application guide](../appguide.md#community-effects) explains the rules.

## Where to start

New to TTE effects? Start with [Architecture](engine/architecture.md), then follow the tutorial from
[Step 1: Scaffold](tutorial/01-scaffold.md). The tutorial and engine chapters are written to be read side by side.

---

### Effectguide 3d Index (`docs/effectguide/3d/index.md`)

# 3D Effects

TTE can move characters through 3D space and draw solid shapes, all in plain Python. This part of the guide explains
the ideas behind it: where things sit, how they turn, how a 3D scene becomes cells on a terminal, and what reads well
at that resolution. By the end you should be able to predict what a transform will look like before you run it, and
know where to look when the high-level calls don't do what you need.

!!! info "New in 0.16.0"

    `Space3D` and the `geometry3d` primitives are available on development `main` and are not in the published 0.15.0
    package.

## Two layers

| Layer | Module | What it gives you |
| --- | --- | --- |
| [`Space3D`](../../engine/space3d.md) | `terminaltexteffects.engine.space3d` | A camera, a light and **bodies**: shapes you move, turn and animate, viewed through a camera you can also move and aim. You mount your characters on bodies, or draw the bodies as filled surfaces. |
| [`geometry3d`](../../engine/utils/geometry3d.md) | `terminaltexteffects.utils.geometry3d` | The primitives underneath: vectors, rotations, the camera's projection, meshes, rasterizing into a depth buffer, wireframes and shading. |

Most effects only need `Space3D`. Each chapter shows the `Space3D` calls first, then the primitives they use, for
the times you want more control.

## Two ways to show things

A body reaches the screen in one of two ways, and most effects use one or both:

| | Mounted characters | Drawn surfaces |
| --- | --- | --- |
| What you call | `body.add_chars(characters, anchors)` or `space.mount(character, anchor)` | `body.draw(Shaded())`, `Solid()`, `Textured()` or `Wireframe()` |
| What moves | Your own characters, each riding one **anchor**: a point on the body | Nothing of yours. The space fills cells with characters it owns |
| Each frame | The body's transform places each anchor in the world, the camera **projects** it to one cell, and the character moves there whole | The body's mesh is transformed and projected, each face is **rasterized** into the cells whose centers it covers, a **depth buffer** keeps the nearest face per cell, and **shading** or a texture picks each cell's symbol and color |
| Hiding | Characters are ordered by layer, which `layer_by_depth` raises for nearer ones | The depth buffer, shared by every drawn body |
| Afterward | Release the characters back to ordinary 2D paths and scenes | The cells disappear when the body is erased or removed |

The chapters follow that order: [coordinates](coordinates.md), [transforms](transforms.md) and
[rotation](rotation.md) place points; the [camera](camera.md) projects them; [depth](depth.md),
[meshes](meshes.md), [light](light.md) and [textures](textures.md) turn faces into cells. Mount text that has to land
somewhere or carry its own colors and scenes; draw a surface for solid shapes and decoration. [Texture
mapping](textures.md#mounted-text-or-textured-surface) compares the two with the same words.

## The frame loop

A 3D effect is an ordinary effect (see the Engine chapter on the [frame loop](../engine/frame-loop.md)) with one
extra call per frame, before `update()`. `space.step()` plays the camera's and bodies' queued animations, redraws any drawn
surfaces and works out where every mounted anchor lands. `update()` then ticks the active characters, and a mounted
character's tick moves it to its anchor's cell.

### A drawn surface

A drawn body needs no characters of yours, so the space alone decides when the effect is done:

```python
from terminaltexteffects.engine.space3d import Cube, Shaded, Space3D

# In the iterator's __init__:
self.space = Space3D(self.terminal)
self.cube = self.space.body(Cube(16)).draw(Shaded())
self.cube.spin("y", turns=1, frames=240)


# In __next__:
def __next__(self) -> str:
    if not self.space.animating:
        raise StopIteration
    self.space.step()
    self.update()
    return self.frame
```

The same loop works when only the camera animates. Leave the body still and queue a viewpoint move instead:

```python
self.space = Space3D(self.terminal)
self.cube = self.space.body(Cube(16)).draw(Shaded())
self.space.camera.truck(12, frames=120)
```

`space.animating` stays true for that camera queue. [Moving the camera](camera-movement.md) explains the controls.

### Mounted text

Mounted characters are your own characters, so the usual lifecycle still applies, with three additions:

```python
from terminaltexteffects.engine.space3d import Space3D

# In the iterator's __init__:
self.space = Space3D(self.terminal)
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)  # Shown...
    self.active_characters.add(character)  # ...and ticked by update().
self.sheet = self.space.body()
self.sheet.add_chars(characters, self.sheet.text_plane(characters))
self.sheet.spin("y", turns=1, frames=240)
self.sheet.on_complete(lambda sheet: sheet.release_all())  # Hand the text back when the spin ends.


# In __next__:
def __next__(self) -> str:
    if not self.space.animating and not self.active_characters:
        raise StopIteration
    self.space.step()
    self.update()
    return self.frame
```

- **Mounting doesn't show or tick a character.** Visibility and membership in `active_characters` are separate, as
  in any effect. A hidden mounted character still moves; one that isn't in `active_characters` never ticks, so it
  stays where it was.
- **An active mount keeps its character active.** Mounted characters never finish on their own, even after the
  body's tweens end and any blend toward the anchor is over, so `update()` never drops them. Release them, as
  `on_complete` does here, or the effect never ends.
- **`space.animating` tracks camera and body tweens.** It turns false when all their queues are empty, while mounted characters,
  blends and any 2D paths you start after release may still be moving. End on `active_characters` too, as above.

Bodies own no characters. A drawn body shows itself with characters the space creates, and characters you mount
stay yours: their scenes, events and layers keep working, and you can release them back to ordinary paths at any time.
[Animating in 3D](animating.md#mount-readings) covers what a mount reports each frame and how to hand text off to 2D.

## Running the examples

Every chapter has a small example effect with options. Each one lives in its own directory under
[`docs/effectguide/examples/3d/`](https://github.com/ChrisBuilds/terminaltexteffects/tree/main/docs/effectguide/examples/3d).
Save it in your community effects directory, as in the Engine chapters. The examples run on the same
[`concept_input.txt`](../engine/characters.md#seeing-the-orders) as the Engine chapters, on the canvas size printed beside each example so the
shapes have room. Most use 80 by 24; the camera lessons use 80 by 32 for captions and framing:

```bash
tte --input-file concept_input.txt --canvas-width 80 --canvas-height 24 --anchor-text c cubes
```

The concept examples end by swapping their shapes for the input text, so you can see each idea on its own. The
[Recipes](recipes.md) combine several ideas into complete effects.

## Chapters

1. [3D in a terminal](terminal.md): what cells can and can't show.
2. [Space and coordinates](coordinates.md): where things sit.
3. [Tall cells](tall-cells.md): why rows and columns aren't the same size, and how the camera corrects for it.
4. [Transforms](transforms.md): moving, turning and scaling, and why order matters.
5. [Rotation](rotation.md): axes, tumbles and getting back to rest.
6. [Camera and projection](camera.md): perspective, focal length, framing, view coordinates and the flat view.
7. [Moving the camera](camera-movement.md): travel, turning, aiming and orbiting a stationary scene.
8. [Depth and hidden surfaces](depth.md): what hides what.
9. [Meshes](meshes.md): the built-in shapes and making your own.
10. [Light and shading](light.md): how faces get their symbols and colors.
11. [Texture mapping](textures.md): wrapping text over surfaces.
12. [Animating in 3D](animating.md): tweens, timing and handing the text back to 2D.
13. [Performance](performance.md): what a frame costs.
14. [Recipes](recipes.md): a spinning logo, a die, a globe, a ticker and wireframe solids.

For a first camera experiment, try the [camera lab](camera-movement.md#run-the-camera-lab).

Then the [Spin tutorial](spin/01-sheet.md) builds a complete 3D effect in six steps.

---

### Appguide (`docs/appguide.md`)

# Application Guide

When used as a system application, TerminalTextEffects produces animations on text passed to standard input or
through the `-i` argument. Pipes and redirection both pass data through standard input.

## Invocation Examples

=== "Piping"

    ```bash title="Piping directory listing output through TTE"
    ls -latr | tte slide
    ```

=== "Redirection"

    ```bash title="Redirecting a file through TTE"
    tte slide < your_file
    ```

=== "File Input"

    ```bash title="Passing a file argument to TTE"
    tte -i path/to/file slide
    ```

To edit text and preview effects interactively, run `tte` with no effect. See
[Interactive Interface](./tui.md).

## Command Syntax

TTE has global terminal options as well as effect-specific options. Global options go before the effect name, and
effect options go after it:

```bash title="TTE usage syntax"
tte [global_options] <effect_name> [effect_options]
```

Use `-h` in place of the global options or the effect options to show the global or effect help, respectively.

The example below passes the output of the `ls` command to TTE with the following options:

* *Global* options:
    - Wrap text that is wider than the terminal.
    - Replace tabs with 4 spaces.

* *Effect* options:
    - Use the [slide](./effects/slide.md) effect.
    - Merge the groups.
    - Set the movement speed to 2.
    - Group by column.

```bash title="TTE argument specification example"
ls | tte --wrap-text --tab-width 4 slide --merge --movement-speed 2 --grouping column
```

## Shell Completion

!!! info "New in 0.16.0"

    PowerShell 7+ completion is available on development `main`. The published 0.15.0 package provides Bash and Zsh
    completion, generated at run time, which also includes custom effects.

Completion is available for Bash, Zsh, and PowerShell 7+. Run `tte --print-completion` to print copy-and-paste setup
commands, or use the commands directly:

```bash title="Enable Bash completion in this session"
eval "$(tte --print-completion bash)"
```

```bash title="Enable Zsh completion in this session"
eval "$(tte --print-completion zsh)"
```

```powershell title="Enable PowerShell 7 completion in this session"
tte --print-completion powershell | Out-String | Invoke-Expression
```

The script registers both `tte` and `terminaltexteffects`. It completes built-in effect names, global and
effect-specific options, allowed values such as gradient directions, grouping modes, and easing functions, and
input-file paths. Completion reads literal arguments; it does not evaluate variables or subexpressions to infer
context. The scripts are bundled with TTE, so no Python process runs on each Tab press. As a result, they cover
built-in effects only, not custom effects or development effects.

To enable completion for future shells, add the relevant command to your `~/.bashrc` or `~/.zshrc`. For PowerShell
7+, add the activation command to your `$PROFILE` file, creating its parent directory and the file if needed, then
restart PowerShell. Setup is opt-in; TTE never edits profiles automatically. To disable completion, remove that
profile line and restart the shell.

On Windows, Windows Terminal handles rendering and may host PowerShell, Command Prompt, or WSL shells. Choose
PowerShell 7 for the PowerShell script; Windows PowerShell 5.1 is not supported. Bash and Zsh sessions use their
respective scripts.

## Random and Repeated Playback

TTE can randomly select an effect with `--random-effect`/`-R`. Use `--seed` to make that selection repeatable, or
limit the pool with `--include-effects` and `--exclude-effects`:

```bash title="Random effect selection"
ls | tte --random-effect --seed 123 --include-effects beams decrypt rain
```

!!! info "New in 0.16.0"

    `--repeat`, `--hold`, and `--alt-screen` are available on development `main` and are not in the published 0.15.0
    package.

Use `--repeat COUNT` to play effects in cycles against the same input. The default is one playback;
`--repeat 3` plays three times, and `--repeat 0` keeps replaying until you press Ctrl+C.
Each playback starts a fresh effect iterator, without storing frames. With `--random-effect`, a new
effect is selected from the available pool for each playback, respecting `--include-effects` and
`--exclude-effects`; the same effect may be selected consecutively. `--seed` makes the selection
sequence repeatable. Randomly selected effects use their default configuration. Empty input remains
a no-op, and an effect that produces no frames stops rather than spinning indefinitely.

```bash title="Repeat an animation"
printf 'Hello!' | tte --repeat 3 wipe
```

```bash title="Continuous animation until Ctrl+C"
tte -i banner.txt --repeat 0 beams
```

Use `--hold SECONDS` (0 to 86400) to keep the final frame of each playback on screen before the next playback
starts or `tte` exits, including after a single or final playback. No hold follows a playback that produced no
frames.

Use `--alt-screen` to run in the terminal's alternate screen, like full-screen programs such as `less`.
The original screen and scrollback are restored when output ends, including after an error or Ctrl+C. The
final frame is not left in the terminal afterward, so combine it with `--hold` to keep the finished frame
visible first. These options suit continuous displays:

```bash title="Full-screen rotating effects with a pause on each result"
tte -i banner.txt --repeat 0 --random-effect --hold 5 --alt-screen
```

## Recording and Playback

!!! info "New in 0.16.0"

    Recording and playback are available on development `main` and are not in the published 0.15.0 package.

Use `--export FILE` to render an effect once and save its frames, then `--play` to replay them without
running the effect again. Playback uses far less CPU than rendering, which suits screensavers, background
displays, and slower hardware that cannot render an effect at full frame rate. Recordings use the `.tterec`
extension.

```bash title="Record an effect, then play it until Ctrl+C"
tte -i banner.txt --export banner.tterec --canvas-width 0 --canvas-height 0 matrix
tte --play banner.tterec --repeat 0
```

### Recording

Recording renders frames as fast as possible and shows nothing; it prints a one-line summary to standard
error when it finishes. `--frame-rate` must be greater than `0` and becomes the playback rate. Effect
phases measured in seconds, such as `matrix --rain-time` and `thunderstorm --storm-time`, last the same
number of frames as they would live, so a recording plays for the intended length. Random choices are
captured as they occur; pass `--seed` to make a recording reproducible. `--repeat COUNT` and
`--random-effect` record a sequence of playbacks into one file; `--repeat 0` is not accepted when recording.
An interrupted or failed recording leaves no partial file, and an existing file at the path is replaced only
when recording completes.

A recording stores only the canvas. Canvas size, text anchoring, and color options such as `--xterm-colors`
and `--no-color` are fixed when recording. Set `--canvas-width` and `--canvas-height` explicitly, or use `0`
to match the terminal at recording time, so the recording does not depend on the input size.

### GIF and WebP

When `FILE` ends in `.gif` or `.webp`, `--export` renders the effect the same way and converts it to an
animation of the canvas instead of writing a recording. Any other extension writes a `.tterec` recording.

```bash title="Record an effect as an animated WebP"
tte -i banner.txt --frame-rate 30 --export banner.webp wipe
```

| Extension | What you get |
| --- | --- |
| `.gif` | An animation of the canvas, with the last frame held for two seconds, at up to 50 frames per second. Colors are reduced to one palette of 255. |
| `.webp` | The same animation as a lossless animated WebP, keeping every color and frame. |

Both formats are drawn with [Pillow](https://python-pillow.org), which needs Python 3.10 or later:

```bash
pip install 'terminaltexteffects[export]'
```

No other programs are needed. Text is drawn in DejaVu Sans Mono, Menlo, Consolas, or Liberation Mono,
whichever is installed first, and symbols that font lacks come from another installed font that has them.
Symbols no installed font has are listed after the export.

A missing Pillow is reported before anything renders. `--frame-rate` becomes the animation's frame rate, and
`--repeat` and `--random-effect` add their playbacks to the same animation. On a terminal, a frame count
updates in place while frames render, then a line names the frames being drawn. When standard error is
redirected, only whole lines are written. An interrupted or failed conversion leaves no partial file. The
[TUI](tui.md#export) exports the same formats.

### Playback

`--play PATH` takes a recording file or a directory. For a directory, every top-level `.tterec` file plays
in filename order; other files and subdirectories are ignored. No effect name or input is needed, and
standard input is not read. Playback options:

- `--repeat COUNT` repeats the file or directory (`0` repeats until Ctrl+C).
- `--frame-rate` overrides the recorded frame rate; `0` plays as fast as possible.
- `--hold SECONDS` keeps the last frame of each recording on screen before the next recording, repeat, or exit,
  including after the final recording (0 to 86400 seconds). Recordings without frames are not held.
- `--shuffle` plays a directory in random order, reshuffled each repeat; `--seed` makes the order repeatable.
- `--alt-screen` plays in the terminal's alternate screen and restores the original screen and scrollback when
  playback ends, as it does for live effects.
- `--anchor-canvas` positions the recording in the terminal. Recordings of different sizes in one directory
  share a canvas sized to the largest, each anchored within it.
- `--seed`, `--reuse-canvas`, `--no-eol`, and `--no-restore-cursor` work as they do for live effects.

```bash title="Background display from a directory of recordings"
tte --play ~/recordings --repeat 0 --shuffle --hold 5 --alt-screen
```

Options fixed when a recording is made cannot be used with `--play`, and are rejected with an error naming
them: `--canvas-width`, `--canvas-height`, `--anchor-text`, `--xterm-colors`, `--no-color`,
`--terminal-background-color`, `--existing-color-handling`, `--tab-width`, `--wrap-text`, and
`--ignore-terminal-dimensions`. The effect-selection filters `--include-effects` and `--exclude-effects` are
rejected too. An effect name, `--random-effect`, and `--input-file` cannot be combined with `--play`. To
change any of these, record again.

Playback exits with an error if the terminal is smaller than the recorded canvas. Text attributes are reset when
playback ends.

??? note "Recording validation"

    Recordings are treated as untrusted input. Every recording is fully validated before anything is shown, and any
    invalid recording is an error. Playback writes only text and color (SGR) sequences, and rejects any recording
    that contains other control or escape sequences, has rows that do not exactly fill the recorded canvas width, or
    is corrupt, truncated, or oversized.

    A recording that changes on disk after validation stops playback with an error. Recordings larger than 256 MiB
    are not held in memory; on later passes each of their frames is checked again as it plays. A replacement with
    another valid recording with the same canvas size, frame rate, and TTE version therefore plays without an
    error, and one that is corrupt near its end can show earlier frames before playback stops. Playback ends early
    when a pass contains no frames.

## Animating Fetch Output

Animate fetch output on shell launch using screenfetch:

```bash title="Shell Fetch"
screenfetch -N | tte slide --merge
```



!!! note

    TTE is not a full terminal emulator, but it does parse common fetch-style input. Supported input includes
    SGR foreground/background colors, common cursor movement CSI sequences, carriage returns, and selected DEC
    private mode toggles for cursor visibility and line wrapping. Unsupported control sequences still fail fast with
    an error so they do not leak into the rendered animation.

## Community Effects

TTE 0.15.0 and later can load community effect modules without installing them into the TTE package. TTE
discovers custom effect modules in `${XDG_CONFIG_HOME}/terminaltexteffects/effects`, or in
`~/.config/terminaltexteffects/effects` when `XDG_CONFIG_HOME` is unset, empty, or a relative path. Any `.py`
file in that directory that provides `get_effect_resources()` registers an effect command alongside the built-in
effects.

### Installing a Community Effect

Download or clone the effect author's source, then place the flat effect `.py` module in the effects directory.
On macOS and Linux, for example:

```bash title="Install a community effect module"
mkdir -p "${XDG_CONFIG_HOME:-$HOME/.config}/terminaltexteffects/effects"
cp path/to/community_effect.py "${XDG_CONFIG_HOME:-$HOME/.config}/terminaltexteffects/effects/"
```

These commands use `XDG_CONFIG_HOME` as written. If it is set to a relative path, unset it or set it to an
absolute directory before running them; TTE ignores relative values and searches `~/.config` instead.

Install any dependencies listed by the author into the same Python environment as TTE. The module
must expose `get_effect_resources()` and use APIs supported by the TTE version you have installed.
Check the author's stated supported TTE version, license, and installation notes before running it;
external effect modules execute as Python code in your environment. Then inspect its options and run it
like a built-in effect:

```bash title="Inspect and run a community effect"
tte community-effect -h
printf 'Hello!\n' | tte community-effect
```

Replace `community-effect` with the effect command name documented by its author. The effect remains hosted and
maintained by its author.

### Loading Behavior

If a custom effect module fails to import or register, TTE prints a warning naming the file to standard
error, skips that module, and keeps the built-in effects and other valid custom effects available.
Each module is imported under its file name, so custom effect modules can import themselves and earlier-loaded
sibling files by name. A file named like a standard-library or installed module, such as `random.py`, is skipped
with a warning instead of replacing that module; rename the file to load it.

!!! warning "Differences in 0.15.0"

    The skip-with-warning protections are available on development `main` and are not in the published 0.15.0
    package. In 0.15.0:

    - A custom effect module that fails to import or register stops `tte` from starting. Remove it from the effects
      directory to restore the built-in effects.
    - A module named like an installed module can replace it.
    - An empty `XDG_CONFIG_HOME` does not fall back to `~/.config`, so unset an empty value.

### Sharing Effects

Browse or share projects in the [TTE Effect Show and Tell discussion category](https://github.com/ChrisBuilds/terminaltexteffects/discussions/categories/tte-effect-show-and-tell).
Include a short demo, a source repository or public Gist, supported TTE version, installation and run
steps, license, and maintenance status. A listing is a community sharing post; it does not mean the
effect is bundled with TTE or endorsed by its maintainers.

### Developing Effects

For prototypes in a repository checkout, set `TTE_DEV_EFFECTS_DIR` to a directory of effect modules. These modules
use the same `get_effect_resources()` interface. This setting is development-only: it is not available in the
released 0.15.0 CLI, must be set explicitly, and does not affect bundled shell completion. Contributors can use
`python -m tools.dev <effect>` from a checkout to load its `dev_effects/` directory.

## Reproducible Example

Run this command in Bash (including Git Bash) or a compatible POSIX shell. It pipes literal
input into Wipe, fixes the canvas, and disables colors and playback delay. Global options
precede the effect name; effect options follow it. The animation ends with `Hi`.


```bash
printf 'Hi' | tte --seed 16 --frame-rate 0 --no-color \
  --canvas-width 2 --canvas-height 1 --ignore-terminal-dimensions \
  wipe --final-gradient-stops ffffff --final-gradient-steps 1 --final-gradient-frames 1
```

With `--frame-rate 0`, the animation plays as fast as possible. For normal playback, choose a nonzero frame rate or
omit `--frame-rate`.

---

### Cookbook (`docs/cookbook.md`)

# Library Cookbook

Below you'll find examples of interesting ways to use the TTE library.

## Animated Prompts

You can use any effect to create an animated prompt by setting the `end_symbol` parameter of the `terminal_output()`
context manager to `" "`. Adjust the effect configuration to achieve a more responsive prompt.

```python
from terminaltexteffects.effects.effect_beams import Beams
from terminaltexteffects.effects.effect_slide import Slide


def slide_animated_prompt(prompt_text: str) -> str:
    effect = Slide(prompt_text)
    effect.effect_config.final_gradient_frames = 1
    with effect.terminal_output(end_symbol=" ") as terminal:
        for frame in effect:
            terminal.print(frame)
    return input()


def beams_animated_prompt(prompt_text: str) -> str:
    effect = Beams(prompt_text)
    effect.effect_config.final_gradient_frames = 1
    with effect.terminal_output(end_symbol=" ") as terminal:
        for frame in effect:
            terminal.print(frame)
    return input()


resp = slide_animated_prompt("Here's a sliding prompt:")
resp = beams_animated_prompt("Here's one with beams:")
```

### Output



## More Examples

- [Effects are Iterable](./libguide.md#effects-are-iterable): send frames to your own TUI, GUI, or other output.
- [Collecting Frames](./libguide.md#collecting-frames): render a fixed-size animation without a live terminal.
- [Colors and Gradients](./engine/utils/gradient.md): build gradients and map them across a canvas.
- [Character Orders](./engine/terminal/terminal.md#character-orders): select characters in rows, rings, or spirals.
- [ParticlePool](./engine/particles.md): reuse helper characters for sparks, smoke, and other short-lived visuals.

---

### Installation (`docs/installation.md`)

# Installation

TerminalTextEffects can be installed as a system application, to run effects from your shell, or as a library, to
use effects in your Python code. The core package has no third-party dependencies.

## Requirements

| Version | Minimum Python |
| --- | --- |
| 0.15.0, the latest published release | 3.8 |
| Development `main`, toward 0.16.0 | 3.9.2 |

Effects use standard ANSI terminal sequences and work in most modern terminals. See
[Support and Compatibility](./support.md) for platform coverage and terminal requirements.

## System Application

When installed as an application, TerminalTextEffects can be called from the shell to produce effects on any input
piped to standard input. Uses include running it on shell launch, aliasing commands to pass their output through
TTE, and SSH login animations.

=== "pipx"

    ```bash
    pipx install terminaltexteffects
    ```

=== "uv"

    ```bash title="Install"
    uv tool install terminaltexteffects
    ```

    ```bash title="Run without installing"
    uv tool run terminaltexteffects -h
    ```

=== "Nix (flakes)"

    ```bash title="Create a shell with TTE"
    nix shell github:ChrisBuilds/terminaltexteffects
    ```

    ```bash title="Run directly"
    echo 'terminaltexteffects is awesome' | nix run github:ChrisBuilds/terminaltexteffects -- beams
    ```

    These commands use development `main` and require the `nix-command` and `flakes` features. See the
    [README](https://github.com/ChrisBuilds/terminaltexteffects#installation) for pinning a release, flake inputs,
    and classic Nix.

=== "pip"

    ```bash
    pip install terminaltexteffects
    ```

    Without pipx, run TTE through the Python module when the `tte` command is not on your `PATH`:

    ```bash title="ls redirection"
    ls -latr | python3 -m terminaltexteffects
    ```

Optionally, enable [shell completion](./appguide.md#shell-completion) for effect names and options.

To compose text and preview effects interactively, install the optional `tui` extra, for example
`pipx install 'terminaltexteffects[tui]'`. See [Interactive Interface](./tui.md).

To save effects as GIF or WebP animations with `--export`, install the optional `export` extra, for example
`pipx install 'terminaltexteffects[export]'`. See [GIF and WebP](./appguide.md#gif-and-webp).

The `all` extra installs both: `pipx install 'terminaltexteffects[all]'`.

[Application Usage](./appguide.md){ .md-button }

## Library

When installed as a library, TerminalTextEffects can be imported to produce animations in your Python applications.

=== "pip"

    ```bash
    pip install terminaltexteffects
    ```

=== "uv"

    ```bash
    uv add terminaltexteffects
    ```

[Library Usage](./libguide.md){ .md-button }

## Installing Development `main`

This documentation follows development `main`, so some documented features are not yet in the published release.
To try them, install from the repository:

```bash
pipx install git+https://github.com/ChrisBuilds/terminaltexteffects.git
```

Development `main` can change at any time. Use the published release for stable behavior.

---

### Libguide (`docs/libguide.md`)

# Library Guide

## Playing Effect Animations

All effects are iterators which return a string representing the current frame. Basic usage is as simple as importing
the effect, instantiating it with the input text, and iterating over the effect. Effects include a helpful context
manager
([effect.terminal_output()](./engine/baseeffect.md#terminaltexteffects.engine.base_effect.BaseEffect.terminal_output))
to handle terminal setup/teardown and cursor positioning.

The following example plays the [Slide](./effects/slide.md) effect animation using the [effect.terminal_output()](./engine/baseeffect.md#terminaltexteffects.engine.base_effect.BaseEffect.terminal_output) context manager.

=== "Syntax"

    ```python
    from terminaltexteffects.effects.effect_slide import Slide

    text = ("EXAMPLE" * 10 + "\n") * 10

    effect = Slide(text)
    effect.effect_config.merge = True # (1)
    with effect.terminal_output() as terminal:
        for frame in effect:
            terminal.print(frame)
    ```

    1. Use the `effect_config` attribute to modify the effect configuration. Setting `merge` to `True` on the Slide effect causes the text to slide
    in from alternating sides of the terminal.

=== "Output"
    

## Effects are Iterable

If you want to handle the output yourself, such as sending the frames to a TUI or GUI, simply iterate over the effect without the context manager.

```python
from terminaltexteffects.effects.effect_slide import Slide

text = ("EXAMPLE" * 10 + "\n") * 10

effect = Slide(text)
effect.effect_config.merge = True # (1)
for frame in effect:
    ...  # frame is a string; send it to your own output
```

1. Use the `effect_config` attribute to modify the effect configuration. Setting `merge` to `True` on the Slide effect causes the text to slide
in from alternating sides of the terminal.

## Configuring Effects

All effect configuration options are available within each effect via the `effect.effect_config` and `effect.terminal_config` attributes.

=== "Syntax"

    ```python
    from terminaltexteffects.effects.effect_slide import Slide
    from terminaltexteffects.utils.graphics import Color

    text = ("EXAMPLE" * 10 + "\n") * 10

    effect = Slide(text)
    effect.effect_config.merge = True  # (1)
    effect.effect_config.grouping = "column"  # (2)
    effect.effect_config.final_gradient_stops = (Color("#0ff000"), Color("#000ff0"), Color("#0f00f0"))  # (3)
    effect.terminal_config.canvas_width = 30  # (4)
    with effect.terminal_output() as terminal:
        for frame in effect:
            terminal.print(frame)
    ```

    1. Use the `effect_config` attribute to modify the effect configuration. Setting `merge` to `True` on the Slide effect causes the text to slide
    in from alternating sides of the terminal.
    2. Columns will slide in, rather than rows.
    3. Change the gradient colors from the defaults.
    4. Set the canvas width explicitly. The canvas height keeps its default, which matches the input text height.

=== "Output"
    

## Configuring the Terminal/Canvas

### Terminal/Canvas Dimensions

TTE uses a [Terminal](./engine/terminal/terminal.md) class and a [Canvas](./engine/terminal/canvas.md) class to handle terminal/canvas dimensions, wrapping text, etc. Effects contain an attribute (`effect.terminal_config`) which allows access to the various terminal configuration options. The configuration should be modified prior to iterating over the effect.

For example, to set the Canvas dimensions manually:

```python
effect.terminal_config.canvas_width = 80
effect.terminal_config.canvas_height = 24
```

Both dimensions default to `-1`, which matches the input text. Set a dimension to `0` to detect it from the terminal
device, or to a positive integer to use that size.

By default, if your Canvas dimensions exceed the visible area of the Terminal, the text outside of that area will not be
output in the frame. If you want to output all characters regardless of their position relative to the visible terminal
area, set `terminal_config.ignore_terminal_dimensions` to `True`. Use this **only** if you are handling the frame
yourself and outputting somewhere other than the terminal. Otherwise, the terminal emulator will wrap the text and
produce unexpected results.

```python
effect.terminal_config.ignore_terminal_dimensions = True
```

### Frame Rate

TTE sets a default target frame rate of 60 FPS. To configure this within your scripts, access the
`effect.terminal_config.frame_rate` attribute. Set this attribute to `0` to remove the frame limit.

For more information on terminal configuration options, check out the [TerminalConfig](./engine/terminal/terminalconfig.md) reference.

## Infinitely Looping Effects

Some effects support infinite looping. For example, setting
[ColorShiftConfig.cycles](./effects/colorshift.md#terminaltexteffects.effects.effect_colorshift.ColorShiftConfig) to
`0` makes [ColorShift](./effects/colorshift.md) cycle indefinitely. Explore the configuration options for a given
effect to see if it supports infinite looping. An infinitely looping effect never finishes, so stop iterating when
your application no longer needs frames.

!!! note

    From the command line, `tte colorshift --cycles 0` loops indefinitely, and `--repeat 0` replays any effect until
    Ctrl+C. In the published 0.15.0 package, `--cycles` requires a positive value and `--repeat` is not available.

## Collecting Frames

This example collects a short [Wipe](./effects/wipe.md) animation and prints its last frame.
A fixed canvas and no frame-rate delay let it run without a live terminal, for example in a
test. For normal animations, print each frame with the context manager shown above.


```python
from terminaltexteffects.effects.effect_wipe import Wipe

effect = Wipe("Hi")
effect.terminal_config.canvas_width = 2
effect.terminal_config.canvas_height = 1
effect.terminal_config.ignore_terminal_dimensions = True
effect.terminal_config.frame_rate = 0
effect.terminal_config.no_color = True
frames = list(effect)
print(frames[-1])
```

The final frame contains `Hi`. Collecting frames retains them in memory; for larger input,
consume the iterator incrementally. Frame strings contain cursor-positioning sequences even
when colors are disabled.

---

### Performance (`docs/performance.md`)

# Performance Testing

This page is for contributors. The benchmark commands run from a repository checkout set up as described in
[CONTRIBUTING.md](https://github.com/ChrisBuilds/terminaltexteffects/blob/main/CONTRIBUTING.md).

Use `tools/perf/benchmark_effects.py` for repeatable effect performance checks. The harness uses only the Python
standard library, disables frame-rate sleeps, renders through the library iterator API, and avoids terminal output.

## Benchmark Harness

### Baseline

Run a baseline before changing performance-sensitive code:

```bash
./.venv/bin/python tools/perf/benchmark_effects.py \
  --effect wipe \
  --input-preset medium \
  --json-out /tmp/tte-baseline.json
```

Use `--effect all` for a broader pass, and use
`--input-preset small|medium|large|wide|tall|color|sparse|unicode|generated` to
select the input shape. Defaults are `--samples 7`, `--warmups 2`, and `--seed 1337`.

The `sparse` preset creates an 80-by-24 virtual canvas containing only two input characters. It is intended to expose
costs that scale with canvas area instead of visible character count; `generated` provides a dense 80-by-24 control.
The `unicode` preset combines ASCII with CJK, full-width, and single-code-point emoji symbols to exercise double-cell
layout and rendering. The `wide` preset remains a long row of single-cell ASCII and measures input shape, not glyph
display width.

Pass `--memory` to record peak traced Python memory for iterator construction and for the complete iteration. Memory
tracking adds substantial runtime overhead, so compare timing results only with another `--memory` report. Report
comparison includes memory deltas automatically when both inputs contain memory measurements.

The default `--lifecycle iterator` mode measures iterator construction and frame generation without output. Use
`--lifecycle terminal-output` to include the complete public context-manager workflow: terminal preparation, iterator
construction, frame generation, in-memory calls to `Terminal.print()`, and cursor restoration. This mode is useful for
finding duplicated setup or output-lifecycle regressions without writing control sequences to the real terminal.

Effects run with their default options unless you pass `tte` options as one quoted string, joined with `=` so the
value is not mistaken for a harness option. `--effect-args` takes the options you would give `tte <effect>` and needs a
single `--effect`; `--terminal-args` takes terminal options you would give `tte` before the effect name, except
`--frame-rate`, since the harness disables frame-rate sleeps:

```bash
./.venv/bin/python tools/perf/benchmark_effects.py \
  --effect waves \
  --effect-args="--wave-count 3 --final-gradient-direction diagonal" \
  --terminal-args="--xterm-colors"
```

Both are parsed by the `tte` parser, so names, values, and errors match the application, and reports record them. The
harness shortens Matrix's rain, Thunderstorm's storm, and ColorShift's cycles to keep long hold phases from dominating
the measurement; setting `--rain-time`, `--storm-time`, or `--cycles` explicitly overrides that.

### Candidate

After making a focused change and running the relevant functional checks, rerun the same benchmark arguments:

```bash
./.venv/bin/python tools/perf/benchmark_effects.py \
  --effect wipe \
  --input-preset medium \
  --json-out /tmp/tte-candidate.json
```

Compare the two reports:

```bash
./.venv/bin/python tools/perf/benchmark_effects.py \
  --compare /tmp/tte-baseline.json /tmp/tte-candidate.json
```

The comparison is advisory by default. Report build, render, and total mean deltas along with frame-count or output-size
changes, but do not treat regressions as failures unless a task explicitly sets a threshold.

### Profiling

Use `--profile` when the timing delta needs a call-level explanation:

```bash
./.venv/bin/python tools/perf/benchmark_effects.py \
  --effect wipe \
  --input-preset medium \
  --samples 1 \
  --warmups 0 \
  --profile
```

The profile reports cumulative time for one scenario and is best used after a benchmark shows a meaningful change.

### Sampling Profiles

Python 3.15 ships a statistical sampling profiler, `profiling.sampling`. It reads the target's stacks from a separate
process instead of instrumenting every call, so overhead is low, and it writes flamegraphs, line-level heatmaps, Firefox
Profiler data, and differential flamegraphs. Record the harness once in the binary format, then convert it as needed:

```bash
python3.15 -m profiling.sampling run --binary -o /tmp/tte.bin tools/perf/benchmark_effects.py \
  --effect all --input-preset medium --samples 2 --warmups 0 > /dev/null
python3.15 -m profiling.sampling replay --flamegraph -o /tmp/tte-flamegraph.html /tmp/tte.bin
python3.15 -m profiling.sampling replay --heatmap -o /tmp/tte-heatmap /tmp/tte.bin
python3.15 -m profiling.sampling replay --diff-flamegraph /tmp/tte-baseline.bin -o /tmp/tte-diff.html /tmp/tte.bin
```

Use an environment synced with `--python 3.15`. The profiler puts the working directory, not the script's directory,
first on `sys.path`, so run it from outside the checkout when profiling an installed build; differential flamegraphs
match frames by absolute file path, so record both builds from the same install location. `tools/perf/summarize_profile.py` turns a `replay --jsonl` export into
Markdown tables of the hottest functions, lines, and files by self samples.

The [Profile workflow](https://github.com/ChrisBuilds/terminaltexteffects/actions/workflows/profile.yml) does this in
CI. On pushes to `main` that touch the package or harness, it profiles all effects with default options in three fixed
scenarios (`medium` and `large` iterator runs, and a `medium` terminal-output run) and compares against the previous
`main` commit, so successive reports stay comparable.

To profile on demand, open the workflow and choose **Run workflow**. The form offers:

| Field | Purpose |
| --- | --- |
| Effect | One effect, or `all` |
| Input preset | One preset, `medium + large`, or `all presets` |
| Lifecycle | `iterator`, `terminal-output`, or `both`; each preset runs in each lifecycle |
| Effect arguments | Options as for `tte <effect>`; needs a single effect |
| Colour mode, existing colour handling | `--xterm-colors`, `--no-color`, and `--existing-color-handling` |
| Timed samples, seed | Harness `--samples` and `--seed` |
| Sampling rate, sampling mode | Profiler rate, and `wall` or `cpu` sampling; `cpu` ignores time spent waiting |
| GC frames, native frames, bytecode detail | Profiler `<GC>` and `<native>` frames, and `--opcodes` for the heatmap |
| Line heatmap | Untick to skip heatmaps, about 40 MB per scenario |
| Baseline | Optional commit, branch, or tag for differential flamegraphs |

Effect and terminal arguments are checked before anything is recorded, so a typo fails within seconds with `tte`'s
error message. With a baseline, both builds run under the current harness, so effect arguments must also exist in the
baseline build. `tools/check_effect_inventory.py` fails if the effect or input-preset drop-downs fall out of step with
the shipped effects or harness presets.

The job summary lists the settings and shows the hotspot tables; the `profile-results` artifact holds each scenario's
flamegraph, heatmap, Firefox Profiler JSON (`profile.gecko.json`, for <https://profiler.firefox.com>), raw
`profile.bin`, and, with a baseline, `diff-flamegraph.html` and a benchmark comparison. The workflow is informational
and never fails on a slowdown. Its timings come from a shared runner under the profiler, so read proportions, not
absolute times, and confirm any change with the benchmark harness.

## Reporting a Change

Put the measurements for a performance change in its PR description, not on this page. A useful report includes:

- **Method:** the harness arguments, samples, warmups, and seeds, and what the baseline was (for example, the parent
  commit with all other code shared).
- **Timing:** before and after mean totals for each input preset that matters to the change, with build and iteration
  split out when they move in different directions.
- **Memory:** peak traced allocation from a separate `--memory` run, since tracing distorts timings.
- **Output parity:** frame counts, output lengths, and full-frame hashes matching across seeds, input shapes, color
  modes, and affected effect options. A speedup that changes output is a behavior change.
- **Controls:** unaffected effects measured alongside, so noise is not mistaken for a gain.

These are local iterator timings and traced Python allocations, not RSS measurements or guarantees for other machines.
Say so instead of rounding small deltas into claims.

## Recorded Optimizations

Optimizations made before this convention recorded their measurements here. The table summarizes them; the
[archived version of this page](https://github.com/ChrisBuilds/terminaltexteffects/blob/ccde68a/docs/performance.md#measured-optimizations)
keeps the full method, per-preset tables, and parity checks. API usage lives in the linked reference pages.

| Optimization | Used by | Result (dense input unless noted) | Reference |
| --- | --- | --- | --- |
| Opt-in row caching | Matrix, Print, Pour, BouncyBalls, ErrorCorrect, SynthGrid, Rain | Enabled only within measured input ranges | [Terminal](engine/terminal/terminal.md#row-caching) |
| Spotlights illumination index and brightness cache | Spotlights | 51–93% faster total across presets; peak allocation 25.5 MB → 8.3 MB | — |
| Opt-in appearance reuse | Overflow, Spotlights (`always` color mode) | Overflow 32% and Spotlights 17% faster; Overflow peak allocation +22% | [Animation](engine/animation/animation.md#appearance-reuse) |
| Straight-line motion activation distances | All path-based effects | Rings about 2–3% faster across presets | — |
| Opt-in eased-scene schedules | Waves | 10–13% faster total on most presets | [Scene](engine/animation/scene.md#eased-scene-schedules) |
| Opt-in encoded appearance construction | Waves | Peak allocation −10% to −20%; total time within ±3% | [Scene](engine/animation/scene.md#encoded-appearance-reuse) |

---

### Showroom (`docs/showroom.md`)

# Effects Showroom



The effects shown below represent the built-in library of effects and their default configuration.

## Beams

Creates beams which travel over the canvas illuminating the characters.



[Reference](./effects/beams.md){ .md-button } [Config](./effects/beams.md#terminaltexteffects.effects.effect_beams.BeamsConfig){ .md-button }

??? example "Beams Command Line Arguments"

    ```
/* Detailed source-code truncated for AI context efficiency. */
```
---

## BinaryPath

Decodes characters into their binary form. Characters travel from outside the canvas towards their input coordinate, moving at right angles.



[Reference](./effects/binarypath.md){ .md-button } [Config](./effects/binarypath.md#terminaltexteffects.effects.effect_binarypath.BinaryPathConfig){ .md-button }

??? example "BinaryPath Command Line Arguments"

    ```
/* Detailed source-code truncated for AI context efficiency. */
```
---

## Blackhole

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



[Reference](./effects/blackhole.md){ .md-button } [Config](./effects/blackhole.md#terminaltexteffects.effects.effect_blackhole.BlackholeConfig){ .md-button }

??? example "Blackhole Command Line Arguments"

    ```
    --blackhole-color (XTerm [0-255] OR RGB Hex [000000-ffffff])
        Color for the stars that comprise the blackhole border. (default: ffffff)
    --star-colors (XTerm [0-255] OR RGB Hex [000000-ffffff]) [(XTerm [0-255] OR RGB Hex [000000-ffffff]) ...]
        Colors used for the collapse point and to color characters during the explosion before they cool to their final
        color. (default: ffcc0d ff7326 ff194d bf2669 702a8c 049dbf)
    --final-gradient-stops (XTerm [0-255] OR RGB Hex [000000-ffffff]) [(XTerm [0-255] OR RGB Hex [000000-ffffff]) ...]
        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. (default: 8a008a 00d1ff ffffff)
    --final-gradient-steps (int > 0) [(int > 0) ...]
        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. (default: 9)
    --final-gradient-direction (diagonal, horizontal, vertical, radial)
        Direction of the final gradient across the text. (default: diagonal)

    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
    ```
---

## BouncyBalls

Characters fall from the top of the canvas as bouncy balls before settling into place.



[Reference](./effects/bouncyballs.md){ .md-button } [Config](./effects/bouncyballs.md#terminaltexteffects.effects.effect_bouncyballs.BouncyBallsConfig){ .md-button }

??? example "Bouncyballs Command Line Arguments"

    ```
/* Detailed source-code truncated for AI context efficiency. */
```
---

## Bubbles

Forms bubbles with the characters. Bubbles float down and pop.



[Reference](./effects/bubbles.md){ .md-button } [Config](./effects/bubbles.md#terminaltexteffects.effects.effect_bubbles.BubblesConfig){ .md-button }

??? example "Bubbles Command Line Arguments"

    ```
/* Detailed source-code truncated for AI context efficiency. */
```
---

## Burn

Characters are ignited and burn up the screen.



[Reference](./effects/burn.md){ .md-button } [Config](./effects/burn.md#terminaltexteffects.effects.effect_burn.BurnConfig){ .md-button }

??? example "Burn Command Line Arguments"

    ```
    --starting-color (XTerm [0-255] OR RGB Hex [000000-ffffff])
        Color of the characters before they start to burn. (default: 837373)
    --burn-colors (XTerm [0-255] OR RGB Hex [000000-ffffff]) [(XTerm [0-255] OR RGB Hex [000000-ffffff]) ...]
        Colors transitioned through as the characters burn. (default: ffffff fff75d fe650d 8a003c 510100)
    --smoke-chance (0 <= float(n) <= 1)
        Chance a given character will produce smoke while burning. Use 0 for no smoke. (default: 0.5)
    --final-gradient-stops (XTerm [0-255] OR RGB Hex [000000-ffffff]) [(XTerm [0-255] OR RGB Hex [000000-ffffff]) ...]
        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. (default: 00c3ff ffff1c)
    --final-gradient-steps (int > 0) [(int > 0) ...]
        Space separated, unquoted, list of the number of gradient steps to use. More steps will create a smoother and
        longer gradient animation. (default: 12)
    --final-gradient-direction (diagonal, horizontal, vertical, radial)
        Direction of the final gradient across the text. (default: vertical)

    Example: terminaltexteffects burn --starting-color 837373 --burn-colors ffffff fff75d fe650d 8a003c 510100 --smoke-chance 0.5 --final-gradient-stops 00c3ff ffff1c --final-gradient-steps 12 --final-gradient-direction vertical
    ```
---

## ColorShift

Display a gradient that shifts colors across the terminal.



!!! note

    Demo GIF uses `--travel` and `--travel-direction radial` arguments.

[Reference](./effects/colorshift.md){ .md-button } [Config](./effects/colorshift.md#terminaltexteffects.effects.effect_colorshift.ColorShiftConfig){ .md-button }

??? example "ColorShift Command Line Arguments"

    ```
/* Detailed source-code truncated for AI context efficiency. */
```
---

## Crumble

Characters crumble into dust before being vacuumed up and reformed.



[Reference](./effects/crumble.md){ .md-button } [Config](./effects/crumble.md#terminaltexteffects.effects.effect_crumble.CrumbleConfig){ .md-button }

??? example "Crumble Command Line Arguments"

    ```
    --final-gradient-stops (XTerm [0-255] OR RGB Hex [000000-ffffff]) [(XTerm [0-255] OR RGB Hex [000000-ffffff]) ...]
        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. (default: 5ce1ff ff8c00)
    --final-gradient-steps (int > 0) [(int > 0) ...]
        Space separated, unquoted, list of the number of gradient steps to use. More steps will create a smoother and
        longer gradient animation. (default: 12)
    --final-gradient-direction (diagonal, horizontal, vertical, radial)
        Direction of the final gradient across the text. (default: diagonal)

    Example: terminaltexteffects crumble --final-gradient-stops 5CE1FF FF8C00 --final-gradient-steps 12 --final-gradient-direction diagonal
    ```
---

## Decrypt

Movie style text decryption effect.



[Reference](./effects/decrypt.md){ .md-button } [Config](./effects/decrypt.md#terminaltexteffects.effects.effect_decrypt.DecryptConfig){ .md-button }

??? example "Decrypt Command Line Arguments"

    ```
    --typing-speed (int > 0)
        Number of characters typed per keystroke. (default: 2)
    --ciphertext-colors (XTerm [0-255] OR RGB Hex [000000-ffffff]) [(XTerm [0-255] OR RGB Hex [000000-ffffff]) ...]
        Space separated, unquoted, list of colors for the ciphertext. Color will be randomly selected for each
        character. (default: 008000 00cb00 00ff00)
    --final-gradient-stops (XTerm [0-255] OR RGB Hex [000000-ffffff]) [(XTerm [0-255] OR RGB Hex [000000-ffffff]) ...]
        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. (default: eda000)
    --final-gradient-steps (int > 0) [(int > 0) ...]
        Space separated, unquoted, list of the number of gradient steps to use. More steps will create a smoother and
        longer gradient animation. (default: 12)
    --final-gradient-direction (diagonal, horizontal, vertical, radial)
        Direction of the final gradient across the text. (default: vertical)

    Example: terminaltexteffects decrypt --typing-speed 2 --ciphertext-colors 008000 00cb00 00ff00 --final-gradient-stops eda000 --final-gradient-steps 12 --final-gradient-direction vertical
    ```
---

## ErrorCorrect

Swaps characters from an incorrect initial position to the correct position.



[Reference](./effects/errorcorrect.md){ .md-button } [Config](./effects/errorcorrect.md#terminaltexteffects.effects.effect_errorcorrect.ErrorCorrectConfig){ .md-button }

??? example "ErrorCorrect Command Line Arguments"

    ```
/* Detailed source-code truncated for AI context efficiency. */
```
---

## Expand

Characters expand from the center.



[Reference](./effects/expand.md){ .md-button } [Config](./effects/expand.md#terminaltexteffects.effects.effect_expand.ExpandConfig){ .md-button }

??? example "Expand Command Line Arguments"

    ```
/* Detailed source-code truncated for AI context efficiency. */
```
---

## Fireworks

Launches characters up the screen where they explode like fireworks and fall into place.



[Reference](./effects/fireworks.md){ .md-button } [Config](./effects/fireworks.md#terminaltexteffects.effects.effect_fireworks.FireworksConfig){ .md-button }

??? example "Fireworks Command Line Arguments"

    ```
/* Detailed source-code truncated for AI context efficiency. */
```
---

## Highlight

Run a specular highlight across the text.



[Reference](./effects/highlight.md){ .md-button } [Config](./effects/highlight.md#terminaltexteffects.effects.effect_highlight.HighlightConfig){ .md-button }

??? example "Highlight Command Line Arguments"

    ```
/* Detailed source-code truncated for AI context efficiency. */
```
---

## LaserEtch

A laser etches characters onto the terminal.



[Reference](./effects/laseretch.md){ .md-button } [Config](./effects/laseretch.md#terminaltexteffects.effects.effect_laseretch.LaserEtchConfig){ .md-button }

??? example "LaserEtch Command Line Arguments"

    ```
/* Detailed source-code truncated for AI context efficiency. */
```
---

## Matrix

Matrix digital rain effect.



[Reference](./effects/matrix.md){ .md-button } [Config](./effects/matrix.md#terminaltexteffects.effects.effect_matrix.MatrixConfig){ .md-button }

??? example "Matrix Command Line Arguments"

    ```
/* Detailed source-code truncated for AI context efficiency. */
```

---

## MiddleOut

Text expands in a single row or column in the middle of the canvas then out.



[Reference](./effects/middleout.md){ .md-button } [Config](./effects/middleout.md#terminaltexteffects.effects.effect_middleout.MiddleOutConfig){ .md-button }

??? example "MiddleOut Command Line Arguments"

    ```
/* Detailed source-code truncated for AI context efficiency. */
```
---

## OrbittingVolley

Four launchers travel along the canvas edges, firing characters from their nearest canvas side to build the text in expanding circular rings. Rings launch in order with up to two still in flight.



[Reference](./effects/orbittingvolley.md){ .md-button } [Config](./effects/orbittingvolley.md#terminaltexteffects.effects.effect_orbittingvolley.OrbittingVolleyConfig){ .md-button }

??? example "OrbittingVolley Command Line Arguments"

    ```
/* Detailed source-code truncated for AI context efficiency. */
```
---

## Overflow

Input text overflows and scrolls the terminal in a random order until eventually appearing ordered.



[Reference](./effects/overflow.md){ .md-button } [Config](./effects/overflow.md#terminaltexteffects.effects.effect_overflow.OverflowConfig){ .md-button }

??? example "Overflow Command Line Arguments"

    ```
    --overflow-gradient-stops (XTerm [0-255] OR RGB Hex [000000-ffffff]) [(XTerm [0-255] OR RGB Hex [000000-ffffff]) ...]
        Space separated, unquoted, list of colors for the overflow gradient. (default: f2ebc0 8dbfb3 f2ebc0)
    --overflow-cycles-range (hyphen separated non-negative int range e.g. '0-4')
        Inclusive range of overflow cycle counts. Use 0-0 to disable overflow cycles. (default: 2 4)
    --overflow-speed (int > 0)
        Speed of the overflow effect. (default: 3)
    --final-gradient-stops (XTerm [0-255] OR RGB Hex [000000-ffffff]) [(XTerm [0-255] OR RGB Hex [000000-ffffff]) ...]
        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. (default: 8a008a 00d1ff ffffff)
    --final-gradient-steps (int > 0) [(int > 0) ...]
        Space separated, unquoted, list of the number of gradient steps to use. More steps will create a smoother and
        longer gradient animation. (default: 12)
    --final-gradient-direction (diagonal, horizontal, vertical, radial)
        Direction of the final gradient across the text. (default: vertical)

    Example: terminaltexteffects overflow --final-gradient-stops 8A008A 00D1FF FFFFFF --final-gradient-steps 12 --final-gradient-direction vertical --overflow-gradient-stops f2ebc0 8dbfb3 f2ebc0 --overflow-cycles-range 2-4 --overflow-speed 3
    ```
---

## Pour

Pours the characters back and forth from the top, bottom, left, or right.



[Reference](./effects/pour.md){ .md-button } [Config](./effects/pour.md#terminaltexteffects.effects.effect_pour.PourConfig){ .md-button }

??? example "Pour Command Line Arguments"

    ```
/* Detailed source-code truncated for AI context efficiency. */
```
---

## Print

Prints the input data one line at at time with a carriage return and line feed.



[Reference](./effects/print.md){ .md-button } [Config](./effects/print.md#terminaltexteffects.effects.effect_print.PrintConfig){ .md-button }

??? example "Print Command Line Arguments"

    ```
/* Detailed source-code truncated for AI context efficiency. */
```
---

## Rain

Rain characters from the top of the canvas.



[Reference](./effects/rain.md){ .md-button } [Config](./effects/rain.md#terminaltexteffects.effects.effect_rain.RainConfig){ .md-button }

??? example "Rain Command Line Arguments"

    ```
/* Detailed source-code truncated for AI context efficiency. */
```
---

## RandomSequence

Prints the input data in a random sequence, one character at a time.



[Reference](./effects/randomsequence.md){ .md-button } [Config](./effects/randomsequence.md#terminaltexteffects.effects.effect_random_sequence.RandomSequenceConfig){ .md-button }

??? example "RandomSequence Command Line Arguments"

    ```
    --speed (0 < float(n) <= 1)
        Fraction of input characters to reveal per tick. Values must be greater than 0 and at most 1. (default: 0.007)
    --final-gradient-stops (XTerm [0-255] OR RGB Hex [000000-ffffff]) [(XTerm [0-255] OR RGB Hex [000000-ffffff]) ...]
        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. (default: 8a008a 00d1ff ffffff)
    --final-gradient-steps (int > 0) [(int > 0) ...]
        Space separated, unquoted, list of the number of gradient steps to use. More steps will create a smoother and
        longer gradient animation. (default: 12)
    --final-gradient-frames (int > 0)
        Number of frames to display each gradient step. Increase to slow down the gradient animation. (default: 8)
    --final-gradient-direction (diagonal, horizontal, vertical, radial)
        Direction of the final gradient across the text. (default: vertical)

    Example: terminaltexteffects randomsequence --speed 0.007 --final-gradient-stops 8A008A 00D1FF ffffff --final-gradient-steps 12 --final-gradient-frames 8 --final-gradient-direction vertical
    ```
---

## Rings

Characters are dispersed and form into spinning rings.



[Reference](./effects/rings.md){ .md-button } [Config](./effects/rings.md#terminaltexteffects.effects.effect_rings.RingsConfig){ .md-button }

??? example "Rings Command Line Arguments"

    ```
/* Detailed source-code truncated for AI context efficiency. */
```
---

## Scattered

Text is scattered across the canvas and moves into position.



[Reference](./effects/scattered.md){ .md-button } [Config](./effects/scattered.md#terminaltexteffects.effects.effect_scattered.ScatteredConfig){ .md-button }

??? example "Scattered Command Line Arguments"

    ```
/* Detailed source-code truncated for AI context efficiency. */
```
---

## Slice

Slices the input in half and slides it into place from opposite directions.



[Reference](./effects/slice.md){ .md-button } [Config](./effects/slice.md#terminaltexteffects.effects.effect_slice.SliceConfig){ .md-button }

??? example "Slice Command Line Arguments"

    ```
/* Detailed source-code truncated for AI context efficiency. */
```
---

## Slide

Slide characters into view from outside the terminal.



[Reference](./effects/slide.md){ .md-button } [Config](./effects/slide.md#terminaltexteffects.effects.effect_slide.SlideConfig){ .md-button }

??? example "Slide Command Line Arguments"

    ```
/* Detailed source-code truncated for AI context efficiency. */
```
---

## Smoke

Smoke floods the canvas colorizing any characters it crosses.



[Reference](./effects/smoke.md){ .md-button } [Config](./effects/smoke.md#terminaltexteffects.effects.effect_smoke.SmokeConfig){ .md-button }

??? example "Smoke Command Line Arguments"

    ```
/* Detailed source-code truncated for AI context efficiency. */
```
---

## Spotlights

Spotlights search the text area, illuminating characters, before converging in the center and expanding.



[Reference](./effects/spotlights.md){ .md-button } [Config](./effects/spotlights.md#terminaltexteffects.effects.effect_spotlights.SpotlightsConfig){ .md-button }

??? example "Spotlights Command Line Arguments"

    ```
/* Detailed source-code truncated for AI context efficiency. */
```
---

## Spray

Sprays the characters from a single point.



[Reference](./effects/spray.md){ .md-button } [Config](./effects/spray.md#terminaltexteffects.effects.effect_spray.SprayConfig){ .md-button }

??? example "Spray Command Line Arguments"

    ```
/* Detailed source-code truncated for AI context efficiency. */
```
---

## Swarm

Characters are grouped into swarms and move around the terminal before settling into position.



[Reference](./effects/swarm.md){ .md-button } [Config](./effects/swarm.md#terminaltexteffects.effects.effect_swarm.SwarmConfig){ .md-button }

??? example "Swarm Command Line Arguments"

    ```
/* Detailed source-code truncated for AI context efficiency. */
```
---

## Sweep

Sweep across the canvas to reveal uncolored text, reverse sweep to color the text.



[Reference](./effects/sweep.md){ .md-button } [Config](./effects/sweep.md#terminaltexteffects.effects.effect_sweep.SweepConfig){ .md-button }

??? example "Sweep Command Line Arguments"

    ```
/* Detailed source-code truncated for AI context efficiency. */
```
---

## SynthGrid

Create a grid of balanced, approximately square cells which fill with characters dissolving into the final text.
Cell sizes differ by at most one position in each dimension. Tiny canvases use fewer cells and omit grid decoration
where needed, supporting canvases down to a single character.



[Reference](./effects/synthgrid.md){ .md-button } [Config](./effects/synthgrid.md#terminaltexteffects.effects.effect_synthgrid.SynthGridConfig){ .md-button }

??? example "SynthGrid Command Line Arguments"

    ```
/* Detailed source-code truncated for AI context efficiency. */
```
---

## Thunderstorm

Create a thunderstorm in the terminal.



[Reference](./effects/thunderstorm.md){ .md-button } [Config](./effects/thunderstorm.md#terminaltexteffects.effects.effect_thunderstorm.ThunderstormConfig){ .md-button }

??? example "Thunderstorm Command Line Arguments"

    ```
/* Detailed source-code truncated for AI context efficiency. */
```
---

## Unstable

Spawns characters jumbled, explodes them to the edge of the canvas, then reassembles them.



[Reference](./effects/unstable.md){ .md-button } [Config](./effects/unstable.md#terminaltexteffects.effects.effect_unstable.UnstableConfig){ .md-button }

??? example "Unstable Command Line Arguments"

    ```
/* Detailed source-code truncated for AI context efficiency. */
```
---

## VHSTape

Lines of characters glitch left and right and lose detail like an old VHS tape.



[Reference](./effects/vhstape.md){ .md-button } [Config](./effects/vhstape.md#terminaltexteffects.effects.effect_vhstape.VHSTapeConfig){ .md-button }

??? example "VHSTape Command Line Arguments"

    ```
/* Detailed source-code truncated for AI context efficiency. */
```
---

## Waves

Waves travel across the terminal leaving behind the characters.



[Reference](./effects/waves.md){ .md-button } [Config](./effects/waves.md#terminaltexteffects.effects.effect_waves.WavesConfig){ .md-button }

??? example "Waves Command Line Arguments"

    ```
/* Detailed source-code truncated for AI context efficiency. */
```
---

## Wipe

Performs a wipe across the terminal to reveal characters.



[Reference](./effects/wipe.md){ .md-button } [Config](./effects/wipe.md#terminaltexteffects.effects.effect_wipe.WipeConfig){ .md-button }

??? example "Wipe Command Line Arguments"

    ```
/* Detailed source-code truncated for AI context efficiency. */
```

---

### Tui (`docs/tui.md`)

# Interactive Interface

The interactive interface lets you type and arrange text, pick an effect, adjust its options, and preview the effect
on your text, all in one terminal window. It is an optional extra built on [Textual](https://textual.textualize.io/),
so the core package keeps no third-party dependencies.



## Install

=== "pipx"

    ```bash
    pipx install 'terminaltexteffects[tui]'
    ```

=== "uv"

    ```bash
    uv tool install 'terminaltexteffects[tui]'
    ```

=== "pip"

    ```bash
    pip install 'terminaltexteffects[tui]'
    ```

## Start

| Command | What opens |
| --- | --- |
| `tte` | The interface with an empty text area. Without the extra installed, `tte` prints usage and the install command. |
| `ls -la \| tte` or `tte -i notes.txt` | The interface with that text loaded, when no effect is named. |
| `tte --tui` | The interface, with any piped or `-i` text loaded. Add an effect name to select it, such as `tte --tui decrypt`. |
| `tte decrypt < notes.txt` | The effect plays as usual. Naming an effect never opens the interface. |

The interface needs a terminal for output. When standard output is redirected, `tte` keeps its usual behavior and
`tte --tui` exits with an error.

## Layout

- **Effects** on the left lists every available effect, including [community effects](appguide.md#community-effects)
  and any [effects you are developing](#developing-effects). Type in the filter above the list to show only effects
  whose name or description contains every word you type, and press ++down++ or ++enter++ to move into the list.
  Highlighting an effect shows its options. Pressing ++enter++ on it plays it.
- **The playback bar** above the text has two groups. On the left, **Play** plays or pauses the effect, **Stop**
  returns to the text, **←** and **→** step back and forward one frame, and **Random** plays a random effect,
  followed by the **Loop** and **Auto** (auto-replay) toggles. On the right, **Open** and **Save** load and save the
  text, **Export** saves the effect as a file, and **Fit** and **Max** size the canvas.
- **The text toolbar** under the playback bar colors text and inserts symbols. See [Text toolbar](#text-toolbar).
- **Text** fills the rest of the top of the screen. Type, paste, and arrange text here. The subtitle shows the canvas
  size, and while an effect is shown, the frame number and the frame rate.
- **Options** at the bottom has two tabs. **Effect** holds the selected effect's options, and **Canvas** holds the
  terminal options that affect previews: canvas size and anchors, text wrapping, frame rate, color handling, and tab
  width.

Click the thin handle to the right of **Effects**, or above **Options**, to hide that panel and give the text more
room, and click it again to bring the panel back. ++ctrl+b++ and ++ctrl+o++ do the same.

The interface uses a TerminalTextEffects theme in purple, cyan, and white. Other Textual themes are available from
the command palette (++ctrl+p++).

Option inputs follow each option's type: a checkbox for on/off options, a list for fixed choices such as gradient
directions and easing functions, which opens inside the options panel and scrolls, and a text box for everything
else. Text boxes use the same spelling as the command line. Lists are space separated, such as `ff0000 00ff00
0000ff`, and colors show a swatch. Hover over an option to see its help, how to write its value with an example, and
its default. An invalid value turns its name and field red, with the reason in its tooltip, and the effect won't
play until it is fixed. Changed options have their names highlighted. Clearing a field and leaving it, or pressing
++shift+f6++ on an option, returns that option to its default. Each effect keeps its own values while the interface
is open. The selected effect's description is shown along the bottom of the options panel.

### Picking colors

Color options open a color picker when you press ++f7++ or ++enter++ in the field, or click the swatch, which shows
each color as a square. Choose from the 256 XTerm colors in the palette, type a hex value, or drag the hue,
saturation, and lightness bars. Palette colors stay XTerm colors on the command line, and any other change gives an
RGB hex color. For gradient options, the picker edits the list of stops: select a stop, change its color, and add,
remove, or reorder stops while a bar previews the gradient. Press ++ctrl+s++ or choose **Apply** to keep the colors,
or ++escape++ to discard them.

### Text toolbar

**Text** and **Background** open the color picker for the text color and the background color. A picked color
applies to the selected text and to text you type or insert afterwards. **No color** in the picker removes just that
color, keeping the other one. **Clear** removes both colors from the selected text and stops coloring new text. Each
chip shows its current color, or a hatched square when there is none.

**Gradient** shows the current gradient. Click it with text selected to spread the gradient across the selection:
the box around the selected characters runs from the first stop to the last in the gradient's direction. Click its
**▾**, or click it with nothing selected, to build the gradient: edit its stops in the color picker, set the steps
between stops and the direction (horizontal, vertical, diagonal, or radial), and choose whether it colors the text or
its background. With text selected, the dialog applies the new gradient right away.

**Symbols** opens a palette of blocks, single and rounded lines, double lines, shapes, and arrows. Click a symbol,
or move with the arrow keys and press ++enter++, to insert it at the cursor, replacing any selected text. The palette
stays open so you can insert several symbols, and ++escape++ or a click outside closes it.

Colors are kept with the text, as [input colors](#input-colors) are. Effects use them when
**existing-color-handling** on the Canvas tab is `always` or `dynamic`.

### Clipboard and right-click menu

++ctrl+x++, ++ctrl+c++, and ++ctrl+v++ cut, copy, and paste in the text area, using the system clipboard
(`pbcopy` and `pbpaste` on macOS, `clip` and PowerShell on Windows, and `wl-copy`, `xclip`, or `xsel` elsewhere).
Most macOS terminals keep ++cmd+c++ and ++cmd+x++ for their own selection, which mouse support in the interface
turns off, so use the Ctrl keys or the menu. ++cmd+v++ pastes as usual. Terminals that pass ++cmd++ through to
applications, such as kitty, WezTerm, and Ghostty, can use the ++cmd++ keys too.

Right-click the text, or the effect while it plays, for a menu with cut, copy, paste, and select all, the text
color, background color, clear colors, and insert symbol actions, play or pause, stop, and fit or maximize the
canvas. Right-clicking keeps the selection, so the menu can cut or copy it.

## Playback

++f5++ or **Play** plays the selected effect on a black background. While it plays, ++f5++ or ++space++ pauses and
resumes it. Once the effect has finished or been stopped, **Play** plays it again from the start, as does ++ctrl+r++
at any time. ++f9++ or **Random** plays a random effect from the listed effects. ++escape++, or a click on the effect,
returns to the text, and a click puts the cursor where you clicked.

++right++ or the **→** button pauses the effect and shows the next frame, and ++left++ or the **←** button pauses it
and steps back one frame, through the last 2,000 frames shown. After stepping back, ++right++ steps forward through
those frames again, and resuming plays them before the effect continues. Stepping back works after the effect has
finished too. While you are looking at an earlier frame, the subtitle shows its number and the number of the newest
frame shown.

Turn on **Loop** to replay the effect a second after it finishes. Loops replay the frames of the first run, so they
start at once, and stop reusing them when you change an option or the text. Turn on **Auto** to replay the effect
shortly after you change an option or highlight another effect, without leaving the field you are editing, or after
you save a [development effect](#developing-effects) you are working on.

Effects build their animations and draw their frames on a background thread, a few frames ahead of the screen, so
the interface stays responsive while a large effect is prepared. The frame rate in the subtitle is the rate the
interface manages to show, next to the **frame-rate** option.

## Developing effects

Keep the interface open in another window while you work on an effect: it checks every effect file each second
and imports a saved file again, so the next play uses your edit. With **Auto** on, the selected effect replays as
soon as you save it. Options you changed keep their values while the effect still has them. This covers the built-in
effects, the [community effects](appguide.md#community-effects) folder, and the directory named by
`TTE_DEV_EFFECTS_DIR`. In a TTE checkout, keep unfinished effects in `dev_effects/` and open the interface with
`python -m tools.dev --tui`, which sets that directory. Changes to other modules, such as the engine or the utilities
an effect uses, need a restart.

A module that can't be played is listed in red under its file name, for example because it raises an error while
importing or uses a command another effect already has. Playing it shows the error in place of the text, and the
entry turns back to normal once a saved fix imports. If the effect raises an error while it plays, the last frame
stays on screen and a notification names the file and line.

## Export

**Export** (++f8++) renders the effect with the current options, without showing it, and saves it in the format
chosen in the dialog. A panel in the bottom-right corner shows the frames rendered and the elapsed time until the
file is written:

| Format | What you get | Needs |
| --- | --- | --- |
| tterec | A recording that `tte --play` replays in a terminal. See [recording and playback](appguide.md#recording-and-playback). | Nothing else. |
| GIF | An animation of the canvas, with the last frame held for two seconds, at up to 50 frames per second. Colors are reduced to one palette of 255. | [Pillow](https://python-pillow.org), which the `tui` extra installs on Python 3.10 or later. |
| WebP | The same animation as a lossless animated WebP, keeping every color and frame. | Pillow, as for GIF. |

Choosing a format changes the file's suffix, and typing a suffix chooses the format. When Pillow is missing, the
dialog says so and won't save GIF or WebP. Exporting needs a **frame-rate** above `0`, and GIF and WebP play at
that rate. The command line makes the same files with [`--export`](appguide.md#gif-and-webp).

## Keys

| Key | Action |
| --- | --- |
| ++f1++ | Show the keys and a few tips. |
| ++f5++ | Play or pause the selected effect. Once it has finished, play it again from the start. |
| ++space++ / ++left++ ++right++ | While an effect is shown: pause or resume it / show the frame before or after. |
| ++ctrl+r++ | Replay the effect from the start. |
| ++f9++ | Play a random effect. |
| ++escape++ | Stop the effect, or clear the finished effect, and return to the text. |
| ++ctrl+t++ / ++ctrl+g++ | Fit the canvas exactly around the text / grow it to the whole text area. |
| ++ctrl+b++ / ++ctrl+o++ | Hide or show the effect list / the options panel. |
| ++f2++ | Open a text file. |
| ++f3++ | Save the text to a file. |
| ++f4++ | Show the `tte` command for the current settings, or load a pasted command. |
| ++f7++ | Open the color picker for the selected color option. |
| ++shift+f6++ | Reset the focused option to its default. |
| ++f6++ | Reset the selected effect's options and the canvas options. |
| ++f8++ | Export the effect to a `.tterec`, GIF, or WebP file. See [Export](#export). |
| ++tab++ | Move between the effect filter, the effect list, the text, and the options. |
| ++ctrl+p++ | Open the command palette, including themes. |
| ++ctrl+q++ | Quit. |

## Canvas

By default the canvas is the whole text area and text is anchored to its top left, so the effect draws your text
where you typed it. To preview an effect on a smaller canvas, set **canvas-width** and **canvas-height** on the
Canvas tab. Setting either to `-1` sizes it to the text, as the command line does by default. **Fit** (++ctrl+t++)
sets the canvas to the size of the text, and **Max** (++ctrl+g++) sets it back to the whole text area.

A cyan outline of full blocks marks the canvas, one cell outside it, and stays visible while you edit and while an
effect plays, with **CANVAS** in the middle of its bottom edge, or of its top edge when only that one moves. Text
outside the canvas is dimmed because the effect won't show it. An edge lights up white under the mouse, and a corner
lights both of its edges. Drag the outline to resize the canvas, and **canvas-width** and **canvas-height** update
to match. The outline is drawn on the edges that move with **anchor-canvas**: with the default `nw` anchor, the
right and bottom edges. The text area keeps one free column or row beyond each of those edges, so effects draw in a
slightly smaller area than the text area, and the outline stays visible when the canvas fills that area. The text
area shows no scrollbars, so they never cover the outline: scroll with the mouse wheel or the cursor keys, and the
subtitle says when there is more text than fits. Dragging an edge all the way out sets that dimension to `0`, the
whole area.

## Commands

++f4++ shows the `tte` command that plays the effect with the current options, with a button to copy it. Copying
uses the terminal's clipboard and also the system clipboard command (`pbcopy` on macOS, `clip` on Windows, and
`wl-copy`, `xclip`, or `xsel` elsewhere), since many terminals ignore clipboard requests from applications. The
command includes the canvas settings when they differ from the command-line defaults, so the effect plays the same
way in your terminal.

The same dialog loads a command: paste a `tte` command, such as one copied from the docs, and choose **Load**. The
interface selects its effect and applies its options. Options the command leaves out take their command-line
defaults, so the preview matches what the command plays. Its `--input-file`, if any, is opened, and flags the
interface doesn't offer, such as `--reuse-canvas`, are listed in a notice and skipped.

## Files

++f2++, ++f3++, and ++f8++ open a file dialog with a folder tree and a path field. Browse the tree with the mouse or
arrow keys, or type a path. Choosing a file twice, such as with a double click, opens it. When saving, choosing a
folder keeps the file name you typed, and saving over an existing file asks for confirmation first. **Home** and
**Up** (++ctrl+up++) change folders, and the **Hidden files** checkbox (++ctrl+t++) shows dot files.

## Input Colors

Text with ANSI colors, such as `ls --color=always` or fetch output, keeps its colors in the text area. Colors stay
attached to their characters as you edit, and text typed inside a colored word takes that word's color. Effects use
the colors when **existing-color-handling** on the Canvas tab is `always` or `dynamic`, as with
`--existing-color-handling` on the command line. When saving, choose whether to keep the colors in the file.

---

### Changeblog Changeblog (`docs/changeblog/changeblog.md`)

# ChangeBlog Home

The ChangeBlog contains optional release write-ups, demos, and in-depth explanations of TTE Engine features.
The [changelog](../changelog.md) is the canonical release history; a blog post is not required for every release.

## Release Entries

* [0.15.0 - Go Fetch](./changeblog_0.15.0.md)
* [0.14.0 - Easier Easing](./changeblog_0.14.0.md)
* [0.13.0 - Still Alive](./changeblog_0.13.0.md)
* [0.12.0 - Color Parsing, Background Color, more Easing](./changeblog_0.12.0.md)
* [0.11.0 - Matrix Effect and Canvas+Text Anchoring](./changeblog_0.11.0.md)
* [0.10.0 - ColorShift, Canvas, Better Compatibility, and more Customization](./changeblog_0.10.0.md)

---

### Changeblog Changeblog 0.10.0 (`docs/changeblog/changeblog_0.10.0.md`)

# 0.10.0 (ColorShift)

## Release 0.10.0

### ColorShift, Canvas, Better Compatibility, and more Customization

This release comes with a shiny new effect [ColorShift](../showroom.md#colorshift), increased customization for
[Waves](../showroom.md#waves) and [Wipe](../showroom.md#wipe) as well as renaming the
VerticalSlice effect to [Slice](../showroom.md#slice) with added slice directions.

Engine changes includes renaming the OutputArea class to [Canvas](../engine/terminal/canvas.md) and building out the
[Color](../engine/utils/color.md) class from what was previously a TypeAlias. In addition, to increase compatibility with
older version of Python (back to 3.8), a few minor changes were implemented including using annotations and removing the
occasional use of modern syntax where unnecessary. Additionally, input can be passed to TTE using the new `-i` argument.

Finally, a few new pages were added to the docs. Check out the [Cookbook](../cookbook.md) for interesting examples using
the TTE library and the [ChangeBlog](changeblog.md) (you're already here) for a more friendly breakdown of each release.

### ColorShift

The new [ColorShift](../showroom.md#colorshift) effect is largely in response to a [Feature Request
issue](https://github.com/ChrisBuilds/terminaltexteffects/issues/9) involving looping effects and the desire for a
simple RGB gradient effect. TTE supports [Gradients](../engine/utils/gradient.md) with an arbitrary number of stops and
steps. This means you can create gradients that transition through as many colors as you want, with as many steps
between colors. Fewer steps results in a more abrupt transition. As a part of the 0.10.0 update, I've added the `loop`
argument which causes the final color stop to blend back to the first. Here's an example:

#### Not Looped

```python
from terminaltexteffects.utils.graphics import Color, Gradient

g = Gradient(Color("#ff0000"), Color("#00ff00"), Color("#0000ff"), steps=10, loop=False)
print(g)
```



#### Looped

```python
from terminaltexteffects.utils.graphics import Color, Gradient

g = Gradient(Color("#ff0000"), Color("#00ff00"), Color("#0000ff"), steps=10, loop=True)
print(g)
```



Notice how the looped [Gradient](../engine/utils/gradient.md) transitions from blue, the final color stop, back to red, the first color stop. That
allows the smooth looped gradient animations seen in the [ColorShift](../showroom.md#colorshift) effect.

#### Traveling

[ColorShift](../showroom.md#colorshift) supports standing Gradients and traveling Gradients in the following directions:

* vertical
* horizontal
* diagonal
* radial

The travel direction can be reversed (think, center -> outside and outside -> center) using the
`--reverse-travel-direction` argument.

#### Never-Ending Effects

[ColorShift](../showroom.md#colorshift) marks the first effect in the TTE library to support infinite looping. This
feature is set by setting the
[ColorShiftConfig](../effects/colorshift.md#terminaltexteffects.effects.effect_colorshift.ColorShiftConfig) `cycles`
attribute to `0`. This will only work when using the effect in your code. The command line argument validator for
`cycles` will not allow values below `1`. This may change in the future, however at this time I'm not sold on having
effects that have to be killed when run as an application.

[ColorShiftConfig](../effects/colorshift.md#terminaltexteffects.effects.effect_colorshift.ColorShiftConfig)
`skip-final-gradient` will cause the effect to end when the last gradient cycle concludes. Otherwise, the
`final_gradient_`* configuration will be used to transition to a final state.

### OutputArea is now Canvas

On the slow but steady journey to documenting the engine from the perspective of writing effects, I realized
`OutputArea` is not a particularly good class name to describe the area of the terminal in which the effect is being
drawn. In addition, setting the terminal dimensions in the [TerminalConfig](../engine/terminal/terminalconfig.md) was not
intuitive when you imagine the terminal dimensions are referring to your actual terminal device. To remedy this,
`OutputArea` was renamed [Canvas](../engine/terminal/canvas.md) and the
`TerminalConfig.terminal_height`/`TerminalConfig.terminal_width` are now
`TerminalConfig.canvas_height`/`TerminalConfig.canvas_width`.

So what is the [Canvas](../engine/terminal/canvas.md)? It's the space in the terminal where the effect is actually being
rendered. When set automatically, it is determined by the bounding box that contains all of your text when it is passed to TTE. So if your
input text is 5 lines high and 30 characters wide, the [Canvas](../engine/terminal/canvas.md) is 5x30. This is independent of your terminal device
dimensions. Of course, if your text extends beyond the terminal device dimensions, it may be wrapped (if
`TerminalConfig.wrap_text` is `True`) which will result in different [Canvas](../engine/terminal/canvas.md) dimensions.

### 0.10.0 Demos

Speaking of [ColorShift](../showroom.md#colorshift), check it out.



Here's one of the new directions for [Waves](../showroom.md#waves), `center_to_outside`.



Here's [Wipe](../showroom.md#wipe) showing one of the new wipe directions, `outside_to_center`.



---

### Plain Old Changelog

[0.10.0](https://github.com/ChrisBuilds/terminaltexteffects/blob/main/CHANGELOG.md)

---

### Changeblog Changeblog 0.11.0 (`docs/changeblog/changeblog_0.11.0.md`)

# 0.11.0 (Enter the Matrix)

## Release 0.11.0

### Release Summary (Matrix Effect, Anchoring, and Canvas Sizing)

This release features the oft requested [Matrix](../showroom.md#matrix) effect and improvements to
[Slice](../showroom.md#slice) and [Print](../showroom.md#print) as well as updates to many effects to support the new
[Canvas Overhaul](#canvas-overhaul).

The Engine underwent major changes to the [Canvas](../engine/terminal/canvas.md) class to enable arbitrary resizing as
requested [in an issue](https://github.com/ChrisBuilds/terminaltexteffects/issues/14) and support for anchoring the Canvas within the
terminal, and anchoring the text within the Canvas.

Minor changes include the addition of a `--version` switch, better handling of `ctrl-c` interrupts during effect
animations and a new [BaseCharacter](../engine/basecharacter.md) attribute `is_fill_character`.

### Housekeeping (We'll do it live.)

In a surprise turn of events, TTE was shared on [HackerNews](https://news.ycombinator.com/item?id=40503202) a few weeks
ago. I hadn't intended to share the project widely as, up to that point, it had only been used and tested by a small
number of people and I assumed the result would be a deluge of Issues that would be better handled slow and steady. I
only noticed it had been shared after seeing the repo star count jump from ~200 to ~500 in the span of an hour. A quick
google query for "TerminalTextEffects" presented the source of this attention.

Not only was TTE on the front page of HN,
it was in the number one spot, where it stayed for the entire day.

[Hacker News 2024-05-28](https://news.ycombinator.com/front?day=2024-05-28)

Even more surprising, the comments were overwhelmingly
positive. And yet more surprising still, the number of issues reported was quite low and easily managed. By the time it
was all over, TTE made the top spot in the HN best-of and held it until it aged off three days later. Overall, an
excellent, if unplanned, "launch".

Since then, TTE has been featured in various blogs, Python news articles, the [VSCode YouTube
Channel](https://www.youtube.com/shorts/E3VP5g3oXX0), and many other place around the web. I'm glad to have more people
checking out this ridiculous project.

Now, on to the release info.

## Matrix Effect

---
When people initially encounter TTE their first thought is something like, "Neat.", and their second thought is probably,
"Why?". Shortly after that, the obvious next thought is, "There must be a Matrix effect.". This led to disappointment
and bewilderment. Who would make a terminal visual effects library and not include the most famous terminal visual
effect in cinema history? Nobody. That would be crazy.



*I don't even see the code...*
*All I see is set_appearance(), set_coordinate(), set_visibility()...*

When I set out to replicate the Matrix digital rain effect, I thought, "surely this will only take a few hours". I mean,
it's basically characters dropping from the top of the terminal and occasionally changing colors between shades of
green, and swapping symbols. Easy.

I was incorrect.

After reviewing clips from the first film, and frame stepping through to learn all the secrets, I have discovered that
The Matrix digital rain effect has a surprising amount of complexity.

Here is the result of my analysis.

???+ Abstract "What Is The Matrix?"

    `Characters are not falling, they're activating in sequence, at varying speeds`

    : Yep, the perception that the characters are falling down the screen is a false memory (with one exception). The
    reality is that the cells are being activated in sequence from top to bottom in a given column. The delay between the
    activations is consistent within a column for the duration of a streak, but changes for each new streak.

    `Symbols consist of numbers, punctuation, Katakana, and a few others`

    : You'll need a good font to represent all of these characters, and the effect config will need an option to
    override and use a custom collection of characters.

    `Symbols and Colors in a given cell change randomly and separately`

    : The changes to symbol and color for a given character cell are not linked and must be calculated separately.

    `The number of visible characters in a column varies from just a few, to the entire column`

    : Sometimes a streak is only three or four characters, other times it's the entire column.

    `Streak lengths are consistent during a streak, but vary between streaks`

    : The number of visible characters remains the same during a streak, once the number has been reached. As new character
    cells are activated, old cells are deactivated.

    `Only a single streak is active in a column`

    : A new streak will not start until the previous streak has completely fallen out of view.

    `Full column streaks are held in place for a random amount of time`

    : On the occasion that a streak is long enough to cover the entire column, it is held (no characters are removed) for a
    random duration.

    `Characters are a brighter color when added and the last few characters fade off`

    : The 'front' character in a streak is a near-white color and loses that color when it is no longer the front. The end
    of the streak fades toward black, but not evenly so.

    `Once a streak reaches the bottom of the terminal, the entire column may begin dropping`

    : Here's the exception to the "characters aren't falling" thing. When the front of a column reaches the bottom of the
    terminal, the entire column will sometimes drop together. Every character will actually shift down a single row. This
    happens at random intervals and is separate from the tail fade off that happens for every streak.

After discovering all of the above, I realized building this effect was going to take a little longer than I had
imagined. I wanted the TTE implementation of the Matrix effect to be as accurate as possible within the limits of the
medium. The actual movie effect has sub-character color variation and glow, which isn't a thing in a real terminal. But
pretty much everything else was doable.

### Matrix Effect Implementation, In Stages

To demonstrate the iterative process of building an effect, I recorded the effect output at each stage of completion.
Check out each section below to see the effect come together.

??? example "0 - Falling"
    Characters are activated in sequence with a random delay between activations, consistent within the streak life.

    

??? example "1 - Kata Symbols"
    Symbols are changed to katakana, punctuation, etc. and empty spaces in the canvas are filled with characters.

    

??? example "2 - Color/Symbol Shifting"
    Colors and Symbols are changed separately and randomly. In addition, the leading character is a brighter color.

    

??? example "3 - Column Lengths"
    Streak columns have varying lengths.

    

??? example "4 - Columns Fall Off and Recycle"
    Once reaching the bottom of the terminal, columns continue to drop characters off the back until completely gone.
    After which, it begins again with a new configuration.

    

??? example "5 - Head/Tail Colors"
    The characters at the head of the streak are bright and the characters near the tail fade off.

    

??? example "6 - Columns Drop"
    Once reaching the bottom of the terminal, sometimes the column will drop all characters by a row at random
    intervals. In this example, the columns shift to red when dropping for demonstration purposes.

    

??? example "7 - Filling"
    The effect conclusion in the Matrix film title sequence doesn't practically work on multi-line text as it would
    require a unique column for every character in every row. This would take a long time to complete. So the TTE
    implementation concludes by filling the screen with rain, then dissolving into the input text.

    

??? example "8 - Dissolving"
    The final dissolve into the input text.

    

## Canvas Overhaul

*18x24, pre-stretched, double primed...*

---

The [Canvas](../engine/terminal/canvas.md) has received a lot of love in this release. An issue was
[raised](https://github.com/ChrisBuilds/terminaltexteffects/issues/14) which asked for an obvious capability. Allow the
Canvas to exceed the size of the input text. This made sense, so here you go.

### Canvas Dimensions? Terminal Dimensions? Input Dimensions?

#### TTE Output 101

To make the most of the possibilities for *where* you output an effect, you need to understand the difference between the
Canvas, the Terminal, and the Input Text dimensions.



The image above helps to explain each component. To achieve this, the following settings were used:

- Canvas height set to match terminal height.
- Canvas width set to an exact value which is smaller than the terminal width.
- Canvas anchored to the center of the terminal.
- Text anchored to the center of the canvas.

Let's break this down.

##### Terminal

The [Terminal](../engine/terminal/terminal.md) class performs many functions, but as it pertains to this discussion, we are only interested in the
terminal dimensions.

`Terminal Dimensions`

: The terminal dimensions are based on your terminal emulator. TTE discovers the dimensions of your terminal emulator by
calling  `shutil.get_terminal_size()`. Should there an issue occur that prevents terminal dimension discovery, a fallback
of `(80, 24)` is used.

: In the image above, you can see the terminal as the black area on the left and right edges. This indicates the Canvas
width is less than the terminal width, and the Canvas was anchored to the center.

##### Canvas

The [Canvas](../engine/terminal/canvas.md) is the space within the terminal where the effect takes place. From the
perspective of an effect, the Canvas is the entire world. Effects *should* be designed to reference the Canvas for all
requests for Coordinates to ensure Coordinates are anchored appropriately and within the correct space within the terminal.

`Canvas Dimensions`

: The Canvas dimensions are determined in one of three ways.
: - Specified exactly via the `--canvas-width` and `--canvas-height` options (or related TerminalConfig attributes).
: - Set to match the discovered Terminal dimensions by setting `--canvas-height` or `--canvas-width` to `0`.
: - Set to match the dimensions of the input text by setting `--canvas-height` or `--canvas-width` to `-1`.
: Note that the separate dimensions, width and height, can be specified through any of these methods independently.

: In the image above, you can see the canvas as the grey area surrounding the inner-text, with solid borders. In the
image, the Canvas is set to match the Terminal height (so there is no exposed terminal above or below) but the width is
less than the Terminal width. By anchoring the Canvas to the center, there is visible unused terminal space on either
side.

##### Input Text

The input text is the data passed to TTE on which the effect operates. TTE attempts to preserve this text in it's
original form unless the `--wrap-text` option has been passed. If the Canvas is set to match the input text dimensions,
the Canvas will be sized to the minimum bounding box that would contain all of the input text.

??? "How Input Text is Processed"

    - Whitespace characters to the right of the last non-whitespace character in a given line are stripped.
    - Empty lines are preserved.
    - Whitespace to the left of a non-whitespace character is preserved.
    - Tabs are converted to four spaces, and the tab width can be specified using the `--tab-width` option.

`Input Text Dimensions`

: The input text dimensions are determined by the input text provided to TTE and will be modified based on the following:
: - If the `--wrap-text` option is passed, the text will be wrapped based on the Canvas dimensions, which will alter the
original text dimensions.

: - If the Canvas is set match the dimensions of the text and the `--wrap-text` option is passed, the text will be
wrapped based on the Terminal dimensions.

: - If the `--ignore-terminal-dimensions` option is passed, wrapping will occur based on the Canvas size or not at
all (if the Canvas is set to match the text), however the output will exceed the dimensions of the terminal and will be
wrapped by the terminal emulator (resulting in unexpected effect behavior) unless the output is directed somewhere else.

Now that you understand how TTE handles the space where the effect is drawn, you can appreciate the latest updates to
the Canvas.

#### Arbitrary Canvas Sizing

As mentioned above, the Canvas size can be specified with the `--canvas-width` and `--canvas-height` options, as well as
the corresponding [TerminalConfig](../engine/terminal/terminalconfig.md) attributes. The following options are
supported:

- `-1` = Match the specified Canvas dimension to the corresponding input text dimension.
- `0` = Match the specified Canvas dimension to the corresponding terminal dimension.
- `n > 0` = Use the exact specified dimension.

Either dimension can be specified with any of the three options.

By sizing the Canvas greater than the input text, you can expand the total effect area and give the effects more room
the breathe.

### Anchoring

*...my adventure in off-by-one errors...*

---

#### Anchoring the Canvas and/or Input Text

The Canvas and/or Input Text can be anchored around the respective container using the `--anchor-[canvas/text]` option. Acceptable values are any of the
Cardinal/Diagonal directions, or centered.

- `sw` = South West (bottom left corner) (**default**)
- `w` = West (centered on left edge)
- `nw` = North West (top left corner)
- `n` = North (centered on top edge)
- `ne` = North East (top right corner)
- `e` = East (centered on right edge)
- `se` = South East (bottom right corner)
- `s` = South (centered on bottom edge)
- `c` = Center (centered within the Canvas/Terminal)

Here is an example of the Canvas anchored to the North East of the terminal, and the text anchored to the center of the
Canvas.



Here is an example of the Canvas centered in the terminal and the text anchored to the East.



Here is an example of the Canvas sized to the text and centered in the terminal. Notice how there is no gray space
outside the text. In this case, anchoring the text has no effect as there is no additional space in the Canvas.



### Effects are Canvas Aware

Effects often base many of their attributes on the Canvas. For example, the [Beams](../effects/beams.md) effect features
beams which travel across the entire Canvas. In the example below, the Canvas has been made wider and taller than the
input text, and the text has been anchored to the center.

Here's the invocation line and result:

`cat testinput/demo.txt | tte --canvas-width 100 --canvas-height 40 --anchor-text c beams`



Here's an example using the [Spray](../effects/spray.md) effect with a wide Canvas and the text anchored to the default
`sw` along with the height set to match the input text height. The spray origin is much further away than usual and the
resulting effect looks better.

`cat testinput/demo.txt | tte --canvas-width 150 --canvas-height -1 --anchor-text sw spray`



All these changes with the Canvas has led to some additional complexity when ensuring all effects operate in reference
to the Canvas dimensions properly in addition to the complexity handling the interactions between anchoring, text wrapping,
and ignoring the terminal dimensions when appropriate. I'm sure there are edge cases somebody will find and report.
Looking forward to fixing those.

## Other Changes

---

- The `--version` switch will show the TTE version. High-tech stuff.

- Inputting a keyboard interrupt (`ctrl-c`) during an animation will gracefully interrupt the effect, restore the
  terminal state, and exit.

- A bug was fixed in the [Swarm](../effects/swarm.md) effect which was causing the first character group to be discarded. This
was noticed by a user who submitted a [great issue](https://github.com/ChrisBuilds/terminaltexteffects/issues/13)
documenting the unfortunate amputation of the *cowsay* cow.

``` bash
$ echo "The cow misses legs!" | cowsay | tte --no-color swarm
 ______________________  
< The cow misses legs! >  
 ----------------------  
        \   ^__^  
         \  (oo)\_______  
            (__)\       )\/\
                ||----w |  
                ||  
```

I'm happy to report that the bug was found and fixed, returning the cow to its intended state.

---

That's all for this release. Thanks for stopping by.

[0.11.0](https://github.com/ChrisBuilds/terminaltexteffects/blob/main/CHANGELOG.md)

---

### Changeblog Changeblog 0.12.0 (`docs/changeblog/changeblog_0.12.0.md`)

# 0.12.0 (Color Parsing)

## Release 0.12.0

### Release Summary (Color Sequence Parsing, Background Colors, and Tests)

This release features three new effects, [Highlight](../showroom.md#highlight), [LaserEtch](../showroom.md#laseretch), and
[Sweep](../showroom.md#sweep) as well as support for parsing existing color sequences from the input data. Support for background
colors has been added throughout the engine. There are many smaller changes such as improved bezier curves, custom easing
functions, and various optimizations, some of which will be detailed below.

### It's Been A While

---

It has been nearly 8 months since the last release. That's entirely due to life changes on my end that kept me away from
TTE development. I have not been able to work on TTE consistently for about six of those months. Things have settled
and I am now able to dedicate time each week to this project (and new projects). Expect more frequent updates from
now on.

### New Effects (Highlight, LaserEtch, Sweep)

---

First up, there are three new effects.

#### Highlight

The [Highlight](../effects/highlight.md) effect runs a specular highlight across the text. To best demonstrate this effect,
I will use a solid block of text.



The highlight brightness, width, and direction are all customizable.

#### LaserEtch

The [LaserEtch](../effects/laseretch.md) effect burns the text into the terminal, sending sparks flying. As the sparks fall, they
cool and disappear. If the sparks reach the bottom of the canvas before burning out, they will land on the bottom.



The etch speed, laser colors, spark colors and all gradients are customizable.

#### Sweep

The [Sweep](../effects/sweep.md) effect makes two passes over the canvas. On the first pass, the text is revealed, dimmed, and
without color. On the second pass, the text is colored.



The sweep directions and sweep noise symbols are customizable. On the second sweep, the noise takes on colors from the final gradient.

### Color Sequence Parsing

---

TTE can now parse 8/24-bit color sequences from the input data, and associate them with the characters to which they
should be applied. This includes both foreground and background sequences.

The following ANSI escape sequence formats are supported:
: 8-bit Sequences
: - `ESC[38:5:⟨n⟩m Select foreground color`
: - `ESC[48:5:⟨n⟩m Select background color`
: 24-bit Sequences
: - `ESC[38;2;⟨r⟩;⟨g⟩;⟨b⟩ m Select RGB foreground color`
: - `ESC[48;2;⟨r⟩;⟨g⟩;⟨b⟩ m Select RGB background color`
: [Wikipedia - ANSI escape codes](https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit)

---

There is a new [TerminalConfig](../engine/terminal/terminalconfig.md#terminaltexteffects.engine.terminal.TerminalConfig.existing_color_handling) command line argument `--existing-color-handling` that determines how these sequences are used within effects.

There are three options for the `--existing-color-handling` argument:

`dynamic`

: When set to 'dynamic', each effect will determine how the input sequences are handled.

`always`

: When set to 'always', no other color will be applied to the characters. They will always reflect the colors as provided
in the input text. In most cases, this setting will result in an effect that loses much of its design.

`ignore`

: When set to 'ignore', the input colors will be ignored. This is the default.

#### Color Handling Example

I recommend using a tool such as [ASCII Silhouettify](https://meatfighter.com/ascii-silhouettify/) to turn your art into ASCII.

In the example below, ASCII Silhouettify was used to process the image into ASCII with color sequences. The ASCII
was piped directly into TTE. Each of the color handling options is demonstrated.

=== "Source Image"

    

=== "Resulting ASCII"

    ```
/* Detailed source-code truncated for AI context efficiency. */
```

??? example "Dynamic Color Sequence Handling"
    The beams effect supports dynamic color handling which results in the standard gradient being
    replaced with the colors parsed from the input data. The beams, however, keep their color.

    

??? example "Always Color Sequence Handling"
    The characters never deviate from the input colors. This results in the beams taking on the color
    of the character as they pass over. The dimming effect is not applied.

    

??? example "Ignore Color Sequence Handling"
    The color sequences are parsed and removed from the input. The result is the normal
    effect behavior.

    

Support for dynamic color handling has not been added to all effects. To track the progress of this feature, see
[this Issue](https://github.com/ChrisBuilds/terminaltexteffects/issues/37).

### Background Colors

---

TTE now supports specifying background colors throughout the engine. In calls which expect color values, a [ColorPair](../engine/utils/colorpair.md) object, providing a foreground and/or background color is used.

There are no effects currently which use background colors.

---

### Ease All The Things

*easing is easy*

#### Easing Closure

A new [easing](../engine/utils/easing.md) function is available that provides a closure around an arbitrary ease.

Example:

```python
import terminaltexteffects as tte

bounce_ease = tte.easing.eased_step_function(easing_func=tte.easing.out_bounce,
                                             step_size=0.01)
print(bounce_ease())
print(bounce_ease())
print(bounce_ease())
print(bounce_ease())
```

Output:

```text
(0.0, 0.0)
(0.01, 0.0007562500000000001)
(0.02, 0.0030250000000000003)
(0.03, 0.00680625)
```

Every call to `bounce_ease` above, outputs a tuple of (current step, eased value). Once the current step reaches 1,
the function will stop increasing the step and the return value will never change.

The [wipe](../effects/wipe.md) effect supports arbitrary easing via this new function. Here is an example of the `out_bounce`
easing function applied to the progression of the wipe.



#### Custom Cubic Bezier Ease

In addition to the function above, a new easing function has been added which allows for the specification of completely
custom easing function using bezier control points. An example follows.

Here, I'll use [cubic-bezier.com](https://cubic-bezier.com/#0,.80,1,.20) to visually build a curve. The x-axis is time,
in our case each call to the ease function will progress one unit across the x-axis. The y-axis is the progression of the
function. The bottom is 0, the top is 1.



I will apply the control points seen in the image above, (0, .81, .98, .22) to the easing function and
pass the function to the `ease` argument when creating a new [Path](../engine/motion/path.md). The target
[Coord](../engine/utils/geometry.md) will be the right side of the canvas.

```python
target_coord = tte.Coord(
    self.terminal.canvas.right,
    self.terminal.canvas.center_row,
)
pth = test_char.motion.new_path(ease=tte.easing.make_easing(0, 0.81, 0.98, 0.22),
                                speed=0.5)
pth.new_waypoint(target_coord)
```

With the custom easing function applied to the motion of a single character traveling across the canvas, we would
expect to see the character move quickly at first, slow down towards the center, and speed up again as it progresses
to the right of the canvas.



### Misc Optimizations and Fixes

This updates includes many fixes and optimizations, details can be found in the full changelog.

### Testing

Now that TTE has grown in scale beyond that which I can meaningfully test manually, a full suite of unittests has been added.
Pytest is fun, and parameterization has enabled the testing of all effects, with all of their arguments, and a representative
sample of the acceptable ranges for those arguments. This was simply impossible manually. All together, there are approximately
30,000 tests run against the codebase, taking about seven minutes.

---

### Plain Old Changelog

[0.12.0](https://github.com/ChrisBuilds/terminaltexteffects/blob/main/CHANGELOG.md)

---

### Changeblog Changeblog 0.13.0 (`docs/changeblog/changeblog_0.13.0.md`)

# 0.13.0 (Still Alive)

## Release 0.13.0

### Release Summary (Thunderstorm, Smoke, Spanning Trees, and Screensaver Support)

This release features two brand new effects, [Smoke](../showroom.md#smoke) and [Thunderstorm](../showroom.md#thunderstorm), as well as refreshed effects [Burn](../showroom.md#burn) and [Laseretch](../showroom.md#laseretch). Behind the scenes, the engine and application received some big changes, especially around argument parsing and configuration.

### This Took Too Long

---

Despite my intention, life stuff caused this update to take far longer than necessary. To address the unpredictability of the universe, I am going to make updates much smaller and more frequent.

### Yer a ScreenSaver, Harry

---

In other news, TerminalTextEffects is currently a screensaver in [Omarchy](https://omarchy.org/).



I found out when they opened an issue/PR to support this TTE use-case. To that end, there are three new command-line options:

* `--reuse-canvas` : Rather than create lines beneath the current prompt as the canvas. This option causes the terminal handler to move the cursor up by the number of visible-lines in the input text. This will eliminate scrolling between effects. Note that this option has unpredictable results when used from a terminal prompt as the prompt itself and any intermediate output will offset the canvas from the prior run.

* `--no-eol` : When an effect ends, TTE normally prints a newline to place the prompt on a new line below the canvas. When this option is used, the newline is suppressed.

* `--no-restore-cursor` : TTE uses an ANSI sequence to hide the cursor during an effect and restores the cursor after the effect ends. When this option is used, the cursor will remain hidden.

Using these three options together, you can run TTE repeatedly in the same location in a terminal as one may do in a screensaver scenario.

These options can be used when using TTE as a library, too. So you can run effects repeated in your application.

??? info "Reusing the terminal in your Python application."
    Here we allow the first effect to run without the `reuse_canvas` option, to get the original canvas setup. Then we set `effect.terminal_config.reuse_canvas` to true for the remaining effects. All effects will run at the same position in the terminal, unless something else produces output.

    ```python
    from terminaltexteffects.effects import Highlight, Slide, Wipe

    text = ("EXAMPLE" * 10 + "\n") * 10

    for pos, effect_class in enumerate((Wipe, Highlight, Slide)):
        effect = effect_class(text)
        effect.terminal_config.reuse_canvas = bool(pos)
        effect.terminal_config.no_eol = True
        with effect.terminal_output() as terminal:
            for frame in effect:
                terminal.print(frame)
    ```

On that note, it's very cool to find TTE in the wild. If you are currently using TTE or have an idea for a use-case, feel free to open an Issue/Discussion for questions/support.

### New Effects (Thunderstorm and Smoke)

---

There are two new effects in this update, which feels a bit underwhelming given how long it took to release. Really, writing effects often takes a few hours +/- some time to polish. Moving into a more frequent and smaller release cycle will significantly aid in new effect production.

#### Thunderstorm

The text darkens as though storm clouds are rolling in. It begins to rain. Lightning strikes randomly around the canvas. When lightning reaches the bottom of the canvas, it flashes and all characters flash in sync with the strike. Sparks fly up from the lightning point of contact. Any characters in the path of the strike have a burning glow that lingers for a bit after the strike dissipates. Eventually, the rain stops and the characters lighten.



The storm time is configurable, as is the lightning strike chance. As usual, colors and gradients are also configurable.

#### Smoke

Text appears uncolored. Smoke begins to flow out of a random point and spread around the canvas. When a character encounters the smoke, the smoke and character flow through the final gradient colors until landing on the correct color. Smoke will expand through the entire canvas and colorize all characters.



The smoke will limit itself to the text boundary unless the `--use-whole-canvas` effect option is passed. Smoke symbols, gradient, and all other colors are configurable.

### Refreshed Effects

---

#### Burn

Burn has been upgraded with a more realistic burn pattern and new smoke particles. A random spot on the canvas is chosen for the initial ignition, the flame spreads out in all direction from this point. Smoke is emitted from some of the burned characters.



#### LaserEtch

Laseretch has a new, and default, etch pattern, in addition to the existing patterns. The laser travels around the input text rather than only moving in rows/columns/diagonals.




### Minimum Spanning Trees and Search/Explore

---

As a bit of background, I have an older project called [TerminalMaze](https://github.com/ChrisBuilds/terminalmaze) which is the primordial soup from which many of the TTE concepts emerged. TerminalMaze has a very different animation mechanism and features a DSL for describing animations via TOML config. None of this is properly documented at this time. I hope to create a nice README for it in the near future.

TerminalMaze uses various Minimum Spanning-Tree algorithms to build mazes in the terminal. The nodes in the graph are animated based on the current activity of the algorithm. For example, below you will see a visualization of the recursive backtracker algorithm. When the algorithm links a node into the graph, the node glows bright greenish-white with a gradient toward green. When the algorithm pops the node off it's stack, the color is returned to white.

There are examples of many algorithms below, with unique animations corresponding to the algorithm's decision logic.

=== "Recursive Backtracker"
    

=== "Prims Simple"

    

=== "Prims Weighted"

    

=== "Aldous Broder"

    

=== "Wilsons"

    

=== "Recursive Division"

    

=== "Kruskals Randomized"

    

=== "Ellers"

    


A more interesting demonstration of the TerminalMaze animation can be seen by building a maze and then running a Breadth First search across it to find the bottom-right-most cell. The Breadth First algorithm has a more elaborate animation.




#### Graph Algorithms in TTE

I've wanted to incorporate many of the algorithms from TerminalMaze into TTE. I finally got around to designing the algorithm interface and relevant methods/attributes on the Terminal/Canvas/EffectCharacter classes and can now easily provide graphing algorithms to the effects.

The Canvas can be treated like a graph with all EffectCharacters as nodes. Nodes can be linked and their neighbors queried. That's pretty much all it takes.

This new feature has already been put to use in the new and refreshed effects.

* Smoke : Smoke is a Prims Weighted tree being explored by a breadth first search. All new EffectCharacters explored are turned into smoke followed by the character animation.

* LaserEtch : The new laser path is a Recursive Backtracker tree with the backtracking removed.

* Burn : The new burn pattern is a Prims Simple tree where the node-link order is used as the ignition order for the characters.

I have many ideas for new effects which are enabled by these sorts of algorithms.

### Misc

---

#### Random Effect

TTE now has a random effect option that is used like this:

`ll | tte random_effect`

You can still pass global terminal options, but you cannot pass effect-specific options.

!!! success "Valid Invocation with Terminal Config"

    `ll | tte --canvas-height 0 --canvas-width 0 --anchor-text c random_effect`

    Passing terminal options such as `--canvas-height` works.

!!! failure "Invalid Invocation with Effect Config"

    `ll | tte random_effect --final-gradient-stops 00ff00 ff0000 0000ff`

    Passing effect options such as `--final-gradient-stops` does not work.

#### Faster TTE Startup

TTE will now launch faster as a result of the changes in Config/Terminal/Argument parsing classes. How much faster? Not sure but definitely something.

#### Framerate Reduced

TTE has always set the target framerate to 100FPS. This framerate was chosen arbitrarily during initial development and I mostly forgot about it. This framerate is unnecessarily high and leaves little time for the effects to do more complicated work between frames. It has been adjusted to 60FPS and all effects have had animation frames and movement speeds adjusted for visual parity with 100FPS. This should result in smoother animation on slower machines for effects which do a lot of dynamic work during the run.

### Moving Forward

---

Look forward to more frequent updates. Likely many of which are only a single effect or two without bundling them with months of engine changes nobody cares about. Who knows, maybe one day I'll get to actually writing a guide to producing your own effects. I won't consider TTE _Done_ until that happens.

---

### Plain Old Changelog

[0.13.0](https://github.com/ChrisBuilds/terminaltexteffects/blob/main/CHANGELOG.md)

---

### Changeblog Changeblog 0.14.0 (`docs/changeblog/changeblog_0.14.0.md`)

# 0.14.0 (Easier Easing)

## Release 0.14.0

### Release Summary (Easing and Random Filters)

This release features two new [Easing](https://easings.net/) classes and improves the functionality of the random effect selection CLI option. It also removes an effect which was inadvertently included in the last release. There are some effect changes/upgrades as well, but no new effects.

### Sequence Easing

Easing is a powerful tool when animating. It's how you bring life to otherwise boring linear motion/progression. TTE uses easing for motion and animation. However, it's also useful to ease progress over a sequence. For example, the [Wipe](../effects/wipe.md) effect uses easing to enable the `--wipe-ease` argument.

In the effect, a subset of a sequence of characters is 'activated' based on the output of an easing function. For example, when the easing function outputs 0.1, the first 10% of the character list is activated. That's pretty simple, until you realize some easing functions increase and decrease the output number as they progress. See [easeOutBounce](https://easings.net/#easeOutBounce) for example.

Handling this means tracking not just the added characters, as the function output increases, but also the removed characters when the function output decreases. These adjustments must be made after each step in the function. If you do it right, you can achieve the following:



I had found myself repeating the logic surrounding this in multiple effects such as [Sweep](../effects/sweep.md) and [Highlight](../effects/highlight.md). Implementing this was always enough of a challenge that I was not exploring the various ways eased sequences could improve other effects. To reduce friction in the future, I created a class `SequenceEaser` which exposes the relevant data while an easing function is applied to n sequence. These attributes include which elements of the sequence were added/removed on the last step. Using the `SequenceEaser` class reduced dozens of lines of logic to:

=== "Wipe Easing"

    ```python
    self.easer.step()
    for group in self.easer.added: # (1)
        for character in group: # (2)
            character.animation.activate_scene("wipe")
            self.terminal.set_character_visibility(character, is_visible=True)
            self.active_characters.add(character)
    for group in self.easer.removed: # (3)
        for character in group:
            character.animation.deactivate_scene()
            character.animation.query_scene("wipe").reset_scene()
            self.terminal.set_character_visibility(character, is_visible=False)
    ```

    1. `easer.added` is all sequence elements that fall within the positive difference between the easing functions last two steps.
    2. Wipe groups characters into lists representing a row/column/diag based on the `--wipe-direction` option.
    3. `easer.removed` is all sequence elements that fall within the negative difference between the easing functions last two steps.

`SequenceEaser` will lead to more experimentation and, hopefully, more interesting effects.

Here's an example that shows how helper classes such as `SequenceEaser` and the new spanning tree algorithms can be used to achieve otherwise complicated results with very few lines of code.


=== "Custom Ease over a Tree"

    ```python
    self.tree = PrimsSimple(
        self.terminal,
        limit_to_text_boundary=True,
        starting_char=self.terminal.get_character_by_input_coord(self.terminal.canvas.text_center),
    )
    self.sequence_easer = tte.easing.SequenceEaser(
        sequence=self.tree.char_link_order,
        easing_function=tte.easing.make_easing(0, 0.97, 0.99, 0.05),
        total_steps=100,
    )

    # ... skip to iterator logic ...

    if self.active_characters or not self.sequence_easer.is_complete():
            self.sequence_easer.step()
            for char in self.sequence_easer.added:
                self.terminal.set_character_visibility(char, is_visible=True)

    ```

This combines a `PrimsSimple` tree with a `SequenceEaser` applying a custom easing function via `tte.easing.make_easing` to the sequence of characters representing the link-order of the Prims tree.

The custom easing function is based on a bezier curve which looks like this:



So we should see fast progression over the tree at the start, followed by a slowing down to a near stop, then fast progression until the end. The Prims tree was set to start from the center of the canvas. Here's the result:




### Do Random Better

Previously, the `random_effect` option was sort-of hacked in by imitating an effect subparser and swapping an effect name into the `sys.argv` list. This had limited functionality without getting even more hack-y. This was refactored and, as such, random now supports the following:

```
  --random-effect, -R   Randomly select an effect to apply
  --include-effects INCLUDE_EFFECTS [INCLUDE_EFFECTS ...]
                        Space-separated list of Effects to include when randomly selecting an effect
  --exclude-effects EXCLUDE_EFFECTS [EXCLUDE_EFFECTS ...]
                        Space-separated list of Effects to exclude when randomly selecting an effect
```

### Plain Old Changelog

[0.14.0](https://github.com/ChrisBuilds/terminaltexteffects/blob/main/CHANGELOG.md)

---

### Changeblog Changeblog 0.15.0 (`docs/changeblog/changeblog_0.15.0.md`)

# 0.15.0 (Go Fetch)

## Release 0.15.0

### Release Summary (Shell Completion, Dynamic Colors, and Better Terminal Input)

This release is mostly about catching up on the quality-of-life work that makes TTE nicer to use in real shells with real terminal output.

There are no new built-in effects this time. Instead, TTE can now generate shell completions for `bash` and `zsh`, handle more of the ANSI escape sequence soup produced by terminal tools such as `neofetch` and `fastfetch`, and do a much better job preserving input colors in `existing_color_handling="dynamic"` mode.

There is also support for discovering user-provided effects from a config directory, along with a pile of engine documentation and test improvements.

### Push Tab, Receive Effect

---

TTE can now generate shell completions:

```bash
tte --print-completion bash
tte --print-completion zsh
```

For one-off use:

```bash
eval "$(tte --print-completion bash)"
```

or:

```zsh
eval "$(tte --print-completion zsh)"
```

If you want completions to stick around, add the relevant command to your shell startup file. The generated completion script includes discovered effects and their options, so custom effects show up there too.

!!! note

    Starting with 0.16.0, completion scripts are bundled with TTE and cover built-in effects only. See
    [Shell Completion](../appguide.md#shell-completion).

One small but useful bit of polish: the `zsh` completion script initializes `compinit` and `bashcompinit` when needed. That means the basic `eval "$(tte --print-completion zsh)"` path works with less shell ceremony.


### Go Fetch

---

TTE has supported parsing ANSI colors from input for a while, but terminal applications do more than color text. Fetch-style tools often move the cursor around to lay out a logo and system information side-by-side. Previously, TTE had no idea what to do with the control sequences fetch tools use for layout, so it treated them as text. That often led to interesting chaos as the sequences were broken apart and reassembled throughout the effect.

TTE now interprets common cursor movement CSI sequences into its virtual input canvas. That includes relative movement up, down, forward, and back; moving to the next or previous line; setting the cursor column; and jumping to an explicit row/column position:

```text
A: cursor up
B: cursor down
C: cursor forward
D: cursor back
E: next line
F: previous line
G: set column
H/f: set row and column
```

In practice, those sequences let TTE reconstruct terminal layouts that are drawn out of order. When a fetch tool prints a logo, moves the cursor, then prints system details next to it, TTE can now place those characters on the intended canvas cells before the effect starts.

It also accepts common DEC private mode toggles for cursor visibility and line wrapping as no-op input state:

```text
?25h ?25l ?7h ?7l
```

That does not mean TTE is a full terminal emulator. But it now understands enough of the layout language used by common fetch applications to preserve the intended shape.

The color parser also got more complete. TTE now supports 3-bit and 4-bit SGR foreground/background colors, reset sequences, and mixed style/color SGR sequences. Bold standard ANSI foreground colors are preserved as bright colors, which better matches how normal terminals display many fetch outputs.

=== "Unparsed Fetch Sequences"

    

=== "Parsed Fetch Sequences"

    

### Dynamic Colors Everywhere

---

In `0.12.0`, TTE added `--existing-color-handling` with three modes:

`ignore`

: Strip parsed input colors and let the effect do its normal thing.

`always`

: Keep input colors on the characters all the time, even when that removes some of the effect's personality.

`dynamic`

: Let the effect decide how to use the input colors.

That last option is the interesting one, but it also requires effect-by-effect care. Some effects should keep their own colors during the action and resolve to input colors at the end. Some should use input colors the whole way. Some need a neutral fallback for uncolored input. Some need to treat background-only spaces as real input. It goes on.

This release fills in a lot of that work.

Examples:

* [Burn](../effects/burn.md), [LaserEtch](../effects/laseretch.md), and [Matrix](../effects/matrix.md) now process input spaces that carry parsed ANSI colors, which preserves background-color art cells.
* [Beams](../effects/beams.md), [BinaryPath](../effects/binarypath.md), and [Overflow](../effects/overflow.md) no longer accidentally finish uncolored input in effect-owned colors when using `dynamic`.
* [Highlight](../effects/highlight.md) now bases the highlight on parsed foreground colors and preserves parsed backgrounds, including background-only spaces.
* [Rings](../effects/rings.md), [Scattered](../effects/scattered.md), [Slice](../effects/slice.md), [Slide](../effects/slide.md), [Spray](../effects/spray.md), and [Wipe](../effects/wipe.md) can now run with input colors driving the whole effect.
* [Thunderstorm](../effects/thunderstorm.md), [Unstable](../effects/unstable.md), and [VHSTape](../effects/vhstape.md) now start from input colors, do their effect-specific chaos, and return to the input colors or terminal default state correctly.

This is one of those changes where the feature sounds simple from the outside:

> keep my colors

And then inside the engine it becomes:

> preserve foreground and background independently, don't invent a foreground for background-only cells, don't erase colored spaces, keep helper/fill characters effect-owned, let uncolored input return to terminal default, and make all of that happen at the visually correct part of each effect

=== "Dynamic Colors"

    

=== "Always Colors"

    

=== "Ignore Colors"

    

### Your Effects Live Here Now

---

TTE can now discover effects from:

```text
${XDG_CONFIG_HOME}/terminaltexteffects/effects
```

If `XDG_CONFIG_HOME` is not set, that resolves to:

```text
~/.config/terminaltexteffects/effects
```

Effect files are normal Python files. For example:

```text
~/.config/terminaltexteffects/effects/effect_custom.py
```

If the file provides a `get_effect_resources()` function, TTE can register it just like a built-in effect. The function returns the CLI command name, the effect class, and the config class:

```python
def get_effect_resources():
    return "custom", CustomEffect, CustomConfig
```

After that, the custom effect can be invoked from the CLI:

```bash
cat message.txt | tte custom
```

The important part is that user effects now participate in the same parser-building path as built-in effects. Runtime parsing, effect help text, random-effect selection, and generated completions all build from the same discovered effect definitions.

### Small API Tweaks

---

There are also a number of engine and documentation improvements in this release.

The final-gradient CLI arguments are now shared helpers:

```python
FinalGradientStopsArg
FinalGradientStepsArg
FinalGradientFramesArg
FinalGradientDirectionArg
```

All effects now use these helpers, which keeps parser defaults and help text more consistent.

RandomSequence now uses `--terminal-background-color` as the starting point for its reveal fade, so its effect-specific `--starting-color` option has been removed.

Several spanning tree generators received bug fixes and focused test coverage. The highlights:

* `SpanningTreeGenerator.get_neighbors()` now honors `unlinked_only=False`.
* `PrimsSimple` applies `limit_to_text_boundary` more consistently.
* `PrimsWeighted` limits random starting-character selection to the text boundary when requested.
* `BreadthFirst` initializes correctly when `starting_char` is omitted and records discovered characters once.
* `AldousBroder` returns immediately once all characters are linked.

Path and scene deactivation are also more convenient. These calls now accept an object, an ID string, or no argument:

```python
character.motion.deactivate_path()
character.motion.deactivate_path("path_id")
character.animation.deactivate_scene()
character.animation.deactivate_scene("scene_id")
```

The no-argument form deactivates the currently active path or scene. Event registrations for `DEACTIVATE_PATH` and `DEACTIVATE_SCENE` support the same shapes.

### Plain Old Changelog

[0.15.0](https://github.com/ChrisBuilds/terminaltexteffects/blob/main/CHANGELOG.md)

---

### Effects Beams (`docs/effects/beams.md`)

# Beams



## Quick Start

``` py title="beams.py"
from terminaltexteffects.effects.effect_beams import Beams

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

::: terminaltexteffects.effects.effect_beams

---

### Effects Binarypath (`docs/effects/binarypath.md`)

# BinaryPath



## Quick Start

``` py title="binarypath.py"
from terminaltexteffects.effects.effect_binarypath import BinaryPath

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

::: terminaltexteffects.effects.effect_binarypath

---

### Effects Blackhole (`docs/effects/blackhole.md`)

# Blackhole



## Quick Start

``` py title="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)
```

::: terminaltexteffects.effects.effect_blackhole

---

### Effects Bouncyballs (`docs/effects/bouncyballs.md`)

# BouncyBalls



## Quick Start

``` py title="bouncyballs.py"
from terminaltexteffects.effects.effect_bouncyballs import BouncyBalls

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

::: terminaltexteffects.effects.effect_bouncyballs

---

### Effects Bubbles (`docs/effects/bubbles.md`)

# Bubbles



## Quick Start

``` py title="bubbles.py"
from terminaltexteffects.effects.effect_bubbles import Bubbles

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

::: terminaltexteffects.effects.effect_bubbles

---

### Effects Burn (`docs/effects/burn.md`)

# Burn



## Quick Start

``` py title="burn.py"
from terminaltexteffects.effects.effect_burn import Burn

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

::: terminaltexteffects.effects.effect_burn

---

### Effects Colorshift (`docs/effects/colorshift.md`)

# ColorShift



## Quick Start

``` py title="colorshift.py"
from terminaltexteffects import Gradient
from terminaltexteffects.effects.effect_colorshift import ColorShift

effect = ColorShift("YourTextHere")
effect.effect_config.travel = True
effect.effect_config.travel_direction = Gradient.Direction.RADIAL
with effect.terminal_output() as terminal:
    for frame in effect:
        terminal.print(frame)
```

::: terminaltexteffects.effects.effect_colorshift

---

### Effects Crumble (`docs/effects/crumble.md`)

# Crumble



## Quick Start

``` py title="crumble.py"
from terminaltexteffects.effects.effect_crumble import Crumble

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

::: terminaltexteffects.effects.effect_crumble

---

### Effects Decrypt (`docs/effects/decrypt.md`)

# Decrypt



## Quick Start

``` py title="decrypt.py"
from terminaltexteffects.effects.effect_decrypt import Decrypt

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

::: terminaltexteffects.effects.effect_decrypt

---

### Effects Errorcorrect (`docs/effects/errorcorrect.md`)

# ErrorCorrect



## Quick Start

``` py title="errorcorrect.py"
from terminaltexteffects.effects.effect_errorcorrect import ErrorCorrect

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

::: terminaltexteffects.effects.effect_errorcorrect

---

### Effects Expand (`docs/effects/expand.md`)

# Expand



## Quick Start

``` py title="expand.py"
from terminaltexteffects.effects.effect_expand import Expand

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

::: terminaltexteffects.effects.effect_expand

---

### Effects Fireworks (`docs/effects/fireworks.md`)

# Fireworks



## Quick Start

``` py title="fireworks.py"
from terminaltexteffects.effects.effect_fireworks import Fireworks

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

::: terminaltexteffects.effects.effect_fireworks

---

### Effects Highlight (`docs/effects/highlight.md`)

# Highlight



## Quick Start

``` py title="highlight.py"
from terminaltexteffects import Gradient
from terminaltexteffects.effects.effect_highlight import Highlight

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

## Highlight Directions and Sorts

!!! info "New in 0.16.0"

    `CharacterOrder` directions and `--reverse-highlight-direction` are available on development `main` and are
    not in the published 0.15.0 package.

`--highlight-direction` accepts every `CharacterOrder`. Group directions activate
whole rows, columns, diagonals, or radial bands. Sort directions activate individual characters in the
exact order returned by `Terminal.get_characters()`, using their input coordinates. All text remains
visible while the highlight travels through it.

See [Spiral Sorting](../engine/terminal/terminal.md#spiral-sorting) for the six spiral orders, their starting
corners, and how multiple arms interleave.

Both grouped and sorted modes retain the same easing schedule, so multiple characters may start highlighting in one frame.
`highlight_width` controls the duration of each character's bright highlight; it does not set the
number of characters in a sorted trail. The default remains `diagonal_bottom_left_to_top_right`.

```bash
tte highlight --highlight-direction spiral_clockwise
tte highlight --highlight-direction spiral_counter_clockwise_quad --highlight-width 12
```

Use `--reverse-highlight-direction` 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.

```bash
tte highlight --highlight-direction spiral_clockwise --reverse-highlight-direction
```

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:

```python
from terminaltexteffects.effects.effect_highlight import Highlight
from terminaltexteffects import CharacterOrder

effect = Highlight("YourTextHere")
effect.effect_config.highlight_direction = CharacterOrder.SPIRAL_CLOCKWISE_DOUBLE
```

::: terminaltexteffects.effects.effect_highlight

---

### Effects Laseretch (`docs/effects/laseretch.md`)

# LaserEtch



## Quick Start

``` py title="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

!!! info "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](../engine/terminal/terminal.md#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.

```bash
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.

```bash
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:

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

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

::: terminaltexteffects.effects.effect_laseretch

---

### Effects Matrix (`docs/effects/matrix.md`)

# Matrix



## Quick Start

``` py title="matrix.py"
from terminaltexteffects.effects.effect_matrix import Matrix

effect = Matrix("YourTextHere\n" * 10)
with effect.terminal_output() as terminal:
    for frame in effect:
        terminal.print(frame)
```

::: terminaltexteffects.effects.effect_matrix

---

### Effects Middleout (`docs/effects/middleout.md`)

# MiddleOut



## Quick Start

``` py title="middleout.py"
from terminaltexteffects.effects.effect_middleout import MiddleOut

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

::: terminaltexteffects.effects.effect_middleout

---

### Effects Orbittingvolley (`docs/effects/orbittingvolley.md`)

# OrbittingVolley



## Quick Start

``` py title="orbittingvolley.py"
from terminaltexteffects.effects.effect_orbittingvolley import OrbittingVolley

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

## Launch Pattern

!!! info "New in 0.16.0"

    The ring launch pattern and the `launch_delay` default of `1` are on development `main`. In the published 0.15.0
    package, `launch_delay` defaults to `30`.

Four launchers travel along the canvas edges, firing characters from the side nearest their final position.
The text builds in expanding circular rings around its center, adjusted for terminal cell height.
Every character in a ring launches before the next ring opens. Up to two rings can have characters
still in flight; a third waits for one of them to finish arriving. Launchers with no characters assigned
in the current ring keep moving while the other launchers fire. The effect finishes after all characters arrive.

The default `launch_delay` is `1`, leaving one animation tick between volleys. Increase it for longer pauses.

::: terminaltexteffects.effects.effect_orbittingvolley

---

### Effects Overflow (`docs/effects/overflow.md`)

# Overflow



## Quick Start

``` py title="overflow.py"
from terminaltexteffects.effects.effect_overflow import Overflow

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

::: terminaltexteffects.effects.effect_overflow

---

### Effects Pour (`docs/effects/pour.md`)

# Pour



## Quick Start

``` py title="pour.py"
from terminaltexteffects.effects.effect_pour import Pour

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

::: terminaltexteffects.effects.effect_pour

---

### Effects Print (`docs/effects/print.md`)

# Print



## Quick Start

``` py title="print.py"
from terminaltexteffects.effects.effect_print import Print

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

::: terminaltexteffects.effects.effect_print

---

### Effects Rain (`docs/effects/rain.md`)

# Rain



## Quick Start

``` py title="rain.py"
from terminaltexteffects.effects.effect_rain import Rain

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

::: terminaltexteffects.effects.effect_rain

---

### Effects Randomsequence (`docs/effects/randomsequence.md`)

# RandomSequence



## Quick Start

``` py title="randomsequence.py"
from terminaltexteffects.effects.effect_random_sequence import RandomSequence

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

::: terminaltexteffects.effects.effect_random_sequence

---

### Effects Rings (`docs/effects/rings.md`)

# Rings



## Quick Start

``` py title="rings.py"
from terminaltexteffects.effects.effect_rings import Rings

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

::: terminaltexteffects.effects.effect_rings

---

### Effects Scattered (`docs/effects/scattered.md`)

# Scattered



## Quick Start

``` py title="scattered.py"
from terminaltexteffects.effects.effect_scattered import Scattered

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

::: terminaltexteffects.effects.effect_scattered

---

### Effects Slice (`docs/effects/slice.md`)

# Slice



## Quick Start

``` py title="slice.py"
from terminaltexteffects.effects.effect_slice import Slice

effect = Slice("YourTextHere")
effect.effect_config.slice_direction = "diagonal"
with effect.terminal_output() as terminal:
    for frame in effect:
        terminal.print(frame)
```

::: terminaltexteffects.effects.effect_slice

---

### Effects Slide (`docs/effects/slide.md`)

# Slide



## Quick Start

``` py title="slide.py"
from terminaltexteffects.effects.effect_slide import Slide

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

::: terminaltexteffects.effects.effect_slide

---

### Effects Smoke (`docs/effects/smoke.md`)

# Smoke



## Quick Start

``` py title="smoke.py"
from terminaltexteffects.effects.effect_smoke import Smoke

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

::: terminaltexteffects.effects.effect_smoke

---

### Effects Spotlights (`docs/effects/spotlights.md`)

# Spotlights



## Quick Start

``` py title="spotlights.py"
from terminaltexteffects.effects.effect_spotlights import Spotlights

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

::: terminaltexteffects.effects.effect_spotlights

---

### Effects Spray (`docs/effects/spray.md`)

# Spray



## Quick Start

``` py title="spray.py"
from terminaltexteffects.effects.effect_spray import Spray

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

::: terminaltexteffects.effects.effect_spray

---

### Effects Swarm (`docs/effects/swarm.md`)

# Swarm



## Quick Start

``` py title="swarm.py"
from terminaltexteffects.effects.effect_swarm import Swarm

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

::: terminaltexteffects.effects.effect_swarm

---

### Effects Sweep (`docs/effects/sweep.md`)

# Sweep



## Quick Start

``` py title="sweep.py"
import terminaltexteffects as tte
from terminaltexteffects.effects.effect_sweep import Sweep

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

## Sweep Directions and Sorts

!!! info "New in 0.16.0"

    `CharacterOrder` directions, `--travel-speed`, and the reverse-direction options are available on
    development `main` and are not in the published 0.15.0 package.

`--first-sweep-direction` and `--second-sweep-direction` each accept every `CharacterOrder`. The first sweep reveals the text with a gray shimmer; the second colors it.
You can choose a group direction for one phase and a sort for the other, or use separate sorts
for both phases. The defaults remain `column_right_to_left` and `column_left_to_right`.

Group directions activate whole spatial bands. Sort directions activate individual characters in
the exact order returned by `Terminal.get_characters()`. Both phases include inner and outer fill
characters, so spiral sorts cover the canvas bounds, including blank space around the input.

See [Spiral Sorting](../engine/terminal/terminal.md#spiral-sorting) for the six spiral orders, their starting
corners, and how multiple arms interleave.

Both grouped and sorted modes retain the same easing schedule; several characters may activate in one frame. Each phase
starts its own sequence, and the final frame restores input symbols and clears the fill characters.

## Travel Speed

`--travel-speed` advances both sweep phases by the specified number of easing steps per frame.
It accepts a positive integer and defaults to `1`, preserving the original pacing. Higher values
activate more characters or complete spatial groups per frame while retaining the easing curve,
selected order, and reversal. The exact activation count varies along the easing curve.

Each phase retains its 100-step schedule, so speed `4` finishes scheduling a phase in 25 frames.
The second phase starts on the following frame, and active character animations still advance
once per frame and finish normally. This differs from Waves, where travel speed counts ordered
entries rather than easing steps.

```bash
tte sweep --first-sweep-direction spiral_clockwise --second-sweep-direction spiral_counter_clockwise
tte sweep --first-sweep-direction spiral_clockwise --second-sweep-direction spiral_counter_clockwise --travel-speed 8
tte sweep --first-sweep-direction column_right_to_left --second-sweep-direction spiral_clockwise_quad
```

Use `--reverse-first-sweep-direction` and `--reverse-second-sweep-direction` to reverse each
phase independently. Group membership is preserved, and both flags default to off.

```bash
tte sweep --first-sweep-direction spiral_clockwise --reverse-first-sweep-direction \
  --second-sweep-direction circle_center_to_outside --reverse-second-sweep-direction
```

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

For library use, assign native enums or their CLI spellings:

```python
from terminaltexteffects.effects.effect_sweep import Sweep
from terminaltexteffects import CharacterOrder

effect = Sweep("YourTextHere")
effect.effect_config.first_sweep_direction = CharacterOrder.COLUMN_RIGHT_TO_LEFT
effect.effect_config.second_sweep_direction = CharacterOrder.SPIRAL_CLOCKWISE_DOUBLE
effect.effect_config.travel_speed = 8
```

::: terminaltexteffects.effects.effect_sweep

---

### Effects Synthgrid (`docs/effects/synthgrid.md`)

# SynthGrid



## Quick Start

``` py title="synthgrid.py"
from terminaltexteffects.effects.effect_synthgrid import SynthGrid

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

## Grid Layout

!!! info "New in 0.16.0"

    The balanced grid layout is on development `main` and is not in the published 0.15.0 package.

SynthGrid divides the canvas into cells that are approximately square after accounting for terminal character
proportions. Cell widths and heights differ by at most one position; leftover space is spread across the grid.
The layout aims for about five cells along the longer visual axis and uses smaller cells on narrow canvases.
Cells normally have at least four columns and two rows. Canvases smaller than those dimensions use a single
cell along the affected axis.

Grid lines overlay the cells, so every input character remains part of the dissolve animation. Intersections
share one grid character, and lines that would obscure tiny cells are omitted. A canvas that fits in one cell,
including a single-character canvas, starts dissolving immediately without drawing a grid.

::: terminaltexteffects.effects.effect_synthgrid

---

### Effects Thunderstorm (`docs/effects/thunderstorm.md`)

# Thunderstorm



## Quick Start

``` py title="thunderstorm.py"
from terminaltexteffects.effects.effect_thunderstorm import Thunderstorm

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

::: terminaltexteffects.effects.effect_thunderstorm

---

### Effects Unstable (`docs/effects/unstable.md`)

# Unstable



## Quick Start

``` py title="unstable.py"
from terminaltexteffects.effects.effect_unstable import Unstable

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

::: terminaltexteffects.effects.effect_unstable

---

### Effects Vhstape (`docs/effects/vhstape.md`)

# VHSTape



## Quick Start

``` py title="vhstape.py"
from terminaltexteffects.effects.effect_vhstape import VHSTape

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

::: terminaltexteffects.effects.effect_vhstape

---

### Effects Waves (`docs/effects/waves.md`)

# Waves



## Quick Start

``` py title="waves.py"
from terminaltexteffects.effects.effect_waves import Waves

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

## Circular Waves

!!! info "New in 0.16.0"

    Circular directions, `CharacterOrder` directions, `--reverse-wave-direction`, and `--travel-speed` are
    available on development `main` and are not in the published 0.15.0 package.

The default direction, `circle_center_to_outside`, spreads waves outward in circular rings. Use
`circle_outside_to_center` for waves that converge on the text center. These directions account for
terminal character proportions. The existing `diamonds_center_to_outside` and `diamonds_outside_to_center` directions
use diamond-shaped groups. The former `center_to_outside` and `outside_to_center` spellings remain
accepted as compatibility aliases.

```bash
tte waves --wave-direction circle_center_to_outside
tte waves --wave-direction circle_outside_to_center
```

## Character Orders and Reversal

`--wave-direction` accepts every `CharacterOrder`: rows, columns, diagonals, diamond and circular
bands, individual row traversals, random order, and all six [spiral orders](../engine/terminal/terminal.md#spiral-sorting). By default, spatial orders
activate one complete group per frame and individual orders activate one character per frame.
Double and quad spiral arms interleave in that individual character sequence.
The default remains `circle_center_to_outside`.

Use `--reverse-wave-direction` to reverse activation order while retaining each group's membership.
Reversed spirals travel outward along the selected path. Reversal defaults to off.

```bash
tte waves --wave-direction spiral_clockwise_quad
tte waves --wave-direction spiral_counter_clockwise_double --reverse-wave-direction --travel-speed 8
```

```python
from terminaltexteffects import CharacterOrder
from terminaltexteffects.effects.effect_waves import Waves

effect = Waves("YourTextHere")
effect.effect_config.wave_direction = CharacterOrder.SPIRAL_CLOCKWISE
effect.effect_config.reverse_wave_direction = True
effect.effect_config.travel_speed = 8
```

CLI names, including the previous eight directions, and legacy `CharacterGroup` / `CharacterSort`
values normalize to `CharacterOrder`. The circular default and grouped activation timing at speed `1` are unchanged.

## Travel Speed

`--travel-speed` sets how many ordered entries activate per frame. For individual orders this is the
number of characters; for spatial orders it is the number of complete groups. The value must be a
positive integer and defaults to `1`. The last batch activates only the remaining entries, retaining
the selected order and reversal. Animation timing still advances once per frame.

```bash
tte waves --wave-direction spiral_clockwise --travel-speed 12
tte waves --wave-direction circle_center_to_outside --travel-speed 2
```

Use `--wave-length` to change the duration of each character's wave animation; `--travel-speed`
controls how quickly new characters or groups start their waves.

::: terminaltexteffects.effects.effect_waves

---

### Effects Wipe (`docs/effects/wipe.md`)

# Wipe



## Quick Start

``` py title="wipe.py"
from terminaltexteffects.effects.effect_wipe import Wipe

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

## Wipe Directions and Sorts

!!! info "New in 0.16.0"

    `CharacterOrder` directions and `--reverse-wipe-direction` are available on development `main` and are not
    in the published 0.15.0 package.

`--wipe-direction` accepts every `CharacterOrder`. Group directions reveal whole
rows, columns, diagonals, or radial bands. Sort directions reveal individual characters in the exact
order returned by `Terminal.get_characters()`, using their input coordinates.

See [Spiral Sorting](../engine/terminal/terminal.md#spiral-sorting) for the six spiral orders, their starting
corners, and how multiple arms interleave.

Both grouped and sorted modes use the same 100-step easing schedule, so multiple characters may appear in one frame.
`wipe_delay` adds frames before the first easing step and between later steps. Easing functions that
reverse temporarily hide characters and restart their wipe scenes when those characters reappear.
The default remains `diagonal_top_left_to_bottom_right`.

```bash
tte wipe --wipe-direction spiral_clockwise
tte wipe --wipe-direction spiral_counter_clockwise_double --wipe-delay 2
```

Use `--reverse-wipe-direction` 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.

```bash
tte wipe --wipe-direction spiral_clockwise --reverse-wipe-direction
```

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:

```python
from terminaltexteffects.effects.effect_wipe import Wipe
from terminaltexteffects import CharacterOrder

effect = Wipe("YourTextHere")
effect.effect_config.wipe_direction = CharacterOrder.SPIRAL_CLOCKWISE_QUAD
```

::: terminaltexteffects.effects.effect_wipe

---

### Engine Basecharacter (`docs/engine/basecharacter.md`)

# EffectCharacter

*Module*: `terminaltexteffects.engine.base_character`

::: terminaltexteffects.engine.base_character.EffectCharacter

---

### Engine Baseconfig (`docs/engine/baseconfig.md`)

# BaseConfig

*Module*: `terminaltexteffects.engine.base_config`

::: terminaltexteffects.engine.base_config

---

### Engine Baseeffect (`docs/engine/baseeffect.md`)

# BaseEffect

*Module*: `terminaltexteffects.engine.base_effect`

::: terminaltexteffects.engine.base_effect

---

### Engine Eventhandler (`docs/engine/eventhandler.md`)

# EventHandler

*Module*: `terminaltexteffects.engine.base_character`

::: terminaltexteffects.engine.base_character.EventHandler

---

### Engine Particles (`docs/engine/particles.md`)

# ParticlePool

*Module*: `terminaltexteffects.engine.effect_support.particles`

!!! info "New in 0.16.0"

    `ParticlePool` and `ParticleReset` are available on development `main` and are not in the published 0.15.0
    package.

`ParticlePool` reuses helper characters for short-lived visuals such as sparks, smoke, or fragments. The pool creates
characters with `Terminal.add_character()`, shows and activates them when emitted, and returns them to the pool when
they are reclaimed. Particles remain ordinary `EffectCharacter` objects, so the effect configures their scenes, paths,
and events with the usual APIs.

Use the `initializer` callback for setup that survives reuse, such as building scenes and registering a reclaim event.
Use the callback passed to `emit()` for per-emission setup, such as paths and scene resets. `ParticleReset` selects
which state is cleared each time a particle is reused; by default, paths are cleared and any active path or scene is
deactivated, while scenes and event registrations are kept.

The following snippet runs inside an effect iterator, where `self.terminal` and `self.active_characters` are available:

```python
from terminaltexteffects import Coord, EffectCharacter, ParticlePool


def initialize_spark(particle: EffectCharacter) -> None:
    scene = particle.animation.new_scene(scene_id="spark")
    for symbol in "*+.":
        scene.add_frame(symbol, 4)
    self.sparks.reclaim_on_event(particle, caller=scene)  # (1)


self.sparks = ParticlePool(
    self.terminal,
    self.active_characters,
    "*",
    max_size=32,
    initializer=initialize_spark,
)


def launch(particle: EffectCharacter) -> None:
    scene = particle.animation.query_scene("spark")
    scene.reset_scene()
    path = particle.motion.new_path(speed=0.5)
    path.new_waypoint(Coord(origin.column, origin.row + 3))
    particle.motion.activate_path(path)
    particle.animation.activate_scene(scene)


self.sparks.emit(origin, launch)  # (2)
```

1. Returns the particle to the pool when its scene completes. Registering it in the initializer keeps the
   registration across reuse.
2. Returns `None` instead of a particle when every particle is in use and the pool has reached `max_size`.

::: terminaltexteffects.engine.effect_support.particles

---

### Engine Space3d (`docs/engine/space3d.md`)

# Space3D

*Module*: `terminaltexteffects.engine.space3d`

!!! info "New in 0.16.0"

    `Space3D` and character mounting are available on development `main` and are not in the published 0.15.0
    package.

`Space3D` lets an effect move its own characters through 3D. It holds a camera centered on the canvas, a light and
any number of bodies. A body is geometry that moves in 3D: a cube, a prism, a sphere, or a plain frame of reference.
Bodies own no characters. The effect mounts its characters on anchors (points on a body) and releases them when it
wants them back. While a character is mounted, only its position comes from 3D, so its scenes, events and layer keep
working as usual.

World units are terminal columns. The origin is the center of the canvas, +x points right, +y points up and +z points
toward the default viewer. World axes stay fixed when the camera turns. Angles are degrees, and axes are `"x"`, `"y"`, `"z"`, `"diagonal"` or any `(x, y, z)` direction.
For the vectors, rotations and projection underneath, see [Geometry 3D](utils/geometry3d.md).

## Moving bodies

An effect can change a body one step at a time from `__next__`. `rotate`, `move_by`, `scale_by` and the `set_*`
methods take effect immediately. It can also queue tweens that `Space3D.step()` plays one frame at a time: `spin`,
`settle`, `turn_to`, `turn_to_rest`, `move_to`, `scale_to`, `random_pose`, `random_position` and `hold`. Each tween
runs after the previous one, or alongside it with `together=True`, and `on_complete` runs a callback when one
finishes. Callbacks run once every body has moved and every mount has been refreshed, so characters released there
are handed over at the final pose. Tweens add their change to the body instead of overwriting it, so a spin keeps turning a body that the
effect is also nudging.

## Moving the camera

`space.camera` is a [`Camera`](#terminaltexteffects.engine.space3d.Camera), extending `geometry3d.Camera` with
immediate movement and a tween queue. It supports dolly, truck, pedestal, pan, tilt, roll, look-at, orbit and zoom.
Camera tweens count toward `space.animating` and advance in `space.step()` before body tweens. Completion callbacks
run after mount readings refresh. See [Moving the camera](../effectguide/3d/camera-movement.md) for the
controls, target sampling, lighting and layout conventions.

## Camera readouts and projection helpers

The high-level `Camera` inherits its settings and projection helpers from `geometry3d.Camera`. They work on
`space.camera` alongside the controls:

| Members | Meaning |
| --- | --- |
| `position`, `rotation` | Read-only world eye and orientation; change them through setters or controls |
| `distance`, `focal` | Legacy initial eye distance / fallback focal, and an optional explicit focal length |
| `orthographic`, `near`, `row_scale` | Projection kind, minimum view-axis depth and tall-cell correction |
| `center_column`, `center_row` | Optical center in canvas cells |
| `project`, `project_points` | World points to projected cells and depths, or `None` before the near plane |
| `depth`, `view_direction` | View-axis depth and direction back toward the viewer |
| `ray`, `cell_to_world` | A world viewing ray, or its intersection with a world-z plane |

Defaults, mutable-setting behavior, orthographic differences and errors are explained in
[Camera and projection](../effectguide/3d/camera.md). `set_position` and `move_to` take absolute coordinates;
`move_by` takes a world displacement; dolly/truck/pedestal use camera-local axes. `look_at` and `orbit` sample body
targets when queued actions begin. Their `frames` argument is optional: omission applies immediately, whereas zero
obeys queue ordering. Other tween methods default to 60 frames.

Camera cancellation drops pending callbacks and keeps the pose. Reset also restores initial projection settings.
`space.clear()` removes bodies; cancel camera work separately if you are ending the whole scene.

## Mounting characters

Anchors move with their body. `body.anchor(x, y, z)` makes one, and layouts make many:

- `text_plane(characters)`: one anchor per character, placed so each sits on its input cell at the body's pose.
- `surface(count)`, `vertices()` and `edges(count)`: spread over a shape.
- `ring(count, radius)`: a circle around the body.
- `wrap(characters)`: text wrapped around a `Cylinder` or `Sphere`.

`space.mount(character, anchor)` hands the character's position to the anchor. The `blend_frames` and `ease` options
make it travel there from wherever it is. Mounting also sets two defaults:

- `layer_by_depth=True` makes nearer characters draw over farther ones.
- `hide_when_facing_away` can hide a character while its side of the body faces away.

`space.release(character)` gives the character back where it stands, ready for a path, and undoes only what the
mount itself changed.

Mounting is part of `Motion`, so it works with the rest of the engine:

- A mounted character counts as active.
- Activating a path releases the mount, and mounting deactivates the path.
- The `MOUNT_COMPLETE` and `MOUNT_RELEASED` events and the `MOUNT` and `UNMOUNT` actions chain mounts with paths
  and scenes. Use `space.new_mount()` to create a mount for the `MOUNT` action.

Each mount reports `depth`, `nearness`, `facing`, `facing_amount` and `light` after every `step()`. An effect can read
these to set its own appearance. A scene created with `sync=Scene.SyncMetric.LIGHT` or `Scene.SyncMetric.DEPTH`
picks its frame from the mount's light or nearness, so 3D shading can be authored as an ordinary gradient scene.

## Drawing surfaces

Mounted characters leave gaps on a solid. To fill every covered cell, draw the body with a look:

- `Shaded(gradient, symbols)` fills faces with a symbol ramp and gradient, picked by each face's angle to the light.
- `Solid(symbol, gradient)` uses one symbol, colored by the light.
- `Textured(text=None, back="hide")` wraps the input text, or any string, over the faces. With `back="dim"` the far
  side shows through the spaces in the text.
- `Wireframe(color, hidden_color)` draws only the edges, with hidden edges dimmed or left out.

Gradients default to the TTE brand colors. `body.draw(look)` starts drawing, `faces={tag: look}` gives faces
their own looks (a die is one argument), and `body.erase()` stops. Each `step()` draws every drawn body into one
depth buffer, so overlapping bodies hide each other correctly.

The space draws surfaces with characters it creates, one per covered cell, and never uses the effect's own
characters. `body.surface_characters` lists the ones showing a body after the last `step()`. An effect can restyle
them, but each `step()` redraws them. Surface cells are ordered by their exact depth, like mounted characters, and
count a hair farther than they are, so text mounted on a drawn body from the default layer 0 or above reads on top
of it.

```python
self.die = self.space.body(Cube(16)).draw(Shaded(), faces={0: Textured("1"), 2: Textured("6")})
self.die.random_pose(frames=90, ease=easing.out_cubic)
```

The following snippet runs inside an effect iterator, where `self.terminal` and `self.active_characters` are
available:

```python
from terminaltexteffects import Color, Gradient, Scene, easing
from terminaltexteffects.engine.space3d import Cube, Space3D

self.space = Space3D(self.terminal)
self.cube = self.space.body(Cube(20))
self.cube.spin("y", turns=2, frames=240, ease=easing.in_out_sine).settle("x", frames=120)  # (1)

characters = [c for c in self.terminal.get_characters() if c.input_symbol != " "]
shade = Gradient(Color("#5a005a"), Color("#00D1FF"), Color("#ffffff"), steps=12)
for character, anchor in zip(characters, self.cube.surface(len(characters))):
    scene = character.animation.new_scene(sync=Scene.SyncMetric.LIGHT)  # (2)
    scene.apply_gradient_to_symbols(character.input_symbol, 1, fg_gradient=shade)
    character.animation.activate_scene(scene)
    self.space.mount(character, anchor, blend_frames=40, hide_when_facing_away=True)
    self.terminal.set_character_visibility(character, is_visible=True)
    self.active_characters.add(character)


def __next__(self) -> str:
    if not self.space.animating:
        raise StopIteration
    self.space.step()  # (3)
    self.update()
    return self.frame
```

1. Two full turns about y, then a turn about x that eases out any tilt and ends at rest.
2. Each frame of this scene is a step along the gradient. The mount's `light` picks the frame, so faces brighten as
   they turn toward the light.
3. Plays the tweens and refreshes every mount's readings. `update()` then ticks the characters, which moves mounted
   characters onto their anchors.

Position arguments accept a `geometry3d.Vec3` or an `(x, y, z)` tuple in terminal columns.
Body/camera placement uses world coordinates; `Body.anchor` and `Body.world_point` take body-local
coordinates. `move_by` takes a relative world displacement. Existing scalar x/y/z calls remain
supported; omit y/z when supplying a complete point and use keywords for animation timing.
See [position input forms](../effectguide/3d/coordinates.md#position-input-forms) for examples.

::: terminaltexteffects.engine.space3d
    options:
      inherited_members:
        - position
        - rotation
        - center_column
        - center_row
        - distance
        - focal
        - orthographic
        - near
        - row_scale
        - project
        - project_points
        - depth
        - view_direction
        - ray
        - cell_to_world
        - for_canvas

---

### Effectguide 3d Animating (`docs/effectguide/3d/animating.md`)

# Animating in 3D

A 3D effect is still a frame-by-frame effect. Bodies and the camera move a little every frame, either because your code moves them
or because they're playing **tweens**. This chapter covers both, how tweens are timed, and how to hand the text back
to the ordinary 2D engine once the 3D part is over.

## The idea

There are two ways to animate a body, and they work together:

- **Step by step**: your `__next__` changes the body directly, with `rotate`, `move_by`, `set_rotation` and the rest.
  You're in full control, frame by frame. Use this when the motion depends on something else in the effect.
- **Tweens**: you describe a change once, such as "two turns about y over 120 frames, easing out", and the space
  plays it a frame at a time. Use this for motion you can describe up front.

Tweens **queue**. Each one starts when the one before it finishes, so a body's queue is its script. A tween queued with
`together=True` joins the one before it and runs at the same time.



Tweens add their change to the body rather than setting it, so step-by-step calls and tweens combine. A spin keeps
turning a body that your code is also nudging.

Each tween has a length in frames and an optional **easing** function, the same ones paths use (see
[Motion](../engine/motion.md#easing)). Easing changes how the motion is spread over its frames, not how long it
takes.

## In TTE

The Handoff example flings the text plane far away, brings it back with a spin that settles exactly at rest, and then
releases every character to an ordinary 2D hop. The 3D part is four calls:

```python
self.plane.set_rotation(rotation_about("x", 60) * rotation_about("y", 200)).set_position(0, 0, -300)
self.plane.move_to(0, 0, 0, frames=150, ease=easing.out_cubic)
self.plane.settle("y", turns=1, frames=150, ease=easing.out_cubic, together=True)
self.plane.on_complete(self.hand_off)  # hand_off releases the characters and starts their hops.
```

??? example "handoff.py"

    Save it in your community effects directory as `handoff.py`.

    ```python
    --8<-- "docs/effectguide/examples/3d/handoff/handoff.py"
    ```

```bash
tte --input-file concept_input.txt --canvas-width 80 --canvas-height 24 --anchor-text c handoff
```



- `set_rotation(...)` and `set_position(...)` place the body at its starting pose at once. `move_to(..., frames=0)`
  would do the same as a tween.
- `move_to` and `settle(..., together=True)` run together, so the text turns as it flies in. Both bring their part of
  the pose home, position and rotation, so it lands where the text plane was built and every character meets its
  input cell.
- `on_complete(callback)` runs when the last queued tween finishes, after the final pose is applied. The example
  releases the characters there.
- `release_all()` hands each character back on the cell its anchor projects to at that moment, with nothing left of the
  mount, so a new path starts from exactly that cell. That holds even if the callback changes the pose before
  releasing.

Other tweens: `turn_to(rotation)`, `turn_to_rest()`, `scale_to(scale)`, `hold(frames)`, and `random_pose()` and
`random_position()` for scattered starts. `body.animating` tracks that body; `camera.animating` tracks the camera; `space.animating` combines both. They tell
you whether their tweens are still queued. They say nothing about mounted characters, blends or the 2D paths you start after a release, so end the
effect on `active_characters` too (see [the frame loop](index.md#mounted-text)).

### Mounting mid-effect

Mounting and paths are both part of a character's `Motion`, so they hand off cleanly in both directions:

- `space.mount(character, anchor, blend_frames=30, ease=...)` lets a character travel to its anchor from wherever it is,
  instead of jumping.
- Activating a path releases the mount, and mounting deactivates the path.
- The `MOUNT_COMPLETE` and `MOUNT_RELEASED` events, and the `MOUNT` and `UNMOUNT` actions, chain mounts with paths and
  scenes through the [event handler](../engine/events.md). `space.new_mount()` creates a mount for the `MOUNT` action
  without starting it.

## Mount readings

Every active mount measures its anchor in `space.step()`, so read these after `step()` and before `update()`:

| Reading | What it is |
| --- | --- |
| `coord` | The cell the anchor projects to |
| `depth` | Distance along the current view axis, in columns: smaller is nearer. Either body or camera motion can change it |
| `nearness` | 0 at the farthest point the body can reach at its size, 1 at the nearest, measured from the body's own center. Pure translation does not change it; turning the body or camera changes which side is nearer |
| `facing`, `facing_amount` | Whether the anchor's surface faces the viewer, and how squarely, from -1 (away) to 1 |
| `light` | How directly the surface faces the light, from 0 to 1 |
| `projected` | Whether the anchor is in front of the near plane; the other readings keep their last values while it isn't |

`facing`, `facing_amount` and `light` need an anchor with a [normal](coordinates.md#normals-for-anchors). Without
one they stay at `True`, 1 and 1.

Scenes can follow two readings without any code in `__next__`: `sync=Scene.SyncMetric.LIGHT` picks the frame from
`light`, and `sync=Scene.SyncMetric.DEPTH` from `nearness`, so the near side of a turning body can show a brighter
color than the far side. For anything else, read the mount yourself. This thins out characters as they turn
edge-on:

```python
self.space.step()
for character, mount in self.space.mounts.items():
    symbol = "·" if mount.facing_amount < 0.3 else character.input_symbol
    character.animation.set_appearance_if_changed(symbol)
self.update()
```

A blend is different: it advances when the character ticks, not in `step()`. `mount.arrived` turns true and
`MOUNT_COMPLETE` fires on the tick the character reaches its anchor.

## Lifecycle

`body.cancel()` discards queued tweens and completion callbacks while keeping the current pose.
`reset()` also restores the starting pose. Both allow new tweens afterwards.

Use `space.remove(body)` to stop tracking a body: it cancels tweens, releases mounts, and hides
surface cells immediately. Repeated removal is harmless; a body from another space is rejected.
Removed bodies are no longer drawn or stepped and their anchors cannot be mounted again.
Create a new body to add geometry back. `space.clear()` removes all current bodies; it does not cancel camera
tweens. Call `space.camera.cancel()` as well when ending all scene animation.
The space retains its surface-character pool for reuse; `erase()` alone only stops surface drawing.


## Under the hood

A tween is a function of progress from 0 to 1. Each frame, `step()` advances the front of the camera's queue and each body's queue by one
frame, eases the progress, and applies the change since the last frame. That's why tweens compose: each applies a
difference, not an absolute pose. `settle` and `turn_to` use `Rotation.slerp` to find the orientation at each step.

## At cell resolution

- Easing matters more in cells than in pixels. A linear spin starts and stops with a visible jolt, and an eased one
  doesn't. The Eases example gives three cubes the same quarter turns with different easing functions:

    ```bash
    tte --input-file concept_input.txt --canvas-width 80 --canvas-height 24 --anchor-text c eases
    ```

    

    `turn_to`, `turn_to_rest`, `settle`, `move_to` and `scale_to` take the same `ease` argument. The example's source
    is `docs/effectguide/examples/3d/eases/eases.py` in a TTE checkout.
- Hold a moment at rest before releasing text, so the reader sees it land.
- After the hand-off, characters are ordinary characters. Their scenes, colors and layers are what you left them as,
  so reset anything the 3D part changed.

## Waiting for a group or a blend

`on_complete(callback)` belongs to the last individual tween. Use
`body.on_group_complete(callback)` to wait for the longest tween in the last queued parallel group.
The hook runs after mount readings refresh, so it can release mounted characters at the final pose.
Groups queued after the hook are independent; cancelling or resetting discards pending hooks.

`space.animating` tracks camera and body tweens. Mount travel is separate: use `space.blending` to wait for
active blends, or `mount.blend_progress` for the fraction of blend frames elapsed before easing.
Blends advance when characters tick, and a zero-frame blend reports 1 immediately.


## Camera shots and timing

The camera owns its own queue. A camera group and a body group begin independently; `together=True` groups actions
on the **same owner**, not across the camera and a body. Two actions on the camera that start together capture the
same starting pose before either applies a frame. In particular, a simultaneous pan does not alter the forward axis
captured by `dolly_by`.



This shot gives the body a 120-frame turn, while the camera waits, trucks for 90 frames and pans for 45 of them, then
zooms. `on_complete` belongs to the last individual tween, so the pan's callback fires while the truck is still moving.
`on_group_complete` waits for both. Both callbacks receive the camera and return `None`:

```python
--8<-- "docs/effectguide/examples/3d/cameralab/cameralab.py:sequence"
```

```bash
tte --input-file concept_input.txt --canvas-width 80 --canvas-height 32 --anchor-text c cameralab --lesson sequence
```



In each frame, `space.step()` advances camera tweens, then body tweens, refreshes mount readings, runs camera
callbacks followed by body callbacks, refreshes again after callbacks, and draws surfaces. The iterator then calls
`update()` to tick the characters. A camera completion can therefore read final mount coordinates or release text
at the final view, just like a body completion.

### Immediate calls, zero frames and deltas

`set_position`, `move_by`, `dolly`, `set_rotation` and `zoom` apply now. Pan, tilt, roll, truck, pedestal, `look_at`
and `orbit` also apply now when `frames` is omitted. Supplying `frames=0` makes them queued zero-frame actions:
they wait behind earlier groups rather than jumping ahead. Negative frame counts raise `ValueError`.

Camera tweens apply **changes** rather than overwrite the pose. For example, after a move tween has begun, an
immediate `move_by(y=2)` adds an offset that the remaining tween preserves. Local travel captures its direction at
the start; turns continue to compose with immediate turns; zoom changes multiply, so an immediate `zoom(2)` doubles
the remaining zoom trajectory. With concurrent edits, the final pose includes those edits and may differ from the
original absolute target.

### Tracking a body tween

[Immediate target movement](camera-movement.md#a-sampled-target-and-a-moving-target) is simple: move the body,
call `camera.look_at(body)`, then step the space. A body **tween** moves inside `space.step()`, after camera tweens.
Aiming before that step sees the body's previous position. To follow the current frame's tweened position, schedule
a one-frame camera hook whose callback aims after body motion:

```python
from terminaltexteffects.engine.space3d import Camera


# A method on the effect iterator:
def aim_after_motion(self, camera: Camera) -> None:
    camera.look_at(self.target)

# In __next__, while the target's animation is running:
self.space.camera.hold(1).on_complete(self.aim_after_motion)
self.space.step()   # Body motion, then the aiming callback, then refreshed projection and drawing.
self.update()
```

Keep the camera queue free for those one-frame hooks; if a shot is already queued, the hook waits behind it.
Do not call `space.step()` a second time to refresh a manual camera change: that would advance both queues twice.
For a fixed sampled aim, use one `look_at(body, frames=...)` instead of scheduling tracking hooks.

## Returning the view before release

`camera.cancel()` clears its tweens and pending callbacks but holds the current view. `camera.reset()` also restores
all camera settings captured at camera creation: position, orientation, focal length, projection, center, near plane
and row scale. Reset is immediate; it does not accept `frames`. Neither operation resets body transforms.

Text anchors built with `text_plane` use the initial camera configuration. To land text on its input cells, return
the body to the pose where those anchors were built and return the camera home too. For an eased camera return,
capture its public readouts before changing it, then tween position, orientation and focal length together:

```python
# Before changing the initial camera:
home_position = camera.position
home_rotation = camera.rotation
home_focal = camera.focal or camera.distance

# When returning, after other shot edits are finished:
camera.move_to(home_position.x, home_position.y, home_position.z, frames=90)
camera.turn_to(home_rotation, frames=90, together=True)
camera.zoom_to(home_focal, frames=90, together=True)
camera.on_group_complete(release_text)  # release_text(camera) releases the mounts.
```

If projection, row scale, optical center or near plane were also changed, restore those settings explicitly, or use
`reset()` when a cut is appropriate. The eased return above makes focal length explicit; a later `reset()` restores
the original `focal=None` relationship as well.

```bash
tte --input-file concept_input.txt --canvas-width 80 --canvas-height 32 --anchor-text c cameralab --lesson restore
```



The camera lab captures its initial view in `setup_restore`, keeps the body's pose unchanged, and releases in the
returning group's completion hook. Its complete return script is:

```python
--8<-- "docs/effectguide/examples/3d/cameralab/cameralab.py:restore"
```

---

### Effectguide 3d Camera Movement (`docs/effectguide/3d/camera-movement.md`)

# Moving the Camera

Keep the scene still and change where you see it from. This is useful for a fly-by along a row of bodies, an orbit
around a sign, or a shot that approaches a face before handing its text back to 2D.

The camera has three independent parts: an **eye position**, an **orientation**, and a **focal length**. Travel changes
the first, turning changes the second, and zoom changes the third. [Camera and projection](camera.md) explains
focal length, perspective and depth.

## Choose a control

| What you want | Immediate call | Queued call | What changes |
| --- | --- | --- | --- |
| Set a world position | `set_position(x, y, z)` | `move_to(x, y, z, frames=60)` | Eye position |
| Add a world displacement | `move_by(x, y, z)` | Use an explicit target with `move_to` | Eye position |
| Move forward/backward | `dolly(amount)` | `dolly_by(amount, frames=60)` | Eye along its local -z |
| Move right/left | `truck(amount)` | `truck(amount, frames=60)` | Eye along its local +x |
| Move up/down | `pedestal(amount)` | `pedestal(amount, frames=60)` | Eye along its local +y |
| Look left/right | `pan(degrees)` | Same call with `frames=60` | Orientation about local +y |
| Look up/down | `tilt(degrees)` | Same call with `frames=60` | Orientation about local +x |
| Turn the image | `roll(degrees)` | Same call with `frames=60` | Orientation about local +z |
| Set a world orientation | `set_rotation(rotation)` | `turn_to(rotation, frames=60)` | Orientation |
| Aim at a point or body | `look_at(x, y, z)` or `look_at(body)` | Same call with `frames=60` | Orientation |
| Circle a target, staying aimed at it | `orbit("y", degrees=30, around=body)` | `orbit("y", turns=1, around=body, frames=240)` | Position and orientation |
| Magnify from the same position | `zoom(factor)` | `zoom_to(focal, frames=60)` | Focal length |

Angles here are degrees; `Rotation` itself uses radians, or use the degree-based `rotation_about(axis, degrees)`
helper. Methods return the camera for chaining. A positive travel amount means forward, right or up; negative
amounts reverse it. `set_position` and `move_to` take absolute world coordinates and default omitted coordinates
to zero. `move_by` takes displacements and leaves omitted components unchanged.

## Travel in camera axes

At the default orientation, `dolly(10)` moves along world -z, `truck(10)` along world +x and `pedestal(10)` along
world +y. Once the camera turns, its axes turn with it. The world axes do not.



```python
from terminaltexteffects.engine.space3d import rotation_about

camera = self.space.camera
camera.set_rotation(rotation_about("y", 35))
camera.truck(10)       # Along the turned camera's right axis.
camera.move_by(x=10)   # Along fixed world +x instead.
```

Travel does not automatically re-aim the camera. Dolly travels along the current forward direction; moving to a
world position leaves orientation alone. To travel while watching a target, choose an orbit or explicitly aim
again. A queued travel action captures its local direction when it starts, so a simultaneous pan does not bend a
dolly path. [Camera timing](animating.md#camera-shots-and-timing) covers that shared starting pose.

### Seeing travel

The camera lab keeps two cubes and three small landmarks fixed. Cyan **F** is the foreground cube; purple **B** is
the background cube. Each phase starts from the same view, moves one way, and pauses. The readout shows the eye's
world coordinates while the objects stay put:

```bash
tte --input-file concept_input.txt --canvas-width 80 --canvas-height 32 --anchor-text c cameralab --lesson travel
```



```python
--8<-- "docs/effectguide/examples/3d/cameralab/cameralab.py:travel"
```

## Turn the view without moving the eye

Pan, tilt and roll rotate about the camera's **current local axes**. A positive pan looks left; a positive tilt looks
up. A positive roll rotates the camera counterclockwise about its backward (+z) axis, so the image turns clockwise.
Watch the eye coordinates stay constant:

```bash
tte --input-file concept_input.txt --canvas-width 80 --canvas-height 32 --anchor-text c cameralab --lesson turns
```



```python
--8<-- "docs/effectguide/examples/3d/cameralab/cameralab.py:turns"
```

A body rotation and a camera rotation have different meanings. Rotating a body changes that body and its anchors.
Turning the camera changes how **every** body is seen. Body-local face names such as a cube's "front" remain attached
to the cube; they do not switch to whichever face is nearest the camera.

`set_rotation(rotation)` sets a world orientation immediately. `turn_to(rotation, frames=...)` takes the shortest
quaternion path to that orientation. Use `pan`, `tilt` and `roll` to describe a turn relative to the current camera;
use `turn_to` for a known final world orientation. Neither moves the eye.

## Aim at a target

`look_at` computes an orientation from the current eye toward a world point. It also chooses a roll using a world-up
direction, +y by default:

```python
camera.look_at(0, 0, 0)                    # Point at the world origin now.
camera.look_at(cube)                        # Point at this body's origin now.
camera.look_at(cube, up=(1, 0, 0))           # Prefer world +x as up.
camera.look_at(cube, frames=60, ease=easing.in_out_sine)  # Turn there over time.
```



`up` is a direction, not another target position. The camera uses its component perpendicular to the view.
Changing it changes the roll around the viewing direction, much
like rolling an aimed camera. An up direction parallel to the view cannot determine that roll; the camera uses
its previous right axis as a fallback. Choose an up direction away from parallel when you need a predictable roll.
A target at the eye and a zero up vector raise `ValueError`.

### A sampled target and a moving target

A queued `look_at(body, frames=...)` samples the body's position **when the tween starts**, after any earlier camera
groups. It then turns toward that sampled point. It does not keep watching the body as it moves. To follow a moving
body, move it and call immediate `look_at(body)` each frame before `space.step()`.

=== "Sample once"

    ```bash
    tte --input-file concept_input.txt --canvas-width 80 --canvas-height 32 --anchor-text c cameralab --lesson sampled
    ```

    

=== "Follow each frame"

    ```bash
    tte --input-file concept_input.txt --canvas-width 80 --canvas-height 32 --anchor-text c cameralab --lesson follow
    ```

    

The two recordings use the same target motion. The following code moves the body **directly**, then aims from its
current position. The normal frame loop steps the space once and ticks the characters afterward:

```python
--8<-- "docs/effectguide/examples/3d/cameralab/cameralab.py:follow"
```

If the target moves through body tweens, those advance inside `space.step()` instead. Use the
[completion-phase tracking pattern](animating.md#tracking-a-body-tween) to aim after that motion without stepping twice.

## Orbit a stationary scene

Orbit changes position and orientation together. A world axis passes through the target, the eye turns around that
axis, and the camera stays aimed at the target. The radius must be nonzero. `around` accepts a body, a `Vec3`, or an
`(x, y, z)` world point. A body target is sampled when the action starts, just like `look_at`.



```python
camera.set_position(0, 20, 170).look_at(cube).zoom(1.25)
camera.orbit("y", turns=1, around=cube, frames=360, ease=easing.in_out_sine)
```

Supply either `degrees` or `turns`, not both. With no angle supplied, orbit defaults to one full turn. With no
`frames` supplied, it applies immediately. With `frames=0`, it obeys queue ordering: it applies now only if nothing
is queued ahead. Its `up` direction controls aiming roll as for `look_at`.

```bash
tte --input-file concept_input.txt --canvas-width 80 --canvas-height 32 --anchor-text c cameraorbit
```



The cube and gold landmarks stay fixed in world space. Their changing overlap and relative position show that the
viewpoint is moving. This differs from the [body-orbit example](transforms.md), where the moon itself travels around
a fixed planet. Moving the camera affects the whole scene, including mount depth and facing, occlusion and clipping.

??? example "Standalone orbit example"

    Save this whole module as `cameraorbit.py` in your community effects directory.

    ```python
    --8<-- "docs/effectguide/examples/3d/cameraorbit/cameraorbit.py"
    ```

## Run the camera lab

All twelve camera lab lessons come from one standalone module,
[`cameralab.py`](https://github.com/ChrisBuilds/terminaltexteffects/blob/main/docs/effectguide/examples/3d/cameralab/cameralab.py).
Save the **whole file** as `cameralab.py` in your community effects directory. The excerpts in the guide are parts of
that iterator, not separate effects. These examples need the development version described in
[3D Effects](index.md), and use an 80-column, 32-row canvas so captions and shapes have room.

| Lesson | Chapter |
| --- | --- |
| `travel`, `turns`, `sampled`, `follow` | This page |
| `dolly`, `zoom`, `orthographic` | [Camera and projection](camera.md) |
| `world-light`, `camera-light` | [Light and shading](light.md#world-light-or-camera-light) |
| `sequence`, `restore` | [Animating in 3D](animating.md#camera-shots-and-timing) |
| `clip` | [Depth and hidden surfaces](depth.md#moving-the-near-plane) |

The captions are ordinary 2D characters at a high layer. They remain readable while the camera moves and do not
participate in 3D depth ordering. Each lesson ends by hiding its scene and restoring the original input text.

---

### Effectguide 3d Camera (`docs/effectguide/3d/camera.md`)

# Camera and Projection

A camera answers two questions: **where are we looking from?** and **how does that view become terminal cells?**
Its position and orientation answer the first. Projection and focal length answer the second. Start here for
framing and perspective, then read [Moving the camera](camera-movement.md) for travel, turning, aiming and orbiting.

## The default view

A new `Space3D` puts its eye at `(0, 0, 120)`, looking along world **-z**, with **+y** up. The canvas center is the
center of the image. This is the view used by the earlier chapters; moving the camera changes the view while the
world axes and body positions stay fixed.



- **Perspective** makes farther things smaller. A point's projected size is multiplied by `focal / depth`, where
  depth is distance along the camera's forward axis. Initially focal length is 120 too, so the world z=0 plane
  keeps its size and the input text meets its original cells.
- **Orthographic** projection skips that multiplication. Depth still exists for clipping and occlusion; it just
  does not change apparent size. The eye has a finite position and a near plane even though its rays are parallel.

Once the camera moves or turns, the world z=0 plane is no longer necessarily face-on, centered or at its initial
size. [Camera coordinates and depth](#camera-coordinates-and-depth) explains the extra transform.

## Setting up perspective

`Space3D(terminal, perspective=...)` chooses the initial projection:

| Setting | Initial eye z | Focal length when `focal=None` | Result |
| --- | --- | --- | --- |
| `0` | 120 | Ignored for size | Orthographic, with no depth magnification |
| `1` | 120 | 120 | Default perspective |
| `2` | 60 | 60 | Closer eye and stronger differences between near and far objects |



The Lens example keeps the bodies still and changes only that initial setup:

```python
self.space = Space3D(self.terminal, perspective=self.config.perspective)
for x, z in ((-26, 0), (0, -60), (26, -120)):
    self.space.body(Cube(14), at=(x, 0, z)).draw(Shaded())
```

??? example "lens.py"

    Save this module in your community effects directory as `lens.py`.

    ```python
    --8<-- "docs/effectguide/examples/3d/lens/lens.py"
    ```

=== "Orthographic"

    ```bash
    tte --input-file concept_input.txt --canvas-width 80 --canvas-height 24 --anchor-text c lens --perspective 0
    ```

    

=== "Default perspective"

    ```bash
    tte --input-file concept_input.txt --canvas-width 80 --canvas-height 24 --anchor-text c lens
    ```

    

=== "Closer initial camera"

    ```bash
    tte --input-file concept_input.txt --canvas-width 80 --canvas-height 24 --anchor-text c lens --perspective 2
    ```

    

`perspective` is a constructor setting. It establishes both eye distance and the default focal length. The controls
below let an effect change position and focal length independently afterward.

## Moving the camera

[Moving the camera](camera-movement.md) teaches eye position, camera-local travel, pan/tilt/roll, aiming and orbiting.
[Camera shots and timing](animating.md#camera-shots-and-timing) combines those controls into queued shots. Start with
the comparison below when deciding between changing position and changing focal length.

## Dolly or zoom?

A **dolly** moves the eye forward or backward. The foreground grows faster than the background as you approach,
and their relative positions can change: that is **parallax**. A **zoom** changes focal length while the eye stays
still. It magnifies the whole perspective image, keeping its perspective proportions.



The camera lab starts with eye z=120 and focal=120. Cyan **F** has its front face at world z=0; purple **B** has its
front face at z=-60. Both are the same physical size. These two moves give F the same final framing:

| Move | Final eye z | Final focal | F's front-face scale | B's front-face scale |
| --- | --- | --- | --- | --- |
| `dolly_by(30)` | 90 | 120 | 120 / 90 = 1.333 | 120 / 150 = 0.800 |
| `zoom_to(160)` | 120 | 160 | 160 / 120 = 1.333 | 160 / 180 = 0.889 |

=== "Move the eye"

    ```python
    camera.dolly_by(30, frames=120, ease=easing.in_out_sine)
    ```

    ```bash
    tte --input-file concept_input.txt --canvas-width 80 --canvas-height 32 --anchor-text c cameralab --lesson dolly
    ```

    

=== "Change focal length"

    ```python
    camera.zoom_to(160, frames=120, ease=easing.in_out_sine)
    ```

    ```bash
    tte --input-file concept_input.txt --canvas-width 80 --canvas-height 32 --anchor-text c cameralab --lesson zoom
    ```

    

`dolly(amount)` and `zoom(factor)` are immediate. `dolly_by(amount, frames=...)` is relative travel;
`zoom_to(focal, frames=...)` targets an **absolute** positive focal length. For example, `zoom(2)` doubles the current
focal length, while `zoom_to(2)` targets a focal length of two columns. Choose dolly to travel through a scene and
zoom to reframe the image from the same viewpoint.

### Orthographic camera movement

Orthographic travel and turns still work. Truck and pedestal move the image; pan, tilt and roll change which way
you see the scene. Dolly changes depth and can cross the near plane, but does not magnify. Focal length is ignored,
so zoom does not magnify either. Watch the readout change during the first two phases of this recording:

```bash
tte --input-file concept_input.txt --canvas-width 80 --canvas-height 32 --anchor-text c cameralab --lesson orthographic
```



The standalone camera lab and its lesson snippets are introduced in [Moving the camera](camera-movement.md#run-the-camera-lab).

## Camera coordinates and depth

Body coordinates describe points on a body. A body's transform places them in **world space**. The camera then
subtracts its eye position and undoes its orientation to express those world points in **view space**. In that
space, forward is always -z, right is +x and up is +y. Projection turns view x/y into canvas columns/rows.



For a world point `point`, the geometry is equivalent to:

```python
view = camera.rotation.inverse().apply(point - camera.position)
depth = -view.z
```

`camera.depth(point)`, `Projected.depth` and `Mount.depth` all use this view-axis depth. It is **not Euclidean range**
to the eye. With the default orientation and position it equals `camera.distance - point.z`; after camera movement,
use `depth(point)` instead of assuming that formula still describes the view.

Projection returns `None` before the near plane, including behind the eye:

```python
projected = self.space.camera.project(point)
if projected is not None:
    cell = projected.coord
    depth = projected.depth
```

The near plane is perpendicular to the current forward axis and moves with the eye. Faces and wireframe edges
crossing it retain their visible portions. [Depth and hidden surfaces](depth.md#moving-the-near-plane) shows this
with a stationary body and a moving side-view camera.

## Cells, rays and the text layout

`camera.cell_to_world(coord, z=...)` intersects the **active** viewing ray with a plane of constant **world z**.
For example, at the default view, `z=0` lifts a cell onto the input-text plane. A moved or rolled camera uses a
different ray, so the resulting world x/y may change. If that ray is parallel to the plane, there is no unique
intersection and the method raises `ValueError`.

For an arbitrary camera view, a ray is often a better building block:

```python
origin, direction = camera.ray(coord)
point_at_depth_30 = origin + direction * 30
```

Perspective rays begin at the eye; orthographic rays begin at the cell's position on the eye plane. The direction
is scaled so advancing by 30 gives a point at **camera depth 30**. It is not normalized to Euclidean length.

`body.text_plane(characters)` has a different job: it lays out text using the **initial camera configuration**,
undoing the body's pose at the time of the call. Moving or zooming the active camera does not rebuild those anchors.
To return text to its input cells, restore the body's construction pose **and** the initial camera view before
release. [Returning the view](animating.md#returning-the-view-before-release) demonstrates it.

## Settings and troubleshooting

| Symptom or task | Check |
| --- | --- |
| Everything disappears after moving the eye | Is the camera aimed at the scene? Movement alone does not turn it. Use `look_at` and check view-axis depth against `near`. |
| Objects enlarge when you wanted a flat view | Use `Space3D(..., perspective=0)`; a long focal length alone is still perspective. |
| Text doubles or loses letters as the camera approaches | Projected characters are colliding or the textured surface is magnified. Use gentler framing or more canvas space. |
| Zoom appears to do nothing | Is the view orthographic? Its size ignores focal length. |
| Input text does not land home | Restore both camera and body; resetting only one does not restore the other. |
| You need a new optical center | `center_column` and `center_row` are canvas coordinates for the image center, not world positions. |



`distance` remains the legacy setup value: the eye follows it until an explicit `set_position` or travel call.
Afterward, changing it does not move the eye. With `focal=None`, it still supplies the focal length. `zoom` makes
focal length explicit. `camera.reset()` restores the initial configuration, including the implicit position/focal
relationship. `position` and `rotation` are read-only properties; change them through the control methods.

Construction checks positive `distance`, `focal` when set, `near` and `row_scale`, and requires `near < distance` for
the initial view. Later field assignments do not revalidate. Keep projection values positive; a moved camera clips
using its current view depth rather than its distance from the world origin. `row_scale` remains the terminal's
tall-cell correction; see [Tall cells](tall-cells.md).

---

### Effectguide 3d Coordinates (`docs/effectguide/3d/coordinates.md`)

# Space and Coordinates

Before you can place anything in 3D, you need to know where the axes point and what the units are. TTE uses one
convention everywhere, chosen so that the input text sits exactly where it does in 2D.

## The idea

- The **origin** is the center of the canvas.
- **x** points right, **y** points up and **z** points toward the **default** viewer. These world axes stay fixed when the camera turns.
- One **unit** is the width of one terminal column. Positions, sizes and distances are all in columns, including
  heights: a shape 10 units tall is 10 columns tall, which is about 5 rows (see [Tall cells](tall-cells.md)).
- **Angles** are in degrees in `Space3D`, and **axes** can be named `"x"`, `"y"`, `"z"` or `"diagonal"`, or given
  as any `(x, y, z)` direction.



In the default view, you can think of the canvas as a window in the world z=0 plane, with the camera on +z
looking straight through it. Negative z is farther away in that view. After the camera moves, distance and facing
follow its view direction instead. [Camera coordinates](camera.md#camera-coordinates-and-depth) distinguishes
world axes from the camera's axes.

The input text lies flat on the plane where z is 0. With the camera at rest, every point on that plane lands back on
the cell it came from, so text mounted there, on a body that hasn't moved, looks exactly like the input.

## Position input forms

Public position and displacement arguments accept a `geometry3d.Vec3` or an `(x, y, z)`
three-tuple. The components are ordered right, up, and toward the default viewer, measured in
terminal columns. World axes stay fixed when the camera turns; body-local points turn with their body.

```python
source = space.body(Cube(10), at=(10, 0, -20))
copy = space.body(Cube(10), at=source.position)
copy.set_position((15, 0, -20))
copy.move_by((0, 5, 0))  # Relative world displacement, not an absolute target.
space.camera.move_to(source.position, frames=60)
anchor = copy.anchor((1, 2, 3))  # Body-local x, y, z, in columns.
```

Existing scalar calls such as `copy.set_position(x=15, z=-20)` still work. Omitted scalar
coordinates default to zero, except `anchor`, which requires all three scalars. When passing a
complete point, omit `y` and `z`: mixing forms raises `TypeError`, even if the extra coordinate
is zero. For queued motion with a point, use keyword arguments for `frames` and `ease`; the
existing scalar positional argument slots have not moved.

Two-tuples elsewhere have their own meaning: `random_position` takes `(minimum, maximum)`
world-coordinate ranges, and a texture offset is `(column_offset, row_offset)` in text cells.
They are not abbreviated 3D points. Argument docstrings specify their component order and units.

## In TTE

The Axes example draws the three axes as lines of dots, mounts the input text on a plane, and turns them all
together to an angled view and back. These are the lines that place the text:

```python
self.text = self.space.body()  # No shape: a plain frame of reference.
characters = [c for c in self.terminal.get_characters() if c.input_symbol != " "]
self.text.add_chars(characters, self.text.text_plane(characters))
```

??? example "axes.py"

    Save it in your community effects directory as `axes.py`.

    ```python
    --8<-- "docs/effectguide/examples/3d/axes/axes.py"
    ```

```bash
tte --input-file concept_input.txt --canvas-width 80 --canvas-height 24 --anchor-text c axes
```



At rest, the Z axis points straight at you, so all its dots land on the center cell. Once the view turns, you can
see it come out of the screen.

- `self.space.body()` with no shape is a plain **frame of reference**: a position, orientation and scale to hang
  anchors on.
- `body.anchor(x, y, z)` is a point in the body's own coordinates. It moves and turns with the body.
- `text_plane(characters)` makes one anchor per character, placed so each one meets its character's input cell
  **at the body's pose when you call it, under the space's initial camera view**. It works backward from the body's current rotation, position and scale.
  The Axes example builds it on a body that hasn't moved yet, so the text meets its input cells whenever the body is
  back at its starting pose.
- `add_chars(characters, anchors)` mounts each character on its anchor. From then on, the anchor decides where the
  character is drawn.

### Rest and pose

A body's **pose** is its whole transform: rotation, position and scale. **Rest** is only the rotation part: the
orientation a body is created in, with no turn applied. `turn_to_rest()` and `settle()` bring the rotation back to
rest, but leave position and scale alone.

So text built with `text_plane` lands on its input cells when the body returns to the pose it had when the anchors
were made. Build the anchors first, on an unmoved body, then apply the starting transforms: the text is then home
whenever the body is back at rest, at the origin and at scale 1. Build them on a body already turned 30 degrees, and
the text reads correctly at that turn instead, and moves off its input cells at rest. These landing guarantees also
require the camera's initial view. Camera movement and zoom do not change the anchors; restore the view before
returning text to its input cells.

## Normals for anchors

`body.anchor(x, y, z, normal="z")` gives the anchor a **normal**: the direction its surface faces, in the body's own
coordinates. Any axis name or `(x, y, z)` direction works. The normal is separate from the anchor's position and
turns with the body. It's what lets a mount report how it faces the camera and the light:

```python
front = card.anchor(0, 0, 0, normal="z")  # Faces the viewer while the card is at rest.
plain = card.anchor(0, 0, 0)  # No normal.
```

- With a normal, `mount.facing`, `mount.facing_amount` and `mount.light` follow the turning surface, and
  `hide_when_facing_away=True` can hide the character while its side faces away (see [Depth](depth.md)).
- Without one, the mount always reports `facing=True`, `facing_amount=1` and `light=1`, however the body turns.
  Lighting scenes and backface hiding then have nothing to follow.

`text_plane` gives every anchor the normal of its plane, so a card of text lights and hides like a card. The layouts
below give normals pointing out of the shape. Anchors you make yourself have none unless you pass one.

## Anchor layouts

A body can lay out anchors for you. Each layout returns a list of anchors to pass to `add_chars` with the same number
of characters:

| Layout | Anchors | Normals | Choose it for |
| --- | --- | --- | --- |
| `text_plane(characters)` | One per character, at the current body pose under the initial camera view | The plane's | Text that must land back on its input cells |
| `surface(count)` | About `count`, spread evenly over the shape | Out of each face | Covering a shape with characters in no particular order |
| `vertices()` | One per corner | Away from the center | Marking a shape's corners |
| `edges(count)` | About `count`, spread along the edges | Away from the center | Outlining a shape with characters |
| `ring(count, radius, axis="y")` | `count`, evenly around a circle | Outward | Orbits, halos and carousels; needs no shape |
| `wrap(characters)` | One per character, the text wrapped once around | Out of the surface | Text that reads around a `Cylinder` or `Sphere`, row by row |

`surface`, `vertices` and `edges` need a body with a shape, and `wrap` needs a `Cylinder` or `Sphere`. A shaped body
carries anchors whether or not it is drawn. This mounts the input text around an undrawn cylinder, so only the
letters show:

```python
from terminaltexteffects.engine.space3d import Cylinder

characters = [c for c in self.terminal.get_characters() if c.input_symbol != " "]
drum = self.space.body(Cylinder(radius=16, height=12))
drum.add_chars(characters, drum.wrap(characters), hide_when_facing_away=True)
drum.spin("y", turns=1, frames=240)
```

`surface(len(characters))` would scatter the same characters over the drum instead, with no reading order. `wrap`
mounts each character on its own anchor, so it keeps its colors and scenes and can be released afterward. A
`Textured` look, by contrast, draws the shape's surface out of text, resampled cell by cell (see
[Texture mapping](textures.md#mounted-text-or-textured-surface)). For the full signatures, see the
[`Body` reference](../../engine/space3d.md).

## Under the hood

A point is a [`Vec3`](../../engine/utils/geometry3d.md). `text_plane` lifts each input cell into 3D with
the initial camera's cell-to-world mapping. The active camera's `cell_to_world` instead intersects its current
viewing ray with the requested world-z plane. Under the default view, that is the familiar exact round trip:

```python
from terminaltexteffects.utils.geometry3d import Vec3

point = self.space.camera.cell_to_world(character.input_coord)  # Vec3(x, y, 0.0)
projected = self.space.camera.project(point)
if projected is not None:
    cell = projected.coord  # Back to character.input_coord in the default view.
```

`body.world_point(local)` turns a point in the body's coordinates into world coordinates, using the body's current
position, orientation and scale. `text_plane` runs the other way: it takes each lifted point and undoes the body's
current transform to find the local anchor, which is why the result depends on the body pose at the time of the call. The active camera's
`cell_to_world` may raise `ValueError` when the viewing ray is parallel to the world-z plane; `text_plane` does not
use that moved view. See [Cells, rays and the text layout](camera.md#cells-rays-and-the-text-layout).

## At cell resolution

- World y is measured in columns, but the canvas counts rows. A row is about two columns tall, so moving a body up
  by 10 moves it up about 5 rows in the default orthographic view. Perspective and a turned camera change that
  screen displacement.
- Anchors snap to the nearest cell. Two anchors less than a cell apart land in the same cell, and only one of their
  characters shows there.
- The canvas center can fall between two cells when the canvas has an even width or height, so a shape at the
  origin can look half a cell off center.

---

### Effectguide 3d Depth (`docs/effectguide/3d/depth.md`)

# Depth and Hidden Surfaces

In a 3D scene, nearer things hide farther ones. The space has to decide, cell by cell, which surface wins. It uses two
tools: a **depth buffer** for surfaces, and **layers** for characters.

## The idea

**Depth** is a point's distance along the camera's current viewing axis, not its Euclidean range to the eye.
Smaller positive depth is nearer. Moving or turning the camera changes that measurement even when bodies stay still.

A **depth buffer** keeps, for every cell, the nearest surface found there so far. Each face is filled cell by cell,
and a cell only takes the new face if it's nearer than what the cell already holds. Draw every shape into the same
buffer and overlaps sort themselves out, even where two shapes pass through each other.



**Back-face culling** skips work before the buffer sees it. Each face of a closed shape has an outward **normal**, an
arrow pointing straight out of it. A face whose normal points away from the camera is on the far side of the shape,
hidden behind the near side, so it can be skipped without testing a single cell.



Mounted characters aren't drawn into the buffer. Each one is a single character on one cell, so the space orders them
with **layers** instead: with `layer_by_depth` on, a mount adds a depth-derived offset to the character's own layer,
so among characters that started on the same layer, the nearer one draws on top. The full ordering is under
[Under the hood](#under-the-hood).

## In TTE

The Depth example passes a sphere through a cube, or shows a cube's far side through its text. Two drawn bodies are
all it takes; the space sorts out which hides which:

```python
cube = self.space.body(Cube(20)).draw(Shaded())
sphere = self.space.body(Sphere(9), at=(-34, 0, 6)).draw(Shaded())
sphere.move_to(34, 0, -6, frames=300, ease=easing.in_out_sine)  # Through the cube and out the far side.
```

??? example "depth.py"

    Save it in your community effects directory as `depth.py`.

    ```python
    --8<-- "docs/effectguide/examples/3d/depth/depth.py"
    ```

=== "Overlap"

    ```bash
    tte --input-file concept_input.txt --canvas-width 80 --canvas-height 24 --anchor-text c depth
    ```

    

=== "See-through"

    ```bash
    tte --input-file concept_input.txt --canvas-width 80 --canvas-height 24 --anchor-text c depth --see-through
    ```

    

- Every drawn body in a space shares one depth buffer, so bodies hide each other correctly with no extra work.
- `Textured(back="dim")` draws a second pass of the faces turned away, in `back_color`, and lets them show through the
  spaces in the text. It's the one place the space draws back faces on purpose.

The Card example mounts the input text on a plane and turns it once, which shows what happens to characters as they
turn edge-on. Its one 3D option is how the characters are mounted:

```python
self.card.add_chars(characters, self.card.text_plane(characters), hide_when_facing_away=self.config.hide_back)
```

??? example "card.py"

    Save it in your community effects directory as `card.py`.

    ```python
    --8<-- "docs/effectguide/examples/3d/card/card.py"
    ```

=== "Both sides"

    ```bash
    tte --input-file concept_input.txt --canvas-width 80 --canvas-height 24 --anchor-text c card
    ```

    

=== "Front only"

    ```bash
    tte --input-file concept_input.txt --canvas-width 80 --canvas-height 24 --anchor-text c card --hide-back
    ```

    

As the card turns edge-on, its columns crowd into fewer cells and only one character can show in each. Past edge-on
you see the back of the card, so the text reads mirrored. `hide_when_facing_away=True` hides each character while its
side faces away, as a solid card would. "Its side" is its anchor's [normal](coordinates.md#normals-for-anchors),
which `text_plane` provides; an anchor without a normal always counts as facing you, so it's never hidden.

## Hidden wireframe lines

By default, `Wireframe` calls an edge front-facing when an adjacent face points toward the
camera. This is a fast approximation for convex solids; concave and custom meshes can have
front-facing edges behind another part of the mesh. Use
`Wireframe(depth_test=True, hidden_color=None)` for hidden-line removal against the same mesh's
filled depth buffer. A hidden color instead draws those occluded lines in that color.
The low-level `geometry3d.wireframe` accepts the same `depth_test=True` option. It adds a fill
pass within the viewport and tests depth at terminal cell centers, so cell rounding can affect silhouettes.


## Moving the near plane

In the default view, clipping is easy to picture as a body approaching a fixed camera:



The camera can move instead, with the same rule. The near plane belongs to the active view. It stays `camera.near` columns **forward from the eye**, perpendicular to
the viewing direction. A side-view camera looking along world -x clips along that direction, rather than along world
z. There is no separate rule for each body: projection, face/edge clipping, mount visibility and depth ordering all
follow the same camera.



The Clip lesson deliberately uses orthographic projection so approach does not enlarge the shape. The stationary
cube is cut by the moving plane; its mounted A is hidden once its anchor is before the plane. This isolates **near
clipping** from **viewport cropping**, where an otherwise visible projection falls outside the terminal canvas.

```python
--8<-- "docs/effectguide/examples/3d/cameralab/cameralab.py:clip"
```

```bash
tte --input-file concept_input.txt --canvas-width 80 --canvas-height 32 --anchor-text c cameralab --lesson clip
```



A camera orbit also changes which faces point toward the viewer and which surfaces hide the mounted text. Compare
the [stationary-cube orbit](camera-movement.md#orbit-a-stationary-scene) with the moving-body examples above.
`Mount.facing` and `facing_amount` need a normal; they follow the active view. `Mount.nearness` measures depth relative
to the body's center and extent, so pure camera translation preserves it, while camera rotation can change it.

## Under the hood

[`geometry3d.rasterize`](../../engine/utils/geometry3d.md) fills a mesh into a buffer and returns it:

```python
from terminaltexteffects.utils import geometry3d as g3

buffer: dict = {}
g3.rasterize(cube_mesh, camera, rotation=turn, buffer=buffer)
g3.rasterize(sphere_mesh, camera, position=g3.Vec3(10, 0, 0), buffer=buffer)
for cell, fragment in buffer.items():
    ...  # fragment.depth, fragment.normal, fragment.facing, fragment.tag, fragment.uv
```

- `cull="back"`, the default, skips faces turned away. `"front"` keeps only those, and `"none"` keeps both.
- Each `Fragment` holds the nearest surface's depth, normal, face tag and texture coordinates.

Where several characters share a cell, the terminal draws the one that sorts last by three keys, in order:

1. **Layer**, the character's `layer`. Higher draws on top.
2. **Depth order**, a fraction from -0.5 to 0.5 that only 3D sets. Characters that never set it stay at 0.
3. **Character ID**, which follows creation order. The most recently created character draws on top.

A mount with `layer_by_depth` splits the character's nearness to the camera, `camera.distance - depth`, into a whole
number of columns and the rest. It adds the whole part to the layer the character had when it was mounted, uses the
rest as its depth order, and restores both on release. So between two characters mounted from the **same base
layer**, the nearer one draws on top, however small the difference. A different base layer shifts that comparison by
the difference in layers, in columns of depth. In the default camera view, a character on layer 10 at z = -2 sorts as `(8, 0)` and still draws
over a nearer one on layer 0 at z = 1, which sorts as `(1, 0)`, but it would lose to one more than 10 columns nearer.
Give characters the same base layer when depth should decide. When a character must always draw on top, turn
`layer_by_depth` off for it and give it a layer above every mounted character's range.

Surface cells are ordered the same way from layer 0, a hair farther than their depth, so text mounted on a drawn
body from base layer 0 or above reads on top of the face it sits on.

## At cell resolution

- Text edge-on loses characters: a plane turned 60 degrees shows only half of its columns. Keep text near facing the
  viewer when it must be read, and move through edge-on quickly.
- Where two characters land in the same cell, the higher layer wins. With `layer_by_depth` off, layers stay put and
  depth order stays 0, so among characters on the same layer the last-created one wins, which looks random on a
  turning body.
- Only cell centers are tested, so a part of a shape thinner than a cell, such as a narrow rim seen edge-on, can
  fall between centers and vanish for a frame.

## Mounted anchors at the near plane

`mount.projected` reports whether the anchor can be projected at its current pose. While the
anchor is closer than `camera.near` (including behind the camera), its character is automatically
hidden on its tick. The mount retains its last valid coordinate, depth, facing, and light readings;
check `projected` before using them. It restores visibility when the anchor returns, unless
backface hiding still applies. A character already hidden by the effect stays hidden. Releasing
a mount restores visibility only if the mount hid it.

---

### Effectguide 3d Light (`docs/effectguide/3d/light.md`)

# Light and Shading

Shading is what makes a filled shape look solid. Without it, a cube is a flat blob of one symbol. With it, each face
gets a symbol and color from how directly it faces the light, and the shape's form reads at once.

## The idea

The space has one **directional** light: a single direction pointing toward it, the same everywhere in the scene,
like sunlight. It has no position, so nothing is nearer to it or farther from it. For each face, the space compares
two arrows: the face's normal and that direction.

- Facing the light squarely, the face is fully lit.
- Turned edge-on to the light, it's unlit.
- In between, brightness falls off smoothly with the angle. The measure is called **Lambert** shading: the cosine of
  the angle between the two arrows, from 1 (facing the light) to 0 (edge-on or facing away).

That number picks a symbol from a ramp and a color from a gradient, both ordered from unlit to fully lit. Unlit faces
still get the first symbol and color, so the shape's dark side stays visible.

Because only the angle counts, there's no falloff with distance and no cast shadows: a face turned toward the light
is lit even if another shape stands between them. Each face is shaded on its own.



## In TTE

The Light example moves the light in a circle around a slowly turning shape. A yellow `☼` and a dotted ray mark
the light's direction, and the shape hides them while the light is behind. The light is just a direction, changed
each frame:

```python
angle = 2 * math.pi * self.frame_number / self.frames
self.space.light = Vec3(math.sin(angle), 0.5, math.cos(angle)).normalized()
```

??? example "light.py"

    Save it in your community effects directory as `light.py`.

    ```python
    --8<-- "docs/effectguide/examples/3d/light/light.py"
    ```

=== "Sphere"

    ```bash
    tte --input-file concept_input.txt --canvas-width 80 --canvas-height 24 --anchor-text c light
    ```

    

=== "Torus with blocks"

    ```bash
    tte --input-file concept_input.txt --canvas-width 80 --canvas-height 24 --anchor-text c light --shape torus --symbols "░▒▓█"
    ```

    

- `Space3D(terminal, light="upper left")` sets the light at creation: `"upper left"`, `"upper right"`, `"front"`,
  `"above"`, `"left"`, `"right"`, or any `(x, y, z)` direction. The default, upper left, gives most shapes a lit
  side, a shadowed side and a top.
- `space.light` is a unit vector you can change each frame, as the example does. The example's sun is only a marker
  placed along that direction so you can see it. Moving the marker wouldn't move the light, and the light doesn't
  shine from the marker's position.
- `Shaded(gradient, symbols)` fills faces from the ramp. The default symbols `.,-~:;=!*#$@` get denser as they get
  brighter. Block symbols such as `░▒▓█` read as solid and suit large shapes.
- `Solid(symbol, gradient)` uses one symbol and lets color alone show the light. It and `Textured` add a little
  **ambient** light, coloring by `0.15 + 0.85 x lambert`, so their unlit faces aren't the darkest color.
- Gradients default to the TTE brand colors, purple to cyan to white.

Mounted characters can follow the light too. Each mount reports `light`, the Lambert value for its anchor's normal,
and a scene created with `sync=Scene.SyncMetric.LIGHT` picks its frame from it. The [logo recipe](recipes.md#spinning-logo)
uses that to brighten text as it turns toward the light. Only anchors with a
[normal](coordinates.md#normals-for-anchors) have a direction to compare: `text_plane` and the shape layouts give
their anchors one, and an anchor made without one always reports `light=1`.

## World light or camera light?

The light direction can live in either of two coordinate spaces:

| Setting | Meaning of `light="front"` | When the camera turns |
| --- | --- | --- |
| `light_space="world"` (default) | Fixed world +z | The same world faces remain lit |
| `light_space="camera"` | Camera-local +z, back toward the eye | The light rotates with the view, like a headlight |



These recordings use the same stationary cube and camera orbit. Only `light_space` changes. The light remains
**directional** in both: camera translation does not cause falloff or turn it into a point light at the eye.

=== "World light"

    ```python
    self.space = Space3D(self.terminal, light="front", light_space="world")
    ```

    ```bash
    tte --input-file concept_input.txt --canvas-width 80 --canvas-height 32 --anchor-text c cameralab --lesson world-light
    ```

    

=== "Camera light"

    ```python
    self.space = Space3D(self.terminal, light="front", light_space="camera")
    ```

    ```bash
    tte --input-file concept_input.txt --canvas-width 80 --canvas-height 32 --anchor-text c cameralab --lesson camera-light
    ```

    

`space.light` stores the configured direction in the selected coordinate space. Surface shading and mount `light`
readings use the same mode. Body normals remain in world space after the body transform, so a manual Lambert
calculation must convert a camera-relative light into world coordinates first.

## Under the hood

[`geometry3d.lambert(normal, light)`](../../engine/utils/geometry3d.md) returns the Lambert value. With a face's
normal from the depth buffer, choosing a symbol is one line:

```python
from terminaltexteffects.utils import geometry3d as g3

world_light = self.space.light
if self.space.light_space == "camera":
    world_light = self.space.camera.rotation.apply(world_light)
shade = g3.lambert(fragment.normal, world_light)
symbol = ramp[min(len(ramp) - 1, int(shade * len(ramp)))]
color = gradient.get_color_at_fraction(shade)
```

Each face is flat, so all of its cells share one normal and one symbol. That's why a sphere shows bands rather than a
smooth fade, and why more segments make the shading smoother.

## At cell resolution

- Pick ramps where each symbol is clearly denser than the last. Ten or twelve steps is plenty, and fewer can look
  bolder.
- Color carries most of the shading in a dark terminal. A gradient from a dark color to a bright one works with
  almost any symbols.
- A camera-relative `"front"` light points back along the current view axis, so visible faces come out bright and
  the shape can look flat. In an orthographic view, facing that light and facing the viewer agree. In perspective,
  off-axis viewing rays differ a little. A light from one side often shows form better. In world mode, `"front"`
  means fixed world +z and is viewer-facing only while the camera has its default orientation.

---

### Effectguide 3d Meshes (`docs/effectguide/3d/meshes.md`)

# Meshes

Every shape is a **mesh**: a list of corner points and the flat faces between them. The built-in shapes build their
meshes for you. Knowing what's inside one lets you pick shapes that read well, and build your own when none fits.

## The idea

- **Vertices** are the corner points, in the shape's own coordinates, centered on its origin.
- **Faces** are flat polygons, each listing the vertices at its corners.
- **Edges** are where two faces meet. A wireframe draws them.
- Each face has a **normal**: an arrow pointing straight out of it. Normals decide which faces turn toward the
  camera and how brightly each face is lit.

The order a face lists its corners in sets which way its normal points. Listed **counterclockwise as seen from
outside**, the normal points out of the shape. List them the other way and the face is inside out: it's culled when
it should show, and shows when it should be hidden.



Faces must be **convex**: no corner may point inward. A shape with inward corners, such as a star, splits each such
face into triangles, which are always convex.



Curved shapes are meshes too. A sphere is a band of flat faces, and more `segments` and `rings` make its outline
rounder at a higher cost per frame.

## Shape ownership

A body's `shape` is read-only: its mesh and texture layouts are built from that shape when
`space.body(shape)` is called. Create another body to use a different shape. Anchors belong to
the body that created them and keep their original local coordinates.


## In TTE

The Gallery example shows every built-in shape, a few at a time so each is large enough to read. Each shape is one body:

```python
body = self.space.body(shape, at=(x, 0, 0)).draw(self.look)  # shape is Cube(...), Torus(...) and so on.
body.rotate("x", 25)
body.spin("y", turns=1, frames=180, ease=easing.in_out_sine)
```

??? example "gallery.py"

    Save it in your community effects directory as `gallery.py`.

    ```python
    --8<-- "docs/effectguide/examples/3d/gallery/gallery.py"
    ```

```bash
tte --input-file concept_input.txt --canvas-width 80 --canvas-height 24 --anchor-text c gallery
```



| Shape | Sizes | Faces tagged |
| --- | --- | --- |
| `Cube(size)`, `Box(width, height, depth)` | Edge lengths | 0 front, 1 right, 2 back, 3 left, 4 top, 5 bottom |
| `Prism(sides, radius, height)` | `radius` is center to corner | Sides 0 to `sides - 1`, then top and bottom |
| `Pyramid(sides, radius, height)` | Base radius and height | Sides 0 to `sides - 1`, then the base |
| `Tetrahedron`, `Octahedron`, `Icosahedron` | `radius` to each corner | |
| `Cylinder(radius, height, segments)` | `segments` around | |
| `Sphere(radius, segments, rings)` | `segments` around, `rings` top to bottom | |
| `Torus(major_radius, minor_radius, segments, sides)` | Ring radius and tube radius | |

Tags let you treat faces differently: `body.draw(Shaded(), faces={0: Textured("1")})` gives the front of a cube its
own look (see the [die recipe](recipes.md#die)).

The Star example builds its own shape. A custom shape subclasses `Shape`, like the built-in ones a frozen dataclass, and returns
a mesh from `build_mesh`. Once defined, it's used like any other shape:

```python
star = self.space.body(StarPrism()).draw(Shaded())
star.rotate("x", -70)  # Stand the star up, with a cap facing the viewer.
```

??? example "star.py"

    Save it in your community effects directory as `star.py`.

    ```python
    --8<-- "docs/effectguide/examples/3d/star/star.py"
    ```

=== "Shaded"

    ```bash
    tte --input-file concept_input.txt --canvas-width 80 --canvas-height 24 --anchor-text c star
    ```

    

=== "Wireframe"

    ```bash
    tte --input-file concept_input.txt --canvas-width 80 --canvas-height 24 --anchor-text c star --wireframe
    ```

    

`StarPrism.build_mesh` places a ring of corners at each end, alternating between the points and the notches. Each
side is a rectangle between the rings. Each cap is a fan of triangles around a center vertex, because the star's
outline isn't convex.

## Under the hood

[`geometry3d.Mesh`](../../engine/utils/geometry3d.md) holds `vertices` (`Vec3`s) and `faces` (`Face`s). A `Face`
has `indices` into the vertices, optional `uvs` for [texture mapping](textures.md), and a `tag`. The mesh works out
each face's normal from its corner order when it's created, and `edges()` lists each edge once.

The builders, such as `g3.box`, `g3.prism` and `g3.uv_sphere`, return meshes, and `mesh.scaled(x, y, z)` stretches
one. A `Shape` adds what a body needs on top of its mesh: `surface_points` for spreading anchors over it and
`texture_cell` for `Textured` looks.

A mesh is also something to mount characters on. `body.surface(count)`, `vertices()` and `edges(count)` read the
body's mesh to place anchors over its faces, on its corners or along its edges, with normals pointing outward, and they
work whether or not the body is drawn. [Anchor layouts](coordinates.md#anchor-layouts) compares them with
`text_plane`, `ring` and `wrap`.

## At cell resolution

- More faces cost more time but don't always look better. Past the point where each face covers only a few cells,
  extra detail is lost at cell resolution.
- Thin parts, like a torus's tube seen edge-on, can fall between cell centers. Keep every part a few cells thick.
- Flat faces show as flat patches of one symbol. That's what makes a cube read as a cube, and why a low-detail
  sphere looks faceted.

## Dimension contracts

Use positive dimensions for built-in shapes and mesh factories. They intentionally do not check
dimension signs: zero can collapse faces, and negative sizes can reverse winding and normals.
Shading and texture layouts for those sizes are not guaranteed. Body scale is a separate transform
and explicitly rejects non-positive values. Custom mesh faces should be flat, convex, and wound outward.

---

### Effectguide 3d Performance (`docs/effectguide/3d/performance.md`)

# Performance

A frame has about 16 milliseconds at 60 frames per second (see the Engine chapter on
[Performance](../engine/performance.md)). 3D adds one call to the frame, `space.step()`, and that call is usually
the most expensive thing in it. This chapter shows what it costs and how to keep it inside the budget.

## The idea

`step()` does two kinds of work, and they grow with different things:

| Work | Grows with |
| --- | --- |
| Drawing surfaces | The number of **triangles** in drawn bodies, and the number of **cells** they cover. Each triangle is projected and filled cell by cell into the depth buffer. |
| Placing mounted characters | The number of **mounted characters**. Each one is turned, moved and projected to a cell. |

Bodies that are neither drawn nor carrying characters cost almost nothing. A body that has finished its tweens and
hasn't moved is still drawn every frame, because the space doesn't know whether anything else in the scene changed.

## Camera movement and frame cost

An unchanged default camera uses the default projection path. A moved or turned camera also transforms world points
into its view coordinates. The renderer prepares those coordinates once per vertex for a mesh and reuses them for
near-plane clipping, two-sided fills and depth-tested wireframe work. Mounts project their anchors independently.

Camera movement can change the **number of covered cells** far more than the arithmetic cost of that view transform.
A dolly close to a large face or a strong zoom may fill the canvas. Keep wide shots and close shots in the same
benchmark scenario, and compare frame counts and output before making timing claims. The older measurements below
are identified by their revisions; they are not measurements of the new camera lessons.

## Measured costs

These are the guide's own examples on the concept input, on an 80 by 24 canvas with the frame-rate limit off. Each
time is the median of five runs, divided by the frames the example produces. They were measured at revision 5d71560
with Python 3.13 on a 2.8 GHz Intel Xeon. Your times will differ with the processor and Python version. Treat them as
a rough guide to which examples cost more, and measure your own effect on the machines you care about.

| Example | What it draws | Time per frame |
| --- | --- | --- |
| [Wireframe solids](recipes.md#wireframe-solids) | Four solids as edges | 2.5 ms |
| [Card](depth.md) | Mounted text on a plane | 2.9 ms |
| [Cubes](terminal.md) | Three shaded cubes | 3.3 ms |
| [Spinning logo](recipes.md#spinning-logo) | Mounted text with lighting scenes | 3.4 ms |
| [Depth](depth.md) | A cube and a sphere | 4.0 ms |
| [Depth](depth.md) `--see-through` | One textured cube, its far side shown with `back="dim"` | 4.1 ms |
| [Light](light.md) | A shaded sphere | 6.0 ms |
| [Light](light.md) `--shape torus` | A shaded torus | 8.4 ms |
| [Gallery](meshes.md) | Ten shapes, two or three at a time | 4.6 ms |
| [Texture](textures.md) `--text` | A textured cube and a textured sphere | 8.3 ms |
| [Globe](recipes.md#globe) | A textured sphere, 48 by 24, back faces shown | 25.5 ms |

The globe is the only one over budget, so it plays at about 39 frames per second instead of 60. Here's the same
sphere drawn different ways, measuring `step()` alone, at revision 4ceaf0c on the same machine:

| Sphere, radius 20 | Time per step |
| --- | --- |
| 8 by 4, `Shaded()` | 2.4 ms |
| 16 by 8, `Shaded()` | 3.4 ms |
| 24 by 12, `Shaded()` | 5.1 ms |
| 32 by 16, `Shaded()` | 6.8 ms |
| 48 by 24, `Shaded()` | 9.9 ms |
| 24 by 12, `Textured(back="dim")` | 11.0 ms |
| 48 by 24, `Textured()` | 12.4 ms |
| 48 by 24, `Textured(back="dim")` | 20.9 ms |

The Detail example draws three of those spheres side by side, smaller, so you can see what the extra faces buy:

```bash
tte --input-file concept_input.txt --canvas-width 80 --canvas-height 24 --anchor-text c detail
```



With `--wireframe`, the 32 by 16 sphere's edges crowd into a tangle of cells, so more detail reads worse:



The script that produced these tables is `tools/perf/guide_3d_costs.py` in a TTE checkout. Run it with
`python -m tools.perf.guide_3d_costs` from the repository root to measure your own machine.

Two things stand out:

- **Doubling the segments and rings about doubles the cost**, even though the sphere covers the same cells.
  Four times the triangles, but each one covers fewer cells.
- **Showing back faces nearly doubles it again.** `back="dim"` fills the far side too, so the space rasterizes the shape
  twice.

Mounted characters cost about 7 to 8 microseconds each per step, mounted on one turning plane. The concept input's
304 characters take about 2 ms. A full 200 by 50 screen of text, 10,000 characters, takes about 80 ms, before
`update()` moves any of them.

## Geometry preparation

A two-sided `Textured(back="dim")` body transforms and projects its vertices once per frame,
then reuses that geometry for the front and back fills. The fills keep independent depth buffers.
Wireframes reuse edge-to-face adjacency stored on the immutable mesh instead of rebuilding it
every frame. Adjacency is built on first use, so filled meshes pay no topology cost; wireframe
meshes retain topology proportional to their edges.

These optimizations do not cache a rendered scene. Camera, light, gradients and body transforms
are still read each step. Static-scene caching would need to track all of those mutable inputs;
it remains deferred rather than risking stale output.

## In TTE

- **Use as few triangles as read well.** At terminal resolution, a sphere with 24 segments and 12 rings looks as
  round as one with 48 and 24 unless it's very large. Raise the counts only when you see facets.
- **Show back faces only when they add something.** `Textured(back=...)` and `Wireframe(hidden_color=...)` draw the
  far side. Leave them off when the front hides it anyway.
- **Erase what's off screen or finished.** `body.erase()` stops drawing a body, and `space.step()` hides its surface
  characters. The space only fills cells on the canvas, so a body out of view costs little, but it is still moved
  and projected every step until you erase it. Erasing only stops the drawing: queued tweens keep playing and mounted
  characters keep riding. Use `body.cancel()` to drop the tweens, `release_all()` to hand back the characters, or
  `space.remove(body)` to do all of it and stop stepping the body.
- **Mount only the characters you need.** Mount the visible text, not the spaces between it, as `text_plane` examples
  do by skipping blank input cells.
- **Release finished characters.** `release_all()` hands characters back to 2D paths, which cost only their own
  `update()`.
- **Watch the count of drawn bodies.** All ten gallery shapes at once took about 9 ms a frame. Two or three larger
  ones at a time take under 5 ms and read better. Many small shapes cost more than a few large ones and read worse
  (see [3D in a terminal](terminal.md)).

## Measuring

Time your effect with the frame-rate limit off, as in the Engine chapter:

```bash
time tte --input-file concept_input.txt --canvas-width 80 --canvas-height 24 --anchor-text c --frame-rate 0 globe > /dev/null
```

Divide the time by the number of frames to get the cost per frame. To find where the time goes, run the effect under
Python's profiler, sorted by the time spent inside each function. The table prints after the last frame, at the end
of the file:

```bash
python -m cProfile -s tottime -m terminaltexteffects --input-file concept_input.txt --frame-rate 0 globe > profile.txt
```

In a 3D effect, `rasterize` near the top means surfaces dominate, so cut triangles or back
faces. `project` and `apply` near the top, without `rasterize`, mean mounted characters dominate, so mount fewer. In a TTE checkout,
the harness in `tools/perf/` measures and profiles effects too (see [Performance Testing](../../performance.md)).

## At cell resolution

- The terminal shows at most a few thousand cells, so detail past that is wasted work. If two meshes look the same
  on screen, use the smaller one.
- A large shape fills more cells, so it costs more to draw than a small one with the same triangles. That's still
  usually the right trade, since large shapes read better.
- Test on a full-size terminal with `--canvas-width 0 --canvas-height 0`, since mounted text and surfaces both grow
  with the canvas.

---

### Effectguide 3d Recipes (`docs/effectguide/3d/recipes.md`)

# Recipes

Each recipe is a complete effect that combines ideas from several chapters. They're short enough to read in one
sitting, and each one is a good starting point for an effect of your own. Run them like the concept examples:

```bash
tte --input-file concept_input.txt --canvas-width 80 --canvas-height 24 --anchor-text c logo
```

## A queued camera shot

Keep bodies in world space and give the camera a short script: hold, truck and pan together, then zoom. The
[camera lab](camera-movement.md#run-the-camera-lab) includes a complete standalone iterator with this setup:

```python
--8<-- "docs/effectguide/examples/3d/cameralab/cameralab.py:sequence"
```

The subject's turn uses its own body queue. The camera's individual and group callbacks label the timing without
changing the body's animation. See [Camera shots and timing](animating.md#camera-shots-and-timing) for the recording,
the frame loop and callback definitions. The `restore` lesson then demonstrates a return shot that restores the
camera before releasing mounted text to 2D.

## Spinning logo

The input text spins like a sign on a post, brightening as it faces the light, then settles back into place and
hands the characters back to 2D.

```python title="logo.py"
--8<-- "docs/effectguide/examples/3d/logo/logo.py"
```



- **Mounting**: the characters are yours, mounted on a shapeless body with `text_plane`, so they land exactly on
  their input cells at rest ([Transforms](transforms.md)).
- **Light**: `light="front"` and scenes built with `sync=Scene.SyncMetric.LIGHT` pick each character's frame from how
  squarely it faces the light ([Light and shading](light.md)).
- **Back faces**: `hide_when_facing_away=True` hides the text while its back is to you, so it never reads mirrored
  ([Depth and hidden surfaces](depth.md)).
- **Timing**: `spin` then `settle` with `ease=easing.out_back` gives a fast spin that overshoots and springs back to
  rest. `on_complete(self.finish)` releases the characters and restores their input colors when it lands
  ([Animating in 3D](animating.md)).

To make it your own, change the gradient, the number of turns, or spin about `"x"` for a flip.

## Die

A die rolls in from the left, tumbling, and lands with the five facing you.

```python title="die.py"
--8<-- "docs/effectguide/examples/3d/die/die.py"
```



- **Per-face looks**: `draw(faces=looks)` gives each of the cube's tagged faces its own `Textured` look
  ([Meshes](meshes.md)). Opposite faces add up to seven, as on a real die.
- **Textures as pictures**: each face's text is a solid block of `█` with blank pips cut out. Each pip is two cells
  wide and one tall, which looks round on tall cells ([Tall cells](tall-cells.md), [Texture mapping](textures.md)).
- **Combining tweens**: `move_to` and `spin(..., together=True)` roll it across the screen, then `turn_to` lands it
  on an exact pose ([Rotation](rotation.md)).
- **Starting pose**: `set_rotation(rotation_about("diagonal", 120))` starts it at an angle so no face is square to
  the camera while it rolls.

## Globe

The input text wraps around a sphere that turns once. You see the far side through the gaps in the text.

```python title="globe.py"
--8<-- "docs/effectguide/examples/3d/globe/globe.py"
```



- **Wrapping**: `Textured()` with no text uses the input text, wrapped around the sphere by its UV layout, and
  repeated to fill it ([Texture mapping](textures.md)).
- **See-through**: `back="dim"` draws the far side's text in dim color wherever the near side has a space
  ([Depth and hidden surfaces](depth.md)).
- **Tilt**: `rotate("z", -15)` then `rotate("x", 15)` leans the axis like a desk globe, and
  `spin("y", ..., local=True)` turns the globe about that leaning axis, which stays put
  ([Rotation](rotation.md)).

The globe is the most expensive example in the guide, because a round sphere has many triangles and `back="dim"`
draws it twice. [Performance](performance.md) shows the numbers and what to cut.

## Ticker on a cylinder

A message runs around a short, wide drum, like a news ticker on a building.

```python title="ticker.py"
--8<-- "docs/effectguide/examples/3d/ticker/ticker.py"
```

```bash
tte --input-file concept_input.txt --canvas-width 80 --canvas-height 24 --anchor-text c ticker --message " HELLO ·"
```



- **Shape as a band**: a `Cylinder` only 2 units tall holds one row of text around its side
  ([Tall cells](tall-cells.md), [Texture mapping](textures.md)).
- **Turning about a tilted axis**: the drum is tipped toward you, so turning it with `rotate("y", ...)` would turn it
  about the world's vertical and make it wobble. Instead, it sets its rotation each frame to the tilt times a turn
  about its own axis ([Rotation](rotation.md)).
- **Perspective**: `perspective=0.5` keeps the near side from magnifying the letters too much
  ([Camera and projection](camera.md)).
- **Driving it yourself**: there's no tween. `__next__` counts frames and sets the angle, which is how to make
  motion that never ends or follows your own rules.

## Wireframe solids

Four solids tumble in a row, each in its own color, with hidden edges dimmed.

```python title="wireframes.py"
--8<-- "docs/effectguide/examples/3d/wireframes/wireframes.py"
```



- **Wireframe look**: `Wireframe(color=..., hidden_color=...)` draws edges only, with the edges behind the shape in
  a dark color ([3D in a terminal](terminal.md), [Depth and hidden surfaces](depth.md)).
- **Tumbling**: two spins about different axes, run `together`, at different rates for each solid
  ([Rotation](rotation.md)).
- **Spacing**: each solid sits 20 columns from the next, so their edges never cross on screen. Edges that touch
  blur into one tangle of line symbols.

## Where to go next

- The [`Space3D` reference](../../engine/space3d.md) lists every call used here.
- The [Effect Tutorial](../tutorial/01-scaffold.md) covers configuration, testing and packaging, which apply to 3D
  effects unchanged.

---

### Effectguide 3d Rotation (`docs/effectguide/3d/rotation.md`)

# Rotation

Turning is what makes 3D look like 3D, and it's the part with the most surprises. This chapter covers turning about
one axis, combining turns into a tumble, and the two ways to bring a body back to rest.

## The idea

A turn has an **axis** and an **angle**. The axis is a line through the body's origin. A positive angle turns
counterclockwise when you look down the axis toward the origin:

- About **y** (up), the body turns like a revolving door.
- About **x** (right), it tips forward or back. A positive angle tips the top toward the default viewer, so you see it from above in that view.
- About **z** (toward the default viewer), it turns like a clock hand, counterclockwise.



Turns don't add up like numbers. Turning a cube -90 degrees about x, which carries its front face up onto the top,
and then 90 degrees about y leaves it facing a different way than making the same two turns in the other order.



So TTE never stores an orientation as three angles, which would depend on the order you applied them in. It stores
a **quaternion**: four numbers that describe the whole orientation as one turn about one axis. You never need to read them. What matters is what they make
easy: combining turns, turning by part of a turn, and finding the shortest way from one orientation to another.

## In TTE

The Turns example has three modes. In the first, each cube spins about one axis:

```python
for axis, x in (("x", -26), ("y", 0), ("z", 26)):
    cube = self.space.body(Cube(14), at=(x, 0, 0)).draw(Shaded())
    cube.hold(30).spin(axis, turns=1, frames=240, ease=easing.in_out_sine).hold(30)
```

??? example "turns.py"

    Save it in your community effects directory as `turns.py`.

    ```python
    --8<-- "docs/effectguide/examples/3d/turns/turns.py"
    ```

### One axis at a time

```bash
tte --input-file concept_input.txt --canvas-width 80 --canvas-height 24 --anchor-text c turns
```



`spin(axis, turns, frames, ease)` queues whole or partial turns about an axis. It's a **tween**: the space plays it
one frame at a time in `step()`. `rotate(axis, degrees)` turns the body by that much right away instead, which is
useful when your own code decides the angle each frame.

Both turn about the **world's** axes by default. `rotate("y", 5)` on a body you've already tipped forward
turns it about the screen's vertical. Use `rotate("y", 5, local=True)` or
`spin("y", local=True)` to turn about the body's own tipped axis. The local axis follows the body
when other turns run alongside it.



`settle` still spins about a world axis while returning to rest. `turn_to` and `turn_to_rest`
target an absolute orientation, so they have no local-axis option.

### Tumbling

```bash
tte --input-file concept_input.txt --canvas-width 80 --canvas-height 24 --anchor-text c turns --mode tumble
```



`together=True` runs a tween alongside the one queued before it. Two spins about different axes running together
combine every frame into a tumble.

### Back to rest

```bash
tte --input-file concept_input.txt --canvas-width 80 --canvas-height 24 --anchor-text c turns --mode rest
```



**Rest** is the orientation a body was created in, with no turn applied. It matters because `text_plane` places each
character on its input cell at the pose the body has when you call it. The usual pattern, used by these examples and
the Spin tutorial, is to build the text plane on an unmoved body and only then turn it away, so returning to rest
brings the text home. Rest is only an orientation: if you also moved or scaled the body, bring its position and scale
back too, with `move_to` and `scale_to` (see [Space and coordinates](coordinates.md#rest-and-pose)).

There are two ways back to rest:

- `turn_to_rest(frames, ease)` takes the shortest path. It looks like a body being straightened.
- `settle(axis, turns, frames, ease)` spins about an axis while it unwinds any other tilt, landing exactly at rest.
  It looks like a coin or a sign coming to a stop.

`turn_to(rotation)` turns along the shortest path to any orientation, and `random_pose()` turns to a random one,
drawn from the space's `rng`.

## Under the hood

[`geometry3d.Rotation`](../../engine/utils/geometry3d.md) is the quaternion. Its angles are in radians:

```python
import math

from terminaltexteffects.engine.space3d import rotation_about
from terminaltexteffects.utils.geometry3d import Rotation, Vec3

tip = Rotation.about_x(math.radians(20))
turn = rotation_about("y", 45)  # The same, but in degrees and with axis names.
both = tip * turn  # Turn first, then tip.
halfway = Rotation().slerp(both, 0.5)  # Halfway from rest to `both`, along the shortest path.
point = both.apply(Vec3(10, 0, 0))
```

- `a * b` is the rotation that applies `b` first, then `a`.
- `inverse()` undoes a rotation.
- `slerp(other, t)` blends along the shortest path. `turn_to` and `settle` are built on it.
- `matrix()` gives the 3 by 3 matrix that `apply` and `transform_points` use.

## At cell resolution

- A fast turn skips cells. At more than a few degrees per frame, a large shape's edges jump visibly. Ease spins in and
  out, or slow them down.
- In the default view, turns about world z look flat because they move points parallel to the image. Once the
  camera turns, world z need not be the view axis. A [camera roll](camera-movement.md#turn-the-view-without-moving-the-eye)
  always turns about the camera's own backward axis; it is distinct from turning a body about world z.
- Text turned edge-on loses characters (see [Depth and hidden surfaces](depth.md)), so don't leave a text plane
  turned past about 60 degrees for long.

---

### Effectguide 3d Tall Cells (`docs/effectguide/3d/tall-cells.md`)

# Tall Cells

Terminal cells are not square. A typical cell is about twice as tall as it is wide, so a shape drawn with one cell per
unit in both directions comes out twice as tall as it should. TTE corrects for this in the camera, so you can work in
plain columns and still get round spheres and square cubes.

## The idea

Measured in pixels, a row is roughly two columns tall. If a 16 unit cube were drawn 16 columns wide and 16 rows tall,
it would look like a tower. To look like a cube on screen, it must be 16 columns wide and about 8 rows tall.



The camera does this by counting each row as `row_scale` units of height, 2 by default
(`geometry.TERMINAL_ROW_SCALE`). Everything above the camera works in columns, as if cells were square. Only the
final step, from world space to canvas rows, divides heights by 2.

## In TTE

The Cells example turns the same cube twice: on the left with the usual camera, and on the right in a second space
whose camera treats rows as one unit tall. The only difference is the second camera's `row_scale`:

```python
corrected = Space3D(self.terminal)
square = Space3D(self.terminal)
square.camera = Camera.for_canvas(self.terminal.canvas, distance=square.camera.distance, row_scale=1.0)
```

??? example "cells.py"

    Save it in your community effects directory as `cells.py`.

    ```python
    --8<-- "docs/effectguide/examples/3d/cells/cells.py"
    ```

```bash
tte --input-file concept_input.txt --canvas-width 80 --canvas-height 24 --anchor-text c cells
```



You never need the right-hand camera in a real effect. It's here to show what the correction does. Notice that it
also changes how the cube turns: the uncorrected cube stretches as its faces turn, because a turn that keeps
distances the same in 3D no longer keeps them the same on screen.

## Under the hood

[`Camera`](../../engine/utils/geometry3d.md) has a `row_scale` field. `project` divides the y offset by it, and
`cell_to_world` multiplies by it, so the two stay exact inverses:

```python
row = center_row + point.y * scale / row_scale
```

Line symbols for wireframes use the same scale. `geometry3d.line_symbol` multiplies the row change by `row_scale`
before measuring an edge's angle, so a 45 degree edge on screen is drawn with `/` and not `|`.

## At cell resolution

- Vertical detail is half as fine as horizontal detail. A sphere 16 columns wide is only 8 rows tall, so its top
  and bottom edges are coarse. Wider shapes hide this best.
- In the default orthographic view, moving a body by 1 unit in y moves it half a row, so slow vertical motion steps
  once every two units. A turned camera or perspective changes screen displacement. Horizontal
  motion steps every unit and looks smoother.
- Turning doesn't favor either axis. Text on a face turning about x and text turning about y both shrink by the
  cosine of the angle, so at 60 degrees each shows half its rows or half its columns. The row correction already
  allowed for tall cells when the text was lifted into 3D.
- Fonts vary, so the real ratio is somewhere around 1.8 to 2.2. TTE uses 2 because it's close for common fonts.

---

### Effectguide 3d Terminal (`docs/effectguide/3d/terminal.md`)

# 3D in a Terminal

A terminal is a very coarse screen. An 80 by 24 terminal has fewer than 2,000 cells, each showing one symbol in one
foreground color. A 3D scene has to read at that resolution, so it helps to know what survives and what doesn't
before you design an effect.

## The idea

Every frame, the space works out which cells each shape covers and draws one symbol in each. Two things decide how
well a shape reads:

- **Area beats outline.** A filled face covers many cells, and its symbol and color say which way it faces. A thin
  edge covers one cell per step, and at a slant it becomes a staircase of `/` and `\`. Filled, shaded shapes read
  as solid at almost any size. Wireframes need room.
- **Size matters more than detail.** A cube 6 columns wide has faces only a few cells across, so its corners and
  edges blur into a blob as it turns. At 12 columns it reads as a cube. At 24 it looks solid and turns smoothly.



A cell is either covered or not. The space tests one point per cell, its center, so a face fills a solid patch of
cells while a line that's thinner than a cell leaves a trail of single cells that steps sideways as it slants.

## In TTE

The Cubes example turns three cubes of different sizes side by side:

```python
for size, x in ((6, -28), (12, -10), (24, 18)):
    cube = self.space.body(Cube(size), at=(x, 0, 0)).draw(look)  # look is Shaded() or Wireframe().
    cube.rotate("x", 20)
    cube.spin("y", turns=1, frames=240, ease=easing.in_out_sine)
```

??? example "cubes.py"

    Save it in your community effects directory as `cubes.py`.

    ```python
    --8<-- "docs/effectguide/examples/3d/cubes/cubes.py"
    ```

=== "Filled"

    ```bash
    tte --input-file concept_input.txt --canvas-width 80 --canvas-height 24 --anchor-text c cubes
    ```

    

=== "Wireframe"

    ```bash
    tte --input-file concept_input.txt --canvas-width 80 --canvas-height 24 --anchor-text c cubes --wireframe
    ```

    

- `space.body(Cube(size), at=(x, 0, 0))` makes a body with a cube shape, `x` columns from the center.
- `.draw(look)` tells the space to fill the cells the body covers. `Shaded()` picks a symbol and color for each face
  from its angle to the light. `Wireframe()` draws only the edges, with hidden edges dimmed.
- `rotate("x", 20)` tips the top toward you so you can see it, and `spin` queues a full turn.

The small cube is a smudge in both looks. The middle one reads as a cube when filled, but its wireframe edges crowd
together. Only the large one works as a wireframe.

## Under the hood

`draw()` asks [`geometry3d.rasterize`](../../engine/utils/geometry3d.md) to fill the shape's faces into a
**depth buffer**, one entry per cell, keeping the nearest surface in each cell. A wireframe uses
`geometry3d.wireframe` instead, which walks each edge cell by cell and picks a line symbol from its slope. The
[Depth and hidden surfaces](depth.md) chapter explains the buffer.

## At cell resolution

- Aim for shapes at least 12 columns across, and 20 or more for wireframes and textured faces.
- Keep a few cells between shapes. Two shapes that touch on screen merge into one blob of symbols.
- Prefer a few large shapes to many small ones. They read better and cost less (see [Performance](performance.md)).
- Text mounted on a shape needs even more room than a surface: a character is one cell, so a 12 column face holds
  12 characters across at most, and fewer as it turns away.

---

### Effectguide 3d Textures (`docs/effectguide/3d/textures.md`)

# Texture Mapping

A `Textured` look wraps text over a shape's faces, so the surface itself is made of letters. To do that, every point
on a face needs to know which character of the text belongs there. That lookup is called **texture mapping**.

## The idea

Each face carries **texture coordinates**, usually called **u** and **v**: u runs across the face from 0 to 1, and v
runs up it from 0 to 1. Any point on the face has a (u, v) position, however the face is turned or stretched on
screen.

The shape turns (u, v) into a position in the text: a column and a row. TTE keeps the text at its natural size, about
one character per column across and one per row down, and repeats it when the face is bigger than the text. Wrapped
shapes, such as cylinders and spheres, run u all the way around, so the text continues across the seam.





When a cell is filled, the space finds the (u, v) at the cell's center, looks up the character there, and draws it in
a color from the light. So the text turns, tilts and foreshortens with the surface, while each cell still shows one
upright character.

## In TTE

The Texture example wraps a grid pattern, or the input text, over a cube and a sphere:

```python
look = Textured() if self.config.text else Textured(GRID)  # Textured() wraps the input text.
cube = self.space.body(Cube(22), at=(-20, 0, 0)).draw(look)
sphere = self.space.body(Sphere(13, segments=32, rings=16), at=(20, 0, 0)).draw(look)
```

??? example "texture.py"

    Save it in your community effects directory as `texture.py`.

    ```python
    --8<-- "docs/effectguide/examples/3d/texture/texture.py"
    ```

=== "Grid"

    ```bash
    tte --input-file concept_input.txt --canvas-width 80 --canvas-height 24 --anchor-text c texture
    ```

    

=== "Text"

    ```bash
    tte --input-file concept_input.txt --canvas-width 80 --canvas-height 24 --anchor-text c texture --text
    ```

    

- `Textured()` wraps the effect's input text. `Textured("...")` wraps any string, with lines separated by `\n`.
- A grid shows the mapping clearly: watch the squares compress toward the sphere's edges, where the surface turns away.
- Each face of a `Box` or `Cube` starts the text at its own top left corner. `Prism` and `Pyramid` run the text
  around their sides. `Cylinder` and `Sphere` wrap it all the way around, starting at the front.
- Faces without texture coordinates, such as prism and cylinder caps, are drawn in `back_color`.
- `back="dim"` shows the far side through the spaces in the text (see [Depth](depth.md)).
- Each cell shows one character, so wide characters such as CJK can't be split across cells and are left blank.

### Mounted text or textured surface

Text can be part of a 3D scene in two ways. Mounting puts each character on an anchor, as in
[Animating in 3D](animating.md): the character keeps its own symbol and moves as a whole. A `Textured` look draws the
surface out of text: each cell samples the text where it lands. The Surfaces example shows the same words both ways.

??? example "surfaces.py"

    ```python
    --8<-- "docs/effectguide/examples/3d/surfaces/surfaces.py"
    ```

```bash
tte --input-file concept_input.txt --canvas-width 80 --canvas-height 24 --anchor-text c surfaces
```



- Mounted characters keep their colors, scenes and symbols, and can be released to 2D paths afterward. Turned away,
  the letters still read upright, in reverse order.
- A textured surface is lit and hides what's behind it, but it can't hand its letters back: they belong to the space.
- Mount text that has to land somewhere. Texture text that decorates a shape.
- `body.wrap(characters)` and `Textured(text)` both put text around a cylinder or sphere, but differently. `wrap` gives
  each of your characters its own anchor, one character per anchor whatever the shape's size, and the characters
  ride the surface as wholes. `Textured` resamples the text into the surface's cells, so the letters stretch, repeat
  and shade with the surface. See [Anchor layouts](coordinates.md#anchor-layouts).

## Under the hood

A mesh face's `uvs` give a (u, v) for each corner. [`rasterize`](../../engine/utils/geometry3d.md) blends them across
the face, correcting for perspective, and stores the result in each cell's `Fragment.uv`. The shape's
`texture_cell(fragment)` turns that into a fractional (column, row) in the text:

```python
column, row = body.shape.texture_cell(fragment)  # For example (12.4, 3.9): column 12, row 3.
```

A custom shape can override `texture_cell` to lay its text out differently. Return `None` for faces that shouldn't be
textured.

## At cell resolution

- Text is sampled once per cell. Where a surface faces you squarely at its natural size, each character lands in its
  own cell. Where the surface is magnified, by perspective or by scaling up, neighboring cells sample the same
  character, and letters double up. Where it's shrunk or turned away, cells skip characters.
- Doubling is worst on the near side of a large shape. A gentler `perspective` or a smaller shape helps. `back="dim"`
  doesn't: it only shows the far side through the near side's spaces.
- Text needs space to read. A face 20 columns wide holds about 20 characters, and fewer as it turns.
- Bold, dense text such as capitals reads better on a surface than lowercase or punctuation.

## Scrolling, repetition and text size

`Textured(offset=(column, row))` adds an offset in text cells before sampling. Positive columns
scroll left; positive rows scroll up. `repeat=True` wraps the text in both directions.
`repeat=False` clamps samples outside the text to the nearest edge cell. `scale=2` doubles each
character's footprint in both dimensions; scale must be finite and positive. Row density follows
`space.camera.row_scale`, including when it differs from the usual tall-cell ratio.

Looks are immutable. Animate an offset by replacing the look each frame, without turning the body:

```python
from dataclasses import replace
from terminaltexteffects.engine.space3d import Textured

look = Textured("A scrolling message")
# In the effect's frame loop, with elapsed_frames advanced by the effect:
body.draw(replace(look, offset=(elapsed_frames / 4, 0)))
space.step()
```

For custom shapes, `texture_cell` continues to return rows in the default terminal cell ratio;
the renderer converts those rows to the camera's ratio before applying scale and offset.

---

### Effectguide 3d Transforms (`docs/effectguide/3d/transforms.md`)

# Transforms

A body's **transform** is where it is, which way it faces and how big it is. Every point of its shape and every
anchor on it is placed by applying that transform. Three operations make it up, and the order you apply them in
changes the result.

## The idea

- **Scale** grows or shrinks the body about its own origin.
- **Rotate** turns it about an axis through its own origin.
- **Move** shifts it to a position in the world.



A body applies them in that order: scale, then rotate, then move. Because the turn happens before the move, the body
always turns about its own center, wherever it is. That's **spinning in place**.

Do it the other way around, moving first and turning second, and the turn swings the body around the world's origin.
That's an **orbit**. A spinning planet and a moon going around it are the same turn applied in different orders.



## In TTE

The Orbit example turns a planet and a moon by the same angle each frame. The planet stays at the origin and only
its orientation changes. The moon's starting position is turned too, so it travels around the planet. Each frame:

```python
turn = rotation_about("y", 360 * self.frame_number / self.config.frames)
self.planet.set_rotation(self.tilt * turn)  # Spin in place.
self.moon.set_rotation(self.tilt * turn)
point = (self.tilt * turn).apply(self.moon_start)  # Orbit: turn the moon's offset yourself...
self.moon.set_position(point.x, point.y, point.z)  # ...then move the moon there.
```

??? example "orbit.py"

    Save it in your community effects directory as `orbit.py`.

    ```python
    --8<-- "docs/effectguide/examples/3d/orbit/orbit.py"
    ```

```bash
tte --input-file concept_input.txt --canvas-width 80 --canvas-height 24 --anchor-text c orbit
```



`Body` has a method for each operation. Some change the transform relative to where it is now, and some set it
outright:

| Change by | Set to | Effect |
| --- | --- | --- |
| `rotate(axis, degrees)` | `set_rotation(rotation)` | Turn about the body's own origin. |
| `move_by(x, y, z)` | `set_position(x, y, z)` | Move the body's origin. |
| `scale_by(factor)` | `set_scale(scale)` | Grow or shrink about the origin. |

`reset()` puts all three back to how the body was created and clears its queued tweens. `rotation`, `position` and `scale` read the current
values. Each method returns the body, so calls chain: `cube.set_position(10, 0, 0).rotate("y", 45)`.

The order you call these methods in doesn't change that pipeline. A body always scales, then turns about its own
origin, then moves, so `move_by(30, 0, 0)` followed by `rotate("y", 45)` still spins the body in place at x = 30;
it doesn't swing it around the world's origin. To orbit with these methods, compute the moon's position yourself by
turning its starting offset, as the example does, then `set_position` there.

The [camera orbit](camera-movement.md#orbit-a-stationary-scene) is a different operation: it leaves these body
transforms fixed and moves the viewpoint around the scene.

## Under the hood

[`geometry3d.transform_points`](../../engine/utils/geometry3d.md) applies a transform to many points at once:

```python
from terminaltexteffects.utils.geometry3d import Rotation, Vec3, transform_points

world = transform_points(points, Rotation.about_y(angle), scale=2.0, position=Vec3(10, 0, 0))
```

It scales each point, rotates it with the rotation's matrix and adds the position. `Body.world_point(local)` does the
same for one point with the body's own transform.

## At cell resolution

- Moving a body by less than a cell often changes nothing on screen. Slow drifts look stepped, so ease them or
  combine them with a turn.
- Scaling changes how many cells a shape covers. A shape scaled below about 6 columns stops reading as a shape at
  all (see [3D in a terminal](terminal.md)).
- Anchors scale with the body, but characters don't. A text plane scaled to half size crowds two characters into
  each cell, and only one of them can show.

---

### Effectguide Engine Animation (`docs/effectguide/engine/animation.md`)

# Animation

Motion decides where a character is. Animation decides what it looks like: its symbol, its colors, and text styles
such as bold. Each character has its own [`Animation`](../../engine/animation/animation.md) object,
`character.animation`, which plays **scenes**. A scene is a list of **frames**, and each frame shows one appearance for
a number of ticks.

## A character's appearance

At any moment a character has one appearance, a [`CharacterVisual`](../../engine/animation/charactervisual.md): a
symbol, a foreground and background color, and style flags. A character that has never played a scene shows its input
symbol with no colors, so the terminal's default colors apply.

There are two ways to change the appearance:

- Play a scene, which changes the appearance frame by frame.
- Call `character.animation.set_appearance(symbol, colors)` to change it once. With no arguments, it restores the input
  symbol with no colors.

A playing scene replaces the appearance on every tick, so `set_appearance()` only lasts while no scene is active.

## A first scene

```python
from terminaltexteffects import Color, ColorPair

scene = character.animation.new_scene()
scene.add_frame(".", duration=5, colors=ColorPair(fg=Color("#ff2000")))
scene.add_frame("o", duration=5, colors=ColorPair(fg=Color("#ffc000")))
scene.add_frame(character.input_symbol, duration=5, colors=ColorPair(fg=Color("#ffffff")))
character.animation.activate_scene(scene)
```

- `new_scene()` creates an empty [`Scene`](../../engine/animation/scene.md) owned by this character.
- `add_frame()` adds a frame: a symbol, how many ticks to show it, and optional colors and styles.
- `activate_scene()` makes the scene the character's active scene and shows its first frame straight away.

Like a path, the scene plays once the character is in your iterator's `active_characters`. Each tick shows the current
frame and counts down its duration. When the last frame finishes, the scene completes and the character keeps the
last frame's appearance. A character stays active while it has an unfinished scene, even after its path is complete.

A frame's symbol must be a single printable character. Wide characters, such as most emoji, are allowed.

### Frame styles

`add_frame()` takes these keyword arguments besides the symbol and duration:

| Argument | Effect |
| --- | --- |
| `colors` | A `ColorPair(fg=..., bg=...)`. Either color can be `None` to use the terminal's default. |
| `bold`, `dim`, `italic`, `underline`, `blink`, `reverse`, `hidden`, `strike` | Text styles. All default to `False`. Terminals differ in which styles they support. |

Colors are covered in detail in the [Color](color.md) chapter.

## Gradients across symbols

Many effects show a short sequence of symbols while the color changes smoothly. `apply_gradient_to_symbols()` builds
those frames for you:

```python
heat = Gradient(Color("#ff2000"), Color("#ffc000"), Color("#ffffff"), steps=6)
scene.apply_gradient_to_symbols((".", "o", "O", character.input_symbol), 3, fg_gradient=heat)
```

It adds one frame for each color in the gradient, each lasting the given duration, and spreads the symbols evenly
across those frames. The `heat` gradient has 13 colors, so this scene has 13 frames of 3 ticks each: about three frames
of each symbol, from a red `.` to the white input symbol. When there are more symbols than colors, it adds one frame per
symbol and spreads the colors instead. Pass `bg_gradient` to change the background as well.

## The Glow example

The examples in this chapter use the Glow effect. Every character slides home from the canvas's left edge, as in the
Travel example from the Motion chapter, while a scene heats it from a red spark to its white input symbol. Save it as
`glow.py` in your community effects directory:

```python title="glow.py"
--8<-- "docs/effectguide/examples/glow/glow.py"
```

```bash
tte --input-file concept_input.txt glow
```



Every scene starts during setup and plays at the same rate, so all the characters change together, wherever they are.
The scene lasts 39 ticks. Characters near the left edge are home before then and stay active until their scene
finishes; characters from the right of the text are still moving when their scene ends, and arrive already showing
their white input symbol.

## Frame duration

A frame's duration is a number of ticks, so it controls how fast a scene plays. With `--frame-duration 10`, each of
Glow's 13 frames lasts 10 ticks, and the scene takes 130 ticks, longer than any character's path:

```bash
tte --input-file concept_input.txt glow --frame-duration 10
```



At 60 frames per second, 10 ticks is a sixth of a second. Durations must be at least 1.

## Syncing a scene to a path

Often a scene should play exactly as long as a path, so a character finishes changing as it arrives. Pass
`sync` to `new_scene()` and the scene follows the progress of the character's active path instead of counting ticks:

| `sync` | Frame shown |
| --- | --- |
| `None` | The default. Frames play for their durations, independent of motion. |
| `Scene.SyncMetric.DISTANCE` | The frame matching how far along the path the character has traveled. |
| `Scene.SyncMetric.STEP` | The frame matching how many of the path's ticks have passed. |

Frame durations are ignored in a synced scene. When the path completes, or if no path is active, the scene shows its
last frame and completes.

The two metrics differ when the path has an easing function. Glow's paths use `out_expo`, which covers most of the
distance in the first few ticks:

=== "distance"

    ```bash
    tte --input-file concept_input.txt glow --sync distance
    ```

    

    The scene follows the eased distance, so it races through the early frames with the character, and each character
    shows nearly white for most of its trip.

=== "step"

    ```bash
    tte --input-file concept_input.txt glow --sync step
    ```

    

    The scene follows elapsed ticks, so it plays at an even rate however the character moves, and the trailing
    characters glow orange and yellow as they settle.

Without easing, distance and step progress at the same rate. Use `DISTANCE` to tie an appearance to position, such as
a trail that changes as a character travels, and `STEP` to tie it to time.

## More scene options

`new_scene()` takes these keyword arguments:

| Argument | Effect |
| --- | --- |
| `is_looping` | Start again from the first frame after the last one, forever. |
| `sync` | Follow the active path's progress, as above. |
| `ease` | An easing function for the scene's progress through its frames, as with paths. The total duration doesn't change. |
| `scene_id` | A name for the scene, so you can find it with `animation.query_scene(scene_id)`. One is generated if you don't give one. |

A looping scene never completes, so a character playing one never becomes inactive. Give a looping scene an end, for
example by activating another scene from an event, or remove the character from `active_characters` yourself.

A character has at most one active scene. Activating another scene replaces the current one;
`animation.deactivate_scene()` stops the active scene and leaves the character showing its current appearance.
Activating a scene doesn't reset it, so a scene that was stopped part way resumes where it stopped. Call
`scene.reset_scene()` first to start it from the beginning.

## Scenes and events

Scenes fire `SCENE_ACTIVATED` and `SCENE_COMPLETE` events, and event actions can activate and deactivate scenes (see
[Events](events.md)). A common pattern is to start a scene when a path completes:

```python
character.event_handler.register_event(Event.PATH_COMPLETE, path, Action.ACTIVATE_SCENE, landing_scene)
```

## Where to go next

- [Tutorial step 5: Color](../tutorial/05-color.md) gives MySpray's characters a scene and a color in a gradient across the text.
- [Color](color.md) explains `Color`, `ColorPair`, and gradients.

---

### Effectguide Engine Architecture (`docs/effectguide/engine/architecture.md`)

# Architecture

Every TTE effect is built from the same four parts: an **effect**, its **configuration**, an **iterator** that
produces frames, and a **terminal** that owns the characters. This chapter explains what each part does and how
input text becomes animation frames.

## The four parts

``` mermaid
flowchart TB
    module["effect module<br/>get_effect_resources()"] --> effect
    module --> config
    config["Config<br/>(BaseConfig subclass)"] --> effect["Effect<br/>(BaseEffect subclass)"]
    effect -- "iter(effect)" --> iterator["Iterator<br/>(BaseEffectIterator subclass)"]
    iterator -- builds --> terminal[Terminal]
    terminal -- creates --> characters["EffectCharacter<br/>one per non-space symbol"]
    iterator -- "next() returns" --> frame["frame string"]
```

| Part | Base class | Responsibility |
| --- | --- | --- |
| Effect | [`BaseEffect`](../../engine/baseeffect.md) | Holds the input text, the effect configuration, and the terminal configuration. Creates a fresh iterator each time it is iterated. |
| Configuration | [`BaseConfig`](../../engine/baseconfig.md) | A dataclass of the effect's options. The same fields drive the command-line parser and library defaults. |
| Iterator | [`BaseEffectIterator`](../../engine/baseeffect.md) | Sets up the animation, then returns one frame string per `next()` call until the effect ends. |
| Terminal | [`Terminal`](../../engine/terminal/terminal.md) | Parses the input into characters, lays them out on a canvas, tracks which are visible, and renders frames. |

An effect module ties the parts together with one function, `get_effect_resources()`, which returns the command name
and the effect and configuration classes. TTE calls it when it discovers the module, as described in
[How TTE finds your effect](#how-tte-finds-your-effect) below.

## From input text to frames

When you run `printf 'Hello' | tte spray`, or iterate over an effect in Python, this happens:

1. **The effect is created.** `BaseEffect.__init__` stores the input text and builds default configurations unless
   you pass your own. Nothing is drawn yet.
2. **An iterator is created.** `iter(effect)` builds a new iterator. `BaseEffectIterator.__init__` deep-copies the
   effect configuration and builds a `Terminal` for the input. If the input contains no visible characters, it raises
   `EmptyInputError`.
3. **The terminal makes characters.** The `Terminal` parses the input (including any ANSI colors), measures the
   available space, creates a [`Canvas`](../../engine/terminal/canvas.md), and creates one
   [`EffectCharacter`](../../engine/basecharacter.md) for each non-space input symbol. Each character remembers its symbol and
   its home position, `input_coord`. All characters start hidden.
4. **Your iterator builds the animation.** In its `__init__`, your iterator gives characters their starting
   positions, paths to follow, and scenes to play, and decides when each one appears.
5. **Frames are produced.** Each `next()` call advances the animation by one step and returns the terminal's
   rendering as a string. When the effect is finished, `next()` raises `StopIteration`.
6. **Something prints the frames.** The `tte` command, or your own code, prints each frame. The effect itself never
   writes to the terminal.

Because the iterator holds all animation state, iterating the same effect twice plays the animation twice from the
start, and an effect can be reused with different configurations.

## Characters own their behavior

TTE treats every character on screen as an independent object. An `EffectCharacter` carries three helpers:

- **`motion`** ([`Motion`](../../engine/motion/motion.md)) moves the character along paths made of waypoints.
- **`animation`** ([`Animation`](../../engine/animation/animation.md)) changes the character's symbol and colors by
  playing scenes made of frames.
- **`event_handler`** ([`EventHandler`](../../engine/eventhandler.md)) reacts when a path or scene starts or
  finishes, for example by starting the next scene.

Your iterator configures these helpers up front, then mostly steps back. Each frame, the engine advances every active
character by one tick, and the terminal draws each visible character at its current position with its current
appearance. The [Frame loop](frame-loop.md) chapter covers that cycle in detail.

This design keeps effects short. The built-in Spray effect, for example, gives every character a path to its home
position and a scene that fades its color, then only decides how many characters to release each frame.

## Two configurations

An effect has two configuration objects:

- **The effect configuration** is your `BaseConfig` subclass. It holds options specific to your effect, such as
  movement speed or gradient colors.
- **The terminal configuration**, [`TerminalConfig`](../../engine/terminal/terminalconfig.md), holds options every
  effect shares: frame rate, canvas size and anchoring, color handling, tab width, and so on. Your effect reads it as
  `self.terminal.config` but doesn't define it.

The Configuration and the CLI chapter explains how a configuration dataclass becomes command-line options.

## How TTE finds your effect

TTE builds its command-line parser from every effect module it can find:

- Built-in effects in the `terminaltexteffects.effects` package.
- Community effects: every `.py` file in `${XDG_CONFIG_HOME:-$HOME/.config}/terminaltexteffects/effects` that
  defines `get_effect_resources()`.

Each module must return a unique command name. If two modules return the same name, TTE reports a duplicate effect
command and stops, so a community effect can't replace a built-in one.

## Where to go next

- [Frame loop](frame-loop.md) explains what happens on every frame.
- [Tutorial step 1: Scaffold](../tutorial/01-scaffold.md) builds the smallest effect that runs, using the parts
  described here.

---

### Effectguide Engine Canvas (`docs/effectguide/engine/canvas.md`)

# Canvas and coordinates

Characters live on a **canvas**: a grid of cells with the input text placed somewhere inside it. This chapter explains
how positions are written, how the canvas and the text area relate, and how to pick positions for your effect.

## Coordinates

A position is a [`Coord`](../../engine/utils/geometry.md) with a `column` and a `row`:

```python
from terminaltexteffects import Coord

home = Coord(column=10, row=3)
column, row = home  # A Coord unpacks to (column, row).
```

Coordinates start at 1, and the origin is the **bottom left** cell:

- `column` increases to the right.
- `row` increases **upward**. Row 1 is the bottom row of the canvas, which is the last line of the input.

Upward rows are the opposite of how a terminal numbers lines, so check this first when characters end up upside
down. `Coord` is immutable and hashable, so you can use it as a dictionary key.

## Canvas and text bounds

The [`Canvas`](../../engine/terminal/canvas.md), available as `self.terminal.canvas`, has two sets of bounds:

| | Canvas | Text |
| --- | --- | --- |
| Edges | `left`, `right`, `bottom`, `top` | `text_left`, `text_right`, `text_bottom`, `text_top` |
| Size | `width`, `height` | `text_width`, `text_height` |
| Center | `center`, `center_column`, `center_row` | `text_center`, `text_center_column`, `text_center_row` |

All edges are inclusive. `left` and `bottom` are always 1. When a dimension is even, the center is the lower of the two
middle cells.

By default the canvas is exactly the size of the text, so both sets of bounds are the same. Users can make the canvas
larger with the terminal options `--canvas-width` and `--canvas-height`: a positive number sets the size, `0` matches
the terminal, and `-1` (the default) matches the text. `--anchor-text` then places the text inside the canvas, and
`--anchor-canvas` places the canvas inside the terminal.

Your effect should work with any canvas size. Use canvas bounds for where things come from or go to, such as a spray
origin, and text bounds for anything about the text itself, such as a gradient across it.

### Seeing the canvas

The Travel example effect starts every character at the canvas's left edge, on its own row, and moves it home. Its
code is shown in the [Motion](motion.md) chapter. Here it runs on a canvas larger than the text, with the text
centered:

```bash
tte --input-file concept_input.txt --canvas-width 60 --canvas-height 14 --anchor-text c travel
```



The characters start at column 1, the canvas's left edge, and cross the empty part of the canvas to reach the text.

## Picking positions

| Method | Returns |
| --- | --- |
| `canvas.random_coord()` | A random cell on the canvas. |
| `canvas.random_coord(within_text_boundary=True)` | A random cell inside the text bounds. |
| `canvas.random_coord(outside_scope=True)` | A random cell one step outside one of the four canvas edges. |
| `canvas.random_column()`, `canvas.random_row()` | A random column or row, with the same `within_text_boundary` option. |
| `canvas.coord_is_in_canvas(coord)` | Whether `coord` is on the canvas. |
| `canvas.coord_is_in_text(coord)` | Whether `coord` is inside the text bounds. |

Characters can move outside the canvas. They aren't drawn while they are outside it, which lets an effect bring
characters in from beyond the edge. `outside_scope=True` is a convenient starting point for that.

The [geometry utilities](../../engine/utils/geometry.md) find coordinates on circles, rectangles, lines, and bezier
curves, for effects that arrange characters in shapes.

## Distances are measured on screen

A terminal cell is about twice as tall as it is wide. TTE accounts for this when it measures how far apart two
coordinates are: one row counts as two columns. A character moving one row up travels as far, on screen, as one moving
two columns sideways. Speeds and the circular character orders use this screen distance, so motion looks even in
every direction.

## Where to go next

- [Motion](motion.md) moves characters between coordinates.
- [Tutorial step 3: Move](../tutorial/03-move.md) starts each character at a canvas edge.

---

### Effectguide Engine Characters (`docs/effectguide/engine/characters.md`)

# Characters

Everything you see in an effect is an [`EffectCharacter`](../../engine/basecharacter.md): one symbol, at one position,
with its own motion and animation. This chapter covers what a character knows about itself, how to get the
characters you need, and the state that decides how each one is drawn.

## What a character knows

The terminal creates one character for every non-space symbol of the input. Spaces don't become characters; the empty
cells they leave are covered under [Fill characters](#fill-characters) below.

| Attribute | Meaning |
| --- | --- |
| `input_symbol` | The symbol from the input, such as `"T"`. |
| `input_coord` | The character's home position in the text, as a `Coord(column, row)`. |
| `character_id` | A unique number, assigned in creation order. |
| `motion` | Moves the character along paths. See [Motion](motion.md). |
| `animation` | Changes the character's symbol and colors. See [Animation](animation.md). |
| `event_handler` | Runs actions when paths and scenes start or finish. See [Events](events.md). |
| `is_visible` | Whether the terminal draws the character. Read-only; change it through the terminal. |
| `is_active` | Whether the character still has an unfinished path or scene. |
| `layer` | Drawing order when characters overlap. |

`input_coord` never changes. The character's current position is `character.motion.current_coord`, which starts at
`input_coord` and changes as the character moves.

## Getting characters

Ask the terminal for characters; never build `EffectCharacter` objects yourself.

| Method | Returns |
| --- | --- |
| `terminal.get_characters()` | A flat list of characters, top to bottom and left to right unless you pass `order`. |
| `terminal.get_characters_grouped(order=...)` | A list of groups, such as one list per row. |
| `terminal.get_character_by_input_coord(coord)` | The character whose home is `coord`, or `None`. |
| `terminal.add_character(symbol, coord)` | A new character that isn't part of the input. See [Added characters](#added-characters). |

Both `get_characters()` and `get_characters_grouped()` accept the same ordering options:

- `order`: a [`CharacterOrder`](../../engine/utils/argutils.md), or its lowercase name such as `"row_top_to_bottom"`.
- `reverse`: reverse the whole traversal.
- `serpentine`: reverse every other group, so rows alternate left-to-right and right-to-left.
- `input_chars`, `inner_fill_chars`, `outer_fill_chars`, `added_chars`: which kinds of character to include. By
  default, only input characters are returned.

Some orders are **spatial**: they form groups of several characters, such as rows, diagonals, or rings around the
center. Others, such as spirals and `random`, are **individual**: each group holds one character. Flattening the
groups gives the same order as `get_characters()` with the same options.

### Seeing the orders

The Reveal example effect shows one group per frame, so you can watch how an order traverses the text. Save it as
`reveal.py` in your community effects directory:

```python title="reveal.py"
--8<-- "docs/effectguide/examples/reveal/reveal.py"
```

Then save this text as `concept_input.txt`, the input used by every example in the engine chapters:

```text title="concept_input.txt"
--8<-- "docs/effectguide/examples/concept_input.txt"
```

Each command below sets a frame rate so the reveal is slow enough to follow; the animation beside it is what the
command shows.

=== "Rows"

    ```bash
    tte --input-file concept_input.txt --frame-rate 8 reveal --order row_top_to_bottom
    ```

    

=== "Diagonals"

    ```bash
    tte --input-file concept_input.txt --frame-rate 30 reveal --order diagonal_top_left_to_bottom_right
    ```

    

=== "Rings"

    ```bash
    tte --input-file concept_input.txt --frame-rate 15 reveal --order circle_center_to_outside
    ```

    

=== "Spiral"

    ```bash
    tte --input-file concept_input.txt --frame-rate 60 reveal --order spiral_clockwise
    ```

    

`tte reveal -h` lists every order.

## Visibility, activity, and layers

Three pieces of state decide how the terminal treats a character each frame:

- **Visible**: `terminal.set_character_visibility(character, is_visible=True)` makes the terminal draw the character.
  Every character starts hidden.
- **Active**: characters in your iterator's `active_characters` set are ticked every frame until they finish. See
  [Active and visible are separate](frame-loop.md#active-and-visible-are-separate).
- **Layer**: when two visible characters occupy the same cell, the one on the higher layer is drawn on top. On the
  same layer, the character with the higher `character_id` wins. Every character starts on layer 0; set
  `character.layer` to change it.

Layers matter as soon as characters move: a character flying across the text overlaps characters that have already
settled. Spray puts moving characters on layer 1 and settled ones back on layer 0, so the flying ones pass in front.

## Fill characters

Cells without a character, such as spaces between words and the area around the text, can be filled with **fill
characters** when an effect needs to animate them. Fill characters have a space as their symbol.

- **Inner fill** characters cover empty cells inside the text's bounding box.
- **Outer fill** characters cover the rest of the canvas.

Pass `inner_fill_chars=True` or `outer_fill_chars=True` to `get_characters()` to include them. They are created only
when first requested, so effects that don't use them pay nothing.

## Added characters

`terminal.add_character(symbol, coord)` creates a character that isn't part of the input, such as a spark or a
falling drop. It starts hidden and inactive like any other character, and `coord` becomes its `input_coord`. Added
characters aren't returned by `get_character_by_input_coord()` and don't appear in `get_characters()` unless you pass
`added_chars=True`.

When an effect creates and recycles many short-lived characters, use a
[`ParticlePool`](../../engine/particles.md) instead of adding new ones every frame.

## Neighbors

`character.neighbors` maps `"north"`, `"east"`, `"south"`, and `"west"` to the adjacent character, or `None` at the
edge of the canvas. Reading it makes the terminal create the fill characters it needs to complete the grid, so empty
cells have neighbors too. Spanning-tree effects use neighbors to grow paths through the text.

## Where to go next

- [Tutorial step 2: Release](../tutorial/02-release.md) reveals characters a few at a time with visibility and
  `active_characters`.
- [Canvas and coordinates](canvas.md) explains `Coord` and the canvas the characters live on.

---

### Effectguide Engine Color (`docs/effectguide/engine/color.md`)

# Color

Frames, appearances, and gradients all take colors. This chapter covers the color classes, gradients, coloring the
text by position, and colors that come with the input.

## Colors and color pairs

A [`Color`](../../engine/utils/color.md) is either an RGB hex string or an XTerm-256 color number:

```python
from terminaltexteffects import Color, ColorPair

orange = Color("#ff8000")  # The leading # is optional.
gray = Color(245)          # XTerm-256 color 245.
```

`Color` objects are immutable and can be compared and used as dictionary keys. Invalid values raise `ValueError`.

A character has a foreground and a background color, so frames and appearances take a
[`ColorPair`](../../engine/utils/colorpair.md):

```python
ColorPair(fg=orange)            # Orange text on the terminal's background.
ColorPair(fg=gray, bg="#000080")  # Plain values are converted to Color.
```

`None` for either color means the terminal's default color.

You don't need to handle how colors reach the terminal. If the user passes `--xterm-colors`, TTE converts RGB colors to
the nearest XTerm-256 color, and with `--no-color` it leaves out colors entirely. Effects write the same code either
way.

## Gradients

A [`Gradient`](../../engine/utils/gradient.md) is a list of colors that blends from one **stop** to the next:

```python
from terminaltexteffects import Gradient

gradient = Gradient(Color("#ff0080"), Color("#ffd000"), Color("#00d0ff"), steps=12)
```

`steps` is the number of transitions between each pair of neighboring stops. This gradient has 12 between magenta and
yellow and 12 more between yellow and cyan, so its `spectrum`, the list of colors, has 25 colors: 12 + 12, plus the
final stop. Pass a tuple, such as `steps=(4, 12)`, to give each pair its own count, and `loop=True` to blend from the
last stop back to the first.

A gradient works like a list: iterate over it, index it, or take `len(gradient)`. To pick a color by position instead
of index, `gradient.get_color_at_fraction(0.5)` returns the color halfway along.

Gradients have two main uses in effects:

- **Over time**, as the colors of a scene's frames. `scene.apply_gradient_to_symbols()` turns a gradient into frames;
  see [Gradients across symbols](animation.md#gradients-across-symbols).
- **Over space**, as a color for each position in the text, described next.

## Coloring the text by position

Most effects end with the text in a gradient that runs across it. `build_coordinate_color_mapping()` assigns a
color from the gradient to every coordinate in an area:

```python
canvas = self.terminal.canvas
color_mapping = gradient.build_coordinate_color_mapping(
    canvas.text_bottom,
    canvas.text_top,
    canvas.text_left,
    canvas.text_right,
    Gradient.Direction.VERTICAL,
)
color = color_mapping[character.input_coord]
```

The arguments are the bottom and top rows, then the left and right columns, of the area to color, followed by a
direction. Use the text bounds, so the gradient spans the text whatever the canvas size, and look up each character by
its `input_coord`.

### The Paint example

Paint shows the four directions. Every character fades from dark gray to its color in the gradient above. Save it as
`paint.py` in your community effects directory:

```python title="paint.py"
--8<-- "docs/effectguide/examples/paint/paint.py"
```

Each character's scene is built from a second, two-stop gradient, from dark gray to the character's color. A string
works as the symbol sequence for `apply_gradient_to_symbols()`, so `character.input_symbol` gives every frame the
input symbol.

=== "vertical"

    ```bash
    tte --input-file concept_input.txt paint --direction vertical
    ```

    

    The first stop is at the bottom row and the last at the top, since rows count upward.

=== "horizontal"

    ```bash
    tte --input-file concept_input.txt paint --direction horizontal
    ```

    

    The first stop is at the left column and the last at the right.

=== "radial"

    ```bash
    tte --input-file concept_input.txt paint --direction radial
    ```

    

    The first stop is at the center and the last at the corners.

=== "diagonal"

    ```bash
    tte --input-file concept_input.txt paint --direction diagonal
    ```

    

    The first stop is at the bottom left corner and the last at the top right.

## Colors in the input

The input can contain its own colors, as ANSI escape sequences, for example when another program's colored output is
piped into `tte`. Each input character records them in `character.animation.input_fg_color` and
`character.animation.input_bg_color`, which are `None` when the character had no color.

The user decides what happens to input colors with the terminal option `--existing-color-handling`:

| Value | What happens |
| --- | --- |
| `ignore` | The default. Input colors are dropped, and the effect's colors are used. |
| `always` | Input colors replace the effect's colors on every frame and appearance. Characters without input colors are shown with no color. |
| `dynamic` | The effect decides. Input colors are available, but the engine applies none of them. |

With `ignore` and `always`, your effect needs no code: the engine handles both. With `dynamic`, check the setting
and use the input colors where they make sense. Paint uses a character's input color as the color to fade to,
when it has one:

```python
dynamic = self.terminal.config.existing_color_handling == "dynamic"
...
if dynamic and character.animation.input_fg_color:
    color = character.animation.input_fg_color
```

To see the difference, create an input file whose first word on each line is red, green, and blue, and whose second
word has no color:

```bash
--8<-- "docs/effectguide/examples/color_input.sh"
```

Then run Paint with each setting:

=== "ignore"

    ```bash
    tte --input-file color_input.txt --existing-color-handling ignore paint
    ```

    

    The input colors are dropped, so the output matches the vertical demo above.

=== "always"

    ```bash
    tte --input-file color_input.txt --existing-color-handling always paint
    ```

    

    Every frame of the fade is replaced with the input colors, so nothing changes from one frame to the next: the
    text appears in its input colors, and the second word, which has none, has no color.

=== "dynamic"

    ```bash
    tte --input-file color_input.txt --existing-color-handling dynamic paint
    ```

    

    Paint fades the first word in to its input colors, and the second word, which has none, to the gradient.

Built-in effects differ in what they do with uncolored characters in `dynamic` mode; Paint falls back to its gradient.
Whatever you choose, say so in your effect's documentation.

## Where to go next

- [Tutorial step 5: Color](../tutorial/05-color.md) gives MySpray a final gradient and a scene that fades each character into it.
- [Animation](animation.md) covers the scenes that colors are shown in.

---

### Effectguide Engine Configuration (`docs/effectguide/engine/configuration.md`)

# Configuration and the CLI

An effect's options are declared once, as fields of its configuration dataclass. From that one declaration TTE builds
the `tte <effect>` command-line options, their help text and validation, and the defaults used when the effect runs
from Python. This chapter explains how a field is declared and how its value reaches your iterator.

## The configuration class

A configuration class is a dataclass that subclasses [`BaseConfig`](../../engine/baseconfig.md):

```python
@dataclass
class GlowConfig(BaseConfig):
    """Configuration for the Glow effect."""

    parser_spec = argutils.ParserSpec(
        name="glow",
        help="Characters slide home, heating from a red spark to white text.",
        description="glow | Characters slide home, heating from a red spark to white text.",
        epilog="Example: tte glow --sync distance",
    )
    frame_duration: int = argutils.ArgSpec(
        name="--frame-duration",
        type=argutils.PositiveInt.type_parser,
        default=3,
        metavar=argutils.PositiveInt.METAVAR,
        help="Ticks to show each frame of the scene. Ignored when the scene is synced.",
    )  # pyright: ignore[reportAssignmentType]
    "int : Ticks to show each frame of the scene. Ignored when the scene is synced."
```

`parser_spec` describes the subcommand. Its fields are passed to argparse:

| Field | Shown |
| --- | --- |
| `name` | The command, as in `tte glow`. It must match the name `get_effect_resources()` returns. |
| `help` | The one-line summary in the effect list of `tte -h`. |
| `description` | The heading of `tte glow -h`. Built-in effects use `"<name> \| <summary>"`. |
| `epilog` | The text at the end of `tte glow -h`, usually an example command. |

Every other field is an option. Its default value is an [`ArgSpec`](../../engine/utils/argutils.md), and its type
annotation is the type of the value your iterator receives.

The `# pyright: ignore[reportAssignmentType]` comment is needed because the field is annotated as `int` but assigned
an `ArgSpec`; `BaseConfig` replaces the `ArgSpec` with its default when the configuration is created. The string after
the field is its documentation. Built-in effects write it as `"<type> : <help>"` so it appears in the generated
Reference pages.

## Declaring an option

`ArgSpec` takes the arguments of argparse's `add_argument()`:

| Argument | Use |
| --- | --- |
| `name` | The option, such as `"--frame-duration"`. The field name must be the option name in snake case, `frame_duration`, because argparse stores the value under that name. |
| `default` | The value used when the option isn't given. It must already be in its final form, such as `Color("#ffffff")` rather than `"ffffff"`. |
| `type` | A function that turns the command-line string into the value and rejects invalid input. See [Validators](#validators). |
| `metavar` | The placeholder for the value in help, such as `(int > 0)`. Each validator provides one as `METAVAR`. |
| `help` | The option's description. The default is appended automatically, as `(default: 3)`. |
| `choices` | A list of allowed values. Help shows them instead of a metavar. |
| `action` | `"store_true"` for an on/off flag, or `argutils.TupleAction` for an option that takes several values. |
| `nargs` | With `TupleAction`, `"+"` for one or more values. |

A few common shapes:

=== "Choice"

    ```python
    sync: typing.Literal["none", "distance", "step"] = argutils.ArgSpec(
        name="--sync",
        default="none",
        choices=["none", "distance", "step"],
        help="Sync the scene to the character's path.",
    )  # pyright: ignore[reportAssignmentType]
    ```

    The value is the chosen string. Map it to what your effect needs in the iterator, as Glow does with its
    `SYNC_METRICS` dictionary.

=== "Flag"

    ```python
    curve: bool = argutils.ArgSpec(
        name="--curve",
        default=False,
        action="store_true",
        help="Start in the bottom left corner and curve home through the bottom right corner.",
    )  # pyright: ignore[reportAssignmentType]
    ```

    The value is `True` when the option is given. This is Travel's option from the [Motion](motion.md#curves) chapter.

=== "Several values"

    ```python
    final_gradient_steps: tuple[int, ...] = argutils.ArgSpec(
        name="--final-gradient-steps",
        type=argutils.PositiveInt.type_parser,
        nargs="+",
        action=argutils.TupleAction,
        default=(12,),
        metavar=argutils.PositiveInt.METAVAR,
        help="Number of steps between each pair of final gradient stops.",
    )  # pyright: ignore[reportAssignmentType]
    ```

    `TupleAction` stores the values as a tuple. `type` checks each value separately.

### Validators

[`argutils`](../../engine/utils/argutils.md) has a validator class for each common kind of value. Pass its
`type_parser` as `type` and its `METAVAR` as `metavar`:

| Class | Accepts | Value |
| --- | --- | --- |
| `PositiveInt`, `NonNegativeInt` | `5` | `int` above 0, or 0 and above |
| `PositiveFloat`, `NonNegativeFloat` | `0.5` | `float` above 0, or 0 and above |
| `PositiveRatio`, `NonNegativeRatio` | `0.25` | `float` above 0, or 0 and above, and at most 1 |
| `PositiveIntRange`, `PositiveFloatRange` | `2-8`, `0.6-1.4` | `tuple` of two numbers |
| `ColorArg` | `ff8000`, `208` | `Color` |
| `GradientDirection` | `radial` | `Gradient.Direction` |
| `Ease` | `out_expo` | an easing function |
| `Symbol` | `*` | a one-character `str` |
| `CharacterOrderArg` | `row_top_to_bottom` | `CharacterOrder` |

A validator raises `argparse.ArgumentTypeError` for bad input, and argparse prints the message and exits:

```text
tte myspray: error: argument --spray-volume: invalid value: '2' must be 0 < n <=1. Example: 0.5
```

To write your own, pass any function that takes the command-line string and returns the value, raising
`argparse.ArgumentTypeError` when the string isn't valid. The same function also checks values set from Python, so it
must accept a value that is already in its final form, too: `PositiveInt.type_parser` accepts both `"5"` and `5`.

### The shared final gradient options

Most built-in effects end with the text in a gradient, and they share three option declarations from
[`terminaltexteffects.engine.base_config`](../../engine/baseconfig.md): `FinalGradientStopsArg`, `FinalGradientStepsArg`,
and `FinalGradientDirectionArg`. Each is an `ArgSpec` with the name, validator, and help already filled in. Pass a
`default` to change only the default:

```python
final_gradient_stops: tuple[Color, ...] = FinalGradientStopsArg(
    default=(Color("#8A008A"), Color("#00D1FF"), Color("#FFFFFF")),
)  # pyright: ignore[reportAssignmentType]
```

Using them gives your effect the same `--final-gradient-*` options users know from the built-in effects.

## Reading the configuration

The iterator receives the configuration as `self.config`, with every option already converted and validated. The
user's terminal options, such as `--existing-color-handling` and `--frame-rate`, are in `self.terminal.config`.

```python
spray_origin = spray_origin_map[self.config.spray_position]
dynamic = self.terminal.config.existing_color_handling == "dynamic"
```

Read options as you build the effect. Don't change them from the iterator.

## Configuration from Python

An effect can also run from Python, without the CLI. The configuration is then created with the `ArgSpec` defaults,
and the caller can change any field before iterating:

```python
effect = MySpray(text)
effect.effect_config.spray_position = "center"
effect.effect_config.movement_speed_range = "0.2-0.5"  # Converted to (0.2, 0.5).
```

Assigning a field runs it through the same `type` and `choices` checks as the CLI, so it converts command-line
spellings and raises `ValueError` for invalid values. This is why the `type` function must accept values in their final
form as well as strings. The [Library Guide](../../libguide.md) covers running effects from Python.

## Shell completion

The completion scripts that `tte --print-completion` prints are bundled with TTE and list only the built-in effects.
They don't complete the commands or options of community effects.

## Where to go next

- [Tutorial step 6: Configure](../tutorial/06-configure.md) turns MySpray's fixed values into options.
- The [`argutils` Reference](../../engine/utils/argutils.md) lists every validator.

---

### Effectguide Engine Events (`docs/effectguide/engine/events.md`)

# Events

Paths and scenes run on their own once activated, but effects often need something to happen at a particular moment:
when a character lands, start a scene; when a scene ends, start the next path. Each character's
[`EventHandler`](../../engine/eventhandler.md), `character.event_handler`, runs **actions** when **events** happen to
that character's paths, waypoints, and scenes.

## Registering an event

```python
from terminaltexteffects import EventHandler

character.event_handler.register_event(
    EventHandler.Event.PATH_COMPLETE,  # when this happens...
    fall,                              # ...to this path, waypoint, or scene...
    EventHandler.Action.SET_LAYER,     # ...do this...
    0,                                 # ...with this target.
)
```

`terminaltexteffects` also exports the enums as `Event` and `Action`, so you can write `Event.PATH_COMPLETE`.

You register events once, during setup. They then fire every time their event happens, for the rest of the effect.
Several actions can be registered for the same event; they run in the order you registered them.

## Events

| Event | Fires when | Caller |
| --- | --- | --- |
| `PATH_ACTIVATED` | A path is activated. | `Path` |
| `SEGMENT_ENTERED` | The character starts the segment that ends at a waypoint. | `Waypoint` |
| `SEGMENT_EXITED` | The character reaches a waypoint. | `Waypoint` |
| `PATH_HOLDING` | The character reaches the end of a path with `hold_time`, before holding. | `Path` |
| `PATH_COMPLETE` | A path finishes, after any hold. | `Path` |
| `SCENE_ACTIVATED` | A scene is activated. | `Scene` |
| `SCENE_COMPLETE` | A scene finishes. | `Scene` |

The caller is the object the event is about. For path and scene activation and completion, you can pass the path or
scene's ID string instead of the object.

Activation events fire **immediately** when the path or scene is activated, even during setup and even if the
character isn't active yet. Register events for a path or scene before you activate it, or the activation event is
already over.

Segment events fire once per traversal of the segment. A looping path fires them again on each loop.

## Actions

| Action | Target | Does |
| --- | --- | --- |
| `ACTIVATE_PATH` | `Path` or path ID | Starts that path from the character's current position. |
| `DEACTIVATE_PATH` | `Path`, path ID, or `None` | Stops that path, or whichever path is active. |
| `ACTIVATE_SCENE` | `Scene` or scene ID | Starts that scene. |
| `DEACTIVATE_SCENE` | `Scene`, scene ID, or `None` | Stops that scene, or whichever scene is active. |
| `SET_LAYER` | `int` | Moves the character to that layer. |
| `SET_COORDINATE` | `Coord` | Moves the character to that position instantly. |
| `RESET_APPEARANCE` | none | Restores the input symbol and its input colors. |
| `CALLBACK` | `EventHandler.Callback` | Calls your function. |

### Callbacks

When no built-in action fits, use a callback. `EventHandler.Callback(function, *args)` calls
`function(character, *args)` when the event fires:

```python
def hide(character, terminal):
    terminal.set_character_visibility(character, is_visible=False)

character.event_handler.register_event(
    Event.PATH_COMPLETE, fall, Action.CALLBACK, EventHandler.Callback(hide, self.terminal)
)
```

Callbacks can do anything, including activating other paths. Keep them short: they run inside the character's tick.

## The Hop example

Hop makes each column of text hop to the top of the canvas and fall back, one column after another. Events do all the
sequencing:

- `PATH_ACTIVATED` on the rise sets layer 1, so rising characters pass in front.
- `PATH_HOLDING` on the rise calls a callback that changes the symbol to `*` while the character waits at the top.
- `PATH_COMPLETE` on the rise activates the fall path.
- `PATH_COMPLETE` on the fall restores the input symbol and sets layer 0.

The iterator only activates each column's rise. Save it as `hop.py` in your community effects directory:

```python title="hop.py"
--8<-- "docs/effectguide/examples/hop/hop.py"
```

```bash
tte --input-file concept_input.txt hop
```



## When events fire in a tick

During a character's tick, motion moves first and animation steps second (see
[Ticking a character](frame-loop.md#ticking-a-character)), and each fires its own events as it goes, so path events
for a tick run before scene events for that tick.

An action that activates a path or scene changes what the character is doing straight away. The engine stops
processing the old path for the rest of that tick, so the new path starts moving on the next tick.

## Where to go next

- [Tutorial step 4: Layer](../tutorial/04-layer.md) uses `PATH_ACTIVATED` and `PATH_COMPLETE` to change layers.
- [Motion](motion.md#sequences-of-paths) shows `chain_paths()`, a shortcut for `PATH_COMPLETE` and `ACTIVATE_PATH`.

---

### Effectguide Engine Frame Loop (`docs/effectguide/engine/frame-loop.md`)

# Frame loop

An effect is an iterator: each call to `next()` advances the animation by one **tick** and returns one **frame**, the
whole canvas rendered as a string. This chapter explains what happens during that call and how an effect decides when
it is finished.

## One frame, step by step

``` mermaid
sequenceDiagram
    participant Caller as tte or your code
    participant It as Your iterator
    participant Ch as Active characters
    participant T as Terminal
    Caller->>It: next()
    It->>It: effect logic (release characters, change phase)
    It->>Ch: update(): tick each active character
    Ch->>Ch: motion.move(), then animation.step_animation()
    It->>It: drop characters that are no longer active
    It->>T: frame: wait for the frame rate, then render
    T-->>It: frame string
    It-->>Caller: frame string
```

Most `__next__` methods follow the same shape. This one is from the built-in Spray effect:

```python
def __next__(self) -> str:
    if self.pending_chars or self.active_characters:
        # Effect logic: release a few more characters this frame.
        ...
        self.update()
        return self.frame
    raise StopIteration
```

1. **Effect logic.** Your code decides what changes this frame: which characters appear, which phase the effect is
   in, what new paths or scenes to start. Keep it cheap, since it runs every frame; do expensive setup once, when the
   iterator is created.
2. **`self.update()`** ticks every character in `self.active_characters`, then removes the ones that have finished.
3. **`self.frame`** waits until the next frame is due, then asks the terminal to render the canvas.
4. **`StopIteration`** ends the effect once nothing is left to do.

## Ticking a character

[`EffectCharacter.tick()`](../../engine/basecharacter.md) does two things, in this order:

1. `motion.move()` moves the character one step along its active path, if it has one, and fires path events such
   as `PATH_COMPLETE`.
2. `animation.step_animation()` advances its active scene, if it has one, and fires scene events such as
   `SCENE_COMPLETE`.

Motion runs first so that a scene synced to movement sees this tick's position.

A character is **active** while it has an unfinished path or an unfinished scene. `update()` checks this after every
tick and removes characters that are no longer active from `active_characters`. A character with a looping path or
looping scene never finishes on its own, so it stays active until your code deactivates it.

## Active and visible are separate

Two independent flags decide what a character does:

| | Visible | Hidden |
| --- | --- | --- |
| **Active** | Moves and changes, and is drawn. The usual state while animating. | Moves and changes, but isn't drawn. Useful for staging a character before it appears. |
| **Inactive** | Drawn as it last looked, without changing. The usual state once a character has settled. | Neither drawn nor updated. Every character starts this way. |

- Make a character visible with `self.terminal.set_character_visibility(character, is_visible=True)`.
- Make it active by adding it to `self.active_characters`. `update()` removes it again when it finishes.

Forgetting either step is the most common reason a new effect shows nothing, or shows characters that never move.

## Frame rate and time

`self.frame` paces output to `TerminalConfig.frame_rate`, 60 frames per second by default. It sleeps if the
previous frame was returned less than one frame period ago, then renders. A frame rate of `0` turns pacing off.

Because pacing happens inside `self.frame`, read it once per `next()` call. Your effect logic should count in ticks
rather than seconds: "release 5 characters per frame" or "hold for 30 frames". When a phase must last a fixed time,
measure it with `self.terminal.clock()` instead of the system clock. During recording, `tte --export` runs the clock
one frame period per frame, so the phase lasts the same number of frames however fast the machine renders.

## Ending the effect

Raise `StopIteration` when there is nothing left to show. Typical conditions are:

- No characters are waiting to be released and none are active, as in Spray.
- The effect's last phase has finished.

Effects that loop forever never raise `StopIteration`. They are valid, and the library guide shows how to
[stop them](../../libguide.md#infinitely-looping-effects); give them an option that limits the number of loops.

## Who prints the frames

Your iterator only returns strings. Printing them is the caller's job:

```python
with effect.terminal_output() as terminal:
    for frame in effect:
        terminal.print(frame)
```

[`terminal_output()`](../../engine/baseeffect.md#terminaltexteffects.engine.base_effect.BaseEffect.terminal_output)
makes room for the canvas and hides the cursor, and restores the cursor when the loop ends, even after an error.
The `tte` command runs this same loop, so you don't need to handle terminal setup in your effect.

## Where to go next

- [Tutorial step 1: Scaffold](../tutorial/01-scaffold.md) writes a `__next__` method that returns a single frame.
- [Architecture](architecture.md) shows how the iterator fits with the other parts of an effect.

---

### Effectguide Engine Loading (`docs/effectguide/engine/loading.md`)

# Discovery and loading

Every time `tte` starts, it finds the available effects, imports their modules, and builds a subcommand for each one.
This chapter explains where TTE looks, what a module needs to be loaded, and what happens when loading fails.

## What makes a module an effect

TTE treats a Python module as an effect if it has a module-level `get_effect_resources()` function that returns three
things:

```python
def get_effect_resources() -> tuple[str, type[BaseEffect], type[BaseConfig]]:
    return "myspray", MySpray, MySprayConfig
```

1. The command name, as typed after `tte`. It must match `parser_spec.name` in the configuration class.
2. The effect class, a subclass of `BaseEffect`.
3. The configuration class, a subclass of `BaseConfig`.

TTE adds the command, with options built from the configuration class (see
[Configuration and the CLI](configuration.md)). When the user runs the command, TTE creates the effect class with the
input text and the parsed configuration, and prints the frames its iterator produces. A module without
`get_effect_resources()` is imported but adds no command.

## Where TTE looks

TTE loads effects from three places, in this order:

| Where | What is loaded |
| --- | --- |
| The `terminaltexteffects.effects` package | The built-in effects. |
| The community effects directory | Every `.py` file in `${XDG_CONFIG_HOME:-$HOME/.config}/terminaltexteffects/effects`. |
| `TTE_DEV_EFFECTS_DIR`, if set | Every `.py` file in that directory. For development; see below. |

Files are loaded in name order. Subdirectories and `__init__.py` are ignored, so a community effect is a single flat
`.py` file. If `XDG_CONFIG_HOME` is unset, empty, or a relative path, TTE uses `~/.config`.

Every module is imported on every run, even when the user runs a different effect, because TTE needs every
configuration class to build its command-line parser. Keep module-level code to imports and definitions: do the
effect's work in the iterator, and avoid slow imports at the top of the module.

## Community effects

The community effects directory is where users install effects, and where you develop yours. Each file is imported
under its file name, so `myspray.py` becomes the module `myspray`. A module in that directory can import sibling files
that were loaded before it by name, but keeping an effect in one file makes it easier to share.

TTE protects the rest of the CLI from a broken community effect. If a module fails to import, raises an error from
`get_effect_resources()`, or registers a command that already exists, TTE prints a warning naming the file to standard
error, skips that module, and runs normally:

```text
Warning: Skipping user effect plugin '/home/me/.config/terminaltexteffects/effects/broken.py': ModuleNotFoundError: No module named 'not_a_real_package'
Warning: Skipping user effect plugin '/home/me/.config/terminaltexteffects/effects/dupe.py': ValueError: Duplicate effect command detected: spray
Warning: Skipping user effect plugin '/home/me/.config/terminaltexteffects/effects/random.py': ImportError: Module name 'random' is already used by an installed or loaded module; rename the file
```

The last warning shows a naming rule: a file named like a standard-library or installed module is skipped, rather than
replacing that module. Name your file after your effect, and pick a command name no built-in effect uses. Built-in
effects are loaded first, so they always win a conflict, which is why the tutorial's command is `myspray` and not
`spray`.

If your effect doesn't appear in `tte -h`, look for one of these warnings at the top of the output.

## Development effects

`TTE_DEV_EFFECTS_DIR` points TTE at a directory of effect modules in progress, without copying them into the community
effects directory:

```bash
TTE_DEV_EFFECTS_DIR="$PWD/my_effects" tte --input-file concept_input.txt glow
```

Development modules follow the same rules, with two differences meant to surface problems early:

- A failure stops `tte` with the error, instead of a warning. The [interactive interface](../../tui.md#developing-effects)
  lists the module in red instead, and imports it again when you save it.
- Each module is imported under a private prefixed name, so development modules can't import each other by file name.

The variable must name an existing directory. In a TTE repository checkout, maintainers put unfinished built-in effects
in `dev_effects/` and run them with `python -m tools.dev <effect>`, which sets this variable for that directory.

## Where to go next

- [Tutorial step 1: Scaffold](../tutorial/01-scaffold.md) creates and loads the first version of MySpray.
- [Configuration and the CLI](configuration.md) covers the configuration class that `get_effect_resources()` returns.

---

### Effectguide Engine Motion (`docs/effectguide/engine/motion.md`)

# Motion

Each character moves with its own [`Motion`](../../engine/motion/motion.md) object, `character.motion`. You give it
**paths**, each a list of **waypoints**, and activate one; the character then moves a little along it every tick until
it reaches the end. This chapter covers building paths and controlling how characters travel along them.

## A first path

```python
path = character.motion.new_path(speed=0.5)
path.new_waypoint(character.input_coord)
character.motion.activate_path(path)
```

- `new_path()` creates an empty [`Path`](../../engine/motion/path.md) owned by this character.
- `new_waypoint()` adds a [`Waypoint`](../../engine/motion/waypoint.md), a coordinate to travel to. A path can have
  any number of waypoints; the character visits them in order.
- `activate_path()` starts the path **from wherever the character is now**. The engine adds a segment from the
  character's current position to the first waypoint, so a path doesn't need a starting waypoint.

To start somewhere other than the character's home, move it first with
`character.motion.set_coordinate(coord)`. The character's position is `character.motion.current_coord`.

The path runs once the character is in your iterator's `active_characters`. When the character reaches the last
waypoint, the path completes and the character becomes inactive, unless it also has a scene playing.

## The Travel example

The examples in this chapter use the Travel effect. It moves every character from the canvas's left edge to its home,
with options for speed, easing, and a curved path. Save it as `travel.py` in your community effects directory:

```python title="travel.py"
--8<-- "docs/effectguide/examples/travel/travel.py"
```

With the default options, every character moves at a constant speed, so characters that start farther from home
arrive later:

```bash
tte --input-file concept_input.txt travel
```



## Speed and duration

`speed` is the distance a character covers per tick. The number of ticks a path takes is its length divided by its
speed, rounded: a path 20 cells long at speed 0.5 takes 40 frames, or two-thirds of a second at 60 frames per second.

Lengths are measured on screen, so one row counts as two columns (see
[Distances are measured on screen](canvas.md#distances-are-measured-on-screen)). Because the length includes the
segment from the character's starting position, characters with different distances to travel take different times
at the same speed. To make characters arrive together, give each a speed proportional to its distance.

## Easing

An easing function changes how progress along the path is spread over its duration. With `ease=None`, the default,
the character moves the same distance every tick. With an easing function, it can start slowly, finish slowly, or
overshoot and settle. The path's duration doesn't change.

```python
from terminaltexteffects import easing

path = character.motion.new_path(speed=0.5, ease=easing.out_expo)
```

=== "out_expo"

    ```bash
    tte --input-file concept_input.txt travel --ease out_expo
    ```

    

=== "in_out_quart"

    ```bash
    tte --input-file concept_input.txt travel --ease in_out_quart
    ```

    

=== "out_bounce"

    ```bash
    tte --input-file concept_input.txt travel --ease out_bounce
    ```

    

The [`easing`](../../engine/utils/easing.md) module has `in_`, `out_`, and `in_out_` variants of the sine, quad,
cubic, quart, quint, expo, circ, back, elastic, and bounce curves, plus `linear`. `easing.make_easing(x1, y1, x2, y2)`
builds a custom cubic bezier curve, like CSS `cubic-bezier()`.

Easing applies to the whole path, not each segment. On a path with several waypoints, an `out_` easing slows the
character down near the last waypoint only.

## Curves

Give a waypoint `bezier_control` to curve the segment that ends at it. One control point makes a quadratic curve;
a tuple of two makes a cubic curve. The character doesn't pass through the control point; the curve bends toward it.

```python
path.new_waypoint(character.input_coord, bezier_control=Coord(canvas.right, canvas.bottom))
```

With `--curve`, Travel starts every character in the bottom left corner and bends its path toward the bottom right
corner:

```bash
tte --input-file concept_input.txt travel --curve
```



## More path options

`new_path()` takes these keyword arguments:

| Argument | Effect |
| --- | --- |
| `speed` | Distance per tick. Must be greater than 0. |
| `ease` | Easing function for the whole path. |
| `layer` | Layer to put the character on when the path is activated. `None` leaves the layer unchanged. |
| `hold_time` | Number of ticks to stay at the last waypoint before the path completes. |
| `loop` | After reaching the last waypoint, travel back to the first and repeat forever. A path needs at least two waypoints to loop. |
| `path_id` | A name for the path, so you can find it with `motion.query_path(path_id)`. One is generated if you don't give one. |

A character has at most one active path. Activating another path replaces the current one, starting from wherever the
character is. `motion.deactivate_path()` stops the active path without completing it, and
`motion.movement_is_complete()` reports whether no path is active.

## Sequences of paths

To run paths one after another, chain them:

```python
character.motion.chain_paths([rise, drift, land])
```

`chain_paths()` registers events so that each path activates the next one when it completes; pass `loop=True` to
return from the last to the first. Chaining is a shortcut for the event handling described in the Events chapter, which
can also start scenes, change layers, or run your own code when a path starts, completes, or reaches a waypoint.

## Where to go next

- [Tutorial step 3: Move](../tutorial/03-move.md) gives MySpray's characters paths from a canvas edge to their homes.
- [Canvas and coordinates](canvas.md) explains where coordinates come from.

---

### Effectguide Engine Performance (`docs/effectguide/engine/performance.md`)

# Performance

TTE aims for 60 frames per second. Each frame has about 16 milliseconds for your `__next__`, the character updates,
and drawing the frame. If a frame takes longer, the animation slows down: TTE never skips frames to catch up. This
chapter explains where a frame's time goes and how to keep it short on large input.

## Where the time goes

Every frame does three kinds of work:

| Work | Grows with | Cost |
| --- | --- | --- |
| Your code in `__next__` | Whatever it loops over | Anything that scans every character costs that much on every frame. |
| `update()` | The number of **active** characters | Each active character moves along its path and steps its scene. |
| Drawing the frame | The number of **visible** characters | Each visible character is placed into its row of the output. |

Visible and active are separate (see [The frame loop](frame-loop.md)). A character that has finished moving and
animating, but is still shown, costs only drawing. That is why effects remove finished characters from
`active_characters`, which `update()` does automatically, and only add a character when it has something to do.

Setup, in the iterator's `__init__()`, runs once. Moving work there from `__next__` almost always pays off.

## Keeping frames fast

- **Compute once.** Build gradients, coordinate color mappings, character groups, and spanning trees during setup,
  not per frame. Glow, Paint, and MySpray build every scene before the first frame.
- **Don't scan every character per frame.** Keep lists of the characters each phase needs, such as MySpray's
  `pending_chars`, and take from them, instead of filtering `terminal.get_characters()` every tick.
- **Keep the active set small.** Release characters as they are needed, and let finished ones become inactive.
- **Cap particles.** Give a `ParticlePool` a `max_size` so large input can't create unlimited particles (see
  [Particles](../writing/patterns/particles.md#when-the-pool-is-full)).
- **Avoid needless fill characters.** Fill characters are created only when first requested. Ask for
  `inner_fill_chars` or `outer_fill_chars` only when the effect animates empty cells, since outer fill on a large
  canvas adds many characters.

## Engine options for large effects

These options trade memory or setup for speed. Each helps only some effects, so measure before and after.

| Option | Helps when |
| --- | --- |
| `new_scene(cache_easing=True)` | Many characters play eased scenes with the same built-in easing function and length. The frame schedule is computed once and shared. |
| `new_scene(cache_appearance=True)` | Many characters build frames with the same symbols and colors. The encoded appearance strings are shared while the scenes are built. |
| `terminal.enable_row_cache()` | Most rows of the output stay the same from one frame to the next, for example when a few characters move over settled text. Unchanged rows are reused instead of drawn again. |

Call `enable_row_cache()` once during setup. Changes to a character's position, visibility, layer, or appearance mark
its row as changed, so the output stays correct. When most rows change every frame, the tracking costs time and saves
none, which is why the cache is off by default.

## Measuring

To see how fast your effect runs without the frame-rate limit, time it with `--frame-rate 0` and discard the output:

```bash
time tte --input-file large_input.txt --frame-rate 0 myspray > /dev/null
```

Compare runs before and after a change, on the same input. Use input at least as large as your users are likely to
send, and try `--canvas-width 0 --canvas-height 0` to fill the terminal.

In a TTE repository checkout, the benchmark harness in `tools/perf/` finds effects the same way `tte` does, so it can
measure your effect too, including one in a directory named by `TTE_DEV_EFFECTS_DIR`. It reports build and render times separately and can profile a run.
[Performance Testing](../../performance.md) explains how to use it.

## Where to go next

- [The frame loop](frame-loop.md) covers `update()`, `active_characters`, and the frame rate.
- [Conventions and quality](../writing/conventions.md#edge-case-checklist) lists the inputs to test, including large
  ones.

---

### Effectguide Engine Toolbox (`docs/effectguide/engine/toolbox.md`)

# Toolbox

The engine chapters cover the parts every effect uses. This page surveys the helpers effects reach for in particular
cases, with a link to the Reference page for each. All of them are public and can be imported as shown.

## Geometry

`terminaltexteffects.utils.geometry` finds coordinates for shapes and measures distances. Functions that work with
circles and distances account for terminal cells being about twice as tall as they are wide (see
[Distances are measured on screen](canvas.md#distances-are-measured-on-screen)).

| Function | Returns |
| --- | --- |
| `find_coords_on_circle(origin, radius, coords_limit=0)` | Coordinates on the outline of a circle that looks round on screen. With `coords_limit`, up to that many evenly spaced points. |
| `find_coords_in_circle(center, radius)` | Every coordinate inside such a circle. |
| `find_coords_on_rect(origin, half_width, half_height)` | Coordinates on the outline of a rectangle. |
| `find_coords_in_rect(origin, distance)` | Every coordinate in a square `distance` cells from `origin` in each direction. |
| `rasterize_line(start, end)` | One cell per step along a line, as for drawing it. |
| `rasterize_supercover_line(start, end)` | Every cell the line touches. |
| `find_coord_on_bezier_curve(start, control, end, t)` | The point a fraction `t` along a bezier curve. |
| `interpolate_coord(start, end, t)` | The point a fraction `t` along a straight line. |
| `extrapolate_along_ray(origin, target, offset_from_target)` | A point beyond `target`, continuing the line from `origin`. |
| `find_length_of_line(coord1, coord2)` | The on-screen distance between two coordinates. |
| `find_normalized_distance_from_center(bottom, top, left, right, coord)` | How far a coordinate is from the center of a rectangle, from 0 to 1. |

Circles are useful for rings, explosions, and orbits; lines for beams and trails. Results can include coordinates off
the canvas, which is fine for paths but not for looking up characters.

See the [geometry Reference](../../engine/utils/geometry.md).

## Easing beyond paths and scenes

Paths and scenes take an easing function directly. The `terminaltexteffects.utils.easing` module also has helpers for
easing anything else:

| Helper | Use |
| --- | --- |
| `make_easing(x1, y1, x2, y2)` | Builds a custom easing curve from two control points, like CSS `cubic-bezier()`. |
| `EasingTracker(easing_function, total_steps)` | Steps through an easing function: each `step()` returns the next eased value, for driving anything you update per tick, such as a radius or a count. |
| `SequenceEaser(sequence, easing_function, total_steps)` | Eases through a list: each `step()` returns the items newly reached, so you can release characters at an eased rate instead of a constant one. |

See the [easing Reference](../../engine/utils/easing.md).

## Particles

`ParticlePool`, exported by `terminaltexteffects`, creates and reuses helper characters for sparks, smoke, and debris.
See [Particles](../writing/patterns/particles.md) and the [`ParticlePool` Reference](../../engine/particles.md).

## Spanning trees

The generators in `terminaltexteffects.utils.spanningtree.algo` connect the characters into a tree, for effects that
grow, burn, or spread through the text, and `BreadthFirst` walks a finished tree. See
[Spanning trees](../writing/patterns/spanning-trees.md) and the
[spanning tree Reference](../../engine/utils/spanningtree/base_generator.md).

## Colors

`Animation.adjust_color_brightness(color, brightness)` returns a lighter or darker version of a color, for glows and
shadows without building a gradient. See [Color](color.md) for gradients, and the
[`Animation` Reference](../../engine/animation/animation.md).

## Where to go next

- [Patterns](../writing/patterns/phases.md) shows several of these helpers in complete effects.

---

### Effectguide Examples Color Input (`docs/effectguide/examples/color_input.txt`)

[38;2;255;80;80mTerminal[38;2;80;220;80mText[38;2;80;140;255mEffects[0m TerminalTextEffects
[38;2;255;80;80mTerminal[38;2;80;220;80mText[38;2;80;140;255mEffects[0m TerminalTextEffects
[38;2;255;80;80mTerminal[38;2;80;220;80mText[38;2;80;140;255mEffects[0m TerminalTextEffects
[38;2;255;80;80mTerminal[38;2;80;220;80mText[38;2;80;140;255mEffects[0m TerminalTextEffects
[38;2;255;80;80mTerminal[38;2;80;220;80mText[38;2;80;140;255mEffects[0m TerminalTextEffects
[38;2;255;80;80mTerminal[38;2;80;220;80mText[38;2;80;140;255mEffects[0m TerminalTextEffects
[38;2;255;80;80mTerminal[38;2;80;220;80mText[38;2;80;140;255mEffects[0m TerminalTextEffects
[38;2;255;80;80mTerminal[38;2;80;220;80mText[38;2;80;140;255mEffects[0m TerminalTextEffects

---

### Effectguide Examples Concept Input (`docs/effectguide/examples/concept_input.txt`)

TerminalTextEffects TerminalTextEffects
TerminalTextEffects TerminalTextEffects
TerminalTextEffects TerminalTextEffects
TerminalTextEffects TerminalTextEffects
TerminalTextEffects TerminalTextEffects
TerminalTextEffects TerminalTextEffects
TerminalTextEffects TerminalTextEffects
TerminalTextEffects TerminalTextEffects

---

### Effectguide Examples Tutorial Input (`docs/effectguide/examples/tutorial_input.txt`)

Welcome to the TTE Effect Guide.
Every character in this text is an EffectCharacter.
This tutorial rebuilds the Spray effect, one step at a time.

---

### Effectguide Tutorial 01 Scaffold (`docs/effectguide/tutorial/01-scaffold.md`)

# Step 1: Scaffold

In this tutorial you rebuild the built-in [Spray](../../effects/spray.md) effect, one feature per step. Spray releases
characters a few at a time from one point on the canvas and sends each one to its place in the text, fading it into
a color gradient as it travels. This is the built-in effect running on the input you use throughout the tutorial:

```bash
tte --input-file tutorial_input.txt spray
```



By the last step, `tte --input-file tutorial_input.txt myspray` looks the same.

Your copy is called **MySpray** and runs as `tte myspray`. TTE rejects two effects with the same command name, so it
can't be called `spray`.

In this step you write the smallest effect that runs: it shows the input text for one frame and stops.

**Engine chapters used:** [Architecture](../engine/architecture.md), [Frame loop](../engine/frame-loop.md).

## Create the input file

Every step runs on the same three lines of text. Save them as `tutorial_input.txt` in the directory you run `tte`
from:

```text title="tutorial_input.txt"
--8<-- "docs/effectguide/examples/tutorial_input.txt"
```

## Create the module

Create `myspray.py` in your community effects directory:

```bash
mkdir -p "${XDG_CONFIG_HOME:-$HOME/.config}/terminaltexteffects/effects"
```

Then add this code to `${XDG_CONFIG_HOME:-$HOME/.config}/terminaltexteffects/effects/myspray.py`. Each part is
explained below.

```python title="myspray.py"
--8<-- "docs/effectguide/examples/myspray_step1/myspray.py"
```

## What each part does

### `get_effect_resources()`

TTE calls this function when it loads your module. It returns three things: the command name, the effect class, and
the configuration class. The command name must match `parser_spec.name` in the configuration.

### `MySprayConfig`

The configuration is a dataclass that subclasses `BaseConfig`. It has no options yet, only `parser_spec`, which
describes the `myspray` subcommand: its one-line `help` in `tte -h`, and the `description` and `epilog` shown by
`tte myspray -h`. You add options in Step 6.

### `MySprayIterator`

The iterator does the work. Its `__init__` runs once, before the first frame:

- `super().__init__(effect)` copies the configuration and builds `self.terminal`, which holds one character for
  each symbol of the input except spaces.
- The loop makes every character visible. Characters start hidden, so without this the frame would be blank.

`__next__` returns `self.frame`, the rendered canvas, the first time it is called, then raises `StopIteration` to end
the effect. It never calls `self.update()`, because no character has anything to animate yet. The
characters stay inactive, but the terminal draws every visible character whether or not it is active, so the
text still appears. See [Active and visible are separate](../engine/frame-loop.md#active-and-visible-are-separate).

### `MySpray`

The effect class is the public face of your effect. Its two properties tell `BaseEffect` which configuration and
iterator classes to use. You rarely add anything else to it.

## Run it

From the directory that holds `tutorial_input.txt`, run:

```bash
tte --input-file tutorial_input.txt myspray
```

The text appears at once, with no animation, and the command exits:



Your effect is also listed in `tte -h`, and `tte myspray -h` shows its help:

```text
--8<-- "docs/effectguide/examples/myspray_step1/help.txt"
```

If `tte myspray` reports an invalid choice, TTE didn't load your module. Check that the file is directly inside the
effects directory and that `get_effect_resources()` is spelled correctly.

## Next step

Step 2 holds the characters back and releases a few each frame, so the text builds up over time.

---

### Effectguide Tutorial 02 Release (`docs/effectguide/tutorial/02-release.md`)

# Step 2: Release

In step 1 the whole text appeared in a single frame. Spray builds the text up instead, releasing characters in a
random order, a few per frame. In this step you add that timing, before any character moves.

**Engine chapters used:** [Frame loop](../engine/frame-loop.md), [Characters](../engine/characters.md).

## Update the module

Replace `myspray.py` with this version:

```python title="myspray.py" hl_lines="8 11 40-48 50-60"
--8<-- "docs/effectguide/examples/myspray_step2/myspray.py"
```

## What changed

### A queue of characters

`build()` puts every character in `self.pending_chars` and shuffles it, so the release order is random. Spray keeps
setup in a `build()` method called from `__init__`; the setup grows over the next steps.

`self._volume` is the most characters to release in one frame: 0.5% of the text, and at least one. The tutorial input
has 121 characters, so its volume is 1; text with 2,000 characters would release up to 10 per frame.

### Releasing characters each frame

Each call to `__next__` now does three things:

1. It pops between 1 and `self._volume` characters off the queue, makes each one visible, and adds it to
   `self.active_characters`.
2. It calls `self.update()`, which ticks the active characters.
3. It returns the frame.

The effect ends when the queue is empty and no character is active.

Released characters have no path or scene yet, so they finish on their first tick and `update()` removes them from
`active_characters` straight away. They stay visible, so they stay on screen. Adding them to `active_characters` now
means they start moving once step 3 gives them somewhere to go.

## Run it

```bash
tte --input-file tutorial_input.txt myspray
```

The characters appear one at a time in a random order until the text is complete:



The order is random, so each run fills in differently.

## Next step

Step 3 starts every character at the edge of the canvas and sends it to its place in the text.

---

### Effectguide Tutorial 03 Move (`docs/effectguide/tutorial/03-move.md`)

# Step 3: Move

So far, released characters appear in place. In Spray they fly out from a single point on the edge of the canvas and
land in their place in the text. In this step each character gets a path from that point to its home.

**Engine chapters used:** [Canvas and coordinates](../engine/canvas.md), [Motion](../engine/motion.md).

## Update the module

Replace `myspray.py` with this version:

```python title="myspray.py" hl_lines="11 44-66"
--8<-- "docs/effectguide/examples/myspray_step3/myspray.py"
```

## What changed

### The spray origin

`spray_origin_map` names nine positions on the canvas: its center, the middle of each edge, and each corner. They use
canvas bounds rather than text bounds, so the spray starts from the edge of the canvas even when the canvas is larger
than the text. Positions on the right edge use `canvas.right - 1`, one column in from the edge.

MySpray always uses `"e"`, the middle of the right edge, for now. Step 6 turns the position into an option, which is why
all nine are listed already.

### A path for every character

For each character, `build()` now:

1. Moves the character to the spray origin with `set_coordinate()`.
2. Creates a path with a random speed between 0.6 and 1.4 and the `out_expo` easing, so characters leave the origin
   fast and slow down as they arrive.
3. Adds one waypoint, the character's home, `input_coord`.
4. Activates the path. Because the character isn't in `active_characters` yet, it doesn't move until it is released.

The random speeds spread out the arrival times, so the text doesn't land all at once.

`__next__` is unchanged. Releasing a character now adds a character with an unfinished path to `active_characters`, so
it stays active and moves each tick until it reaches home. The effect ends when the last character lands.

## Run it

```bash
tte --input-file tutorial_input.txt myspray
```

Characters stream out from the right edge, spread across the text, and settle into place:



Moving characters cross characters that have already landed. Every character is on layer 0, so where two overlap, the
one with the higher `character_id` is drawn on top, and a flying character can disappear behind a settled one.

## Next step

Step 4 uses events to put moving characters on a higher layer, so they always pass in front.

---

### Effectguide Tutorial 04 Layer (`docs/effectguide/tutorial/04-layer.md`)

# Step 4: Layer

At the end of step 3, every character was on layer 0, so a flying character could be hidden behind one that had already
landed. In this step two events keep moving characters in front: one raises a character to layer 1 when its path
starts, and the other lowers it back to layer 0 when it lands.

**Engine chapters used:** [Characters](../engine/characters.md#visibility-activity-and-layers),
[Events](../engine/events.md).

## Update the module

Replace `myspray.py` with this version:

```python title="myspray.py" hl_lines="11 65-76"
--8<-- "docs/effectguide/examples/myspray_step4/myspray.py"
```

## What changed

`build()` registers two events on each character's path, between creating the path and activating it:

- `PATH_ACTIVATED` with `SET_LAYER` and target `1`: when the path starts, the character moves to layer 1.
- `PATH_COMPLETE` with `SET_LAYER` and target `0`: when the character reaches home, it returns to layer 0.

Characters on a higher layer are drawn on top, so a flying character on layer 1 always shows in front of the settled
characters on layer 0.

The order matters. `PATH_ACTIVATED` fires the moment `activate_path()` is called, here during `build()`, long before
the character is released. Registering the event after `activate_path()` would miss it, and the character would stay
on layer 0. Because the layer is set during `build()`, every queued character is already on layer 1 when it is released.

`Path` also has a `layer` argument that does the same as the first event. MySpray uses events to match the built-in
Spray effect, and because the second event, lowering the layer on landing, has no shortcut.

## Run it

```bash
tte --input-file tutorial_input.txt myspray
```

The animation looks like step 3, except that flying characters now always pass in front of characters that have
already landed:



## Next step

Step 5 colors the characters: each one fades from a random color into its place in a gradient across the text.

---

### Effectguide Tutorial 05 Color (`docs/effectguide/tutorial/05-color.md`)

# Step 5: Color

So far every character has been drawn in the terminal's default color. In Spray, the text ends in a gradient that runs
from purple at the bottom, through cyan, to white at the top, and each character fades into its color from a random
color of that gradient while it flies. In this step each character gets a scene that does this.

**Engine chapters used:** [Animation](../engine/animation.md), [Color](../engine/color.md).

## Update the module

Replace `myspray.py` with this version:

```python title="myspray.py" hl_lines="11 46-53 85-92"
--8<-- "docs/effectguide/examples/myspray_step5/myspray.py"
```

## What changed

### The final gradient

`build()` starts by creating the final gradient, from purple through cyan to white with 12 steps between each pair of
stops. `build_coordinate_color_mapping()` then gives every coordinate in the text a color from it. With
`Gradient.Direction.VERTICAL`, every character in a row gets the same color, from the first stop on the bottom row to
the last stop on the top row.

The mapping uses the text bounds, not the canvas bounds, so the gradient spans the text even when the canvas is larger.

### A droplet scene for every character

For each character, `build()` also:

1. Creates a scene with `new_scene()`.
2. Builds a short gradient from a random color in the final gradient to the character's own color, with 7 steps, so
   8 colors.
3. Adds a frame for each of those colors with `apply_gradient_to_symbols()`. Every frame shows the input symbol and
   lasts 20 ticks, so the scene lasts 160 ticks.
4. Activates the scene.

The random starting colors make the flying characters a mix of the gradient's colors, and each one settles to its
place in the gradient.

The scene isn't synced to the path, so it plays at its own pace. Every path here takes under 100 ticks, so each
character lands before its scene ends and keeps fading in place. A character stays active until both its path and its
scene are complete, so the effect now ends when the last character finishes fading rather than when it lands.

## Run it

```bash
tte --input-file tutorial_input.txt myspray
```

The characters fly in colored and fade to the final gradient, purple at the bottom and white at the top:



MySpray now looks like the built-in Spray effect with its default options.

## Next step

Step 6 adds command-line options for the spray position, speed, colors, and more, in place of the values written into
the code.

---

### Effectguide Tutorial 06 Configure (`docs/effectguide/tutorial/06-configure.md`)

# Step 6: Configure

MySpray now looks like Spray, but every value is fixed in the code: the spray comes from the right edge, the speeds are
between 0.6 and 1.4, and the gradient is always purple to white. In this step those values become command-line options,
with the values used so far as their defaults.

**Engine chapter used:** [Configuration and the CLI](../engine/configuration.md).

## Update the module

Replace `myspray.py` with this version:

```python title="myspray.py" hl_lines="9 13-18 30-88 102 108 121 125-126 153"
--8<-- "docs/effectguide/examples/myspray_step6/myspray.py"
```

## What changed

### The options

`MySprayConfig` now has seven options, the same as the built-in Spray:

| Option | Validator | Default |
| --- | --- | --- |
| `--spray-position` | `choices`, the nine keys of `spray_origin_map` | `e` |
| `--spray-volume` | `PositiveRatio` | `0.005` |
| `--movement-speed-range` | `PositiveFloatRange` | `0.6-1.4` |
| `--movement-easing` | `Ease` | `out_expo` |
| `--final-gradient-stops` | `FinalGradientStopsArg` | `8A008A 00D1FF FFFFFF` |
| `--final-gradient-steps` | `FinalGradientStepsArg` | `12` |
| `--final-gradient-direction` | `FinalGradientDirectionArg` | `vertical` |

The first four are declared with `ArgSpec`. The three final gradient options use the shared declarations from
`terminaltexteffects.engine.base_config`, which already have their names, validators, and help, so MySpray only sets
their defaults. The epilog now shows an example that uses two of the options.

### Using the options

`build()` reads each value from `self.config` where it used a fixed value before. The values are already converted:
`movement_speed_range` is a tuple of two floats, so `random.uniform(*self.config.movement_speed_range)` picks a speed
between them; `movement_easing` is the easing function itself; and `final_gradient_stops` is a tuple of `Color`
objects ready to pass to `Gradient`.

`spray_position` is one of the keys of `spray_origin_map`, the dictionary from step 3, so it selects the origin
directly.

## Run it

With no options, MySpray runs as it did in step 5:

```bash
tte --input-file tutorial_input.txt myspray
```



The options change it. This command, the one in the epilog, sprays from the center of the canvas and colors the text
with a radial gradient:

```bash
tte --input-file tutorial_input.txt myspray --spray-position center --final-gradient-direction radial
```



`tte myspray -h` lists the options with their defaults:

```text
--8<-- "docs/effectguide/examples/myspray_step6/help.txt"
```

Invalid values are rejected before the effect starts:

```console
$ tte --input-file tutorial_input.txt myspray --spray-volume 2
tte myspray: error: argument --spray-volume: invalid value: '2' must be 0 < n <=1. Example: 0.5
```

argparse prints the usage lines shown above before the error.

## Next step

Step 7 handles colors in the input, tests MySpray against unusual input, and compares the result with the built-in
Spray.

---

### Effectguide Tutorial 07 Harden (`docs/effectguide/tutorial/07-harden.md`)

# Step 7: Harden

MySpray works on the tutorial text with its default options. A shared effect also meets input you didn't plan for:
colored text, a single character, wide characters, a canvas much larger than the text. In this step MySpray learns to
keep the input's colors, gets tests that run it on unusual input, and is compared with the built-in Spray.

**Engine chapters used:** [Color](../engine/color.md#colors-in-the-input),
[Discovery and loading](../engine/loading.md).

## Update the module

Replace `myspray.py` with this version:

```python title="myspray.py" hl_lines="12 122 143-153"
--8<-- "docs/effectguide/examples/myspray_step7/myspray.py"
```

## What changed

When the user passes `--existing-color-handling dynamic`, the engine leaves input colors to the effect. MySpray now
checks for that setting and, when it is set, gives each character a droplet scene with a single frame in the
character's input colors, instead of the fade into the final gradient. The frame lasts 160 ticks, as long as the
gradient scene, so the effect's timing doesn't change.

A character without input colors gets a `ColorPair` of two `None` colors, so it is shown in the terminal's default
colors. This matches the built-in Spray; Paint, in the Color chapter, falls back to its gradient instead. Either
choice is reasonable, as long as your effect's documentation says which it makes.

The other two settings need no code. With `ignore`, the default, the engine drops the input colors. With `always`, the
engine replaces the colors of every frame with the input colors.

## Run it

Use the colored input file from the [Color](../engine/color.md#colors-in-the-input) chapter:

```bash
tte --input-file color_input.txt --existing-color-handling dynamic myspray
```

The first word lands in its input colors, red, green, and blue, and the second word, which has no colors, lands in
the terminal's default color:



## Test it

Testing an effect means running it on input that is likely to break it and checking that it finishes with the text
in place. Save this file as `test_myspray.py` outside your community effects directory, since TTE imports every `.py`
file in that directory:

```python title="test_myspray.py"
--8<-- "docs/effectguide/examples/myspray_tests/test_myspray.py"
```

The tests use MySpray from Python, as described in [Configuration from
Python](../engine/configuration.md#configuration-from-python). `play()` sets the frame rate to 0 so the tests don't
wait between frames, and sets `ignore_terminal_dimensions` so the effect runs without a terminal.

- `test_ends_with_the_input_text` runs inputs that often expose bugs: one character, one row, one column, a sparse
  grid with empty lines, wide characters that take two cells, and tabs. It checks that the last frame contains every
  line of the input.
- `test_empty_input_is_rejected_by_the_engine` shows that your effect never sees input without visible characters: the
  engine raises `EmptyInputError` first, and the CLI exits without playing anything.
- `test_every_spray_position_on_a_larger_canvas` runs every spray position on a canvas larger than the text, where
  canvas and text bounds differ.
- `test_dynamic_color_handling_keeps_input_colors` checks that the input's red reaches the last frame.

Run the tests with your effects directory on the import path, so `import myspray` finds the module:

```bash
PYTHONPATH="${XDG_CONFIG_HOME:-$HOME/.config}/terminaltexteffects/effects" python -m pytest test_myspray.py
```

All 17 tests pass. Test anything your own effect adds the same way: each option at its limits, and any input your
effect treats specially.

## Compare with Spray

MySpray now has the same options, defaults, and behavior as the built-in Spray. Here is Spray on the same input:

```bash
tte --input-file color_input.txt --existing-color-handling dynamic spray
```



Read [`effect_spray.py`](https://github.com/ChrisBuilds/terminaltexteffects/blob/main/terminaltexteffects/effects/effect_spray.py)
next to your `myspray.py`. The differences are in structure, not behavior:

- Spray converts `--spray-position` to a `SprayPosition` enum and keys its origins by the enum. MySpray keys
  `spray_origin_map` by the option's strings.
- Spray computes every character's final color first and stores it in `character_final_color_map`. MySpray looks the
  color up as it builds each scene.
- With dynamic color handling, Spray's scene has seven 20-tick frames in the input colors, 140 ticks in all, and
  MySpray's has one 160-tick frame. Both show the input colors until the scene ends.
- Spray's docstrings list every option in an `Attributes:` section, and its epilog includes the shared description of
  easing functions, `argutils.EASING_EPILOG`, as built-in effects do.

## What you built

Over seven steps, MySpray went from an effect that printed the text in place to a full copy of Spray. Along the way
it used every major part of the engine: character visibility and release, paths with easing, events, scenes and
gradients, command-line options, and input colors.

From here, the [Patterns](../writing/patterns/phases.md) pages show techniques Spray doesn't use, such as multi-phase
effects and particles, and [Sharing your effect](../writing/sharing.md) explains how to publish yours.

---

### Effectguide Writing Conventions (`docs/effectguide/writing/conventions.md`)

# Conventions and quality

The built-in effects follow shared conventions, so users can guess an option's name and trust that every effect
handles the same inputs. Following them makes your effect feel like part of TTE. This page collects those conventions,
then gives a checklist of inputs to try before you share.

## Options

### Names

- Write option names in lowercase, with words separated by hyphens: `--movement-speed`. The configuration field is the
  same name in snake case, `movement_speed`. It has to be, because argparse stores the value under that name (see
  [Declaring an option](../engine/configuration.md#declaring-an-option)).
- Start with what the option affects and end with what it controls, as in `--spray-position`, `--movement-speed`,
  and `--wipe-delay`. The built-in effects use these endings most:

| Ending | Controls | Examples |
| --- | --- | --- |
| `-speed` | Distance per tick. | `--movement-speed`, `--travel-speed` |
| `-easing` | An easing function. | `--movement-easing`, `--wave-easing` |
| `-color`, `-colors` | One color, or several to choose from. | `--starting-color`, `--rain-colors` |
| `-symbols` | Symbols to choose from. | `--rain-symbols`, `--wave-symbols` |
| `-gradient-stops`, `-gradient-steps`, `-gradient-direction` | A gradient. | `--wave-gradient-stops` |
| `-delay`, `-time`, `-frames` | A number of ticks. | `--launch-delay`, `--final-gradient-frames` |
| `-range` | A pair of values to pick between, written `low-high`. | `--movement-speed-range` |

- Use the shared `--final-gradient-stops`, `--final-gradient-steps`, and `--final-gradient-direction` declarations
  for the gradient the text ends in. Nearly every built-in effect has them (see
  [The shared final gradient options](../engine/configuration.md#the-shared-final-gradient-options)).

### Values and defaults

- Validate every value with a validator from `argutils` or your own `type` function, so bad input fails with a clear
  message instead of an exception from inside the effect.
- Choose defaults that look good on ordinary text in an ordinary terminal. Most users never change them.
- Measure durations in ticks, and say so in the help. At the default 60 frames per second, 60 ticks is one second.
- For a count where `0` means "forever", such as cycles, say so in the help, and keep the default finite (see
  [Looping and infinite effects](patterns/looping.md)).

### Help text

- Give every option a one-sentence `help` ending in a period. The default is added automatically.
- Use the `epilog` for an example command that shows a useful combination of options.
- Write the field's documentation string as `"<type> : <help>"`, as the built-in effects do.

## Code

- Import only public names: the `terminaltexteffects` package exports and the public modules under
  `terminaltexteffects.engine` and `terminaltexteffects.utils`. Names that start with an underscore can change in any
  release.
- Do the effect's setup in the iterator's `__init__()`, or a `build()` method it calls, and keep module-level code to
  definitions, since TTE imports every effect module on every run (see [Discovery and loading](../engine/loading.md)).
- Keep `__next__` to the work for one frame. Anything that can be computed once, such as a gradient mapping, belongs in
  setup.
- Use `random` from the standard library for randomness. `tte --seed` seeds it, which makes runs repeatable for
  recordings and bug reports.
- Write docstrings for the module, the classes, and their methods. Refer to code with single backticks, such as
  `ParticlePool`.

## Input colors

Decide what your effect does with each `--existing-color-handling` setting, and document it:

- With `ignore` and `always`, the engine does the work, so check only that the effect still looks right.
- With `dynamic`, the effect decides. Use `character.animation.input_fg_color` and `input_bg_color` where your effect
  would otherwise apply its own final colors, and choose what happens to characters without input colors.

[Color](../engine/color.md#colors-in-the-input) and [Step 7: Harden](../tutorial/07-harden.md) show both sides.

## Edge-case checklist

Run your effect on each of these before you share it. [Step 7: Harden](../tutorial/07-harden.md#test-it) shows how to
turn them into tests.

| Input or option | What can go wrong |
| --- | --- |
| One character | Divisions by the text width or height, such as gradient mappings or spacing, divide by zero. |
| One row, one column | Effects that pick a random row or column, or need two of them, fail or look wrong. |
| Sparse text with empty lines and leading spaces | Groups or rows are empty, and code that assumes every row has characters fails. |
| Wide characters, such as `漢字` | Code that assumes one character per cell overlaps or misplaces characters. |
| Tabs | Tabs become spaces according to `--tab-width`; the text is wider than its character count. |
| A canvas larger than the text, `--canvas-width 0 --canvas-height 0` | Code that mixes up canvas bounds and text bounds places things in the wrong area. |
| Text larger than the terminal | Characters outside the visible area are clipped; check that the effect still ends. |
| Colored input with each `--existing-color-handling` setting | Colors are lost, or `dynamic` ignores characters without input colors. |
| `--no-color` and `--xterm-colors` | The effect depends on color to be readable. |
| `--frame-rate 0` | The effect relies on time passing between frames instead of counting ticks. |
| Every value of every choice option, and the limits of every numeric option | A rarely used value fails. |

Input with no visible characters never reaches your effect: the engine rejects it first.

## Where to go next

- [Sharing your effect](sharing.md) covers what to publish with the effect.
- [Performance](../engine/performance.md) covers keeping frames fast on large input.

---

### Effectguide Writing Sharing (`docs/effectguide/writing/sharing.md`)

# Sharing your effect

A community effect is a single `.py` file that other people copy into their community effects directory. This page
covers what to publish alongside that file and where to share it.

## Before you share

- Run your effect's tests and try it on your own terminal with a few inputs, including colored input if your effect
  supports it. [Step 7: Harden](../tutorial/07-harden.md) shows how.
- Check that it uses only public APIs: names exported by `terminaltexteffects` and the public modules under
  `terminaltexteffects.engine` and `terminaltexteffects.utils`. Anything starting with an underscore can change in any
  release.
- Pick a command name no built-in effect uses, and a file name that isn't the name of a standard-library or installed
  module (see [Discovery and loading](../engine/loading.md#community-effects)).
- Keep the effect in one file. If it needs another package, say so; users install it into the same Python environment
  as TTE.

## What to publish

Put the module in a public repository or Gist, with:

| Item | Why |
| --- | --- |
| A short demo | People decide from the animation. A screen recording works, or an animated WebP from [`tte --export`](../../appguide.md#gif-and-webp). |
| The supported TTE version | Engine APIs can change between releases. State the version you tested with, such as "TTE 0.16.0". |
| Install and run steps | Where to put the file, any dependencies, and an example command. |
| The options | `tte <effect> -h` output, or a list of options and their defaults. |
| How it handles input colors | Which `--existing-color-handling` settings it supports, and what `dynamic` does. |
| A license | So people know how they may use and change it. |
| Maintenance status | Whether you plan to update it for new TTE releases. |

The install steps can be as short as this, with your file and command names:

```bash
mkdir -p "${XDG_CONFIG_HOME:-$HOME/.config}/terminaltexteffects/effects"
cp myspray.py "${XDG_CONFIG_HOME:-$HOME/.config}/terminaltexteffects/effects/"
tte myspray -h
```

## Where to share it

Post it in the [TTE Effect Show and Tell](https://github.com/ChrisBuilds/terminaltexteffects/discussions/categories/tte-effect-show-and-tell)
discussion category with the items above. A post there is a community listing: the effect stays hosted and
maintained by you, and isn't bundled with TTE or endorsed by its maintainers.

## When TTE changes

The [changelog](../../changelog.md) lists breaking engine changes in each release. When a new TTE version comes out,
run your tests against it, fix what broke, and update the supported version you state.

## Proposing a built-in effect

To propose your effect for TTE itself, open an issue describing it, with a demo. Built-in effects go through review
and the [promotion checklist](https://github.com/ChrisBuilds/terminaltexteffects/blob/main/dev_effects/README.md#promotion-checklist),
which adds tests, documentation, and shell completion. Read [CONTRIBUTING.md](https://github.com/ChrisBuilds/terminaltexteffects/blob/main/CONTRIBUTING.md)
first.

---

### Engine Animation Animation (`docs/engine/animation/animation.md`)

# Animation

*Module*: `terminaltexteffects.engine.animation`

## Appearance Reuse

!!! info "New in 0.16.0"

    `set_appearance_if_changed()` is available on development `main` and is not in the published 0.15.0 package.

`Animation.set_appearance_if_changed(symbol=None, colors=None)` follows the ordinary setter's symbol defaults,
color overrides, and width tracking. It reuses its last visual when the effective request, color policy, visual
identity, and all visual instance fields remain unchanged. Scene stepping, an ordinary setter call, or direct
style, color-code, width, or formatted-symbol edits force the next helper call to reapply the requested appearance.
The helper does not stop an active scene from overwriting that appearance on its next step.

The snapshot is allocated lazily for each animation using the helper. It retains one visual and field snapshot;
visuals are not shared across characters. Callers should not rely on receiving a new visual object each call; use
`Animation.set_appearance()` when a fresh visual is required.

For input-derived characters in `existing_color_handling="always"` mode, the helper ignores requested colors in the
comparison because parsed input colors override them. Helper characters without input-color ownership still compare
the requested colors.

Opt in only after measuring (see [Performance Testing](../../performance.md)): comparing and retaining snapshots can
cost more than it saves when effective appearances change frequently. Overflow row coloring and Spotlights use the
helper only in `always` mode.

## Animation Reference

::: terminaltexteffects.engine.animation.Animation

---

### Engine Animation Charactervisual (`docs/engine/animation/charactervisual.md`)

# CharacterVisual

*Module*: `terminaltexteffects.engine.animation`

::: terminaltexteffects.engine.animation.CharacterVisual

---

### Engine Animation Frame (`docs/engine/animation/frame.md`)

# Frame

*Module*: `terminaltexteffects.engine.animation`

::: terminaltexteffects.engine.animation.Frame

---

### Engine Animation Scene (`docs/engine/animation/scene.md`)

# Scene

*Module*: `terminaltexteffects.engine.animation`

## Opt-in Caches

!!! info "New in 0.16.0"

    `cache_easing` and `cache_appearance` are available on development `main` and are not in the published 0.15.0
    package.

Both options default to `False`. Opt in only after measuring an effect; each cache has overhead that can outweigh
its benefit. See [Performance Testing](../../performance.md) for the benchmark harness and a summary of the
measurements behind each cache.

### Eased-Scene Schedules

Set `cache_easing=True` when constructing a scene (directly or through `Animation.new_scene`) to reuse
frame-index schedules across scenes with the same built-in easing function and frame durations.
Each scene keeps its own frames, visuals, playback cursor, and lifecycle events.

The shared cache retains at most 32 schedules, with at most 4096 ticks per schedule. Scenes hold weak
references so eviction can release a schedule; playback reacquires it when needed. Assigning a different
`ease` or adding frames selects a new schedule. Resetting or looping reuses the existing layout.
Custom easing functions, longer scenes, and motion-synced scenes keep their ordinary playback behavior.
Distinct layouts, short playback, and frequent eviction can cost more than schedule reuse saves.

### Encoded Appearance Reuse

Set `cache_appearance=True` on `Scene` or `Animation.new_scene` to reuse immutable ANSI strings when
constructing frames with repeated symbols, styles, and resolved foreground/background colors.
Frames, visuals, colors, and playback state retain their usual independent ownership;
editing a visual's formatted string still invalidates terminal output. Explicit `format_symbol()` calls
retain ordinary formatting.

The shared LRU cache holds at most 2048 encodings. Existing visuals retain their strings after eviction.
Subclass/formatter overrides and custom style or color-code values use the ordinary path. Unicode symbol
validation and width calculation remain unchanged. Color policy is applied before selecting an encoding,
including preexisting colors/bold, XTerm conversion, and no-color output.
Lookup overhead can outweigh reuse when appearances vary.

Waves enables both options for its repeated wave scene.

## Scene Reference

::: terminaltexteffects.engine.animation.Scene

---

### Engine Motion Motion (`docs/engine/motion/motion.md`)

# Motion

*Module*: `terminaltexteffects.engine.motion`

::: terminaltexteffects.engine.motion.Motion

---

### Engine Motion Path (`docs/engine/motion/path.md`)

# Path

*Module*: `terminaltexteffects.engine.motion`

::: terminaltexteffects.engine.motion.Path

---

### Engine Motion Segment (`docs/engine/motion/segment.md`)

# Segment

*Module*: `terminaltexteffects.engine.motion`

::: terminaltexteffects.engine.motion.Segment

---

### Engine Motion Waypoint (`docs/engine/motion/waypoint.md`)

# Waypoint

*Module*: `terminaltexteffects.engine.motion`

::: terminaltexteffects.engine.motion.Waypoint

---

### Engine Terminal Canvas (`docs/engine/terminal/canvas.md`)

# Canvas

*Module*: `terminaltexteffects.engine.canvas`

::: terminaltexteffects.engine.canvas.Canvas

---

### Engine Terminal Terminal (`docs/engine/terminal/terminal.md`)

# Terminal

*Module*: `terminaltexteffects.engine.terminal`

!!! info "New in 0.16.0"

    `CharacterOrder`, spiral sorting, grid grouping, and `clock()` are available on development `main` and are not
    in the published 0.15.0 package.

## Character Orders

`CharacterOrder` unifies spatial groups and individual character sorts. Both terminal methods accept
`order=...` and use immutable input coordinates:

```python
from terminaltexteffects import CharacterOrder, Terminal

terminal = Terminal("abc\ndef")
groups = terminal.get_characters_grouped(order=CharacterOrder.ROW_TOP_TO_BOTTOM)
characters = terminal.get_characters(order=CharacterOrder.ROW_TOP_TO_BOTTOM)
backward = terminal.get_characters(order=CharacterOrder.ROW_TOP_TO_BOTTOM, reverse=True)
```

Spatial orders preserve their group boundaries; individual orders such as spirals produce singleton
groups. Flattening the grouped form gives the flat traversal. Row groups and individual row traversals
remain separate choices because they have different activation batches despite matching flat order.
`CharacterOrder.is_grouped` identifies spatial orders.

`reverse=True` reverses the complete traversal. In grouped output it reverses both the group sequence
and each group's internal sequence, preserving membership. `serpentine=True` reverses alternate spatial
groups before optional global reversal. Random ordering is sampled once per call; reversal does not
reshuffle it. Separate random calls require the same random state to produce matching output forms.

`canvas_only=True` excludes off-canvas input coordinates; `False` includes them. When omitted, spatial
orders clip to the canvas and individual orders include the complete selected inventory, independently
of the output form. This preserves legacy selection behavior while allowing explicit overrides. All four
input, inner-fill, outer-fill, and added-character selection flags remain available.

Existing `CharacterGroup`, `CharacterSort`, `sort=...`, and `grouping=...` remain supported. Pass either
`order` or the compatibility keyword, not both. The no-argument defaults remain row groups from top to
bottom for `get_characters_grouped()` and top-to-bottom, left-to-right for `get_characters()`.

## Spiral Sorting

Use `get_characters(order=...)` to order characters in a rectangular spiral from the outside inward:

```python
from terminaltexteffects import Terminal
from terminaltexteffects import CharacterOrder

terminal = Terminal("YourTextHere")
characters = terminal.get_characters(order=CharacterOrder.SPIRAL_CLOCKWISE)
```

| Clockwise | Counterclockwise | Starting corners |
| --- | --- | --- |
| `SPIRAL_CLOCKWISE` | `SPIRAL_COUNTER_CLOCKWISE` | Top-left |
| `SPIRAL_CLOCKWISE_DOUBLE` | `SPIRAL_COUNTER_CLOCKWISE_DOUBLE` | Top-left and bottom-right |
| `SPIRAL_CLOCKWISE_QUAD` | `SPIRAL_COUNTER_CLOCKWISE_QUAD` | Top-left, top-right, bottom-right, bottom-left |

Spirals use the bounding box of the selected characters' immutable input coordinates. Each rectangular ring
is completed before the next begins, and empty coordinates are skipped. Multiple arms are interleaved by
the number of steps from their starting corners, with ties resolved in the corner order listed above.
Characters reached by more than one arm are returned once; distinct characters at the same coordinate
retain their selection order. On a final single row or column, arms move inward from their endpoints
for both direction options. With `reverse=True`, spirals follow the same path outward;
this differs from choosing the other clockwise/counterclockwise option.

All character selection flags are supported, including fill characters and added characters outside the canvas.
Moving a character does not change its sort position, and wide symbols are sorted once at their input coordinate.

## Grid Grouping

Use `get_characters_grouped_by_grid()` with a `geometry.GridLayout` to collect characters into rectangular cells:

```python
from terminaltexteffects import Terminal, geometry

terminal = Terminal("YourTextHere")
canvas = terminal.canvas
grid = geometry.find_balanced_grid(canvas.left, canvas.bottom, canvas.right, canvas.top)
groups = terminal.get_characters_grouped_by_grid(grid)
```

Groups use immutable input coordinates and are returned bottom-to-top, then left-to-right. Empty cells and
characters outside the grid or canvas are omitted. The method supports the same input, fill, and added-character
selection flags as `get_characters_grouped()` and preserves selection order within each cell.
Pass `reverse=True` to reverse both the cell sequence and each cell's internal sequence.

## Effect Clock

Effects that measure a phase in seconds should read `terminal.clock()` instead of calling `time.time()` or
`time.monotonic()` directly:

```python
self.phase_start = self.terminal.clock()
...
if self.terminal.clock() - self.phase_start >= self.config.phase_seconds:
    self.phase = "next"
```

During live output, `clock()` returns `time.monotonic()`. When `tte --export` renders frames ahead of time,
the terminal uses a frame clock that advances by `1 / frame_rate` per frame, so a phase lasts the same
number of frames as it would live and recordings are reproducible with `--seed`. When an effect reads wall-clock
time directly, a recorded phase runs for that many seconds of rendering time instead, so its length in frames
depends on rendering speed and is usually far longer than intended.

## Row Caching

!!! info "New in 0.16.0"

    `enable_row_cache()` is available on development `main` and is not in the published 0.15.0 package.

An effect can call `self.terminal.enable_row_cache()` to reuse formatted rows until their characters change.
The engine tracks coordinates, animation visuals, visibility, layers, and direct `formatted_symbol` assignments.
Wide characters use the existing width-aware renderer; caching resumes when all visible characters are single-cell.
Existing motion and animation references, paths, and scenes remain valid when caching is enabled.

Measure before enabling caching (see [Performance Testing](../../performance.md)): tracking and row membership consume
extra memory, and frequent changes can reduce the benefit. Cache activation changes rendering work without changing
effect options, scheduling, or random decisions.

These built-in effects enable caching automatically within the input ranges where it measured faster:

| Effect | Activation requirements |
| --- | --- |
| Matrix | Canvas at least 4 rows with at least 256 cells. |
| Print | Text at least 16 columns by 4 rows, with at least 256 input characters. |
| Pour | Up/down pouring; text at least 64 by 12, with at least 768 characters and three-quarter occupancy. |
| BouncyBalls | Text at least 64 by 16, with at least 1,024 characters and three-quarter occupancy. |
| ErrorCorrect | Text at least 8 by 5, with at least 80 input characters. |
| SynthGrid | Visible canvas at least 8 by 4, including sparse text. |
| Rain | Text at least 80 by 24, with at least 1,680 characters and seven-eighths occupancy. |

All but Matrix also require single-cell input symbols, and the text-based gates require the text bounds to fit within
the visible canvas. Occupancy is the input-character count divided by the area of the text bounds; fill and helper
characters do not contribute. These checks run before effect build and helper allocation. Later wide visual changes
still use the width-aware fallback. Smaller inputs and horizontal Pour keep the default renderer.

To compare Matrix runs reproducibly, advance its wall-clock-based rain phase with the same simulated clock in both
runs and verify frame counts and output hashes.

::: terminaltexteffects.engine.terminal.Terminal

---

### Engine Terminal Terminalconfig (`docs/engine/terminal/terminalconfig.md`)

# TerminalConfig

*Module*: `terminaltexteffects.engine.terminal`

::: terminaltexteffects.engine.terminal.TerminalConfig

---

### Engine Utils Ansitools (`docs/engine/utils/ansitools.md`)

# ANSItools

*Module*: `terminaltexteffects.utils.ansitools`

::: terminaltexteffects.utils.ansitools

---

### Engine Utils Argutils (`docs/engine/utils/argutils.md`)

# ArgUtils

*Module*: `terminaltexteffects.utils.argutils`

::: terminaltexteffects.utils.argutils

## Example Usage

The `RandomSequenceConfig.speed` option uses `ArgSpec` with `PositiveFloat.type_parser`
to require a value greater than zero. This minimal config follows the same declaration
pattern and works with both direct construction and CLI parsing:

```python
import argparse
from dataclasses import dataclass

from terminaltexteffects.engine.base_config import BaseConfig
from terminaltexteffects.utils import argutils


@dataclass
class SpeedConfig(BaseConfig):
    parser_spec: argutils.ParserSpec = argutils.ParserSpec(
        name="speed-example",
        help="Show the speed option.",
        description="Example speed configuration.",
        epilog="",
    )
    speed: float = argutils.ArgSpec(
        name="--speed",
        type=argutils.PositiveFloat.type_parser,
        default=0.007,
        metavar=argutils.PositiveFloat.METAVAR,
        help="Speed of the animation.",
    )  # pyright: ignore[reportAssignmentType]


assert SpeedConfig(speed="0.01").speed == 0.01

parser = argparse.ArgumentParser()
SpeedConfig._populate_parser(parser)
config = SpeedConfig._build_config(parser.parse_args(["--speed", "0.01"]))
assert config.speed == 0.01
```

---

### Engine Utils Color (`docs/engine/utils/color.md`)

# Color

*Module*: `terminaltexteffects.utils.graphics`

## Basic Usage

`Color` objects represent colors throughout TTE. They are immutable and hashable, so they can safely be reused as
dictionary keys and configuration values.

### Supports Multiple Specification Formats

```python
from terminaltexteffects.utils.graphics import Color

red = Color("ff0000")
xterm_red = Color(9)
rgb_red_again = Color("#FF0000")
```

RGB strings must contain exactly six hexadecimal digits and may have one leading `#`. They are normalized to lowercase
without the leading hash. XTerm colors must be non-boolean integers from `0` through `255`. Invalid values raise
`ValueError`.

`rgb_color` exposes the normalized six-digit string, `rgb_ints` exposes its three integer channels, and `xterm_color`
contains the original XTerm index or `None` for an RGB specification.

### Equality and Hashing

Equality preserves the normalized color *specification*, not only its displayed RGB value:

```python
from terminaltexteffects.utils.graphics import Color

assert Color("#FF0000") == Color("ff0000")
assert Color(9) != Color("ff0000")
```

The second comparison is unequal even though XTerm color `9` converts to the same RGB value. Keeping XTerm provenance
distinct lets terminal output continue to choose between XTerm and RGB escape sequences.

### Printing Colors

Colors can be printed to show the code and resulting color appearance.

```python
from terminaltexteffects.utils.graphics import Color

red = Color("#ff0000")
print(red)
```



### Using Colors to Build a Gradient

```python
from terminaltexteffects.utils.graphics import Color, Gradient

rgb = Gradient(Color("#ff0000"), Color("#00ff00"), Color("#0000ff"), steps=5)
for color in rgb:
    assert isinstance(color, Color)
    print(color.rgb_color)
```

### Shifting Between Colors

`shift_color_towards()` accepts a factor from `0` through `1` and uses the same nearest, ties-to-even channel rounding
as gradient generation:

```python
from terminaltexteffects.utils.graphics import Color, shift_color_towards

midpoint = shift_color_towards(Color("000000"), Color("ffffff"), 0.5)
assert midpoint == Color("808080")
```

An out-of-range or non-finite factor raises `ValueError`.

### Passing Colors to Effect Configurations

```python
from terminaltexteffects.effects.effect_colorshift import ColorShift
from terminaltexteffects.utils.graphics import Color

text = ("EXAMPLE" * 10 + "\n") * 10
red = Color("#ff0000")
green = Color("#00ff00")
blue = Color("#0000ff")
effect = ColorShift(text)
effect.effect_config.gradient_stops = (red, green, blue)
with effect.terminal_output() as terminal:
    for frame in effect:
        terminal.print(frame)
```

---

## Color Reference

::: terminaltexteffects.utils.graphics.Color

---

### Engine Utils Colorpair (`docs/engine/utils/colorpair.md`)

# ColorPair

*Module*: `terminaltexteffects.utils.graphics`

## Basic Usage

`ColorPair` objects are immutable foreground/background color pairs. The canonical fields are `fg` and `bg`; either
field may be `None` to leave that channel unset.

### Usage

```python
import terminaltexteffects as tte

color_pair = tte.ColorPair(fg=tte.Color("#FF0000"), bg=tte.Color("#00FF00"))
```

### Foreground-Only and Background-Only Pairs

```python
import terminaltexteffects as tte

foreground_only = tte.ColorPair(fg=tte.Color("#FF0000"))
background_only = tte.ColorPair(bg=tte.Color("#0000FF"))

assert foreground_only.bg is None
assert background_only.fg is None
```

### Automatic Color Conversion

Colors can be specified using strings or integers. Color objects will be created automatically.

```python
import terminaltexteffects as tte

foreground_only = tte.ColorPair(fg="#FF0000")
background_only = tte.ColorPair(bg=21)
color_pair = tte.ColorPair("#FF0000", "#00FF00")
```

Strings and integers are converted to `Color` objects. Both arguments are optional and default to `None`; invalid
color values raise the same `ValueError` as direct `Color` construction.

### Printing ColorPairs

ColorPair objects can be printed to see the resulting colors.



---

## ColorPair Reference

::: terminaltexteffects.utils.graphics.ColorPair

---

### Engine Utils Colorterm (`docs/engine/utils/colorterm.md`)

# ColorTerm

*Module*: `terminaltexteffects.utils.colorterm`

!!! info "New in 0.16.0"

    `reset_fg()` and `reset_bg()` are available on development `main` and are not in the published 0.15.0 package.

Use `reset_fg()` to restore the terminal's default foreground color, or `reset_bg()` to restore its default background
color. Each emits a selective SGR reset and leaves the other color and text attributes in effect. Use
`ansitools.reset_all()` to clear all formatting.

::: terminaltexteffects.utils.colorterm

---

### Engine Utils Easing (`docs/engine/utils/easing.md`)

# Easing Functions

*Module*: `terminaltexteffects.utils.easing`

Easing functions take a progress ratio from 0 to 1. Back and elastic functions
can return values below 0 or above 1. Custom curves from `make_easing` can also
overshoot when their vertical control points are outside `[0, 1]`. The horizontal
control points, `x1` and `x2`, must be finite values in `[0, 1]`.

`EasingTracker` preserves overshoot by default; pass `clamp=True` to limit its
stepped values to `[0, 1]`. `SequenceEaser` always clamps its internal tracker
before selecting elements.

```python
from terminaltexteffects.utils import easing

assert easing.in_back(0.5) < 0
assert easing.out_elastic(0.5) > 1

tracker = easing.EasingTracker(lambda _: 1.25, total_steps=1, clamp=True)
assert tracker.step() == 1.0
```

::: terminaltexteffects.utils.easing

---

### Engine Utils Exceptions (`docs/engine/utils/exceptions.md`)

# Exceptions

*Module*: `terminaltexteffects.utils.exceptions`

Every TTE exception derives from `TerminalTextEffectsError`, so catch it to handle any engine error. The specific
exceptions are importable from `terminaltexteffects.utils.exceptions` and are grouped below by the module that defines
them. Import the base class from `terminaltexteffects.utils.exceptions.base_terminaltexteffects_exception`.

## Base Exception

::: terminaltexteffects.utils.exceptions.base_terminaltexteffects_exception

## Animation Exceptions

::: terminaltexteffects.utils.exceptions.animation_exceptions

## Character Exceptions

::: terminaltexteffects.utils.exceptions.base_character_exceptions

## Motion Exceptions

::: terminaltexteffects.utils.exceptions.motion_exceptions

## Terminal Exceptions

::: terminaltexteffects.utils.exceptions.terminal_exceptions

---

### Engine Utils Geometry (`docs/engine/utils/geometry.md`)

# Geometry

*Module*: `terminaltexteffects.utils.geometry`

## Balanced Grids

!!! info "New in 0.16.0"

    `find_balanced_grid()` and `GridLayout` are available on development `main` and are not in the published
    0.15.0 package.

`find_balanced_grid(left, bottom, right, top)` returns a `GridLayout` covering inclusive bounds. It accounts for
terminal character height when choosing row and column counts, then divides each axis so cell sizes differ by
at most one position. `target_cells`, `min_cell_width`, and `min_cell_height` control the preferred size.

The layout's `column_boundaries` and `row_boundaries` include an exclusive final boundary, one beyond the
canvas edge. Each cell includes its lower boundaries and excludes its upper boundaries. For example,
an 80-column by 24-row canvas produces five columns of cells and three rows, with cells measuring 16 by 8.

::: terminaltexteffects.utils.geometry

---

### Engine Utils Geometry3d (`docs/engine/utils/geometry3d.md`)

# Geometry 3D

*Module*: `terminaltexteffects.utils.geometry3d`

!!! info "New in 0.16.0"

    `geometry3d` is available on development `main` and is not in the published 0.15.0 package.

Low-level building blocks for placing and drawing things in 3D on a terminal canvas: vectors, rotations, a
camera, meshes, rasterization into a per-cell depth buffer, wireframe edges and Lambert shading.

World space is measured in terminal columns. The origin is the center of the canvas, +x points right, +y points
up, and +z points toward the default viewer. A `Camera` projects world points onto canvas cells and corrects for cells
being about twice as tall as they are wide, so a cube looks like a cube. With the default settings, a cell lifted
onto the z=0 plane with `Camera.cell_to_world()` projects back onto the same cell.

```python
import math

from terminaltexteffects.utils import geometry3d as g3

camera = g3.Camera(center_column=40.5, center_row=12.5)
cube = g3.box(20, 20, 20)
turn = g3.Rotation.about_y(math.radians(30)) * g3.Rotation.about_x(math.radians(20))
light = g3.Vec3(-0.5, 0.6, 1.0)

for cell, fragment in g3.rasterize(cube, camera, rotation=turn).items():
    brightness = g3.lambert(fragment.normal, light)  # 0 in shadow, 1 facing the light
```

Camera position and orientation can change with `set_position` and `set_rotation`. `depth(point)` reports
view-axis depth. `ray(coord)` supplies a world ray, and `cell_to_world(coord, z=...)` intersects it with a world-z
plane, raising `ValueError` for parallel rays. For movement helpers and tweens, use `Space3D.camera`.

`position` and `rotation` are read-only. `distance` moves the implicit eye until `set_position` makes that
position explicit; with `focal=None`, focal length continues to follow `distance`. A camera's world-z plane
intersection can lie behind its eye; project the result and handle `None` when visibility matters.
`ray` directions are scaled to one unit of view-axis depth rather than normalized to unit Euclidean length.
The low-level class has pose setters and projection, while `space3d.Camera` adds travel/aiming controls and a
queue played by `Space3D.step()`.

Angles are in radians. Rotations compose with `*`: `second * first` applies `first`, then `second`.

`rasterize` and `wireframe` take `bounds=g3.Bounds.for_canvas(canvas)` to fill only cells on the canvas. Without
bounds they fill every cell a shape covers, which can be a great many for a shape close to the camera.

Camera point queries accept `Vec3` values or `(x, y, z)` world positions in terminal columns.
`transform_points` also accepts tuples for local input points and its world translation;
`rasterize` and `wireframe` accept either form for the mesh origin's world `position`.
Returned positions remain `Vec3` values.

::: terminaltexteffects.utils.geometry3d

---

### Engine Utils Gradient (`docs/engine/utils/gradient.md`)

# Gradient

*Module*: `terminaltexteffects.utils.graphics`

## Basic Usage

```python
from terminaltexteffects.utils.graphics import Color, Gradient

rgb = Gradient(Color("#ff0000"), Color("#00ff00"), Color("#0000ff"), steps=5)
for color in rgb:
    assert isinstance(color, Color)
    print(color.rgb_color)
```

Iteration and indexing return `Color` objects. Use `color.rgb_color` when a six-digit RGB string is needed.

## Stops, Steps, and Loops

Provide one or more `Color` stops. A step count is the number of transitions between two adjacent stops, so a
two-stop gradient with `steps=4` contains five colors, including both stops:

```python
from terminaltexteffects.utils.graphics import Color, Gradient

gradient = Gradient(Color("000000"), Color("ffffff"), steps=4)
assert [color.rgb_color for color in gradient] == [
    "000000",
    "404040",
    "808080",
    "bfbfbf",
    "ffffff",
]
```

A single integer applies to every transition. A tuple assigns counts in stop-pair order; when it is shorter than the
number of transitions, its final value is repeated. A tuple cannot be empty or longer than the number of transitions,
and every count must be a positive, non-boolean integer. A single-stop gradient always contains that one stop after
validating its step values.

With `loop=True`, a gradient with multiple stops adds a final transition from the last stop back to the first. The
first stop therefore also appears as the final spectrum color:

```python
loop = Gradient(Color("ffffff"), Color("000000"), steps=(2, 3), loop=True)
assert [color.rgb_color for color in loop] == [
    "ffffff",
    "808080",
    "000000",
    "555555",
    "aaaaaa",
    "ffffff",
]
```

Invalid stops raise `TypeError`. Missing stops, invalid step counts, and overlong step tuples raise `ValueError`.

## Fractional Lookup

`get_color_at_fraction()` accepts a finite number from `0` through `1` and selects the nearest precomputed spectrum
sample. Half-sample ties use Python's ties-to-even `round()` behavior. Non-numeric values raise `TypeError`; non-finite
or out-of-range values raise `ValueError`.

## Coordinate Mappings

Coordinate mappings include every row and column bound and return `dict[Coord, Color]`:

```python
from terminaltexteffects.utils.geometry import Coord
from terminaltexteffects.utils.graphics import Color, Gradient

gradient = Gradient(Color("000000"), Color("ffffff"), steps=2)
mapping = gradient.build_coordinate_color_mapping(
    min_row=1,
    max_row=1,
    min_column=4,
    max_column=6,
    direction=Gradient.Direction.HORIZONTAL,
)

assert [mapping[Coord(column, 1)].rgb_color for column in range(4, 7)] == [
    "000000",
    "808080",
    "ffffff",
]
```

- `HORIZONTAL` maps the first and final colors to `min_column` and `max_column`.
- `VERTICAL` maps the first and final colors to `min_row` and `max_row`.
- `DIAGONAL` maps the first color at `(min_column, min_row)` and the final color at `(max_column, max_row)`.
- `RADIAL` measures from the rectangle's geometric center toward its corners; the center is fraction `0` and every
  corner is fraction `1`.

A direction with no span uses the first color. Bounds must be non-boolean integers greater than zero, each minimum must
not exceed its maximum, and `direction` must be a `Gradient.Direction`. Invalid bound types or directions raise
`TypeError`; invalid bound values raise `ValueError`.

## Printing Gradients

Gradients can be printed to the terminal to show information about the stops, steps, and resulting spectrum.



---

## Gradient Reference

::: terminaltexteffects.utils.graphics.Gradient

---

### Engine Utils Hexterm (`docs/engine/utils/hexterm.md`)

# HexTerm

*Module*: `terminaltexteffects.utils.hexterm`

::: terminaltexteffects.utils.hexterm

---

### Effectguide 3d Spin 01 Sheet (`docs/effectguide/3d/spin/01-sheet.md`)

# 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](../index.md#running-the-examples) chapters. If you haven't written an effect before, the
[MySpray tutorial](../../tutorial/01-scaffold.md) covers the parts every effect has, which this one skips over.

In this step you mount the text on a sheet, turn the sheet away, and turn it back.

**3D chapters used:** [Space and coordinates](../coordinates.md), [Transforms](../transforms.md),
[Rotation](../rotation.md).

## Create the module

Save this as `myspin.py` in your community effects directory:

```python title="myspin.py"
--8<-- "docs/effectguide/examples/3d/myspin_step1/myspin.py"
```

## How it works

### The space and the sheet

`Space3D(self.terminal, perspective=0)` sets up a camera looking at the canvas. Perspective 0 is a flat view, so
nothing grows or shrinks with distance yet. Step 3 turns it on.

`self.space.body()` makes a body with no shape. A body is a position, a rotation and a scale. It owns no characters
and draws nothing; it only carries the characters you mount on it.

### Mounting the text

`text_plane(characters)` returns one **anchor** per character: a point on the body, placed so that it lands on the
character's input cell at the body's pose right now. The sheet hasn't moved yet, so that's its rest pose. `add_chars`
mounts each character on its anchor. From then on, `space.step()` works out where each anchor is on screen, and each
character's tick in `update()` moves it there.

Mounting doesn't show or tick anything by itself, so `build` also makes each character visible and adds it to
`active_characters`. A mounted character is never finished while its mount is active, so `update()` keeps ticking it
until it's released.

Spaces are left out. They're invisible, so mounting them would only cost time.

### Turning away and back

`set_rotation(rotation_about("y", 70))` turns the sheet 70 degrees about the vertical axis, right away. The order
matters: the anchors were placed first, at rest, so they turn with the sheet, and the text is home again whenever the
sheet is back at rest. Built after the turn, the anchors would put the text on its input cells at 70 degrees instead.
`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.

### Handing the text back

`on_complete(self.finish)` calls `finish` when the sheet's last tween ends. `release_all()` gives the characters back,
on their input cells, as ordinary 2D characters. Once they have nothing left to do, `update()` drops them from
`active_characters` and the effect ends.

`__next__` keeps going while `self.space.animating` or `self.active_characters` is true. `animating` only tracks the
sheet's tween queue, so it can't tell whether the characters are done; `active_characters` can. Without the release,
the mounted characters would stay active and the effect would never end.

## Run it

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



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.

---

### Effectguide 3d Spin 02 Spin (`docs/effectguide/3d/spin/02-spin.md`)

# Spin, step 2: Spinning

In step 1 the sheet turned once, back to rest. In this step it spins twice about the vertical axis and lands facing
you again.

**3D chapters used:** [Rotation](../rotation.md), [Animating in 3D](../animating.md).

## Update the module

Replace `myspin.py` with this version:

```python title="myspin.py"
--8<-- "docs/effectguide/examples/3d/myspin_step2/myspin.py"
```

## What changed

The `set_rotation` and `turn_to_rest` calls are replaced with one call:

```python
self.sheet.spin("y", turns=2, frames=240, ease=easing.in_out_sine)
```

`spin` turns the body about an axis by whole or partial turns, spread over `frames`. Two whole turns end where they
started, so the text lands at rest. `in_out_sine` starts slowly, speeds up and slows down again.

A spin turns about the **world's** axis, not the body's own. That doesn't matter yet, because the sheet isn't tilted.
It will in step 5.

## Run it

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



Halfway through each turn the sheet faces away, and you read the text mirrored. Step 4 deals with the back side.

## Next step

Step 3 adds perspective, and starts the sheet far behind the canvas.

---

### Effectguide 3d Spin 03 Perspective (`docs/effectguide/3d/spin/03-perspective.md`)

# 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](../camera.md), [Animating in 3D](../animating.md).

## Update the module

Replace `myspin.py` with this version:

```python title="myspin.py"
--8<-- "docs/effectguide/examples/3d/myspin_step3/myspin.py"
```

## What changed

### Two constants

`START_DISTANCE` and `SPIN_FRAMES` name the numbers the new code shares. Step 6 turns them into options.

### Perspective

`Space3D(self.terminal, perspective=1)` uses the default camera. 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

`set_position(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.

Two tweens now run together:

```python
self.sheet.spin("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)
```

`together=True` starts `move_to` alongside the spin instead of after it. Both take `SPIN_FRAMES`, which is what makes
them 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.

## Run it

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



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](../camera-movement.md).

## Next step

Step 4 colors each character by how directly it faces the light, and decides what to show when the back faces you.

---

### Effectguide 3d Spin 04 Shading (`docs/effectguide/3d/spin/04-shading.md`)

# Spin, step 4: Shading and the back side

Every character has been the same color so far. In this step each one is colored by how squarely it faces you: dark
when edge-on or turned away, its gradient color facing forward. The turning sheet now reads as a lit surface.

**3D chapters used:** [Light and shading](../light.md), [Depth and hidden surfaces](../depth.md).

## Update the module

Replace `myspin.py` with this version:

```python title="myspin.py"
--8<-- "docs/effectguide/examples/3d/myspin_step4/myspin.py"
```

## What changed

### A light in front

`Space3D(..., light="front")` points the light along +z, from behind the viewer toward the canvas. It's a direction,
not a lamp at a position, so a surface turned toward the viewer is turned toward the light too. How lit the sheet is
and how squarely it faces you are nearly the same thing here. They match exactly in an orthographic view; with
perspective, the parts of the sheet away from the center are seen at a slight angle.

### Scenes synced to the light

Every mount measures its anchor's light each frame: 1 when its surface faces the light squarely, 0 when edge-on or
facing away. A scene created with `sync=Scene.SyncMetric.LIGHT` picks its frame from that reading instead of playing
in order.

Each character gets a scene with one frame per color of a gradient, from `SHADOW_COLOR` to its final color. Its final
color comes from `FINAL_GRADIENT` mapped over the text from top to bottom, as in most built-in effects.

### The back side

When the sheet faces away, its light reading is 0, so the mirrored text shows in the shadow color. That reads as the
back of the sheet. `HIDE_BACK = True` would pass `hide_when_facing_away=True` to `add_chars` instead, which hides each
character while its back faces you.

### Final colors

`finish` now also stops each scene and sets the character to its final color. Without this, a released character
would keep whichever frame its scene showed last.

## Run it

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



## Next step

Step 5 starts the sheet at a random tilt and lets it settle.

---

### Effectguide 3d Spin 05 Settle (`docs/effectguide/3d/spin/05-settle.md`)

# 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](../rotation.md#back-to-rest).

## Update the module

Replace `myspin.py` with this version:

```python title="myspin.py"
--8<-- "docs/effectguide/examples/3d/myspin_step5/myspin.py"
```

## What changed

Two lines replace the `spin` call:

```python
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

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



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

## Next step

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

---

### Effectguide 3d Spin 06 Options (`docs/effectguide/3d/spin/06-options.md`)

# Spin, step 6: Options

In this step the constants become options, and the effect respects the colors of colored input. The result is the
finished effect from the start of the tutorial.

**Chapters used:** [Configuration](../../engine/configuration.md), [Color](../../engine/color.md).

## Update the module

Replace `myspin.py` with this version:

```python title="myspin.py"
--8<-- "docs/effectguide/examples/3d/myspin_step6/myspin.py"
```

## What changed

### Options

Each constant is now a field of `MySpinConfig`, with an `ArgSpec` that names its command-line option, type, default
and help:

| Option | Default | Replaces |
| --- | --- | --- |
| `--spin-turns` | `2` | The `turns=2` passed to `settle` |
| `--spin-frames` | `240` | `SPIN_FRAMES` |
| `--spin-easing` | `OUT_CUBIC` | `easing.out_cubic` |
| `--start-distance` | `200` | `START_DISTANCE` |
| `--perspective` | `1` | `perspective=1` |
| `--shadow-color` | `303030` | `SHADOW_COLOR` |
| `--hide-back` | off | `HIDE_BACK` |
| `--final-gradient-stops`, `--final-gradient-steps`, `--final-gradient-direction` | `8A008A 00D1FF ffffff`, `12`, `vertical` | `FINAL_GRADIENT` |

The final gradient options use the shared `FinalGradientStopsArg`, `FinalGradientStepsArg` and
`FinalGradientDirectionArg` from `base_config`, so they behave like every built-in effect's.

### Colored input

With `--existing-color-handling dynamic`, each character keeps its input colors. `build` reads them into
`final_colors` instead of the gradient color. A character with an input foreground still shades from the shadow color
to that foreground, over its input background. One with no foreground keeps its input colors in a single frame
throughout. Spaces with an input background are mounted too, since their color shows.

## Run it

```bash
tte --input-file concept_input.txt --canvas-width 80 --canvas-height 24 --anchor-text c myspin --spin-turns 1 \
    --start-distance 60 --hide-back --final-gradient-stops ffb000 ff5fd7
```



With the defaults, MySpin plays the finished effect from step 1:



## Where to go next

- The [Recipes](../recipes.md) show other effects built on `Space3D`.
- The MySpray tutorial's [Step 7: Harden](../../tutorial/07-harden.md) covers testing an effect, which applies here
  unchanged.

---

### Effectguide Examples Myspray Step1 Help (`docs/effectguide/examples/myspray_step1/help.txt`)

usage: tte myspray [-h]

myspray | Sprays the characters from a single point.

options:
  -h, --help  show this help message and exit

Example: tte myspray

---

### Effectguide Examples Myspray Step6 Help (`docs/effectguide/examples/myspray_step6/help.txt`)

usage: tte myspray [-h] [--spray-position {n,ne,e,se,s,sw,w,nw,center}]
                   [--spray-volume (0 < float(n) <= 1)]
                   [--movement-speed-range (hyphen separated float range e.g. '0.25-0.5')]
                   [--movement-easing (Easing Function)]
                   [--final-gradient-stops (XTerm [0-255] OR RGB Hex [000000-ffffff]) [(XTerm [0-255] OR RGB Hex [000000-ffffff]) ...]]
                   [--final-gradient-steps (int > 0) [(int > 0) ...]]
                   [--final-gradient-direction (diagonal, horizontal, vertical, radial)]

myspray | Sprays the characters from a single point.

options:
  -h, --help            show this help message and exit
  --spray-position {n,ne,e,se,s,sw,w,nw,center}
                        Position for the spray origin. (default: e)
  --spray-volume (0 < float(n) <= 1)
                        Number of characters to spray per tick as a fraction of the total number
                        of characters. (default: 0.005)
  --movement-speed-range (hyphen separated float range e.g. '0.25-0.5')
                        Movement speed range of the characters. (default: 0.6-1.4)
  --movement-easing (Easing Function)
                        Easing function to use for character movement. (default: out_expo)
  --final-gradient-stops (XTerm [0-255] OR RGB Hex [000000-ffffff]) [(XTerm [0-255] OR RGB Hex [000000-ffffff]) ...]
                        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. (default: 8a008a 00d1ff ffffff)
  --final-gradient-steps (int > 0) [(int > 0) ...]
                        Space separated, unquoted, list of the number of gradient steps to use.
                        More steps will create a smoother and longer gradient animation. (default:
                        12)
  --final-gradient-direction (diagonal, horizontal, vertical, radial)
                        Direction of the final gradient across the text. (default: vertical)

Example: tte myspray --spray-position center --final-gradient-direction radial

---

### Effectguide Writing Patterns Looping (`docs/effectguide/writing/patterns/looping.md`)

# Looping and infinite effects

Some effects play a cycle that repeats: a color wave, a pulse, a rotation. They usually let the user choose how many
cycles to play, and treat `0` as "forever". This page shows how to count cycles, how to end a looping effect cleanly,
and what an infinite effect means for the people running it. Built-in effects with this option include ColorShift,
whose `--cycles 0` loops until interrupted.

## The Wave example

Wave gives every character a looping scene that cycles through a looping gradient. Each character starts at a
different point in the cycle, offset by its column, so a band of color sweeps across the text from right to left.
After the last cycle, every character settles to a final color. Save it as `wave.py` in your community effects
directory:

```python title="wave.py"
--8<-- "docs/effectguide/examples/wave/wave.py"
```

```bash
tte --input-file concept_input.txt wave
```



With `--cycles 0`, the wave keeps sweeping until you press Ctrl+C:

```bash
tte --input-file concept_input.txt wave --cycles 0
```

## Looping scenes and gradients

Two looping options work together here:

- `Gradient(..., loop=True)` adds a transition from the last stop back to the first, so the spectrum ends where it
  starts, and cycling through it has no jump (see [Gradients](../../engine/color.md#gradients)).
- `new_scene(is_looping=True)` starts the scene again from its first frame after the last one, forever (see
  [More scene options](../../engine/animation.md#more-scene-options)).

Rotating each character's list of colors by its column, `colors[offset:] + colors[:offset]`, is what turns the same
cycle into a moving wave: neighboring characters are one color apart at every tick.

## Ending a loop

A looping scene never completes, so its character never becomes inactive, and an iterator that waits for
`active_characters` to empty would never stop. The iterator has to decide when to stop, and Wave counts ticks:

- One cycle lasts `len(colors) * TICKS_PER_COLOR` ticks, the length of the scene.
- After `cycles` full cycles, `__next__` stops each character's scene with `deactivate_scene()`, gives it its final
  appearance with `set_appearance()`, and clears `active_characters`. It returns that settled frame, and the next call
  raises `StopIteration`.

`set_appearance()` changes what is drawn at once, without an `update()`, so the settled frame shows the final colors.
Ending on a deliberate final appearance, rather than wherever the loop happened to be, makes the effect look finished.

Other ways to end a loop:

- Replace the looping scene with a final, non-looping scene, and wait for it to complete as usual.
- For a looping path, call `motion.deactivate_path()` or activate a final path.

## Infinite effects

With `--cycles 0`, Wave never raises `StopIteration`. That is fine on the command line, where the user stops it with
Ctrl+C, but it changes how the effect can be used:

- Someone running the effect from Python must stop iterating themselves, as the
  [Library Guide](../../../libguide.md#infinitely-looping-effects) explains for the built-in effects.
- Tests and recordings need a finite setting, so make sure every option has a finite value, as Wave's default of 2
  cycles is.
- Say in the option's help that `0` loops until interrupted, as Wave does.

## Where to go next

- [Animation](../../engine/animation.md) covers scenes, frame durations, and looping.
- [Phases and staggered starts](phases.md) covers effects that end in a final stage.

---

### Effectguide Writing Patterns Particles (`docs/effectguide/writing/patterns/particles.md`)

# Particles

Sparks, smoke, debris, and rain aren't part of the input text. Effects draw them with **particles**: extra characters
that appear, play a short animation, and disappear, often many times over. [`ParticlePool`](../../../engine/particles.md)
creates these characters and reuses them, so an effect can emit thousands of particles while only a few dozen exist at
once. Built-in effects that use particles include Burn, Laseretch, and Thunderstorm.

## The Sparks example

Sparks reveals the text one column at a time, from left to right. Every character throws off a spark that flies up and
fades from a white `*` through a yellow `+` to an orange `.`. Save it as `sparks.py` in your community effects
directory:

```python title="sparks.py"
--8<-- "docs/effectguide/examples/sparks/sparks.py"
```

```bash
tte --input-file concept_input.txt sparks
```



## How the pool works

A particle is an ordinary `EffectCharacter` created with `terminal.add_character()`, so it has paths, scenes, and
events like any other character (see [Added characters](../../engine/characters.md#added-characters)). The pool keeps
track of which particles are in use and which are free.

`ParticlePool(terminal, active_characters, symbols, ...)` takes the iterator's `active_characters`, because emitting
and reclaiming add particles to it and remove them from it. Its other arguments:

| Argument | Use |
| --- | --- |
| `symbols` | The symbol for new particles, or several to choose from at random. |
| `max_size` | The most particles the pool creates. `None`, the default, has no limit. |
| `initial_count` | How many particles to create up front, instead of when first needed. |
| `initializer` | A function run once for each new particle. |

Setup is split between two functions:

- The **initializer** runs once per particle. Sparks uses it for everything that stays the same between uses: the
  layer, the fade scene, and the event that returns the particle to the pool.
- The **emit callback** runs on every use. Sparks's `launch_spark()` gives the spark a new path, resets its scene to
  the first frame, and activates it.

`emit(origin, callback)` takes a free particle, or creates one, moves it to `origin`, calls the callback, makes it
visible, and adds it to `active_characters`. When a particle is reused, the pool first clears its old paths and stops
its path and scene; its scenes and events are kept. Pass `reset=ParticleReset(...)` to `emit()` to clear more or less.

`reclaim_on_event(particle, caller=scene)` returns the particle to the pool when its scene completes: it is hidden,
removed from `active_characters`, and becomes free again. Without a reclaim, a particle stays in use for the rest of
the effect.

## When the pool is full

When every particle is in use and the pool has reached `max_size`, `emit()` returns `None` and emits nothing. Sparks
ignores the result, so characters revealed while the pool is full throw no spark. With a small pool, the effect has
fewer sparks on screen:

```bash
tte --input-file concept_input.txt sparks --max-sparks 8
```



A cap keeps the cost of a frame predictable on large input, since every active particle is updated every tick. Choose
`max_size` from how many particles should be visible at once, not from the size of the text.

## Where to go next

- The [`ParticlePool` Reference](../../../engine/particles.md) lists every method, including `acquire()` for manual
  setup and `ParticleReset`.
- [Phases and staggered starts](phases.md) explains the column-by-column reveal.

---

### Effectguide Writing Patterns Phases (`docs/effectguide/writing/patterns/phases.md`)

# Phases and staggered starts

Many effects happen in stages: characters appear, move into place, and then the finished text does something
together. Within a stage, groups of characters often start one after another rather than all at once. This page shows
how to structure both in an iterator. Built-in effects that use this pattern include Beams, Blackhole, and Middleout.

## The Gather example

Gather runs in three phases:

1. **Scatter:** every character sits at a random position as a gray dot, for 30 ticks.
2. **Gather:** groups of characters, one row at a time by default, travel home every 4 ticks, turning from dots into
   their symbols on the way.
3. **Flash:** when every character is home, the whole text flashes from white to blue.

Save it as `gather.py` in your community effects directory:

```python title="gather.py"
--8<-- "docs/effectguide/examples/gather/gather.py"
```

```bash
tte --input-file concept_input.txt gather
```



## A phase is state in the iterator

The iterator keeps two attributes: `phase`, the name of the current phase, and `ticks_in_phase`, the number of ticks
since that phase began. `__next__` runs the code for the current phase, updates the characters, and returns the frame,
so every phase still produces one frame per call.

Each phase decides when it ends, and starts the next one:

- **Scatter** ends after a fixed number of ticks. Nothing is active, so the scattered dots stay still: visible
  characters are drawn whether or not they are active.
- **Gather** ends when no groups are waiting and `active_characters` is empty, meaning every character has finished its
  path and scene.
- **Flash** gives every character a new scene and adds them all to `active_characters` again. When they are all
  inactive, the effect raises `StopIteration`.

Checking `active_characters` is the usual way to wait for a phase's motion and animation to finish. If a phase needs
to wait for something else, such as a particular character reaching a waypoint, set a flag from an event callback
(see [Events](../../engine/events.md#callbacks)) and check the flag in `__next__`.

Keep phase changes in one place, as Gather does with `start_flash()`, so the setup for each phase is easy to find.
For effects with many phases, an `Enum` of phase names avoids typos in the strings.

## Staggered starts

`terminal.get_characters_grouped(order=...)` splits the characters into groups, such as rows, columns, or diagonals
(see [Characters](../../engine/characters.md#getting-characters)). Gather reverses the list once, so `pop()` takes the
groups in order, and releases the next group every `TICKS_BETWEEN_GROUPS` ticks.

With `--gather-order`, any order can drive the stagger:

```bash
tte --input-file concept_input.txt gather --gather-order column_left_to_right
```



Other common ways to stagger starts:

- Release a fixed number of characters from a shuffled list each tick, as MySpray does in
  [step 2](../../tutorial/02-release.md).
- Start every character at once, but give each a different `speed` or a `hold_time`, so arrival times differ.
- Release a group when the previous group's path completes, using a `PATH_COMPLETE` callback.

## Where to go next

- [The frame loop](../../engine/frame-loop.md) explains when an effect ends and what `update()` does.
- [Animation](../../engine/animation.md#syncing-a-scene-to-a-path) explains the synced scene each character plays as
  it gathers.

---

### Effectguide Writing Patterns Spanning Trees (`docs/effectguide/writing/patterns/spanning-trees.md`)

# Spanning trees

A spanning tree connects every character to its neighbors without loops, like a maze carved through the text.
Revealing characters in the order a tree links them makes the text grow outward in branches, and walking the finished
tree from its root sends a pulse down those branches. Built-in effects that use spanning trees include Burn,
Laseretch, and Smoke.

## The Grow example

Grow builds a spanning tree over the text, reveals the characters in the order the tree linked them, and then runs a
pulse from the tree's root out along its branches. Save it as `grow.py` in your community effects directory:

```python title="grow.py"
--8<-- "docs/effectguide/examples/grow/grow.py"
```

The algorithm that builds the tree decides the shape of the growth:

=== "prims_simple"

    ```bash
    tte --input-file concept_input.txt grow --algorithm prims_simple
    ```

    

    Prim's algorithm links a random character on the edge of the tree each step, so the tree grows outward evenly
    from its root.

=== "recursive_backtracker"

    ```bash
    tte --input-file concept_input.txt grow --algorithm recursive_backtracker
    ```

    

    The recursive backtracker follows one branch as far as it can before backing up, so the tree has long corridors
    and the pulse takes winding routes.

=== "aldous_broder"

    ```bash
    tte --input-file concept_input.txt grow --algorithm aldous_broder
    ```

    

    Aldous-Broder wanders at random and links each character the first time it visits it, so growth appears in
    patches and slows down as fewer unvisited characters remain.

## Building the tree

The generators are in `terminaltexteffects.utils.spanningtree.algo`. Each one takes the terminal and, optionally, a
starting character:

```python
tree = PrimsSimple(self.terminal, limit_to_text_boundary=True)
while not tree.complete:
    tree.step()
```

Each `step()` links at most one more character. You can step a generator once per frame to animate the tree as it is
built, or, as Grow does, build the whole tree during setup and replay `char_link_order`, the characters in the order
they were linked. Building up front keeps `__next__` simple and lets you change the reveal rate freely.

Without a starting character, the generator picks a random one, inside the text bounds with
`limit_to_text_boundary=True`. That option also keeps the tree from linking characters outside the text.

### Fill characters

Trees link neighboring cells, and spaces inside the text are cells too. The engine fills them with **fill
characters**, so a tree can pass between words (see [Fill characters](../../engine/characters.md#fill-characters)).
Fill characters have `is_fill_character` set and a space as their symbol. Grow skips them when revealing and pulsing.

### Links are permanent

A generator records the tree in each character's `links`, the set of neighbors it is linked to. A character that
already has links can't be part of a new tree: the generators raise `ValueError`. Build one tree per effect run.

## Walking the tree

`BreadthFirst(terminal, starting_char)` traverses a tree that is already built. Each `step()` moves one layer out from
the start, and `explored_last_step` lists the characters reached by that step. Grow starts a pulse scene on those
characters each tick, so the pulse moves one link per tick along every branch at once. The traversal is complete when
a step finds no new characters.

## Where to go next

- The [spanning tree Reference](../../../engine/utils/spanningtree/base_generator.md) lists the generator attributes
  each algorithm records, such as `edge_chars` in Prim's algorithm.
- [Phases and staggered starts](phases.md) covers effects that run in stages, as Grow does.

---

### Engine Utils Spanningtree Base Generator (`docs/engine/utils/spanningtree/base_generator.md`)

# base_generator

*Module*: `terminaltexteffects.utils.spanningtree.base_generator`

::: terminaltexteffects.utils.spanningtree.base_generator

---

### Engine Utils Spanningtree Algos Aldous Broder (`docs/engine/utils/spanningtree/algos/aldous_broder.md`)

# Aldous Broder

*Module*: `terminaltexteffects.utils.spanningtree.algo.aldousbroder`

::: terminaltexteffects.utils.spanningtree.algo.aldousbroder

---

### Engine Utils Spanningtree Algos Breadthfirst (`docs/engine/utils/spanningtree/algos/breadthfirst.md`)

# Breadth First

*Module*: `terminaltexteffects.utils.spanningtree.algo.breadthfirst`

::: terminaltexteffects.utils.spanningtree.algo.breadthfirst

---

### Engine Utils Spanningtree Algos Prims Simple (`docs/engine/utils/spanningtree/algos/prims_simple.md`)

# Prims Simple

*Module*: `terminaltexteffects.utils.spanningtree.algo.primssimple`

::: terminaltexteffects.utils.spanningtree.algo.primssimple

---

### Engine Utils Spanningtree Algos Prims Weighted (`docs/engine/utils/spanningtree/algos/prims_weighted.md`)

# Prims Weighted

*Module*: `terminaltexteffects.utils.spanningtree.algo.primsweighted`

::: terminaltexteffects.utils.spanningtree.algo.primsweighted

---

### Engine Utils Spanningtree Algos Recursive Backtracker (`docs/engine/utils/spanningtree/algos/recursive_backtracker.md`)

# Recursive Backtracker

*Module*: `terminaltexteffects.utils.spanningtree.algo.recursivebacktracker`

::: terminaltexteffects.utils.spanningtree.algo.recursivebacktracker

---

--- METRICS ---
- Files Extracted: 141
- Estimated Token Budget: ~116124 tokens
- Canonical Reference: https://codewiki.google/github.com/ChrisBuilds/terminaltexteffects
