Most people in tech have probably seen ASCII art at some point, and it has been written about plenty of times already, but letβs do the obligatory intro anyway.
ASCII art is a way to draw using plain text.
Instead of pixels, you have characters. Instead of a graphics editor, any text editor will do. And instead of a viewer, there is usually cat and a terminal.
/\_/\
( o.o )
> ^ <
Historically, "ASCII art" is often used as an umbrella term for almost any kind of text-based art, even when it has long since escaped the boundaries of ASCII itself and started using Unicode, ANSI colors, box-drawing characters, Braille symbols, terminal control sequences, and so on.
I will use the term in that broader sense too: an image or animation that can be stored, edited, and displayed as text.
Why Yet Another Format?
About five years ago, before my interest in ricing and OS customization had fully evaporated, I really wanted to add an animated ASCII logo of my distro to neofetch, mostly for the sake of nice-looking GIFs on r/unixporn.
I had already seen examples of animated fetch tools, and for some reason I assumed there must be some common format for that kind of thing.
As it turned out, it wasnt. (Well, technically it was, but we will get to that near the end).
The examples I found were usually custom scripts with hardcoded strings containing ANSI colors and a sleep between frames. There was no "standard".
And if there is no such format that it should be created.
That is how Animated ASCII Art appeared.
The characteristic difference between 3a and a good old plaintext file with ANSI escape codes is that 3a preserves formatting in a text editor: the art looks the same in the editor as it does when rendered. This means you can work with it directly, without needing a specialized editor like DurDraw.
For a long time, the project existed as a somewhat ambiguous format description, a minimal implementation without many features, and literally a couple of artworks for which the whole thing had been created in the first place.
Recently I finally cleaned it all up: rewrote the specification, brought the CLI tool into a more usable state, split out libraries, added conversion to different formats, and organized a small collection of openly licensed ASCII animations.
File Structure
A 3a file consists of blocks.
Each block starts with a line containing the block name, beginning with @.
The first block is always @3a; it is the header with metadata.
The main block with frames is called @body.
A minimal file without colors or metadata may look like this:
@3a
@body
<=>\
,..\\..,
' // '
| |
| |
'.__.~._.'
If there are multiple frames, they are separated by blank lines:
@3a
title just an apple
delay 300
loop yes
@body
<=>\
,..\\..,
' // '
| |
| |
'.__.~._.'
<=>\
,..\\..,
' //,_.--'
| / {
| \_,''-.
'.__.~._.'
<=>\
,..\\..,
',--,_.--'
} {
,-,_,''-.
'.__.~._.'
A few fields appeared in the header here:
-
title- the title of the art; -
delay- delay between frames in milliseconds; -
loop- whether the animation should loop.
If delay is not specified, the default is 50 ms.
If loop is not specified, the animation is considered looped.
So simple artworks do not need to be bloated with metadata unless there is a reason for it.
The header can also contain authors, original author, source link, license, editor, preview frame, tags, and so on.
For example:
@3a
title just an apple
author ASCIIMoth
license CC0-1.0
src https://github.com/asciimoth/openascii
delay 300
loop yes
preview 0
#apple #fruit #food
Comments, in places where they are allowed, start with ;; and take the whole line.
Colors
The most important idea behind colored 3a is that text and colors are stored in separate columns, not mixed together.
Instead of inserting ANSI escape codes directly into the art, each text line is followed by a color line of the same length, consisting of color names.
@3a
colors yes
@body
<=>\ 112228111111
,..\\.., 111118811111
' // ' 111991111111
| |111111111111
| |111111111111
'.__.~._.' 111111811111
The built-in names represent standard 4-bit ANSI colors:
0 black
1 red
2 green
3 yellow
4 blue
5 magenta
6 cyan
7 white
8 bright black / gray
9 bright red
a bright green
b bright yellow
c bright blue
d bright magenta
e bright cyan
f bright white
_ default (depends on the terminal)
You can also define custom colors:
@3a
colors yes
col r fg:196
col l fg:bright-green
col b fg:94
A color can be an ANSI color by name, a 256-color code, or an RGB hex value.
For terminal output, this is later mapped to ANSI sequences; for SVG, PNG, GIF, WebP, and MP4, it is mapped to the corresponding graphical representation.
If the color channel is the same for all frames, it does not have to be repeated every time.
For that there is the @color-pin block, which defines one "pinned" color-channel frame for the whole animation.
And conversely, if the text stays the same and only colors change, @text-pin can be used.
@3a
title apple with pinned colors
colors yes
delay 300
@color-pin
112228111111
111118811111
111991111111
111111111111
111111111111
111111811111
@body
<=>\
,..\\..,
' // '
| |
| |
'.__.~._.'
<=>\
,..\\..,
' //,_.--'
| / {
| \_,''-.
'.__.~._.'
<=>\
,..\\..,
',--,_.--'
} {
,-,_,''-.
'.__.~._.'
aaa
The main tool for working with 3a is the aaa CLI utility.
At the top level, it has several command groups:
list List builtin art
gen Generate new art
play Play art in terminal
fetch Show system info side by side with animated logo
preview Show art preview
edit Editing subcommands
convert Format conversion subcommands
from-text Constructs art from plain text with ANSI color escape codes
completions Generate shell completions
To simply play a .3a file in the terminal:
aaa play ./apple.3a
If the file has colors, aaa converts the color channel into ANSI escape sequences at output time.
To show a preview - one frame, by default frame zero:
aaa preview apple.3a
And the fetch command, the reason this whole thing started:
aaa fetch distro_nixos_big.3a
By default, aaa tries to use one of the already installed fetch tools: neofetch, fastfetch, screenfetch, nitch, profetch, leaf, or fetch-scm.
So the system information comes from the usual fetch tooling, while the logo comes from the 3a file.
Generation and Editing
A template can be created with gen:
aaa gen > apple.3a
After that, the file can be opened and edited manually.
But some operations can also be done through aaa edit, which is especially useful from scripts.
Set the title:
aaa edit ./apple.3a title "just an apple" > apple2.3a
Add tags:
aaa edit ./apple.3a tag-add apple fruit food > apple2.3a
Change the license:
aaa edit ./apple.3a license CC0-1.0 > apple2.3a
Besides metadata, there are frame operations too: duplicate a frame, delete one, swap frames, reverse frame order, cut out a range, deduplicate repeated frames, shift the animation forward or backward, and so on.
Conversion
The second major part of aaa is conversion.
The list of currently supported targets looks like this:
to-frames ANSI-colored frames separated by blank lines
to-cast asciicast v2
to-dur durdraw format
to-json JSON document
to-ttyrec ttyrec
to-png PNG image
to-gif GIF animation
to-webp WebP animation
to-mp4 MP4 video
to-svg SVG animation
to3a print art back in 3a format
For example, to make a GIF:
aaa convert apple.3a to-gif > apple.gif
Or an asciinema cast:
aaa convert apple.3a to-cast > apple.cast
asciinema play apple.cast
The normalization command is useful on its own:
aaa convert apple.3a to3a > normalized.3a
It reads the file and prints it back in the 3a format.
This is convenient for checking how the parser understands the file and for bringing artworks to a more uniform shape.
Another practical scenario is taking an existing artwork with ANSI sequences and converting it into 3a:
cat old-logo.txt | aaa from-text > logo.3a
Libraries
aaa is built on top of the rs3a Rust library.
It can read and write the new 3a format, partially read the legacy version, edit art programmatically, and export it to SVG, asciicast v2, and ANSI-colored text.
use rs3a::{Art, font::Font, CSSColorMap};
use std::fs::File;
use std::io::Write;
fn main() {
let mut art = Art::from_file("./examples/dna.3a").unwrap();
let color_pair = "fg:black bg:yellow".parse().unwrap();
let color = art.search_or_create_color_map(color_pair);
for frame in 0..art.frames() {
art.print(frame, 0, 0, &format!("{}", frame), Some(Some(color)));
}
art.to_file("./examples/edited_dna.3a").unwrap();
let mut output = File::create("./examples/dna.svg").unwrap();
write!(
output,
"{}",
art.to_svg_frames(&CSSColorMap::default(), &Font::default())
).unwrap();
}
There are also py3a and go3a, but they are less feature-complete and are mostly useful for parsing.
OpenASCII
The last part of the "ecosystem" is openascii.
It is a tiny collection of ASCII art, mostly mine, in the 3a format, under permissive or copyleft licenses, with a web player based on asciinema.
I would be happy if something from the collection turns out useful for your CLI tools / ASCII games / etc., or if you happen to have something to contribute.
Alternatives
After the fact, I did eventually discover that several other attempts to do something similar had already existed. But they either solved a slightly different problem, or never got far enough.
The most noteworthy one is the terminal ASCII animation editor DurDraw.
It has its own format (gzipped JSON) and can use it in its built-in fetch command.
Still:
- the format has exactly one very tangled implementation;
- the format documentation contradicts the de facto implementation;
- editing that spaghetti of nested JSON by hand in a normal text editor is not much easier than the "reference" plaintext-plus-ANSI-codes approach.
That said, had DurDraw been a little more famous and a little less buggy back then, maybe 3a would never have happened.
However, aaa now supports conversion between .3a and .dur.



Top comments (0)