(2023-04-28) Giving ASCII art the third dimension
-------------------------------------------------
I usually try not to remember my school years, but some cool things
definitely could be seen in that time. And one of them were stereograms, or, 
to put it more strictly, autostereograms. They were on the backs of some 
notebooks and day books, and to those schoolchildren who really could see 
them, they looked like magic or, to more tech-savvy ones like me, like 
holograms I had only read about in some old encyclopedias or seen in the 
hologram museum (yes, we did have one in our country). I had always been 
wondering, how does one draw such images? Little did I know back then that 
these graphical autostereograms (called SIRDS, single-image random dot 
stereograms, in case of randomized dots, or just SIS in case of hand-crafted 
image patterns) were preceded by SIRTS, single image random TEXT 
stereograms. I guess if I knew that it was possible to generate 3D images 
out of pure text abracadabra when I was a schoolboy, I... wouldn't have made 
a lot of silly choices in my life that I did.

Anyway, now I understand the principle behind stereograms is very simple and
based on how our sight and brain perceive objects at different distance, and 
there also is a ton of software generating both text and pixel 3D images out 
of depth maps and some other parameters, but it's baffling how few resources 
actually explain what's going on and how to implement the same effect from 
scratch. The best one of the few, called "SIRDS FAQ" ([1]), contains 
probably the fullest explanation of the entire phenomenon and even has some 
code and pseudocode examples of the algorithm. Still, the best way to 
understand the algorithm is to code it up yourself, so I've created my own 
SIRTS implementation in AWK called Textereo and published it on the main 
hoi.st page as usual. It doesn't use bitwise operations or any other 
non-standard extensions so should work on any AWK variant, but I only tested 
it on GAWK and Busybox. As an input, this script accepts a map file in the 
following format:

* line 1: space-separated (desired) image width and pattern length > 8 chars
* line 2: entire alphabet of characters to build the image from
* depth map lines: either empty or a digit sequence from 0 to 7

Textereo follows the standard depth map convention that 0 is background and 7
is the highest level of embossment and visually appears the closest to the 
viewer. Normally though, SIRTS images don't contain depth levels higher than 
3. The width is an important parameter that defines how your map will be 
positioned. All lines that have fewer depth digits than the width value are 
centered to fit the width from the first line. If the line is empty or 
contains only zeroes (fewer than the width), it's fully filled with zeroes. 
This allows to adjust not only width but the height of the background 
canvas. The second number of the first line defines the base pattern length, 
and I'll get to this parameter shortly.

Here's an example of the 78x27 3D map in Textereo format using two depth
levels and 10-character base patterns:

78 10
abcdefghijklmnopqrstuvwxyz@/0123456789$%!#ABCDEFGHIJKLMNOPQRSTUVWXY



0000000000000000000000000000000000000000000000000
0000000000000000000011111111111111111111111111100
0000000000000000000011111111111111111111111111100
0000011111111000000011111222222222221111111111100
0000111111111110000011111222222222222211111111100
0001111110111111000011111222222222222222111111100
0011111000011111100011111112222212222222221111100
0011111000000111100011111112222211111222221111100
0000000000001111100011111112222211111122221111100
0000001111111111000011111112222211111122222111100
0000001111111111000011111112222211111122222111100
0000001111111111000011111112222211111122222111100
0000000000001111100011111112222211111222221111100
0011111000000111100011111112222211111222221111100
0011111000011111100011111112222211122222211111100
0001111111111111000011111222222212222222111111100
0000111111111100000011111222222222222211111111100
0000011111111000000011111222222222222111111111100
0000000000000000000011111111111111111111111111100
0000000000000000000011111111111111111111111111100
0000000000000000000000000000000000000000000000000



Note the blank lines before and after the digits. They allow to tweak the
height parameter to make sure the image is easier to visually perceive. And 
here is an example of what Textereo generates out of this map (sorry mobile 
users, you need to have 78-char width to see this properly):

