Math and Science Art Series: Visualising Pi with a HTML5 canvas




I was inspired by a friend to do some art using the math/scientific world as a basis, so I decided to start with the classic: pi.

I’ll be looking at some different ways of visualising the digits of pi visually using HTML5 and the canvas element to iterate over the digits. If you have any cool or interesting ideas regarding Math, Science and Art visualisations, especially concerning pi and other interesting numbers, then leave a comment below and we’ll feature your thoughts or works in our next post!

Technologies Used:

Demo

Fullscreen version here.

Libraries used in this demonstration:

  • A tiiiny bit of jQuery (definitely not needed)

Browser support:

  • All modern browsers should support this

Step 1: Get pi

The first thing to do for this is to establish a reliable method for generating digits of pi. There are several methods you could use to do this:

  • Use the JS internal Math.PI constant
  • Generate the whole of pi to a certain accuracy then read off the digits one by one
  • Generate digits successively using a spigot algorithm such as the BBP algorithm
  • Store the digits of pi in a pre-generated variable

I used the last method, as the internal constant isn’t really long enough for our purposes and generating either each consecutive digit or the whole number can take some time when we want a lot of digits.

A quick Google search came up with a million digits of pi (though you’ll have to work out the actual page it’s contained on for yourself!) of which I took the first 100,000 or so and just stuck it in a variable at the top of my source code.

An interesting fact, just thirty-nine digits of pi are sufficient to perform most cosmological calculations, because that is the accuracy necessary to calculate the volume of the known universe with a precision of one atom. [source]

Step 2: Make our canvas

Using javascript, it’s easy to make our canvas element and inject it into the page. Here I’ve used a little bit of jQuery, but you could just as easily use vanilla javascript to accomplish this:

// our onload event callback
$(function(){
// create a canvas and append it to the body
var canvas = document.createElement('canvas');
$('body').append(canvas);
// set the dimensions to the window dimensions (or whatever)
canvas.width = $(document).width();
canvas.height = $(document).height();
// grab the 2D context
var ctx = canvas.getContext('2d');
// fill it with black
ctx.fillStyle = "#000000";
ctx.fillRect( 0, 0, canvas.width, canvas.height);
// here we use requestAnimationFrame to create
// an animation loop.  It just calls our mainLoop
// function (nice and clean this way). This is
// repeated in mainLoop so that it repeats.
requestAnimationFrame(function(){
mainLoop(canvas);
});
}); 

I think it’s pretty self explanatory, we’re just injecting our canvas into the document body and making a call to requestAnimationFrame to start our main animation loop function mainLoop.

Step 2: Make our animation

So now we can start building the steps for our main animation loop. What I’m going to do for this first part is have a scrolling canvas that will create different size bubbles depending on the digit of pi we’re on.

We’ll need a counter to keep track of which digit we’re on, which I’ll call i. We’ll also need a few rendering functions to keep things nice and modular. So, for this example we have our mainLoop, a function for rendering circles drawCircle and a function for scrolling our canvas moveCanvas. Here is our mainLoop:

// pi to 106,785 digits.
var PI = "3141592653..."
// our counter variable
var i = 0;
// for changing the rate at which circles are produced
var mod = 15;
function mainLoop(canvas){
// move the canvas left 5 pixels and replace the
// background with black
moveCanvas(canvas, "#000000", -5, 0);
// The rate at which we draw bubbles is determined
// by this if loop. We do this because we want the
// scrolling to be smooth but the bubble production
// to be every mod frames.
if(i % mod == 0) {
// We calulte the actual digit we want as we are
// only drawing a circle every mod digits.
var digit = i/mod
// Here we generate our colour for our circle 
// using digits of pi. I'm using the consecutive
// 3 digits because otherwise they would be all
// grey!
var color = "rgba(" + 
Math.floor(PI.charAt(digit)*(255/9)) + 
"," + Math.floor(PI.charAt(digit+1)*(255/9)) + 
"," + Math.floor(PI.charAt(digit+2)*(255/9)) + 
",0.5)";  // Opacity of 0.5 for nicely overlayed bubbles
// Here we set our radius using the next two digits of pi.
// There is a scaling factor of the canvas height / (9*9*4)
// so that they fit on our canvas (the maximum radius is 9).
var radius = (PI.charAt(digit) + PI.charAt(digit+1)) * (canvas.height/(9*9*4));
// we draw our circle given the radius and colour above.
drawCircle(canvas, {radius: radius, color: color});
}
// increment our counter
i++;
// reset our counter if we run out of digits
if( (i * mod) > PI.length + 2) i = 0;
// repeat the animation loop
requestAnimationFrame(function(){mainLoop(canvas);});
}

It’s all nicely commented so it should be self explanatory, however, I have used a few tricks here that I’ll explain.

requestAnimationFrame gives us a good way of creating looping animations without having to use a fixed setInterval. In this example we want smooth scrolling, so I am moving the canvas every frame, however, we only want to make a bubble every n frames (which I’ve stored in the mod variable). We can achieve this using a simple if statement that only creates a bubble every mod frames using the % operator.

Step 3: Create our rendering functions

We have two rendering functions, one that moves the canvas and one that draws a circle. These are pretty simple. Here is our function for moving the canvas:

