Help us shape the future of Go! Take the 2026 Go Developer Survey

termanim

module
v0.0.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 3, 2026 License: MIT

README

termanim

Thirty-six terminal animations in Go, on tcell. They run in a terminal, and — because they never create or destroy the screen they are given — equally in a browser through tuiwasm or inside any program that already owns a tcell.Screen.

Live demo — every animation here runs in a browser tab as part of tuiwasm, which registers them from this package. Each has a page of its own: matrix · donut · fireworks · aquarium · boids · plasma · starfield · life — and the rest from the launcher there.

The animations

Each tile above is a real frame, screenshotted from the browser build rather than drawn for the README — so what a tile shows is what the animation does at some moment, including the ones that take a while to establish themselves.

go install github.com/0magnet/termanim/cmd/termanim@latest

termanim --list
termanim donut

Press q, Escape or Ctrl-C to stop.

What is here

Fields and simulations, drawn on the pixel surface:

package effect
fire a heat grid seeded with noise and cooled upward
plasma summed sine waves of position, offset by time
metaballs blobs that bulge and merge as they approach
moire two drifting ripples interfering
lavalamp wax that heats, rises, cools and sinks in a vessel
tunnel flying down a textured tube
starfield stars streaming past the viewer
donut a lit torus with a z-buffer
cube a rotating wireframe solid, shaded by depth
atom electrons on tilted orbits round a glowing nucleus
boids flocking by separation, alignment and cohesion
rain drops with depth, slant, streaks and splashes
snow flakes that sway, settle and drift into banks
fireworks shells that rise, burst and droop into willows
life Conway's life, colored by how long a cell has lived
langton Langton's ants: chaos, then the highway
sand grains heaping at their angle of repose
maze a maze carved by backtracking, then solved
ripple a ripple tank: rain falling, rings meeting and interfering
reaction Gray–Scott reaction–diffusion: spots that divide into a labyrinth
physarum slime mold laying trails and building a transport network
flow particles carried through a divergence-free curl-noise field
frost a crystal growing by diffusion-limited aggregation
wolfram elementary cellular automata scrolling upward — 30, 90, 110
pendulum double pendulums released together, shearing apart
lightning a branching discharge: leader, return stroke, afterglow
aurora curtains of light, folding, with the creases where they turn
julia a Julia set morphing as its parameter walks the cardioid
parrot the party parrot, rolling once around the hue wheel
bounce the screensaver logo, and the wait for it to hit a corner
clock an analog clock, after aclock
magnetosphere the logo from magnetosphere.net, its rings and field counter-scrolling

Made of characters, painting glyphs directly:

package effect
matrix falling columns of glyphs
aquarium fish swimming past swaying seaweed
pipes pipes growing and turning, with correct elbows
bonsai a bonsai tree growing branch by branch

canvas holds the surface, the palettes and the frame loop they share.

Behind other text

matrix/backdrop composes text over the rain instead of replacing the screen with it. The text keeps a cell of clear either side of every word, so it stays readable, and the rain fills what the layout left empty. Two shapes use it, and both are in service.

A still frame behind a help screen. matrix/backdrop/cobrarain wraps a cobra command's help function, buffers what it would have printed, and renders the rain behind it:

cobrarain.On(rootCmd, backdrop.Options{})

That is the whole integration. Help for every command under the root goes through it, because cobra looks up the help function on the parent when a command has none of its own. cmd/rainhelp is a worked example — a CLI with nothing in it but that one call.

It is a still: nothing takes over the terminal, nothing waits for a key, and the help scrolls up the scrollback the way help does. Seed it and the same frame comes back every time:

go run ./cmd/rainhelp --seed 1 --help     # the same frame every time
go run ./cmd/rainhelp --help | cat        # plain, as a pipe should be

Redirecting or piping turns it off, so --help | less and a --help pasted into a bug report are both plain text. Wire it after anything else that styles the help — coloredcobra, say — since it captures whichever help function is installed when it is called.

It does not have to be the rain. Set Options.Anim and any of the animations above goes behind the text instead — through Render, through the Painter, and through cobrarain, which needed no changes to gain it:

cobrarain.On(rootCmd, backdrop.Options{Anim: plasma.New(), Dim: 3})
p := backdrop.New(backdrop.Options{Anim: fire.New(0), Dim: 2, Pad: -1, GapMin: 4})

