Skip to main content

Command Palette

Search for a command to run...

3a: A Format and Tooling for Creating ASCII Animations

Published
9 min read
3a: A Format and Tooling for Creating ASCII Animations

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

https://asciinema.org/a/ZtzhhTOmVWCAmrfz

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.