// Takes the canvas, replacement colour and distance to move in x and y
function moveCanvas(canvas, color, dx, dy){
// get our canvas' 2D context
var ctx = canvas.getContext('2d');
// grab our current background
var bg = ctx.getImageData(0,0, canvas.width, canvas.height);
// make our canvas the colour of the color given
ctx.fillStyle = color;
ctx.fillRect(0,0,canvas.width, canvas.height);
// and now put our old background back on the canvas in the correct place.
ctx.putImageData(bg,dx,dy);
}   

Pretty simple, and here is our function for drawing circles:

// Takes the canvas and a few options
function drawCircle(canvas, options){
// get our canvas' 2D context
var ctx = canvas.getContext('2d');
// these are our default options
var dftoptions = {color : "#ff0000", radius: 20};
options = options || {};
// using jQuery's extend function to add to our
// default options
var options = $.extend(dftoptions, options);
// This draws our bubble on the right hand side - the bubble radius
// so that the bubble doesn't appear off the edge of the screen.
options.x = options.x || canvas.width - options.radius;
options.y = options.y || canvas.height/2;
// and now we draw our bubble according to radius and color
ctx.fillStyle = options.color;
ctx.beginPath();
ctx.arc(options.x, options.y, options.radius, 0, Math.PI*2, true);
ctx.closePath();
ctx.fill();
}

Again, pretty simple.

If you run this code you should now get a nice smooth animation that scrolls from right to left creating bubbles whose colour and size is set according to the digits of pi.

While you’re running your simulation look out for a few cool patterns, such as the six consecutive 9’s occurring at the 762nd digit (on our animation they should be biiiig white circles!).

Step 3.141…: Another visualisation

Note: This example definitely looks better in fullscreen so it’s not constantly getting cut off, take a look at the fullscreen version here.

Here is another popular method of visualising pi known as a random walk. In this visualisation we loop through digits of pi and assign each number a direction, for example 1 = up, 2 = up and right, 3 = right, and so on. Obviously we only have 8 cardinal directions so we can ignore 0 and 9 for this example (importantly, it’s still a random walk).

I’ve re-used the code for generating the colours of the lines and the function for moving the canvas but I’ve modified our main loop and added a function for drawing lines. Here is my drawLine function:

function drawLine(canvas, options){
var ctx = canvas.getContext('2d');
var dftoptions = {color : "#ff0000", radius: 20};
options = options || {};
var options = $.extend(dftoptions, options);
options.x = options.x || canvas.width/2;
options.y = options.y || canvas.height/2;
options.dx = options.dx || 0;
options.dy = options.dy || 0;
ctx.fillStyle = options.color;
ctx.strokeStyle = options.color;
ctx.beginPath();
ctx.moveTo(options.x, options.y);
ctx.lineTo(options.x + options.dx, options.y + options.dy);
ctx.stroke();
}

and the mainLoop is fairly similar, this time though we have a switch statement that assigns an x and y direction to move in depending on the digit and we’ve changing our moveCanvas call to move the distance drawn by the line, so that we’re always centered on where we’re currently drawing:

var linelength = 10;
function mainLoop(canvas){
if(PI.charAt(i)*1 !== 9 && PI.charAt(i)*1 !== 0) {
var color = "rgba(" + Math.floor(PI.charAt(i)*(255/9)) + 
"," + Math.floor(PI.charAt(i+1)*(255/9)) + 
"," + Math.floor(PI.charAt(i+2)*(255/9)) + 
",0.5)";
var x = 0, y = 0;
switch(PI.charAt(i) * 1){
case 1:
x = 0;
y = 1;
break;
case 2:
x = 1;
y = 1;
break;
// etc...
case 8:
x = -1;
y = 1;
break;
}
x = x * linelength;
y = y * linelength;
moveCanvas(canvas, "#000000", x, y);
drawLine(canvas, {color: color, dx: x, dy: y});
}
i++;
if(i > PI.length + 2) i = 0;
requestAnimationFrame(function(){mainLoop(canvas);});
}

We can keep everything else the same (this is the beauty of breaking our code down functionally).

Exercises

  • How many bytes does it take to store 100,000 digits of pi? What about 1,000,000?
  • For the random walk, how could we eliminate the need to skip digits? (hint: think about number bases)
  • Can you give the animations user controls, such as bubble size, line length, play, pause, fast forward, rewind, etc?
  • For the both the examples, if the animation reaches the edge, the image is lost, how can we stop this from happening?
  • How could you create a visualisation that creates bubbles in a spiral, instead of moving along a fixed right-to-left path?

I’d love to hear your answers to these exercises in the comments section below, if you come up with some good answers I’ll use them in this post or create a new post with some of your extensions and ideas with credit to you and a link to your website.

Conclusion

We can create some pretty cool visualisations using the really simple methods above, but this opens up a world of possibilities. People have used pi in the past to create some awesome visualisations, for example, this work by Martin Krzywinski visualises the progression of Pi, Phi (the golden ratio) and e:

There’s this cool Kickstarter project about visualising pi with a big mural:

Here’s a list of pi visualisations on the wolfram alpha page (including random walks). Here’s their random walk in base 10:

Random walk base 10

 

And there’s even someone who has turned pi into music (from the numberphile YouTube channel, be sure to subscribe to them):

I’d love to hear about any ideas you have about visualising pi, use the comment box below to suggest any ideas or if you have done any visualisations of your own or even used the code above to create your own, then share them with us and we’ll feature your visualisation on our next instalment of Math and Science Art: Visualising Pi.




Alternatively, you can use this contact form to reach me privately to share your thoughts:













Leave a Reply