RenderAnim and NewFor are the same thing with the animation passed directly, for a caller holding one rather than filling in options. Which suits it is a question of density: the rain is mostly gaps, which is what makes it read as something behind the text, while a plasma or a fire covers every cell and puts the text on a solid field. Dim and GapMin are the knobs for that, and something sparse — a starfield, fireworks, frost — needs neither. The examples above set Dim for that reason: an undimmed plasma is bright enough across every cell that the help sits on it rather than in front of it.

An animated background behind a TUI. backdrop.New returns a Painter that keeps the rain's state between frames, so a full-screen program can redraw its own text over a rain that keeps falling:

p := backdrop.New(backdrop.Options{Pad: -1, GapMin: 4, Force: true})
for {
    io.WriteString(out, p.Frame(render(), dt)) // or p.Tick(render())
}

Frame advances by elapsed time and Tick by one step, which is the same distinction the animations themselves draw — see Frame rate below. The options above are skywire's, which drives this under skywire --tui: Pad: -1 and GapMin: 4 because that program composes its own screen and tells the backdrop where the empty space is, and Force: true because it is undimmed — the clear cell either side of each word is what keeps the text legible, and GapMin already confines the rain to the gaps.

Any of the pixel animations, not just the rain. RenderAnim is Render with something else behind the text, and NewFor is New the same way:

fmt.Print(backdrop.RenderAnim(help, starfield.New(0), backdrop.Options{}))

p := backdrop.NewFor(plasma.New(), backdrop.Options{Pad: -1, GapMin: 4})

Anything implementing canvas.Animation works, which is sixteen of the effects here and anything you write. What made this possible was giving the compositor a cell type of its own: it used to read matrix.Cell, which carries an Intensity into a green ramp and a Hot flag for the highlighted leading glyph — the rain's own vocabulary, asking questions a plasma cannot answer. backdrop.Cell is a glyph and the colors to draw it in, backdrop.Frame is a grid of those, and the two adapters that fill one — Frame.FromMatrix and Frame.FromSurface — are where each animation's own model is resolved. Fill a Frame yourself and RenderFrame will composite over it.

FromSurface is the half-block trick working under text rather than on a screen of its own: two pixel rows to the cell row, and a cell with one pixel lit drawn as whichever block leaves the other half to the terminal.

Sparse effects suit this best — snow, rain, starfield, life, fireworks, boids leave most cells clear, which is what makes a backdrop read as something behind. The dense fields, plasma and metaballs and fire, cover every cell and put the text on a solid field; Dim and GapMin are the knobs for that. Warm is the pixel animations' equivalent of Steps, since they open on an empty screen and take a second to fill.

The four CellAnimation effects — aquarium, pipes, clock, bonsai — paint a tcell.Screen rather than a surface and are still out of reach. They would need a screen-to-cell adapter, which is a different piece of work.

Two pixels per cell

A terminal cell is about twice as tall as it is wide, so an animation drawn one pixel per cell looks squat and coarse. canvas.Surface draws every cell as an upper half block — ▀, foreground coloring the top half, background the bottom — which gives two independently colored pixels per cell, roughly square, at no cost. An animation sees a surface twice the height of the terminal and never thinks about it.

Not everything wants that. matrix, aquarium, pipes and bonsai are made of characters, and squeezing them onto a pixel surface would throw away the thing they are made of, so they implement canvas.CellAnimation and paint glyphs. Both shapes share the same loop, keys and resize handling.

Writing another one

Implement two methods and hand it to canvas.Run:

type Animation interface {
	Resize(w, h int)                    // allocate here, not per frame
	Frame(s *canvas.Surface, dt float64) // dt is seconds since the last frame
}

Resize is called once before the first frame and again on every size change, so buffers are allocated there and the frame path stays allocation-free — which matters at sixty frames a second in a browser. Most packages here have a test asserting Frame allocates nothing.

Scale every motion by dt and express its constant per second. canvas clamps dt to 0.1 s, so a stalled process resumes rather than teleporting everything across the screen in one step.

Frame rate

