Dust in Braille

By Claude Fable 5, in the style of Gardner

The following is one line of bash, 386 characters long:

f(){ for((i=0;a*a+b*b<4*P*P&&i++<99;a=((c=a)*a-b*b)/P+u,b=2*c*b/P+w)){ :;};};for((P=10**8,Q=P/100,H=105*Q,V=160*Q;i<50|i>99;)){ ((u=RANDOM%2501*Q/10-2*P,w=RANDOM%2401*Q/10-6*P/5,a=b=0));f;};for((X=2*V/(2*COLUMNS-1),Y=H/(2*LINES),y=-H;y<H;y+=4*Y)){ for((x=-V;x<V;x+=2*X)){ for((d=0,q=8;q--;)){ ((a=x+q/4*X,b=y+q%4*Y));f;((d|=(64/(i+8)>q*5%8)<<q));};printf \\u28$(printf %02x $d);};echo;}

Paste it into a terminal — any terminal window, any size — and the window fills edge to edge with a shaded picture of a Julia set, drawn in the raised-dot patterns of the braille alphabet, a different set on every run. Figure 1 shows one. The shell that produces this picture has no graphics library, no complex numbers, and no floating-point arithmetic of any kind; everything on the screen is manufactured from 64-bit integers and the printf builtin. By the end of this article every one of the 386 characters will be accounted for.

Braille rendering of a Julia set
Braille rendering of a Julia set

Fig. 1 — The script’s output for c = −0.776 − 0.123 i, at a width of 256 columns, exactly as a terminal with dark text on a light background prints it: raised braille dots mark the points that escape, and the set haunts the blank spaces between them.

The sets are a century older than the shell. In January 1915 Gaston Julia, a 21-year-old French soldier, was shot in the face during a German attack. Surgeons could not rebuild his nose, and he wore a black leather strap across the middle of his face for the rest of his life; much of his mathematics was done in hospitals, between operations. The result, a 199-page memoir on the iteration of rational functions published in the Journal de mathématiques pures et appliquées in 1918, won the Grand Prix of the Académie des Sciences and made him famous at 25. Pierre Fatou, an astronomer at the Paris Observatory, had announced substantially the same theory in short notes in 1917, and rather than fight the wounded veteran for the prize he stayed out of the competition. The subject now divides its honors: the region where iteration behaves tamely is the “Fatou set,” and the wild boundary is the “Julia set.”

The experiment both men studied is short enough to state in a breath. Fix a complex number c, and feed every point z of the plane through the mapping z → z² + c, over and over. Each starting point suffers one of two fates: either its orbit flies off to infinity, or it stays bounded forever. The Julia set of c is the frontier between the two kingdoms. For c = 0 the frontier is the unit circle — points inside spiral to zero, points outside escape. For nearly any other c the circle shatters into something infinitely crinkled, and neither Julia nor Fatou, who proved theorem after theorem about these frontiers, ever saw one. In 1945 Julia’s memoir was pressed on the young Benoit Mandelbrot by his uncle, a professor at the Collège de France, as a masterpiece worth a career; Mandelbrot found it unreadable and put it away for thirty years, until the computers at IBM let him draw what Julia could only deduce.

Bash presents an obstacle Julia did not face: it cannot multiply 0.3 by 0.3. Shell arithmetic is 64-bit integers, and nothing else. The script’s dodge is as old as computing — pick a big constant and let it stand for the number one. Here P=10**8, so the value 1.5 is stored as the integer 150000000, and every quantity secretly carries eight decimal places. Addition needs no ceremony; a product of two such numbers carries a factor of P too many, so each multiplication is followed by /P. In this costume, with z = a + bi and c = u + wi, the rule z → z² + c becomes the function at the front of the line (spaces added here for air):

f(){ for((i=0; a*a+b*b<4*P*P && i++<99; a=((c=a)*a-b*b)/P+u, b=2*c*b/P+w)){ :;};}

Everything happens in the loop header, and the loop body — which the grammar insists must exist — is :, the shell’s venerable name for “do nothing.” (The braces themselves are a quiet extension: the manual admits only do ... done after for ((;;)), but bash accepts a brace group.) The condition a*a+b*b<4*P*P asks whether |z| is still less than 2, and this is the famous escape radius: once an orbit leaves the disk of radius 2 it can never return, and is certainly bound for infinity — a two-line estimate the reader may enjoy reconstructing. The update clause computes the new a and b together, stashing the old a in c mid-expression with (c=a) so that the new b can still use it. And i, which counts iterations up to a ceiling of 99, is a global like every other variable here; when f returns, i simply holds the verdict — the “dwell,” in the jargon of fractal artists: how many steps the point survived before escaping, or 100 if it never escaped at all. Why P is 10⁸ and not something finer: intermediate values in the loop reach six times P, whose square is 3.6 × 10¹⁷, comfortably under the 64-bit ceiling of 9.2 × 10¹⁸. At P = 10⁹ the same expression overflows. Eight decimal places is the most a power of ten can offer.

The middle loop is the best part of the program.

for(( P=10**8, Q=P/100, H=105*Q, V=160*Q; i<50|i>99; )){
  (( u=RANDOM%2501*Q/10-2*P, w=RANDOM%2401*Q/10-6*P/5, a=b=0 )); f;
}

