Donut Math

A visual walkthrough of how the ASCII spinning donut works
Overview

The ASCII donut is a torus rendered in a terminal using only characters and arithmetic. This document builds it from scratch: two parametric angles trace every surface point, three rotations animate the spin, a perspective divide projects to screen, a z-buffer resolves depth, and a dot product with a light direction maps brightness to ASCII characters.

§ 1
Two angles cover the whole donut

θ (theta) sweeps around the tube cross-section. φ (phi) sweeps around the hole. As both angles run from 0 to 2π, together they visit every point on the surface exactly once.

Fig. 1 — θ orbits inside the tube (red dot); φ swings the whole cross-section around the hole (green arc). The static blue circle on the left shows the cross-section at rest.
θ goes around the tube cross-section
φ goes around the whole donut hole
§ 2
The cross-section: parametrizing a circle

Before rotating anything, start with the small circle in the xz-plane. cos and sin trace a circle as θ changes. Multiply by R1 for the tube radius, then shift right by R2 to push the circle away from the donut's central axis.

45°
Fig. 2 — Drag θ to trace the circle. The red dot marks the point on the surface; the dashed arms show R2 and R1.
x = R2 + R1·cos(θ)    y = R1·sin(θ)    z = 0
§ 3
Swing the circle to form the full donut

Rotate the cross-section circle around the y-axis using φ. This is just a 2D rotation in the xz-plane: multiply x by cos φ to get the new x, and negate x times sin φ for z. As φ runs 0° → 360°, the circle sweeps out the entire donut surface.

30°
60°
Fig. 3 — Blue ring is the cross-section at the current φ. Dim dots show the full donut sweep. Red dot is the point at (θ, φ).
new_x = old_x · cos(φ)
new_z = −old_x · sin(φ)
§ 4
Two more rotations for the spin animation

The donut is stationary so far. Two time-varying angles animate it: A tilts it toward and away from the viewer (rotation about the x-axis), and B spins it left and right (rotation about the z-axis). Each frame, A and B are incremented by small constants, producing the tumbling effect.

30°
20°
Fig. 4 — A tilts the donut (x-axis rotation); B spins it (z-axis rotation). Each rotation blends only two coordinates at a time.
A tilts (x-axis) · B spins (z-axis)
Every rotation blends just two coordinates
§ 5
Perspective projection

The donut exists in 3D; the screen is 2D. Things farther away should appear smaller. Dividing x and y by z implements this exactly — the deeper a point, the more it is squeezed toward the center. K₁ scales the result to screen pixels; K₂ is an offset that keeps the donut in front of the eye.

5
Fig. 5 — Drag z to move the object farther away. The red bar on the screen plane shrinks as z increases.
screen_x = K₁ · x / (K₂ + z)   screen_y = K₁ · y / (K₂ + z)
§ 6
Z-buffer: only draw the closest point

Multiple donut surface points can project to the same screen pixel. Rather than drawing them all and hoping the last one wins, maintain a per-pixel depth table. For each new point, compare its depth (1/z, so larger = closer) to what is already stored. Draw only if the new point is closer; otherwise skip it.

Fig. 6 — Two points project to the same pixel. The front point (z = 3) passes the z-buffer test and is drawn; the back point (z = 8) is discarded.
if 1/z > zbuffer[pixel]: draw it
otherwise skip — something closer is already there
§ 7
Lighting: dot product shading

Each surface point has a normal vector perpendicular to the surface. The brightness of a point is the dot product of that normal with the light direction. A normal pointing straight at the light scores 1 (fully lit); one perpendicular scores 0 (edge-on); one pointing away scores negative (shadow, clamped to 0).

135°
Fig. 7 — Three surface patches with different normals (blue arrows). The orange arrow is the light. L values and ASCII characters update as the light rotates.
L = dot(surface normal, light direction)
L = 1 is fully lit · L = 0 is edge-on · L < 0 is shadow
§ 8
ASCII shading: brightness → character

Scale L (0 to 1) to an integer index 0–11 and look up into the 12-character palette. Characters with more ink appear darker; sparse ones appear lighter. The palette is ordered from least to most visual density, so the mapping is a direct lookup with no extra math.

click any character to see its brightness level

!
Fig. 8 — The 12-character palette. Drag brightness or click any character.
palette: .,-~:;=!*#$@ — 12 characters, dimmest to brightest