Terminal text effects for Go. A port of ttfx, itself a port of TerminalTextEffects by ChrisBuilds.
  • Go 98.4%
  • HTML 1%
  • JavaScript 0.4%
  • CSS 0.2%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Gaurav Gosain 240b18dfef Add the command: animate an argument, a file or a pipe
cmd/tuiffects reads whatever arrives, turns it into a cell grid and runs an
effect over it. `echo hi | tuiffects` is the headline, so it works with no
flags at all: an effect at random, the frame rate the terminal can take, and
the colour policy the input asks for.

The colours in the input are the reason it is worth running. A pipe from
`ls --color` or `git diff --color` carries its screen in SGR escapes, and
NewTerminalFromCells with DynamicExistingColors resolves every character back
to the colour it arrived with, so what dissolves and re-forms is your own
output rather than a recolouring of it. So the reader handles SGR properly:
the eight base colours and their bright pairs, the 256 entry palette, direct
colour, the colon form some terminals write, inverse, and reset. Bold over one
of the base eight resolves to that colour's bright twin, because that is what
a terminal does with it and what `ls` means by it, and the library carries a
colour into the animation but not a weight. Every other escape sequence is
recognised far enough to be skipped, CSI and OSC and the ones with
intermediate bytes alike, because a sequence half read prints its own tail as
text. Cursor movement is not honoured. This is a filter over a stream of lines
and not a terminal, and a program that positions its cursor is drawing
something no filter can reconstruct.

Plain text carries no colours, and dynamic would resolve it to nothing and run
the whole animation in the terminal's default foreground. So the default
policy is auto: dynamic when the input brought colours, the effect's own
gradient when it did not. --colors pins it either way.

The engine clock is set from the frame rate the command actually paints at.
matrix and thunderstorm and tuffbaby are written in seconds and read them off
that clock, and left at the default sixty they run at the wrong speed at every
other rate. Over five lines of text, matrix takes 19.1 seconds at thirty
frames a second, 17.3 at sixty and 16.4 at a hundred and twenty: the rain is
the same length every time and only the part written in frames shortens. With
the clock pinned to sixty the same run takes 34.5 seconds, 17.3 and 8.6.

An animation nobody can see is worse than no program, so output that is not a
terminal gets the input copied through byte for byte. `ls | tuiffects | less`
then shows what `ls | less` shows and `ls | tuiffects > out` writes what
`ls > out` writes. Whether it is a terminal is a question for the kernel and
not for the file's mode: /dev/null is a character device too, and a mode check
alone spends twelve seconds animating into it.

The canvas is as wide as the window, because the effects that need room need it
sideways, and as tall as the input, because a program in a pipeline has no
business taking twenty rows of scrollback to animate one line. One row is held
back for the prompt. A line too long for the window wraps rather than being
cropped, since a piece of ANSI art is mostly right hand side, and the lines
below the window are printed under the animation in the colour they were
written in. Nothing is lost in either direction. A line that would be half on
the canvas and half under it goes under it whole, because drawing it twice is
worse than a canvas one row short.

Effects are not all the same length. Over a six row `ls` at sixty frames a
second, twenty nine of the thirty six finish inside twelve seconds and the
rest run from binarypath at 14.6 to swarm at 37.6. Half a minute is not
something to hand a pipeline by default, so the run has a wall clock limit,
twelve seconds unless --seconds says otherwise. Past it the effect stops
painting and stops sleeping and runs out at full speed, and the last frame is
painted from the state it finished in. A capped run is a short animation and
never a half formed picture, which is why random still picks from all thirty
six.

The animation is drawn in the flow of the shell rather than on the alternate
screen, so the finished picture stays where the command's own output would
have been. The cursor is hidden for the run and shown again on every path out,
the interrupt included: restore is one function, the signal handler and the
end of the animation both go through it, and it is safe to call twice.