Uc5rWnQtbQUc5rWnQtbQUc5rWnQtbQUc5rWnQtbQUc5rWnQtbQUc5rWnQtbQUc5rWnQtbQUc5rWnQt
3vB@n@ov0I3vB@n@ov0I3vB@n@ov0I3vB@n@ov0I3vB@n@ov0I3vB@n@ov0I3vB@n@ov0I3vB@n@ov
IF$fwYwBu9IF$fwYwBu9IF$fwYwBu9IF$fwYwBu9IF$fwYwBu9IF$fwYwBu9IF$fwYwBu9IF$fwYwB
EPI7KrhFWlEPI7KrhFWlEPI7KrhFWlEPI7KrhFWlEPI7KrhFWlEPI7KrhFWlEPI7KrhFWlEPI7KrhF
2@UK3A7OaB2@UK3A7OaB2@UK3A7OaB2@UK37OaB2@UK37OaB2@UK37OaB2@UK317OaB2@UK317OaB2
#uEyQB%R7V#uEyQB%R7V#uEyQB%R7V#uEyQ%R7V#uEyQ%R7V#uEyQ%R7V#uEyQ6%R7V#uEyQ6%R7V#
pcdoCJuBxgpcdoCJuBxgcdoCJuBxjgcdoCJBxjgcoCJBxjgcoCJGBxjgcoCJGBXxjgcoCJGBXxjgco
DzgMn#irjgDzgMn#irjDzgMn#irjDz@gMn#rjDz@Mn#rjDz@Mn#rjyDz@Mn#rjtyDz@Mn#rjtyDz@M
D!%3isSRtHD!%3isSRHD!%3ieSRHD!%53ieRHD!%3ieRHD!%3ieRHD!6%3ieRHyD!6%3ieRHyD!6%3
!MxGt/zF2N!MxGt/z2N!MxYGt/2N!MxYSGt2N!MxYSt2N!MXYSt2N!MXYbSt2NH!MXYbSt2NH!MXYb
%@ks3rLM$3%@ks3rL$3%@k8s3rL$%@k8ds3L$%@k8d3L$%@Rk8d3$%@RkO8d3$v%@RkO8d3$v%@RkO
Omq/ksansaOmq/ksansaOmq/ksasaOmqN/kasaOmqNkasaOJmqNkaaOJmhqNka7aOJmhqNka7aOJmh
xTbt9btvwoxTbt9btvwoxbt9btvwoxbpt9bvwoxbptbvwox%bptbvox%bputbv@ox%bputbv@ox%bp
kUCI4e18!skUCI4e18!skCI4e18!skCMI4e8!skCMIe8!skfCMIe8skfCMqIe8oskfCMqIe8oskfCM
rg6nETnT3Trg6nETnT3Tr6nETnT3Tr6%nETT3Tr6%nTT3Trt6%nTTTrt6%HnTTyTrt6%HnTTyTrt6%
c!bLbTPzqic!bLbTPzqic!bLbTPqic!bfLbPqic!bfbPqicY!bfbqicY!hbfbq3icY!hbfbq3icY!h
jXsOphd$RyjXsOphdRyjXsCOphdRjXsCJOpdRjXsCJpdRjXVsCJpRjXVsHCJpR6jXVsHCJpR6jXVsH
2Ft!Radwvo2Ft!Radvo2Ftm!Ravo2Ftmf!Rvo2FtmfRvo2F7tmRvo2F7ptmRvoA2F7ptmRvoA2F7pt
7HEsMg4E7R7HEsMg4ER7HEsMg4ER7HE%sMgER7HEsMgER7H9sMgER7H69sMgERu7H69sMgERu7H69s
7XcgSiygKP7XcgSiygK7XcgSiygK7mXcgSigK7mXgSigK7mXgSigKq7mXgSigK!q7mXgSigK!q7mXg
v2I8b%!eLbv2I8b%!eLb2I8b%!eLHb2I8b%eLHb28b%eLHb28b%eFLHb28b%eFnLHb28b%eFnLHb28
yB20POSRWCyB20POSRWCyB20POSRWCyB20PSRWCyB20PSRWCyB20PSRWCyB20P8SRWCyB20P8SRWCy
taBAzK7BGvtaBAzK7BGvtaBAzK7BGvtaBAz7BGvtaBAz7BGvtaBAz7BGvtaBAz67BGvtaBAz67BGvt
#hBD6s9Eko#hBD6s9Eko#hBD6s9Eko#hBD6s9Eko#hBD6s9Eko#hBD6s9Eko#hBD6s9Eko#hBD6s9E
FjcBa6w%6sFjcBa6w%6sFjcBa6w%6sFjcBa6w%6sFjcBa6w%6sFjcBa6w%6sFjcBa6w%6sFjcBa6w%
Iu#fzcAO4VIu#fzcAO4VIu#fzcAO4VIu#fzcAO4VIu#fzcAO4VIu#fzcAO4VIu#fzcAO4VIu#fzcAO
F$Kf!GtBvaF$Kf!GtBvaF$Kf!GtBvaF$Kf!GtBvaF$Kf!GtBvaF$Kf!GtBvaF$Kf!GtBvaF$Kf!GtB

