Making art in the terminal window

The Wall

A naive approach to a random brick wall would be to assign a new random color to each brick every frame. This, however creates a distracting "vibrating" or "shimmering" effect. The wall needs to be random, but predictably random. I achieve this with a clever seeding trick:

  • A global random seed, g_wall_seed, is initialized once when the program starts.
  • The random number generator, std::mt19937 gen, is re-seeded with this exact same g_wall_seed at the beginning of every single draw_frame().

This means that get_random_brick_color() – even though it is called 30 times a second – will produce the exact same sequence of "random" colors for every frame, resulting in a perfectly stable, static wall. A new random seed is only generated when the terminal is resized, giving you a new wall pattern.

The wall itself is drawn using a 6-row pattern (y % 6) to create bricks that are two rows tall, separated by one row of mortar, achieving the 2:1 brick-to-mortar ratio. The horizontal mortar lines (render_mortar_line) use Unicode box-drawing characters to create the grid.

Banksy on the Wall

The true "graffiti" effect comes from the fact that I don't just print the letters. I load a standard 8x16-pixel font from a font.bin file, but for every single "on" pixel in a character, I "stamp" a large, 7x7 rounded brush. For more details on fonts, see the box entitled "Font, Oh My Font."

Font, Oh My Font

The last question is the most interesting. How do I get the font files? Although other solutions exist, the Linux way is to extract them from the console fonts of the system. These fonts are located in /usr/share/consolefonts. For my purposes, I need the one that most closely resembles the 8x16 format used in the code, hence I go for Lat38-VGA16.psf.gz. Since these gzipped files are of a specific format, namely PC Screen Font (PSF), they start with a short header. I need to run:

dd if=vga-font.psf of=font.bin bs=1 skip=4 count=4096

on the extracted font, and suddenly I have a full binary character map at my disposal.

This step is handled by stampBrush. The stampBrush function "stamps" a multi-layered circle onto the screen buffer: It iterates in a 7x7 (-3 to +3) square around a center point, and it calculates the distance of each cell from the center.

Because terminal character cells are about twice as tall as they are wide, a 7x7 square of cells looks like a tall rectangle. To make the brush look circular, I correct for this by multiplying the y distance by a CELL_ASPECT_RATIO of 2.0 before calculating the distance. This "squashes" the brush vertically so it appears round on the non-square grid.

Based on this corrected distance, it stamps the corresponding layer:

  • Fill (Priority 4): Inner-most pixels get the rainbow color
  • Black Border (Priority 3): Pixels just outside the fill
  • White Border (Priority 2): The outermost pixels of the brush

When these brushes are stamped for every pixel of a letter, they overlap and "bleed" together, forming one continuous, "fluffy" shape with a clean two-tone border. You can observe this behavior if you zoom the terminal in or out while the text is displayed.

Text on a Curve (with a Shadow)

The curves are the most mathematically complex part of the program, combining trigonometry, calculus, and a two-pass rendering system.

The render_graffiti_to_buffer function actually runs in two passes:

  • Pass 0 (Shadow): The first pass calculates the position of all the letters, but adds a global offset (SHADOW_OFFSET_X/Y). It then calls stampCharacter in "shadow mode." This special mode only darkens the brick wall cells (Priority 0) and sets their priority to 1.
  • Pass 1 (Text): The next pass runs at the normal position and not in shadow mode. This pass draws the full graffiti (priorities 2, 3, 4) on top of both the wall and its own shadow, creating the "floating" effect.

For both passes, the function lays out the text along a parametric sine curve (because, as you have certainly observed, the text is not drawn on a straight line):

  • The function maps the text's progress (0.0 for R, 1.0 for w) to a position on the screen. x is mapped linearly, while y is mapped using std::sin() to create the arc.
  • To make the letters "follow the bend," they are rotated to be tangent to the curve at that exact point.
  • This angle is found using calculus: I calculate the derivative dy/dx of the curve at that point. The tangent_angle is simply std::atan(dy/dx).
  • This position and unique tangent_angle are passed to stampCharacter.
  • stampCharacter takes the 8x16 font, scales it by 2x, and then applies a 2D rotation matrix using the tangent_angle. It also uses the CELL_ASPECT_RATIO during this rotation, "squashing" the y coordinates before rotating and "un-squashing" them after. This prevents the letters from looking stretched or sheared when rotated.

Finally, this function calls stampBrush for each of its own rotated, scaled pixels, completing the effect.

Buy this article as PDF

Download Article PDF now with Express Checkout
Price $2.95
(incl. VAT)

Buy Linux Magazine

Related content

  • Animation with OpenToonz

    OpenToonz is a professional animation tool for comic and manga artists.

  • FOSSPicks

    Graham has just recorded a one-off podcast featuring the wonderful open source VCV Eurorack emulator, often written about here. He's now strongly considering doing a more regular synth-related podcast.

  • Next Steps in Vector Graphics

    What are vector graphics and how could we make them better?

  • Graphics in Python with Cairo

    Build graphic elements into your Python programs with the Cairo graphics library. We'll show you how to draw an analog clock face that displays the current time.

  • FOSSPicks

    This month Graham looks at Plasma System Monitor, projectM audio visualizer, yt-dlg downloader GUI, and more.

comments powered by Disqus
Subscribe to our Linux Newsletters
Find Linux and Open Source Jobs
Subscribe to our ADMIN Newsletters

Support Our Work

Linux Magazine content is made possible with support from readers like you. Please consider contributing when you’ve found an article to be beneficial.

Learn More

News