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 sameg_wall_seedat the beginning of every singledraw_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 callsstampCharacterin "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 forw) to a position on the screen. x is mapped linearly, while y is mapped usingstd::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_angleis simplystd::atan(dy/dx). - This position and unique
tangent_angleare passed tostampCharacter. stampCharactertakes the 8x16 font, scales it by 2x, and then applies a 2D rotation matrix using thetangent_angle. It also uses theCELL_ASPECT_RATIOduring 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
(incl. VAT)
Buy Linux Magazine
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.
News
-
New Linux Flaw Lets Attackers Escape VMs
A 16-year-old vulnerability allows an attacker to escape a virtual machine, gain access to the host, and execute malicious code.
-
Hannah Montana Linux Is Back!
Developer Noah Cagle decided the world needed the once obscure but beloved Linux distribution and gave it a decidedly pink refresh.
-
System76 Refreshes the Lemur Laptop
If you're looking for a laptop with tons of power and battery, look no further than the latest iteration of the System76 Lemur Pro.
-
More than 43 Million Lines of Code in Linux Kernel 7.2
Using the cloc utility, Michael Larabel of Phoronix discovered that Linux kernel 7.2 has over 43 million lines of code.
-
Kubuntu Focus Goes Ultra
The Kubuntu Focus team has upped the performance ante of its M2 and Zr laptops with the latest, greatest CPUs from Intel.
-
Linux Gamers May Soon See Less Mouse Lag in KDE Plasma
Gamers using KDE’s Plasma desktop have been suffering from a slight input delay in mouse movement that could lead to getting fragged.
-
Three Lines of Code Improve Linux Storage Performance
A developer changed three lines of code, giving Linux storage performance a 5% bump.
-
AUR Hit Again with Malicious Packages
Once again the Arch User Repository is plagued by a high volume of malicious packages.
-
Alpine Linux 3.24 Features Fresh Desktops and a Newer Kernel
If you're a fan of Alpine Linux, it's time to upgrade because the latest version has been released with KDE Plasma 6.6, Gnome 50, and Linux kernel 6.18 LTS.
-
EU Open Source Strategy Plays Key Role in Tech Sovereignty Package
Comprehensive measures adopted by the European Commission aim to reduce dependency on non-EU countries.