The default is 60, and it fits: measured on a 200x50 terminal these effects compute a frame in 0.05 to 4.3 ms, while handing a full screen to tcell costs about 6.8 ms whatever is being drawn. The output path dominates and is the same for all of them, so even the heaviest sits inside a 16.7 ms budget.

Every animation advances by elapsed time, not by frame. Frame is given the seconds since the last one, and every rate is expressed per second. That buys two things:

  • The target rate can change without changing how fast anything appears to move.
  • A browser tab that misses ticks drops frames instead of running in slow motion, which is what happens when motion is counted per frame.

Most packages have a test asserting it: the same wall-clock interval, divided into frames two different ways, must reach the same state. Several are pixel-identical between 30 and 60 fps.

Where the rule is a discrete step — a Life generation, an ant's move, a grain falling one cell — the animation keeps a time accumulator and runs whole steps from it, rather than a fractional one. Those are documented as a rate per second on the type.

Converting this exposed two bugs that had nothing to do with frame rate as such. A drag written as v *= 0.9 per frame is not a property of the fluid at all but of how often it is drawn, and had to become exp(-rate·dt). And a projectile under gravity does not follow the same arc when stepped at a different size unless it is integrated with the midpoint form.

Provenance

Every effect here is written from a description of the technique, not from anyone's source.

The methods are old and widely documented: a heat grid cooled toward the average of the cells beneath it; a sum of sine waves of the pixel coordinates offset by time; an inverse-square field per blob; a torus parametrised by two angles and depth-tested; Reynolds' three flocking rules; B3/S23; a recursive backtracker. Matrix rain, an aquarium, pipes and a bonsai are each fully described by what they look like.

Programs that draw the same things — libcaca's cacafire and cacademo, aalib's aafire, TMatrix, neo, cmatrix, asciiquarium, cbonsai, pipes.sh — were used as a reference for what the result should look like, and most of those are GPL. None of their code was read into this, and none of their artwork is used: the fish here are drawn from scratch and are deliberately simpler than asciiquarium's. That is why this repository can be MIT.

License

MIT — see LICENSE.

Dependency Graph

Made with goda:

# GOOS=js: the import edges of a wasm program live in js/wasm-tagged
# files and are invisible to a host-context run
GOOS=js GOARCH=wasm go run github.com/loov/goda@latest graph github.com/0magnet/termanim/... | dot -Tsvg -o docs/termanim-goda-graph.svg

Dependency Graph

Lines of Code

Made with gocloc (excludes vendor/, node_modules/, .git/):

gocloc --not-match-d='(vendor|node_modules|\.git)' .
-------------------------------------------------------------------------------
Language                     files          blank        comment           code
-------------------------------------------------------------------------------
Go                              66           1264           3347          10609
Markdown                         1             31              0            109
YAML                             1              0              7             98
-------------------------------------------------------------------------------
TOTAL                           68           1295           3354          10816
-------------------------------------------------------------------------------

Directories