It adds no dependency. The SGR reader, the rune widths and the window size are
all written against the standard library, so importing the library costs
exactly what it did before this existed.
2026-09-01 18:16:42 +04:00
.github/workflows Guard the page's argv against the hash, select by row, keep a pause across a resize 2026-08-28 07:45:13 -04:00
cmd/tuiffects Add the command: animate an argument, a file or a pipe 2026-09-01 18:16:42 +04:00
demo Guard the page's argv against the hash, select by row, keep a pause across a resize 2026-08-28 07:45:13 -04:00
upstream Match the patch header to the repository's git identity 2026-09-01 09:02:42 +04:00
web Read the grid once a frame, not once a row 2026-08-31 23:22:06 +04:00
.gitignore Add the command: animate an argument, a file or a pipe 2026-09-01 18:16:42 +04:00
animation.go Add the spanning trees, the particle pool and the clock 2026-08-26 23:12:09 +04:00
canvas.go Terminal text effects for Go, ported from ttfx 2026-08-26 22:06:59 +04:00
catalogue_test.go Add tuffbaby, the first effect original to this package 2026-08-27 16:02:22 +04:00
character.go Add the spanning trees, the particle pool and the clock 2026-08-26 23:12:09 +04:00
clock.go Add the spanning trees, the particle pool and the clock 2026-08-26 23:12:09 +04:00
clock_test.go Add the spanning trees, the particle pool and the clock 2026-08-26 23:12:09 +04:00
color.go Port the rest of the catalogue: thirty-five effects 2026-08-27 00:29:42 +04:00
colorpolicy_test.go Give the colour policy test a cell with a foreground and no background 2026-08-27 17:43:39 +04:00
easing.go Terminal text effects for Go, ported from ttfx 2026-08-26 22:06:59 +04:00
effect.go Add tuffbaby, the first effect original to this package 2026-08-27 16:02:22 +04:00
effect_binarypath.go Port the rest of the catalogue: thirty-five effects 2026-08-27 00:29:42 +04:00
effect_binarypath_test.go Port the rest of the catalogue: thirty-five effects 2026-08-27 00:29:42 +04:00
effect_blackhole.go Port the rest of the catalogue: thirty-five effects 2026-08-27 00:29:42 +04:00
effect_blackhole_test.go Port the rest of the catalogue: thirty-five effects 2026-08-27 00:29:42 +04:00
effect_bouncyballs.go Port the rest of the catalogue: thirty-five effects 2026-08-27 00:29:42 +04:00
effect_bouncyballs_test.go Port the rest of the catalogue: thirty-five effects 2026-08-27 00:29:42 +04:00
effect_bubbles.go Port the rest of the catalogue: thirty-five effects 2026-08-27 00:29:42 +04:00
effect_bubbles_test.go Port the rest of the catalogue: thirty-five effects 2026-08-27 00:29:42 +04:00
effect_burn.go Port the rest of the catalogue: thirty-five effects 2026-08-27 00:29:42 +04:00
effect_burn_test.go Port the rest of the catalogue: thirty-five effects 2026-08-27 00:29:42 +04:00
effect_crumble.go Port the rest of the catalogue: thirty-five effects 2026-08-27 00:29:42 +04:00
effect_crumble_test.go Port the rest of the catalogue: thirty-five effects 2026-08-27 00:29:42 +04:00
effect_decrypt.go Port the rest of the catalogue: thirty-five effects 2026-08-27 00:29:42 +04:00
effect_errorcorrect.go Port the rest of the catalogue: thirty-five effects 2026-08-27 00:29:42 +04:00
effect_errorcorrect_test.go Port the rest of the catalogue: thirty-five effects 2026-08-27 00:29:42 +04:00
effect_expand.go Port the rest of the catalogue: thirty-five effects 2026-08-27 00:29:42 +04:00
effect_expand_test.go Port the rest of the catalogue: thirty-five effects 2026-08-27 00:29:42 +04:00
effect_fireworks.go Port the rest of the catalogue: thirty-five effects 2026-08-27 00:29:42 +04:00
effect_fireworks_test.go Port the rest of the catalogue: thirty-five effects 2026-08-27 00:29:42 +04:00
effect_highlight.go Port the rest of the catalogue: thirty-five effects 2026-08-27 00:29:42 +04:00
effect_highlight_test.go Port the rest of the catalogue: thirty-five effects 2026-08-27 00:29:42 +04:00
effect_laseretch.go Port the rest of the catalogue: thirty-five effects 2026-08-27 00:29:42 +04:00
effect_laseretch_test.go Port the rest of the catalogue: thirty-five effects 2026-08-27 00:29:42 +04:00
effect_matrix.go Port the rest of the catalogue: thirty-five effects 2026-08-27 00:29:42 +04:00
effect_matrix_test.go Port the rest of the catalogue: thirty-five effects 2026-08-27 00:29:42 +04:00
effect_middleout.go Port the rest of the catalogue: thirty-five effects 2026-08-27 00:29:42 +04:00
effect_middleout_test.go Port the rest of the catalogue: thirty-five effects 2026-08-27 00:29:42 +04:00
effect_orbittingvolley.go Port the rest of the catalogue: thirty-five effects 2026-08-27 00:29:42 +04:00
effect_orbittingvolley_test.go Port the rest of the catalogue: thirty-five effects 2026-08-27 00:29:42 +04:00
effect_overflow.go Port the rest of the catalogue: thirty-five effects 2026-08-27 00:29:42 +04:00
effect_overflow_test.go Port the rest of the catalogue: thirty-five effects 2026-08-27 00:29:42 +04:00
effect_pour.go Port the rest of the catalogue: thirty-five effects 2026-08-27 00:29:42 +04:00
effect_pour_test.go Port the rest of the catalogue: thirty-five effects 2026-08-27 00:29:42 +04:00
effect_print.go Port the rest of the catalogue: thirty-five effects 2026-08-27 00:29:42 +04:00
effect_print_test.go Port the rest of the catalogue: thirty-five effects 2026-08-27 00:29:42 +04:00
effect_rain.go Port the rest of the catalogue: thirty-five effects 2026-08-27 00:29:42 +04:00
effect_randomsequence.go Port the rest of the catalogue: thirty-five effects 2026-08-27 00:29:42 +04:00
effect_randomsequence_test.go Port the rest of the catalogue: thirty-five effects 2026-08-27 00:29:42 +04:00
effect_rings.go Port the rest of the catalogue: thirty-five effects 2026-08-27 00:29:42 +04:00
effect_rings_test.go Port the rest of the catalogue: thirty-five effects 2026-08-27 00:29:42 +04:00
effect_scattered.go Port the rest of the catalogue: thirty-five effects 2026-08-27 00:29:42 +04:00
effect_scattered_test.go Port the rest of the catalogue: thirty-five effects 2026-08-27 00:29:42 +04:00
effect_slice.go Port the rest of the catalogue: thirty-five effects 2026-08-27 00:29:42 +04:00
effect_slice_test.go Port the rest of the catalogue: thirty-five effects 2026-08-27 00:29:42 +04:00
effect_slide.go Port the rest of the catalogue: thirty-five effects 2026-08-27 00:29:42 +04:00
effect_slide_test.go Port the rest of the catalogue: thirty-five effects 2026-08-27 00:29:42 +04:00
effect_smoke.go Port the rest of the catalogue: thirty-five effects 2026-08-27 00:29:42 +04:00
effect_smoke_test.go Port the rest of the catalogue: thirty-five effects 2026-08-27 00:29:42 +04:00
effect_spotlights.go Port the rest of the catalogue: thirty-five effects 2026-08-27 00:29:42 +04:00
effect_spotlights_test.go Port the rest of the catalogue: thirty-five effects 2026-08-27 00:29:42 +04:00
effect_spray.go Port the rest of the catalogue: thirty-five effects 2026-08-27 00:29:42 +04:00
effect_spray_test.go Port the rest of the catalogue: thirty-five effects 2026-08-27 00:29:42 +04:00
effect_swarm.go Port the rest of the catalogue: thirty-five effects 2026-08-27 00:29:42 +04:00
effect_swarm_test.go Port the rest of the catalogue: thirty-five effects 2026-08-27 00:29:42 +04:00
effect_sweep.go Port the rest of the catalogue: thirty-five effects 2026-08-27 00:29:42 +04:00
effect_sweep_test.go Port the rest of the catalogue: thirty-five effects 2026-08-27 00:29:42 +04:00
effect_synthgrid.go Port the rest of the catalogue: thirty-five effects 2026-08-27 00:29:42 +04:00
effect_synthgrid_test.go Port the rest of the catalogue: thirty-five effects 2026-08-27 00:29:42 +04:00
effect_thunderstorm.go Port the rest of the catalogue: thirty-five effects 2026-08-27 00:29:42 +04:00
effect_thunderstorm_test.go Port the rest of the catalogue: thirty-five effects 2026-08-27 00:29:42 +04:00
effect_tuffbaby.go Let tuffbaby's surplus leave at its own speed, not the gather's 2026-08-28 07:44:26 +04:00
effect_tuffbaby_test.go Let tuffbaby's surplus leave at its own speed, not the gather's 2026-08-28 07:44:26 +04:00
effect_unstable.go Port the rest of the catalogue: thirty-five effects 2026-08-27 00:29:42 +04:00
effect_unstable_test.go Port the rest of the catalogue: thirty-five effects 2026-08-27 00:29:42 +04:00
effect_vhstape.go Port the rest of the catalogue: thirty-five effects 2026-08-27 00:29:42 +04:00
effect_waves.go Port the rest of the catalogue: thirty-five effects 2026-08-27 00:29:42 +04:00
effect_wipe.go Port the rest of the catalogue: thirty-five effects 2026-08-27 00:29:42 +04:00
effect_wipe_test.go Port the rest of the catalogue: thirty-five effects 2026-08-27 00:29:42 +04:00
effects_new_test.go Terminal text effects for Go, ported from ttfx 2026-08-26 22:06:59 +04:00
engine.go Add the spanning trees, the particle pool and the clock 2026-08-26 23:12:09 +04:00
engine_test.go Add tuffbaby, the first effect original to this package 2026-08-27 16:02:22 +04:00
events.go Terminal text effects for Go, ported from ttfx 2026-08-26 22:06:59 +04:00
framesize_test.go Terminal text effects for Go, ported from ttfx 2026-08-26 22:06:59 +04:00
fullscreen_test.go Terminal text effects for Go, ported from ttfx 2026-08-26 22:06:59 +04:00
geometry.go Add the porting guide and the helpers the rest of the catalogue needs 2026-08-26 22:29:41 +04:00
go.mod Terminal text effects for Go, ported from ttfx 2026-08-26 22:06:59 +04:00
helpers_test.go Add the porting guide and the helpers the rest of the catalogue needs 2026-08-26 22:29:41 +04:00
LICENSE Terminal text effects for Go, ported from ttfx 2026-08-26 22:06:59 +04:00
motion.go Add the spanning trees, the particle pool and the clock 2026-08-26 23:12:09 +04:00
NOTICE Port the rest of the catalogue: thirty-five effects 2026-08-27 00:29:42 +04:00
ordered.go Add the spanning trees, the particle pool and the clock 2026-08-26 23:12:09 +04:00
particles.go Add the spanning trees, the particle pool and the clock 2026-08-26 23:12:09 +04:00
particles_test.go Add the spanning trees, the particle pool and the clock 2026-08-26 23:12:09 +04:00
PORTING.md Port the rest of the catalogue: thirty-five effects 2026-08-27 00:29:42 +04:00
README.md Add the command: animate an argument, a file or a pipe 2026-09-01 18:16:42 +04:00
rng.go Terminal text effects for Go, ported from ttfx 2026-08-26 22:06:59 +04:00
screen_fidelity_test.go Add tuffbaby, the first effect original to this package 2026-08-27 16:02:22 +04:00
spanning_tree.go Add the spanning trees, the particle pool and the clock 2026-08-26 23:12:09 +04:00
spanning_tree_test.go Add the spanning trees, the particle pool and the clock 2026-08-26 23:12:09 +04:00
Taskfile.yml Add the command: animate an argument, a file or a pipe 2026-09-01 18:16:42 +04:00
terminal.go Add the spanning trees, the particle pool and the clock 2026-08-26 23:12:09 +04:00
tuffbaby_frames.go Add tuffbaby, the first effect original to this package 2026-08-27 16:02:22 +04:00
vhstape_slip_test.go Terminal text effects for Go, ported from ttfx 2026-08-26 22:06:59 +04:00