RANDOM is a bash variable that reads as a fresh number between 0 and 32767 every time it is consulted. Unwinding the fixed-point costume, the loop draws a random c = u + wi with u between −2 and 0.5 and w between −1.2 and 1.2, and that rectangle is not arbitrary: it is a snug frame around the Mandelbrot set, which spans −2 to 0.25 in the real direction and about ±1.11 in the imaginary. The loop then starts z at 0, runs f, and keeps the candidate only if the orbit of 0 escapes in no fewer than 50 and no more than 99 steps; otherwise the single | (a bitwise OR moonlighting as a logical one, one character cheaper) sends it back for another draw.

Why the point 0, of all starting points? Because 0 is the critical point of z² + c — the one place the mapping folds the plane — and the deepest theorem in Julia’s memoir is that this single orbit speaks for the entire plane. Julia and Fatou each proved, with pencils, around 1919: if the orbit of 0 stays bounded, the Julia set of c is connected, one unbroken curve-like whole; if the orbit of 0 escapes, the set explodes into dust — a totally disconnected Cantor set, no piece of which touches any other. There is no third case. The Mandelbrot set is nothing but the ledger of this test, the collection of every c whose critical orbit stays bounded, which is why the script’s rectangle frames it. A dwell between 50 and 99 means c is a near miss: outside the Mandelbrot set, but barely, and it is in this thin margin along the boundary that Julia sets are at their most elaborate. The middle loop is, in other words, a Mandelbrot-set membership test run in reverse — the script consults one fractal to choose another.

Notice, though, what the filter rejects: a dwell of 100 means the orbit of 0 never escaped, so every parameter the script accepts lies strictly outside the Mandelbrot set. By the dichotomy, then, every picture this script has ever drawn is a picture of dust — a set of area zero. The blank white continents in Figure 1 are not the set, and they are not an interior, for the set has none. They are merely the points that outlast the artist’s patience: the shading, as we are about to see, leaves anything that survives 57 iterations as empty as anything that survives forever.

The rest of the line is the display driver.

for(( X=2*V/(2*COLUMNS-1), Y=H/(2*LINES), y=-H; y<H; y+=4*Y )){
  for(( x=-V; x<V; x+=2*X )){
    for(( d=0, q=8; q--; )){
      (( a=x+q/4*X, b=y+q%4*Y )); f;
      (( d |= (64/(i+8) > q*5%8) << q ));
    }
    printf \\u28$(printf %02x $d)
  }
  echo
}

A terminal prints characters, not pixels, and the escape hatch is the braille block, 256 characters from U+2800 to U+28FF. Each one is a cell of dots two wide and four tall, every combination of the eight dots raised or flat gets its own character, and — the stroke of luck the whole method rests on — the low eight bits of the codepoint are the dot pattern: add 1 to the codepoint and dot 1 appears, add 128 and dot 8 appears. In this one block a terminal font is a free 8-bit framebuffer. So the screen becomes a grid of 2·COLUMNS by 4·LINES subpixels (COLUMNS and LINES are kept up to date by every interactive bash, which is why the line is pasted into a terminal rather than saved as a script; a script would inherit neither unless exported). The variables X and Y size the subpixels so that the window, whatever its shape, exactly frames the region from −1.6 to 1.6 and −1.05 to 1.05. For each character cell, the innermost loop runs f eight times, at the subpixel in column q/4, row q%4, for q from 7 down to 0, and assembles the verdicts into the byte d one bit at a time. Then printf %02x turns d into two hex digits, and the outer printf \\u28... welds them onto the constant prefix 28 — constant because the block begins at exactly 0x2800 — to form an escape like \u28f7, which bash’s printf has understood since version 4.2, and out comes ⣷. One character of dust, delivered.

The dot-or-no-dot decision is a shade more cunning than a threshold. The quantity 64/(i+8) converts dwell to brightness on a nine-step scale: 8 for a point that escapes instantly, sliding down — in widening steps, a crude logarithmic response, like film — to 0 for anything that survives 57 iterations. Comparing the brightness against q*5%8 is what turns eight binary dots into perceived gray: since 5 is coprime to 8, the multiples of 5 visit all eight residues in the scrambled order 0, 5, 2, 7, 4, 1, 6, 3, so each dot in the cell gets a different threshold and a brightness of, say, 3 lights roughly three dots in a scattered arrangement. It is an ordered-dither matrix in five characters, and it is what gives the picture in Figure 1 its even, stippled halo.

One confession is owed on the braille block. Louis Braille numbered his six dots down the left column, then down the right; when computers extended the cell, dots 7 and 8 were appended underneath, and Unicode inherited the whole history, so the eight bits do not run down the columns the way a raster programmer would lay them out. The script pretends they do — q/4 and q%4 assume a plain column order — and consequently four of the eight dots in every cell are printed one position away from the spot where they were sampled. At the scale of a braille dot, under dithering, the picture does not care.

A closing experiment for the reader. As written, the script explores the dynamical plane: c is fixed while x and y sweep the starting point z. One small edit to the innermost loop — feed the subpixel into u and w instead, and start a and b at zero — makes each pixel ask the critical-orbit question instead, and the terminal prints the parameter plane: the Mandelbrot set itself, cardioid, bulbs and all. I have tried it; the figure appears obediently, though with its westernmost tip at −2 clipped by a window that was tailored for Julia sets, and the random hunt at the front of the line survives as a harmless vestige. The reader who works out why nothing else needs to change has understood the whole program.

There is a rough justice in the medium, too. Fatou and Julia proved everything above — the escape radius, the dichotomy, the speaking role of the single point 0 — and went their whole lives without seeing one of these sets, sixty years before a computer first drew one. Now that the dust can be conjured in a single line, it arrives printed in an alphabet made for reading without sight.