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
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:
"""Sparks: columns of text appear from left to right, each character throwing off a spark.
An Effect Guide example for the Particles pattern.
"""
from __future__ import annotations
import random
from dataclasses import dataclass
from terminaltexteffects import CharacterOrder, Color, ColorPair, Coord, EffectCharacter, ParticlePool, easing
from terminaltexteffects.engine.base_config import BaseConfig
from terminaltexteffects.engine.base_effect import BaseEffect, BaseEffectIterator
from terminaltexteffects.utils import argutils
SPARK_COLORS = (Color("#ffffff"), Color("#ffd000"), Color("#ff6000"))
TICKS_BETWEEN_COLUMNS = 2
def get_effect_resources() -> tuple[str, type[BaseEffect], type[BaseConfig]]:
"""Return the command name, effect class, and configuration class TTE registers."""
return "sparks", Sparks, SparksConfig
@dataclass
class SparksConfig(BaseConfig):
"""Configuration for the Sparks effect."""
parser_spec = argutils.ParserSpec(
name="sparks",
help="Columns of text appear from left to right, each character throwing off a spark.",
description="sparks | Columns of text appear from left to right, each character throwing off a spark.",
epilog="Example: tte sparks --max-sparks 8",
)
max_sparks: int = argutils.ArgSpec(
name="--max-sparks",
type=argutils.PositiveInt.type_parser,
default=60,
metavar=argutils.PositiveInt.METAVAR,
help="Most sparks on screen at once.",
) # pyright: ignore[reportAssignmentType]
"int : Most sparks on screen at once."
class SparksIterator(BaseEffectIterator[SparksConfig]):
"""Produce the frames of the Sparks effect."""
def __init__(self, effect: Sparks) -> None:
"""Create the spark pool and queue the columns."""
super().__init__(effect)
self.ticks = 0
self.pending_columns = self.terminal.get_characters_grouped(order=CharacterOrder.COLUMN_LEFT_TO_RIGHT)
self.pending_columns.reverse() # So pop() takes the leftmost column.
self.sparks = ParticlePool(
self.terminal,
self.active_characters,
"*",
max_size=self.config.max_sparks,
initializer=self.initialize_spark,
)
def initialize_spark(self, spark: EffectCharacter) -> None:
"""Set up a new spark once: its layer, its fading scene, and its return to the pool."""
spark.layer = 1
scene = spark.animation.new_scene(scene_id="fade")
for symbol, color in zip("*+.", SPARK_COLORS):
scene.add_frame(symbol, 6, colors=ColorPair(fg=color))
self.sparks.reclaim_on_event(spark, caller=scene)
def launch_spark(self, spark: EffectCharacter) -> None:
"""Send a spark up and to one side from where it was emitted, and start its fade."""
start = spark.motion.current_coord
path = spark.motion.new_path(speed=0.4, ease=easing.out_quad)
path.new_waypoint(Coord(start.column + random.randint(-3, 3), start.row + random.randint(1, 3)))
spark.motion.activate_path(path)
scene = spark.animation.query_scene("fade")
scene.reset_scene()
spark.animation.activate_scene(scene)
def __next__(self) -> str:
"""Reveal the next column every few ticks and return the frame."""
if not self.pending_columns and not self.active_characters:
raise StopIteration
if self.pending_columns and self.ticks % TICKS_BETWEEN_COLUMNS == 0:
for character in self.pending_columns.pop():
self.terminal.set_character_visibility(character, is_visible=True)
self.sparks.emit(character.input_coord, self.launch_spark)
self.ticks += 1
self.update()
return self.frame
class Sparks(BaseEffect[SparksConfig]):
"""Columns of text appear from left to right, each character throwing off a spark."""
@property
def _config_cls(self) -> type[SparksConfig]:
return SparksConfig
@property
def _iterator_cls(self) -> type[SparksIterator]:
return SparksIterator

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). 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:

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
ParticlePoolReference lists every method, includingacquire()for manual setup andParticleReset. - Phases and staggered starts explains the column-by-column reveal.