If you look at this correctly, you should see a large 3 and a box on one
plane and then the D on top of that box on the second plane. The more 
distant-focused and relaxed your sight is, the better the effect. Don't 
strain your eyes too much though.

Now that I have a working SIRTS generator written by myself, I can fully
explain how it works. Besides all the preparation of the alphabet and 
equally-wide map strings, the general algorithm is as follows (assuming all 
positions and indexes are starting with 0):

1. Determine the desired image width W and pattern length PL (in our case,
they are read from the first line of the input file). Select the alphabet A 
(in our case, it's read from the second line of the input file).
2. For each line M of width W in the depth map, repeat steps 3 to 17.
3. Generate a character pattern string P of basic length PL. The characters
in P must be taken from the alphabet A. They can be chosen randomly or 
consecutively, they also can appear multiple times, but no _adjacent_ 
characters in the generated pattern P must be the same.
4. Shape a set of characters F consisting of all characters in the alphabet A
except the ones present in P (so that, in set notation, {F} + {P} = {A}).
5. Duplicate the initial pattern length PL as the current length L.
6. Set the pattern tracking pointer PP to 0.
7. For each depth value V in the current map line M, repeat steps 8 to 16.
8. Calculate the new pattern length NL: NL = PL - V.
9. Calculate the delta value D: D = NL - L.
10. If D < 0, delete one character at the position PP from the pattern P (-D)
times and go to step 14.
11. If D >= 0, perform steps 12 to 13 D times and go to step 14.
12. Retrieve a random character C from the set F and remove it from the set F.
13. Insert the character C into the pattern P at the position PP.
14. Copy the value of NL into L.
15. Emit a character from pattern P at the position PP mod NL.
16. Increment PP modulo NL: PP = (PP + 1) mod NL. End of iteration.
17. Emit a newline character. End of iteration.

You can find a bit different description of these steps at [1], but I think
my approach is more understandable. In short, we delete a character from our 
pattern at the current pattern position (before emitting anything at this 
position) if the depth value increases, and insert a new character from the 
set of unused characters if the depth value decreases. Since depth can 
change by more than 1 at a time, we must make sure the amount of 
deletions/insertions matches this delta. What all this does visually is 
creating a sharp boundary between layers where shorter patterns correspond 
to closer layers and vice versa. This is why we can't make a base pattern 
length too short: we must make sure it allows us to distinguish between all 
depth levels.

A nice thing about this algorithm is that it also is fully line-oriented:
each line is processed independently and can have its own character pattern 
to draw with. That's why it works so nicely with the AWK runtime. The only 
quirk I had to deal with was the fact that all string indexes in AWK start 
with 1 instead of 0. This is why several places in the Textereo code (which, 
by the way, is under 60 SLOC in total) have these 1's explicitly added. But 
the algo itself is quite flexible and can be adapted to virtually any 
programming language, including...

Yes, you guessed it. But it's definitely not for today.

--- Luxferre ---

[1]: https://the.sunnyspot.org/asciiart/docs/sirdsfaq.html