tuiffects

Terminal text effects as a Go library. Feed it text or a captured cell grid, pick an effect, and pull frames off it one at a time.

Credit where it is due

This is a port of a port, and it originates none of the art.

TerminalTextEffects by ChrisBuilds is the original. Every effect in this package, and the architecture of the engine that runs them, are that project's design. ttfx by omacom-io translated that work to Rust and says the same thing about itself. This package translates ttfx to Go.

All three are MIT. All three copyrights are preserved in LICENSE, and NOTICE maps every file here to both the ttfx source and the TerminalTextEffects source it came from.

One effect, tuffbaby, is the exception: it is original to this package and has no upstream. It is written against ChrisBuilds' engine like every other effect here. It is not in NOTICE, because NOTICE records what was translated and from where; it declares where its material came from in its own Descriptor.Origin instead. catalogue_test.go holds every effect to one or the other and refuses both, so an original cannot quietly claim an upstream and a port cannot quietly lose one.

If you like what you see, star the original. Effect ideas belong upstream, where they were invented.

Using it

import "github.com/Gaurav-Gosain/tuiffects"
terminal := tuiffects.NewTerminalFromText("hello", tuiffects.TerminalConfig{
    Width: 80, Height: 24,
})
engine := tuiffects.NewEngine(terminal, tuiffects.NewRng(1))

