1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
|
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Recreating the Matrix rain with ANSI escape sequences</title>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Recreating the Matrix rain with ANSI escape sequences</title>
<link rel="stylesheet" href="/assets/css/main.css">
<link rel="stylesheet" href="/assets/css/skeleton.css">
</head>
</head>
<body>
<div id="nav-container" class="container">
<ul id="navlist" class="left">
<li >
<a href="/" class="link-decor-none">hme</a>
</li>
<li class="active">
<a href="/log/" class="link-decor-none">log</a>
</li>
<li >
<a href="/projects/" class="link-decor-none">poc</a>
</li>
<li >
<a href="/about/" class="link-decor-none">abt</a>
</li>
<li><a href="/feed.xml" class="link-decor-none">rss</a></li>
</ul>
</div>
<main>
<div class="container">
<div class="container-2">
<h2 class="center" id="title">RECREATING THE MATRIX RAIN WITH ANSI ESCAPE SEQUENCES</h2>
<h6 class="center">21 DECEMBER 2025</h5>
<br>
<div class="twocol justify"><p>My 2022 implementation of the Matrix rain had too many loose ends. Unicode
support was inflexible: the character set had to be a single contiguous block
with no way to mix ASCII with something like Katakana; Phosphor decay level was
stored in a dedicated array–still don’t understand why I did that when I had
already used bit-packing for the RGB channels; The algorithm was difficult to
decipher. The 2022 version worked, but that’s not the same thing as correct.</p>
<p>I began by placing the decay factor in the LSB of the 4-byte RGB value. Let’s
call that RGB-PD. PD plays a somewhat analogous role to an alpha channel in
that both influence transparency. However, they work very differently. So, I
avoided labelling it A so as not to cause confusion:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>enum {
R, /* Red */
G, /* Green */
B, /* Blue */
PD /* Phosphor decay level */
};
typedef union color_tag {
uint32_t value;
unsigned char color[4];
} color;
</code></pre></div></div>
<p>The decision to use union over more portable bit twiddling was made three years
ago, as I recall, for readability. Seeing as all my systems are little-endian,
this is unlikely to cause any trouble. Besides, if union is never to be used,
why is it in the language anyway?</p>
<p>The blend() function, which emulates the dim afterglow of Phosphor by eroding
the RGB channels towards the background, with minor refactoring, remains as
elegant as it did three years ago:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>#define DECAY_MPLIER 2
static inline void blend(matrix *mat,
size_t row, size_t col)
{
unsigned char *color;
color = mat->rgb[index(mat, row, col)].color;
color[R] = color[R] - (color[R] - RGB_BG_RED) / DECAY_MPLIER;
color[G] = color[G] - (color[G] - RGB_BG_GRN) / DECAY_MPLIER;
color[B] = color[B] - (color[B] - RGB_BG_BLU) / DECAY_MPLIER;
}
</code></pre></div></div>
<p>While the memory inefficiency of Phosphor decay was a technical oversight I
hadn’t noticed, the limitation around mixing nonadjacent Unicode blocks was a
nagging concern even three years ago. So, a fix was long overdue.</p>
<p>In the new version, I introduced an array that enables a user to add as
many Unicode blocks as they want. The insert_code() function picks a block
from it at random, and then picks a character from that block at random:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>#define UNICODE(min, max) (((uint64_t)max << 32) | min)
static uint64_t glyphs[] = {
UNICODE(0x0021, 0x007E), /* ASCII */
UNICODE(0xFF65, 0xFF9F), /* Half-width Katakana */
};
static uint8_t glyphlen = (sizeof glyphs) / (sizeof glyphs[0]);
static inline void insert_code(matrix *mat,
size_t row, size_t col)
{
uint64_t block;
uint32_t unicode_min, unicode_max;
block = glyphs[(rand() % glyphlen)];
unicode_min = (uint32_t)block;
unicode_max = (uint32_t)(block >> 32);
mat->code[index(mat, row, col)] = rand()
% (unicode_max - unicode_min)
+ unicode_min;
}
</code></pre></div></div>
<p>The Unicode blocks are stored in 8-byte containers: the low four bytes form the
first codepoint and the high four bytes the last. Here, I chose bitwise
operations over unions because, first and foremost, the operations themselves
are trivial and idiomatic, and the UNICODE() macro simplifies the management of
charsets. The insert_code() function is now ready to take its rightful place
next to blend().</p>
<p>The init_term() function is the arbiter of this zero-dependency software. It
prepares the graphical environment so that I can interact with it via ANSI
escape codes instead of unnecessary layers of abstraction:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>static inline int init_term(const struct winsize *ws)
{
struct termios ta;
if (tcgetattr(STDIN_FILENO, &ta) == 0) {
ta.c_lflag &= ~ECHO;
if (tcsetattr(STDIN_FILENO, TCSANOW, &ta) == 0) {
wprintf(L"\x1b[48;2;%d;%d;%dm",
RGB_BG_RED, RGB_BG_GRN, RGB_BG_BLU);
wprintf(L"%s", ANSI_FONT_BOLD);
wprintf(L"%s", ANSI_CRSR_HIDE);
wprintf(L"%s", ANSI_CRSR_RESET);
wprintf(L"%s", ANSI_SCRN_CLEAR);
setvbuf(stdout, 0, _IOFBF, 0);
ioctl(STDOUT_FILENO, TIOCGWINSZ, ws);
return 1;
}
}
return 0;
}
</code></pre></div></div>
<p>All credit for the terminal control function belongs to Domsson, whose <a href="https://github.com/domsson/fakesteak" class="external" target="_blank" rel="noopener noreferrer">Fakesteak</a> inspired my own three years ago.</p>
<p>insert_code() seeds the Matrix, blend() creates the old monochrome CRT display
nostalgia, and ANSI control sequences paint the screen. The result is a digital
rain that captures the original Matrix aesthetic with high visual fidelity:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ cc -O3 main.c -o matrix
$ ./matrix
</code></pre></div></div>
<video style="max-width:100%;" controls="" poster="poster.png">
<source src="matrix.mp4" type="video/mp4" />
</video>
<p>There was no cause to measure the program’s performance characteristics
precisely; it’s gentle on the CPU. On my ThinkPad T490 running OpenBSD, which
has a resolution of 1920x1080, it uses about 2-3% of the CPU, with occasional
jumps of up to about 8%; the cores remain silent, the fans don’t whir, the rain
falls in quiet.</p>
<p>Files: <a href="source.tar.gz">source.tar.gz</a></p>
</div>
<p class="post-author right">by W. D. Sadeep Madurange</p>
</div>
</div>
</main>
<div class="footer">
<div class="container">
<div class="twelve columns right container-2">
<p id="footer-text">© ASCIIMX - 2025</p>
</div>
</div>
</div>
</body>
</html>
|