May 12, 2023

The Beauty of Simplicity: Understanding the Fascinating donut.c Code

The donut.c code, created by Andy Sloane in 2006, is a beautiful example of how a few dozen lines of code can produce fascinating results. This code generates an ASCII-art donut using mathematical calculations and 3D projection techniques. The code is as follows:

int k; ⁠double sin(), cos() ⁠main (){ float A=0, B=0, i, j, z[1760]; char b[1760]; printf("\x1b[2J"); ⁠⁠⁠ for(; ; ) { memset(b,32,1760); ⁠⁠ memset(z,0,7040); ⁠ for(j=0; 6.28>j; j+=0.07) { ⁠ for(i=0; 6.28 >i; i+=0.02) { float sini=sin(i), ⁠ cosj=cos(j), sinA=sin(A), ⁠⁠ sinj=sin(j), cosA=cos(A), cosj2=cosj+2, mess=1/(sini*cosj2*sinA+sinj*cosA+5), ⁠⁠⁠⁠ cosi=cos(i), cosB=cos(B), sinB=sin(B), ⁠⁠⁠ t=sini*cosj2*cosA-sinj* sinA; int x=40+30*mess*(cosi*cosj2*cosB-t*sinB), y= 12+15*mess*(cosi*cosj2*sinB +t*cosB), o=x+80*y, N=8*((sinj*sinA-sini*cosj*cosA)*cosB-sini*cosj*sinA-sinj*cosA-cosi *cosj*sinB); if(22>y&&y>0&&x>0&&80>x&&mess>z[o]){ z[o]=mess; ⁠⁠⁠⁠⁠⁠⁠ b[o]=".,-~:;=!*#$@"[N>0?N:0]; ⁠ } ⁠ } ⁠ } ⁠ printf("\x1b[d"); ⁠ for(k=0; 1761>k; k++) ⁠ putchar(k%80?b[k]:10); ⁠ A+=0.04; ⁠ B+= 0.02; ⁠ } ⁠}

In barely under 40 lines, we are left with the following spinning donut:

donut gif.gif

The goal of this post is to go through the donut.c code line by line to understand what it is doing here. ⁠

Firstly, the program defines a few things globally such that they are accessible throughout the program. ⁠

int k; 
⁠Firstly, the variable k is defined outside of C's main function such that it is a global variable accessible to any function in the program. The variable k will be used to loop through each of the 1760 pixels on the screen.

double sin(), cos()
⁠Now sin and cos are defined outside of C's main function too as functions that return a decimal number of type double, which will be needed later for trigonometric calculations for the 2D projection of a 3D object.

now inside of the main function, the program defines the following

float A=0, B=0, i, j, z[1760];

Where A and B represent the angles used to rotate the donut in 3D space and will loop from 0 to 2π. Floats are used in this case instead of a double because the range is limited to only 0 to 2π and the precision of a double is not needed. i and j are defined to be used as loop counters to loop over every point in the donut.

In this program, the screen is 80 pixels by 22 pixels where the pixels are ascii characters : .,-~:;=!*#$@

the

z[1760]
array is an array of 1760 floats to be used as a "z-buffer", which stores the depth of each pixel shown on the screen.
⁠char b[1760];
is an array of 1760 ASCII characters that will be shown on screen. spaces ' ' will be used to show empty space, and ascii characters will be used to depict the donut. This array will be used in combination with the k variable in on the first line in order to loop through and display each pixel on screen.

Next, the line

printf("\x1b[2J");
is used to clear the console screen to make room for the donut.

Then, the program beings an infinite loop

for(; ; ) {
memset(b,32,1760);
⁠    memset(z,0,7040);

All elements of the array b is set with the value of 32 which corresponds to the ASCII character of space ' '. The 3rd argument 1760 specifies that the first 1760 bytes of memory will be set to that space (which is the entire array). All elements of the array z is set to 0. Because each element of the z array is 32-bit float. Each 32-bit float is comprised of 4 bytes (each byte is 8 bits), so 1760 float elements x4 bytes each=7040 bytes to set.

for(j=0; 6.28>j; j+=0.07) {
    ⁠for(i=0; 6.28 >i; i+=0.02) {

now the program beings a nested loop between 0 and 2π to create the donut.

In order to create a donut, we would need to first draw a circle.

draw circle.gif

Then in 3d, we would need to draw more circles around the donut

draw circle 2.gif

After many circles have been drawn around the initial circle, the shape begins to resemble a donut!

draw circle 3.gif

Thats exactly what these for loops are doing. The j for loop goes around the inner circle at increments of 0.07 radians for the entire circle of 2π. Then, at every point of the inner circle, the outer circle of the donut is plotted at increments of 0.02 radians by the i loop.

With the for loops in place, we have the angles needed to reach each point on the donut. Some trigonometry is then applied to convert the i and j angles into cartesian coordinates

Great, now we have all the points making up the donut. However, the points are currently described in the angles i and j and the radius of the two circles. We'll need a way to convert that into cartesian coordinates. We can do this by using some trigonometry

2D coordinates on the cross section

$$(x, y, z) = (r_{1}, 0, 0) + (r_{2}cos(\theta),r_{2}sin(\theta),0)$$

3D coordinates for any point on the surface of the donut not yet spinning

(x, y, z) = (r_{1} + r_{2}cos(\theta),r_{2}sin(\theta), 0)\cdot\begin{pmatrix}

cos\phi & 0 & sin\phi\\

0 & 1 & 0\\

-sin\phi & 0 & cos\phi

\end{pmatrix}

3D coordinates for any point on the surface of the donut, now spinning at angle A by the x axis and angle B by the Z-axis

(x, y, z) = (r_{1} + r_{2}cos(\theta),r_{2}sin(\theta), 0)\cdot\begin{pmatrix}

cos\phi & 0 & sin\phi\\

0 & 1 & 0\\

-sin\phi & 0 & cos\phi

\end{pmatrix}\cdot\begin{pmatrix}

1 & 0 & 0\\

0 & cos A & sin A\\

0 & -sinA & cosA

\end{pmatrix}\cdot\begin{pmatrix}

cosB & sinB & 0\\

-sinB & cosB & 0\\

0 & 0 & 1

\end{pmatrix}

Bogosort

Bogosort

Aspiring learner who is fascinated with algorithms and computer science! Always trying to make sense of the complex and put it into simpler words.

Leave a comment

Related Posts