effect := tuiffects.NewDecrypt(tuiffects.DefaultDecryptConfig())
if err := effect.Build(engine); err != nil {
    return err
}
for effect.Advance(engine) {
    fmt.Print("\x1b[H", engine.Frame())
}

Advance does not return the frame. Read it with engine.Frame() for an ANSI string, or engine.FrameRows() for rows of visuals you can style yourself. That is the one place this port deliberately differs in shape from ttfx, which writes to a tty it owns. It is what lets this drive a pane, a widget or a screen saver rather than a terminal.

To animate a screen rather than a string, hand it a cell grid:

terminal := tuiffects.NewTerminalFromCells(cells, tuiffects.TerminalConfig{
    Width:                 cols,
    Height:                rows,
    ExistingColorHandling: tuiffects.DynamicExistingColors,
})

DynamicExistingColors makes every character resolve back to the colour it arrived with, so the screen reassembles as itself rather than in the effect's own palette. It is the mode to use when the input is a picture that was already on the screen.

The command

tuiffects animates whatever you give it: an argument, a file, or a pipe.

go install github.com/Gaurav-Gosain/tuiffects/cmd/tuiffects@latest
echo hello | tuiffects
ls --color=always | tuiffects
git diff --color | tuiffects -e burn
tuiffects "shipped"
tuiffects capture.ans