Path Synopsis
Package aquarium is fish swimming across the terminal.
Package aquarium is fish swimming across the terminal.
Package atom is the textbook atom: electrons on tilted orbits around a nucleus, the whole assembly turning slowly in space.
Package atom is the textbook atom: electrons on tilted orbits around a nucleus, the whole assembly turning slowly in space.
Package aurora is curtains of light standing in the sky, rippling and folding.
Package aurora is curtains of light standing in the sky, rippling and folding.
Package boids is Reynolds' flocking.
Package boids is Reynolds' flocking.
Package bonsai grows a small tree, one branch at a time, in the terminal.
Package bonsai grows a small tree, one branch at a time, in the terminal.
Package bounce is the bouncing logo screensaver, and the wait for it to hit a corner.
Package bounce is the bouncing logo screensaver, and the wait for it to hit a corner.
Package canvas is the shared machinery every animation in this repository needs: a pixel surface twice the height of the terminal, and a frame loop that survives the places these run.
Package canvas is the shared machinery every animation in this repository needs: a pixel surface twice the height of the terminal, and a frame loop that survives the places these run.
Package clock is an analog clock drawn in a terminal.
Package clock is an analog clock drawn in a terminal.
cmd
rainhelp command
Command rainhelp is a cobra CLI whose help screen has the code rain behind it, generated fresh on every run.
Command rainhelp is a cobra CLI whose help screen has the code rain behind it, generated fresh on every run.
termanim command
Command termanim plays one of the terminal animations in this repository.
Command termanim plays one of the terminal animations in this repository.
Package cube is a wireframe solid turning in space.
Package cube is a wireframe solid turning in space.
Package donut is a lit torus turning in space.
Package donut is a lit torus turning in space.
Package fire is a burning flame.
Package fire is a burning flame.
Package fireworks is shells launching and bursting over the terminal.
Package fireworks is shells launching and bursting over the terminal.
Package flow drifts thousands of particles through a noise field and lets their trails draw it.
Package flow drifts thousands of particles through a noise field and lets their trails draw it.
Package frost is diffusion-limited aggregation: frost on a window, or coral.
Package frost is diffusion-limited aggregation: frost on a window, or coral.
internal
simscreen
Package simscreen builds a tcell screen for tests to draw into and read back.
Package simscreen builds a tcell screen for tests to draw into and read back.
Package julia draws a Julia set whose shape will not hold still.
Package julia draws a Julia set whose shape will not hold still.
Package langton is Langton's Ant.
Package langton is Langton's Ant.
Package lavalamp is wax rising and sinking in a heated glass vessel.
Package lavalamp is wax rising and sinking in a heated glass vessel.
Package life is Conway's Game of Life.
Package life is Conway's Game of Life.
Package lightning is a branching electrical discharge.
Package lightning is a branching electrical discharge.
Package magnetosphere animates magnetosphere.net's logo: a waisted funnel standing in a field of stripes, the funnel scrolling one way and the field the other.
Package magnetosphere animates magnetosphere.net's logo: a waisted funnel standing in a field of stripes, the funnel scrolling one way and the field the other.
Package matrix is falling columns of glyphs.
Package matrix is falling columns of glyphs.
backdrop
Package backdrop prints a block of text over a still frame of the code rain.
Package backdrop prints a block of text over a still frame of the code rain.
backdrop/cobrarain
Package cobrarain puts the code rain — or any other animation — behind a cobra command's help.
Package cobrarain puts the code rain — or any other animation — behind a cobra command's help.
Package maze carves a maze and then solves it, both in view.
Package maze carves a maze and then solves it, both in view.
Package metaballs is the merging-blob effect.
Package metaballs is the merging-blob effect.
Package moire is the interference pattern of two overlapping ripples.
Package moire is the interference pattern of two overlapping ripples.
Package parrot is the party parrot: a bird's head that rolls in a circle while its color runs once around the hue wheel per roll.
Package parrot is the party parrot: a bird's head that rolls in a circle while its color runs once around the hue wheel per roll.
Package pendulum is a fan of double pendulums released from almost the same angle, drawn as fading trails.
Package pendulum is a fan of double pendulums released from almost the same angle, drawn as fading trails.
Package physarum is slime mold, the transport networks of Physarum polycephalum grown out of an agent rule three lines long.
Package physarum is slime mold, the transport networks of Physarum polycephalum grown out of an agent rule three lines long.
Package pipes is the pipes screensaver: colored runs of box drawing that crawl across the terminal, turn corners, and eventually fill it.
Package pipes is the pipes screensaver: colored runs of box drawing that crawl across the terminal, turn corners, and eventually fill it.
Package plasma is the drifting colored field of the demoscene.
Package plasma is the drifting colored field of the demoscene.
Package rain is falling rain with depth.
Package rain is falling rain with depth.
Package reaction is Gray–Scott reaction–diffusion: two chemicals stirred together on a grid, one of which eats the other.
Package reaction is Gray–Scott reaction–diffusion: two chemicals stirred together on a grid, one of which eats the other.
Package ripple is a ripple tank: drops fall on still water and the rings they leave run into one another.
Package ripple is a ripple tank: drops fall on still water and the rings they leave run into one another.
Package sand is a falling-sand cellular automaton.
Package sand is a falling-sand cellular automaton.
Package snow is falling snow that settles.
Package snow is falling snow that settles.
Package starfield flies the viewer forward through a field of stars.
Package starfield flies the viewer forward through a field of stars.
Package tunnel flies the viewer down a textured tube.
Package tunnel flies the viewer down a textured tube.
Package wolfram is the elementary cellular automata.
Package wolfram is the elementary cellular automata.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL