The article 10 stunning images show the beauty hidden in pi shows several ways to use Pi to generate pictures. This example demonstrates one of them.

The program starts drawing at the origin (0, 0). It then scans through the digits of Pi and moves in a direction that depends on the digits it reads. The picture on the right shows the direction the program takes for each digit.

The idea is relatively simple but the program does have some fairly involved details. For example, it determines the bounds of the points it will draw in coordinate space, scales the drawing to fit the available space nicely, and uses an inverse transformation to draw a circle and rectangle with lines that are two pixels wide.

The program uses a file containing 1 million digits of Pi after the decimal point (so a total of 1,000,001 digits). I got the digits at Digits of Pi. To make reading the file slightly easier, I removed the decimal point and made sure that the file does not end with a carriage return.

When the program starts, it executes the following code to load the digit data.

// The digits of pi. private int NumDigits; private int[] Digits; // The image. private Bitmap PiPicture = null; // Load the digits of pi. private void Form1_Load(object sender, EventArgs e) { int asc_0 = (int)'0'; string pi = File.ReadAllText("Pi1million.txt"); NumDigits = pi.Length; Digits = new int[NumDigits]; for (int i = 0; i < NumDigits; i++) { Digits[i] = (int)pi[i] - asc_0; } }

The program stores the digits in the `Digits` array. The form’s `Load` event handler reads the file Pi1million.txt into a string. It allocates the `Digits` array and then loops through the string’s characters. It subtracts the ASCII value of the character 0 from each digit to get the digit as a number and stores the result in the array.

The following `DrawImage` method actually draws the picture.

// Make a bitmap holding the Pi image. private Bitmap DrawImage(Rectangle rect) { // Point math. int wid = rect.Width; int hgt = rect.Height; double theta = -Math.PI / 2; const double dtheta = Math.PI / 5.0; float[] dx = new float[10]; float[] dy = new float[10]; const double move_distance = 10; for (int i = 0; i < 10; i++) { dx[i] = (float)(move_distance * Math.Cos(theta)); dy[i] = (float)(move_distance * Math.Sin(theta)); theta += dtheta; } // Get the number of points we will visit. int num_points = int.Parse(txtNumMoves.Text); if (num_points > NumDigits) { num_points = NumDigits; txtNumMoves.Text = num_points.ToString(); } else if (num_points < 1) { num_points = 1; txtNumMoves.Text = num_points.ToString(); } // Find the bounds. float x = 0; float y = 0; float wxmin = x; float wxmax = x; float wymin = y; float wymax = y; for (int i = 0; i < num_points; i++) { int digit = Digits[i]; x += dx[digit]; y += dy[digit]; if (wxmin > x) wxmin = x; if (wxmax < x) wxmax = x; if (wymin > y) wymin = y; if (wymax < y) wymax = y; } // Draw the image. Bitmap bm = new Bitmap(wid, hgt); using (Graphics gr = Graphics.FromImage(bm)) { // Scale to fit without distortion. // See "Map drawing coordinates without distortion in C#" // http://csharphelper.com/blog/2016/02/map-drawing-coordinates-without-distortion-in-c/ RectangleF world_rect = new RectangleF( wxmin, wymin, wxmax - wxmin, wymax - wymin); const float margin = 5; RectangleF device_rect = new RectangleF( margin, margin, wid - 2 * margin, hgt - 2 * margin); SetTransformationWithoutDisortion(gr, world_rect, device_rect, false, false); gr.SmoothingMode = SmoothingMode.AntiAlias; gr.Clear(Color.Black); using (Pen pen = new Pen(Color.White, 0)) { PointF last_point = new PointF(0, 0); for (int i = 0; i < num_points; i++) { int digit = Digits[i]; PointF new_point = new PointF( last_point.X + dx[digit], last_point.Y + dy[digit]); pen.Color = MapRainbowColor(i, 0, num_points); gr.DrawLine(pen, last_point, new_point); last_point = new_point; } } #if SHOW_GEOMETRY // Mark the data. // Get 2 pixels in world coordinates. Matrix inverse = gr.Transform; inverse.Invert(); PointF[] pts = { new PointF(2, 0) }; inverse.TransformVectors(pts); float thickness = pts[0].X; using (Pen pen = new Pen(Color.Yellow, thickness)) { // Draw a circle at the starting point. float dist = thickness * 3; gr.DrawEllipse(pen, -dist / 2, -dist / 2, dist, dist); // Outline the world coordinates. pen.Color = Color.Yellow; gr.DrawRectangle(pen, wxmin, wymin, wxmax - wxmin, wymax - wymin); } #endif } return bm; }

The method first creates two arrays `dx` and `dy` to hold the amounts by which the program should move when it encounters each of the digits.

Next the code parses the number of points you entered in the program’s text box and verifies that it is between 1 and the number of digits available.

The program then loops through the digits calculating points that the program would visit. It uses those points to find the minimum and maximum X and Y coordinates that it will visit.

You could store those points in an array so you can draw them more easily later. That would take up to around 8 MB of memory. (Four bytes for X and four bytes for Y, times 1 million points.) That’s not an unreasonable amount of memory, but I decided to not save the points and just recreate them later. The `dx` and `dy` arrays make calculating the points fast so the array probably wouldn’t save very much time. That might be different if the program could use the `Graphics` object’s `DrawLines` method to draw all of the lines at once, but I want to give the line segments different colors so that’s not an option.

After it calculates the drawing’s coordinate bounds, the program uses the `SetTransformationWithoutDisortion` method to transform the `Graphics` object so it draws the image nicely on the bitmap. It makes the image as large as possible without distorting it. For information on that method, see the posts Map drawing coordinates without distortion in C#.

Next the code finally draws the picture. It loops through the digits again, this time drawing lines connecting adjacent points. It uses the `MapRainbowColor` method to give the segments the colors of the rainbow. Segments near the beginning are red and later segments shade through orange, yellow, lime, aqua, and blue. For information about that method, see the post Map numeric values to colors in a rainbow in C#.

The code finishes by drawing a circle at the origin and a rectangle around the drawing’s world coordinates. It uses a pen with a thickness of two pixels, so it needs to figure out how thick to make the pen in drawing coordinates. To do that the code gets the `Graphics` object’s transformation, inverts it, and uses it to transform a vector of length 2. The result is a vector with a length that maps to 2 pixels in the drawing. The program uses that length for its pen so the result is a line that is 2 pixels wide.

After creating the two-pixel-wide pen, the program simply draws a circle at the origin and a rectangle around the drawing.

That’s the end of the drawing code, but the program also includes a few other features. For example, it lets you save its image. To do that it uses file dialog filters (see Use file dialog filters in C#) and it saves images in different file formats such as JPG or PNG (see Save images with an appropriate format depending on the file name’s extension in C#).

Download the example and look at its code to see additional details.