It reads the SGR colours in the input and keeps them, so what dissolves and re-forms is your own output rather than a recolouring of it. With no colours in the input the effect uses its own palette instead. --list names the effects, --effect picks one, and with no flags it picks at random.

The animation is drawn where the command's output would have been, and the finished picture stays there. If the output is not a terminal, ls | tuiffects | less, the input is copied through and nothing is animated.

It lives under cmd/ and it adds no dependency to the module: everything it needs beyond the library is the standard library.

Demo

Every effect runs live at neomantra.github.io/tuiffects: the whole catalogue in a real terminal emulator, compiled to WebAssembly with go-booba. The same program runs in your own terminal:

cd demo && go run ./cmd/tuiffects-demo rain

/ step through the catalogue, r replays, space pauses, q quits. The demo lives in its own module under demo/ so the library stays free of dependencies. To build and serve the page locally, task serve (needs Task and npx), then open http://localhost:3000/.

One change to how the page draws

web/ghostty-web-viewport.js is ours, and the page loads it. It is the only part of the demo that reaches into a dependency.

ghostty-web's renderer paints a frame one row at a time, and each row asks the terminal for that row. That request reads every cell in the grid, so a frame that paints forty rows reads the grid forty times. The file wraps two methods so a frame reads the grid once. On this catalogue that makes each frame three to four times cheaper and about doubles the frames the page can paint.

It does not edit anything under web/ghostty-web/. Those files are copied in by go tool booba-assets and stay exactly as go-booba ships them.

The same change is written up for ghostty-web itself in upstream/. When it lands there, delete web/ghostty-web-viewport.js and its two lines in web/index.html.

What is here

The engine, in the shape ttfx found it:

