Endless procedural screensaver animations for your terminal: 20+ pixel-art scenes in true color from a single Rust binary, also playable in the browser via WebAssembly.
See the code
Endless, procedural screensaver animations for your terminal. More than twenty of them, from a self-exploring roguelike dungeon to firing neurons, drawn in true color with half-block pixels, in a single native Rust binary with no dependencies beyond libc.
tanim # a random animation
tanim aurora # a specific one
Try them in your browser → (the same code, compiled to WebAssembly)
▀ half blocks, so each terminal cell holds two square pixels in 24-bit color.![]() aurora: northern lights over mountains, mirrored in a lake | ![]() neurons: spikes racing down axons and crossing synapses |
![]() dog: a dog running, napping and chasing butterflies through the day | ![]() metaballs: glossy drops of gel that bulge and merge |
![]() fireworks: shells bursting over a city skyline | ![]() pipes: the classic 3D pipes screensaver |
You need a Rust toolchain (rustup).
git clone https://github.com/SantiagoSchez/tanim && cd tanim
make install # builds and installs ~/.local/bin/tanim
make install PREFIX=/usr/local # or anywhere else
make uninstall
For the web export, also add the WebAssembly target before building: rustup target add wasm32-unknown-unknown. Without it, tanim builds and runs the same, just without --export-html.
tanim needs a terminal with 24-bit color: Ghostty, iTerm2, WezTerm, kitty, Alacritty, a recent Terminal.app, and most modern ones.
tanim # a random animation from the catalog
tanim aquarium # a specific one
tanim aq # any unique prefix works
tanim --list # the catalog
tanim --seed 42 aurora # the exact same run every time
tanim --zoom 2 fire # start zoomed in
tanim --opaque matrix # paint the background instead of showing the terminal's own
tanim --export-html # web pages in html/
| Key | Action |
|---|---|
Enter | Switch to another random animation |
↑ / ↓ | Zoom in and out, up to 4x, without restarting the animation |
W A S D | Pan while zoomed in |
Space | In dungeon: build a new level |
Q, Esc, Ctrl+C | Quit, restoring the terminal as it was |
Resizing the window restarts the animation at the new size. The animations with a plain dark background (starfield, galaxy, matrix, life, physarum, neurons and pipes) show the terminal's own background instead, so they sit on top of its transparency or blur. If the terminal can't keep up, tanim drops frames rather than slowing down.
| Animation | |
|---|---|
aquarium | Pixel-art fish of seven species, a shark, crabs, bubbles and swaying seaweed |
garden | Plants sprouting, branching and blooming through the seasons |
dog | A dog running across a meadow, napping and chasing butterflies from dawn to night |
dungeon | A roguelike level that builds itself (rooms and corridors, or caves) and a hero who explores it, fights and loots |
rain | A thunderstorm with splashes, puddles and branching lightning |
snow | Snow settling on pines and roofs under shifting wind |
ripples | Raindrops rippling across a pond |
starfield | Warp-speed flight through the stars |
tunnel | Flying down an endless tiled tunnel that bends and rolls |
fireworks | Rockets bursting into colored sparks over the city |
galaxy | A spiral galaxy with dust lanes, slowly turning |
aurora | Northern lights over mountains and a lake |
boids | A flock of birds moving as one, until a hawk shows up |
matrix | Cascading digital rain |
fire | A roaring bonfire |
plasma | Psychedelic flowing plasma |
lava | A lava lamp with blobs that melt and merge |
metaballs | Glossy blobs merging like drops of gel |
life | Conway's Game of Life, colored by age |
physarum | Slime mould growing a living transport network |
neurons | Neurons under a microscope, firing in cascades across their synapses |
sand | Colored sand falling into dunes |
maze | A maze carving itself, then being solved |
snake | Self-playing Snake |
tetris | Self-playing Tetris |
pipes | The classic 3D pipes screensaver |
toasters | Flying toasters in chrome and fifties enamel, popping toast, with a winged coffee mug, jam jar, fried egg and croissant tagging along; after the classic After Dark screensaver (Space: how toasted) |
city | A night skyline scrolling by, with traffic and planes |
tanim --export-html [dir] [names...] # default: html/, every animation
The live demo is exactly this, rebuilt and published by GitHub Actions on every push. It writes index.html, with every animation and a picker (deep links like index.html#aurora work), plus one standalone page per animation. The pages run the very same Rust code compiled to WebAssembly, so they are endless, different on every load, adapt to the window and take the same keys, plus +/− for the cell size and F for fullscreen. Each page is self-contained (about 570 KB) and opens straight from disk, no server needed. On the live demo, the ⋯ button next to each animation downloads it as a Stream Deck screensaver for any model.
The script in scripts/ records animations from tanim --dump and encodes them with ffmpeg. It needs Python with Pillow, numpy and fontTools, plus ffmpeg.
make videos # a WebM per animation in videos/ (2160x1296, VP9)
make streamdeck MODELS=neo # looping GIF screensavers in streamdeck/neo/
python3 scripts/video.py --streamdeck all aurora dungeon
The Stream Deck GIFs come at each model's screensaver resolution: neo (480x320), mk2 (480x272), xl (768x384), plus (800x480) and plusxl (1280x800). The virtual terminal is sized so that every half-block pixel maps to whole pixels in the GIF. Since the Stream Deck app keeps only the first 120 frames of a screensaver, each GIF is at most 8 seconds at 15 fps. Simulations never return to an earlier state, so to make them loop the script records extra footage, picks the start and end that look most alike, and cross-fades the last 0.75 s into the frames just before the start: the GIF begins clean and loops without a jump. Set one from Stream Deck → Preferences → Set Screensaver.
The live demo's GIFs are made by GitHub Actions and kept in the gh-pages-assets branch: on every push to main, scripts/video.py --sync renders every model for new animations and for the ones whose module changed since their GIFs were made (recorded in sources.txt there). Run the workflow by hand to regenerate others, say after changing the rasterizer. The pages look for streamdeck/<model>/<name>.gif next to them and only offer the ones they find.
cargo build --release
cargo test --release
./target/release/tanim --check # run every animation headless at several sizes, down to 1x1, and time each frame
./target/release/tanim --check fire,maze # just some
./target/release/tanim --snapshot maze 100x30 500 # frame 500 as text
./target/release/tanim --dump fire 80x24 300 30 # 300 raw frames after 30 of warm-up
The animations and the canvas they draw on are a library (src/lib.rs). The terminal binary plays them; the web/ crate exposes them to JavaScript, and build.rs compiles it to WebAssembly and embeds it in tanim for --export-html. Each animation lives in src/anims/<name>.rs and exposes pub fn new(w, h, rng) -> Box<dyn Animation>, drawing into a cell canvas (usually through a buffer of half-block pixels). To add one, create the module and register it in the catalog! macro in src/anims/mod.rs with its frame rate and description.
New animations are the best kind of contribution, and ideas are just as welcome.
cargo test --release passes and tanim --check <name> runs it down to 1x1. Keep it light on the terminal: check the B/f column of --check, since bytes sent per frame are what really costs. AGENTS.md has the details on how rendering works and the conventions to follow.Rust
93.1%
Python
4.2%
HTML
2.6%
Endless procedural screensaver animations for your terminal: 20+ pixel-art scenes in true color from a single Rust binary, also playable in the browser via WebAssembly.
See the code
Endless, procedural screensaver animations for your terminal. More than twenty of them, from a self-exploring roguelike dungeon to firing neurons, drawn in true color with half-block pixels, in a single native Rust binary with no dependencies beyond libc.
tanim # a random animation
tanim aurora # a specific one
Try them in your browser → (the same code, compiled to WebAssembly)
▀ half blocks, so each terminal cell holds two square pixels in 24-bit color.![]() aurora: northern lights over mountains, mirrored in a lake | ![]() neurons: spikes racing down axons and crossing synapses |
![]() dog: a dog running, napping and chasing butterflies through the day | ![]() metaballs: glossy drops of gel that bulge and merge |
![]() fireworks: shells bursting over a city skyline | ![]() pipes: the classic 3D pipes screensaver |
You need a Rust toolchain (rustup).
git clone https://github.com/SantiagoSchez/tanim && cd tanim
make install # builds and installs ~/.local/bin/tanim
make install PREFIX=/usr/local # or anywhere else
make uninstall
For the web export, also add the WebAssembly target before building: rustup target add wasm32-unknown-unknown. Without it, tanim builds and runs the same, just without --export-html.
tanim needs a terminal with 24-bit color: Ghostty, iTerm2, WezTerm, kitty, Alacritty, a recent Terminal.app, and most modern ones.
tanim # a random animation from the catalog
tanim aquarium # a specific one
tanim aq # any unique prefix works
tanim --list # the catalog
tanim --seed 42 aurora # the exact same run every time
tanim --zoom 2 fire # start zoomed in
tanim --opaque matrix # paint the background instead of showing the terminal's own
tanim --export-html # web pages in html/
| Key | Action |
|---|---|
Enter | Switch to another random animation |
↑ / ↓ | Zoom in and out, up to 4x, without restarting the animation |
W A S D | Pan while zoomed in |
Space | In dungeon: build a new level |
Q, Esc, Ctrl+C | Quit, restoring the terminal as it was |
Resizing the window restarts the animation at the new size. The animations with a plain dark background (starfield, galaxy, matrix, life, physarum, neurons and pipes) show the terminal's own background instead, so they sit on top of its transparency or blur. If the terminal can't keep up, tanim drops frames rather than slowing down.
| Animation | |
|---|---|
aquarium | Pixel-art fish of seven species, a shark, crabs, bubbles and swaying seaweed |
garden | Plants sprouting, branching and blooming through the seasons |
dog | A dog running across a meadow, napping and chasing butterflies from dawn to night |
dungeon | A roguelike level that builds itself (rooms and corridors, or caves) and a hero who explores it, fights and loots |
rain | A thunderstorm with splashes, puddles and branching lightning |
snow | Snow settling on pines and roofs under shifting wind |
ripples | Raindrops rippling across a pond |
starfield | Warp-speed flight through the stars |
tunnel | Flying down an endless tiled tunnel that bends and rolls |
fireworks | Rockets bursting into colored sparks over the city |
galaxy | A spiral galaxy with dust lanes, slowly turning |
aurora | Northern lights over mountains and a lake |
boids | A flock of birds moving as one, until a hawk shows up |
matrix | Cascading digital rain |
fire | A roaring bonfire |
plasma | Psychedelic flowing plasma |
lava | A lava lamp with blobs that melt and merge |
metaballs | Glossy blobs merging like drops of gel |
life | Conway's Game of Life, colored by age |
physarum | Slime mould growing a living transport network |
neurons | Neurons under a microscope, firing in cascades across their synapses |
sand | Colored sand falling into dunes |
maze | A maze carving itself, then being solved |
snake | Self-playing Snake |
tetris | Self-playing Tetris |
pipes | The classic 3D pipes screensaver |
toasters | Flying toasters in chrome and fifties enamel, popping toast, with a winged coffee mug, jam jar, fried egg and croissant tagging along; after the classic After Dark screensaver (Space: how toasted) |
city | A night skyline scrolling by, with traffic and planes |
tanim --export-html [dir] [names...] # default: html/, every animation
The live demo is exactly this, rebuilt and published by GitHub Actions on every push. It writes index.html, with every animation and a picker (deep links like index.html#aurora work), plus one standalone page per animation. The pages run the very same Rust code compiled to WebAssembly, so they are endless, different on every load, adapt to the window and take the same keys, plus +/− for the cell size and F for fullscreen. Each page is self-contained (about 570 KB) and opens straight from disk, no server needed. On the live demo, the ⋯ button next to each animation downloads it as a Stream Deck screensaver for any model.
The script in scripts/ records animations from tanim --dump and encodes them with ffmpeg. It needs Python with Pillow, numpy and fontTools, plus ffmpeg.
make videos # a WebM per animation in videos/ (2160x1296, VP9)
make streamdeck MODELS=neo # looping GIF screensavers in streamdeck/neo/
python3 scripts/video.py --streamdeck all aurora dungeon
The Stream Deck GIFs come at each model's screensaver resolution: neo (480x320), mk2 (480x272), xl (768x384), plus (800x480) and plusxl (1280x800). The virtual terminal is sized so that every half-block pixel maps to whole pixels in the GIF. Since the Stream Deck app keeps only the first 120 frames of a screensaver, each GIF is at most 8 seconds at 15 fps. Simulations never return to an earlier state, so to make them loop the script records extra footage, picks the start and end that look most alike, and cross-fades the last 0.75 s into the frames just before the start: the GIF begins clean and loops without a jump. Set one from Stream Deck → Preferences → Set Screensaver.
The live demo's GIFs are made by GitHub Actions and kept in the gh-pages-assets branch: on every push to main, scripts/video.py --sync renders every model for new animations and for the ones whose module changed since their GIFs were made (recorded in sources.txt there). Run the workflow by hand to regenerate others, say after changing the rasterizer. The pages look for streamdeck/<model>/<name>.gif next to them and only offer the ones they find.
cargo build --release
cargo test --release
./target/release/tanim --check # run every animation headless at several sizes, down to 1x1, and time each frame
./target/release/tanim --check fire,maze # just some
./target/release/tanim --snapshot maze 100x30 500 # frame 500 as text
./target/release/tanim --dump fire 80x24 300 30 # 300 raw frames after 30 of warm-up
The animations and the canvas they draw on are a library (src/lib.rs). The terminal binary plays them; the web/ crate exposes them to JavaScript, and build.rs compiles it to WebAssembly and embeds it in tanim for --export-html. Each animation lives in src/anims/<name>.rs and exposes pub fn new(w, h, rng) -> Box<dyn Animation>, drawing into a cell canvas (usually through a buffer of half-block pixels). To add one, create the module and register it in the catalog! macro in src/anims/mod.rs with its frame rate and description.
New animations are the best kind of contribution, and ideas are just as welcome.
cargo test --release passes and tanim --check <name> runs it down to 1x1. Keep it light on the terminal: check the B/f column of --check, since bytes sent per frame are what really costs. AGENTS.md has the details on how rendering works and the conventions to follow.Rust
93.1%
Python
4.2%
HTML
2.6%