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.
θ (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.
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.
x = R2 + R1·cos(θ) y = R1·sin(θ) z = 0
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.
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.
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.
screen_x = K₁ · x / (K₂ + z) screen_y = K₁ · y / (K₂ + z)
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.
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).
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
.,-~:;=!*#$@ — 12 characters, dimmest to brightest