Piece What it is
Coord, geometry.go 1-based grid coordinates, origin bottom left, lines and bezier curves
Color, Gradient colour ramps and the coordinate mappings effects paint across the canvas
Easing the thirty-one standard curves
Waypoint, Path, Motion where a character goes and how fast
Frame, Scene, Animation what a character looks like over time
Event, Action how a scene or path hands off to the next one
Character one cell: its animation, its motion, its handlers
Canvas, Terminal the grid, the character populations, and the frame painter
ParticlePool recycles the short-lived characters an effect throws off
PrimsSimple and the other spanning trees join the canvas into a tree, and give an effect its running order
Clock seconds, for the effects written in seconds rather than frames
Engine the stepping loop that ties all of it together

Thirty-five effects, every one ttfx ships except beams and colorshift:

Effect What it does What it shows
binarypath every character breaks into the binary digits of its code point, which travel the canvas and collapse back into it added characters, right-angled paths, a group released one member a frame, and a two-phase run ending in a diagonal wipe
blackhole the text scatters into a starfield, a ring of stars eats it, then the singularity explodes and it drifts home five phases, looping ring paths, distance-synced scenes, layers, and a colour ramp on both foreground and background
bouncyballs balls fall in from above the screen and bounce into place motion that starts off the canvas, a non-monotonic easing on a path, and a row-by-row release from the bottom up
bubbles groups of characters ride a bubble down the screen, which pops and drops them into place an added anchor character stepped by hand, a rigid ring redrawn around it each frame, and paths chained through a burst
burn fire spreads through the text and each character cools into its colour a spanning tree as the running order, a recycled particle pool for the smoke, and a background carried through a dynamic run
crumble the text dims, falls to the floor as dust, is vacuumed out the top, then flies home and re-forms four stages over one shared set of paths and scenes, a distance-synced dust animation, and a layer change mid-fall
decrypt types out ciphertext, then decrypts it per-character scenes and scene-to-scene chaining, no motion at all
errorcorrect some characters start in each other's places and swap back a scene handing off to a path and back, layer changes while a character is in flight, and a queue released on a delay
expand the text starts piled on one cell and grows out of it eased paths out of a single point, distance-synced colour ramps, and layer swaps in flight
fireworks characters climb as shells, burst apart, and fall into place three chained paths per character, bezier arcs, a looping scene and a step-synced one
highlight a band of light sweeps over the text an eased sequence releasing character groups, brightness-derived gradients, no motion
laseretch a laser beam cuts the text on, one character at a time a spanning tree for the etch order, a particle pool for the sparks
matrix green rain falls down the screen and the text resolves out of it the engine clock, columns cut from the whole canvas including the fill, and drawing by appearance instead of scenes
middleout text collapses onto the centre, spreads along one axis, then expands out two paths per character run in phases the effect drives itself, and a colour ramp that opens on a fixed starting colour
orbittingvolley four launchers circle the canvas and fire the text into place one moving character driving three others' positions, layered paths, and a per-frame launch queue
overflow rows of the text scroll up past the screen out of order, then the real picture scrolls in from the bottom copies of the input as extra characters, whole-canvas row groups, and a scroll that only lands correctly with fill characters
pour characters pour in from one edge and fill the canvas from the near side first row and column groups released in alternating order, one path and one colour ramp per character
print types the canvas out one line at a time on the bottom row and scrolls the page up under it a character of its own as the print head, a path rebuilt per line, row groups over the whole canvas, and one scene per cell
rain characters fall in and settle paths, easing, and a path completion handing off to an animation
randomsequence fades the text back in one character at a time, in a random order a shuffled reveal order, a per-character colour ramp with no motion, paths or events at all
rings text gathers into spinning rings, scatters, and goes home many chained looping paths per character, phase timers, and rings that turn opposite ways
scattered characters start in random places and gather into the text paths from random start coordinates, a distance-synced colour ramp, and layer swaps in flight
slice the picture is cut in two and the halves slide back in from opposite edges eased paths over the fill characters as well as the input, and two halves shearing past each other
slide rows, columns or diagonals push in from off screen groups released on a gap timer, one character per group per frame, each on its own eased path
smoke smoke seeps out from one cell and colours the text as it passes a weighted spanning tree, a breadth-first walk of it one layer per frame, scene-to-scene handover
spotlights beams of light search the screen, meet in the middle, then widen until everything is lit direct appearance changes with no scenes at all, chained looping paths, and a distance-based falloff
spray characters shoot out of one point on the edge and fly into place per-character path speed, a layer lifted for the flight and dropped on arrival, and a burst release sized by the character count
swarm groups of characters fly between gathering points, then land grouped characters, chained paths, and one group member pulling the rest along
sweep two bands cross the canvas, the first uncovering the characters in grey and the second colouring them one eased sequence run twice over different groupings, and fill characters so the whole canvas shimmers
synthgrid a grid draws itself across the screen, fills its blocks in a few at a time, then takes itself back down added characters on a layer above the text, fill characters, and phases driven by a per-block completion count
thunderstorm the text dims, rain crosses it, and lightning strikes and leaves it glowing two particle pools, the seconds clock, and characters the effect adds to the terminal itself
unstable the screen scrambles, shakes itself apart, and flies back together whole-screen coordinate shoves, two eased flights, and a three-phase run
vhstape rows slip and the picture is redrawn paths driving synced scenes, row groups, and several phases
waves a band of blocks sweeps across eased scenes released in bands, a sweep with no motion at all
wipe a line crosses the screen and the text appears behind it an easing curve deciding which character groups are released, and taking them back when it reverses

And one that is not a port:

Effect What it does What it shows
tuffbaby the text on screen gathers into a picture, a short clip plays in it a tone at a time, and everything goes home a deflated frame sequence decoded once and scaled to the canvas, characters appended when the screen has too few and swept off the nearest edge when it has too many, and animation by repaint rather than by motion

tuffbaby is the one effect here nobody upstream wrote; see Credit where it is due and its Origin, which names where its frames came from. It takes whatever is already on the screen and arranges it into the picture, so what the picture is drawn out of is your own text. The cells are the union of every frame of the clip, which works out at a bit over half the canvas at any size: a denser screen parks its surplus off the edges, a sparser one has the rest appended, recycling the glyphs that were there.

It is the one effect that carries data: the frames are 18KB of deflated base64 in tuffbaby_frames.go, decoded once on first use, which is about half the size of the largest hand-written effect here. And it is a continuous-tone photograph rendered in text, so it is softer than the shapes the other effects draw: a glyph fills about a third of its cell and how much varies per glyph, which is louder than a five step ramp. The light end is drawn bold to claw some of that back. It reads best on a wide canvas.

Adding an effect

Write one file. Implement Build (set up scenes and paths on every character) and Advance (release a few characters, call engine.Update(), say whether you are done), and call Register from an init. The ones here are 160 to 1000 lines each and the engine does the rest.

PORTING.md is the full guide for bringing one across from ttfx: the call-for-call mapping, the quirks that are wrong on purpose, what the colour policy does, and what a finished port has to include.

Differences from ttfx

  • No parity with the Python original, and no Mersenne Twister clone. The same effect will not produce the same frames as either upstream. NewRng(seed) makes a run reproducible within this package, which is what the tests need.
  • Time is virtual by default. The engine's clock advances one frame's worth per Update rather than reading the machine, so an effect written in seconds runs to the same number of frames every time. Set Engine.Clock to the rate the host really paints at: NewEngine assumes sixty, and every effect written in seconds runs at the wrong speed on a host that paints at anything else. NewRealClock is there for a host that would rather have wall time.
  • No command line, no tty writer, no resize handling. The host owns the screen.
  • Thirty-five effects rather than thirty-seven. beams and colorshift are not ported.
  • Rounding quirks that change how effects look are kept: half-to-even rounding on coordinates, floor division on gradient channel steps, and the bezier arc-length estimate that stops at t=0.9. Removing them would retune every effect by a little, silently.
  • Several effects behave differently under DynamicExistingColors, because upstream is written for piped text and that mode means the input was already on the screen. Backgrounds a captured cell carried survive the run rather than blinking out; anything an effect throws across the screen carries the background of the cell it is over rather than punching a hole through it; a ramp that closes on a background starts from that background rather than flushing the bar white first; and an effect whose subject is a colour change is given a neutral foreground to work with on a cell that arrived with none. Each is commented where it is made, and the default behaviour is unchanged.

Licence

MIT. See LICENSE and NOTICE.