Pizza: Stage 1

To complete this tutorial, we need two text files. We’ll write the HTML for our webpage in the first text file, and we’ll write the JavaScript for our drawing in the second text file. For help setting up these two text files on your own, go to the set up instructions on the How to draw on <canvas> page.

If you’d rather not set up these two text files on your own, copies of the two blank text files are here: stage00.zip, and copies of the two finished text files are here: stage01.zip. Download and unzip the files, and make sure both text files are saved in the same folder. Double-click on the HTML text file with the “.html” extension to open it in your web browser. Right-click or control-click on either file to open and edit it in your text editor. Saving any changes you make in the text editor will not affect the file’s extension.

Set up the webpage

The first thing we do is set up our webpage in index.html.

<!DOCTYPE html>
<html>
<head>
<title>Pizza</title>
<script type="text/javascript" src="scripts.js"></script>
</head>
<body>
<div style="width:400px; margin:20px auto">
<canvas id="pizzaCanvas" width="400" height="400"></canvas>
</div>
</body>
<script>
drawPizza();
</script>
</html>

The title of the webpage is Pizza. Our scripts are stored in a second file named scripts.js in the same folder. The style="width:400px; margin:20px auto" on the <div> tag isn’t really necessary; it centers the <canvas> on the page and places a 20 pixel margin above and below it. The <canvas> is named pizzaCanvas and it’s 400 pixels wide and 400 pixels tall. When the web browser reaches the bottom of the text document, it will run a script named drawPizza(), which is a subroutine in scripts.js.

Draw the pizza

Next we need to set up scripts.js—this is where we do all our drawing.

var ctx;
 
function drawPizza() {
ctx = document.getElementById("pizzaCanvas").getContext("2d");
ctx.save();
ctx.translate(200, 200);
 
drawCrust();
drawSauce();
drawCheese();
drawToppings(16, drawMushroom);
drawToppings(16, drawPepperoni);
drawToppings(32, drawGreenPepper);
 
ctx.restore();
}

At the top of scripts.js, we declare a variable named ctx. Because we are declaring it outside of any subroutine, ctx is a global variable that any subroutine can use.

Then we define the subroutine drawPizza(), which index.html will run once the webpage is loaded. In JavaScript, subroutines are called functions. Functions have parentheses at the end of their names, and their definitions are enclosed in curly brackets. (The tabs and line breaks aren’t necessary for the scripts to run, but they make the scripts easier to read.)

The first thing drawPizza() does when run is store a reference to the context of our <canvas> in ctx. It gets this reference by asking the webpage for the element named pizzaCanvas. Then it saves the context; moves the origin of the context to (200, 200); and draws the crust, the sauce, the cheese, 16 mushrooms, 16 pepperoni, and 32 green peppers. At the end, it restores the context back to its last save point, which is its original state.

The coordinates (200, 200) is the center of the <canvas>. We can move the pizza around just by changing those coordinates.

Draw the crust

Of course, it would be nice if we could simply tell the web browser to draw crust, draw sauce, and draw cheese for us… but it doesn’t really know how to do any of those things. We have to tell it how to draw crust using commands that it does understand. We do that by creating a drawCrust() function. We could draw the crust directly in drawPizza() without creating a separate function, but breaking our scripts up into logical chunks makes our scripts easier to read and maintain.

function drawCrust() {
ctx.fillStyle = "rgba(230, 192, 117, 1)";
ctx.beginPath();
ctx.arc(0, 0, 200, 0, 2 * Math.PI, false);
ctx.fill();
}

In the drawCrust() function, we’re giving the ctx four commands that it already understands: we’re telling it to set the fillStyle, begin a new path, draw an arc, and fill() the path. To set the fillStyle, we assign it a text string that defines a color. Even though "rgba(230, 192, 117, 1)" looks like a function call, it’s not. It’s a string of text, which is why it’s in quotes. We could have assigned it the same color using the string "#e6c075", or a similar color using "burlywood". We can assign the fillStyle any color recognized in CSS. I prefer "rgba(230, 192, 117, 1)" because I can set the alpha value: red 230, green 192, blue 117, and alpha 1.

The next three lines in the function is how we tell the ctx to draw a circle. All shapes on <canvas> start out as paths. Once we draw a path, we can either fill() it to fill it with color or stroke() it to outline it with a line. In this case, we are going to fill it with our fill color, "rgba(230, 192, 117, 1)". The arc() function takes six parameters separated by commas: the arc is centered at (0, 0), it’s radius is 200, it starts at angle 0 and ends at angle 2 * Math.PI, and its counterclockwiseness is false. To use the arc() function to draw a full circle, we want the arc to start at 0° and end at 360°. Because the arc() function uses radians instead of degrees, 360° = 2π radians, and 2π is 2 * Math.PI in JavaScript. Because we are drawing a full circle, it doesn’t matter if the arc goes clockwise or counterclockwise, but clockwise is the default direction for the arc() function.

Now that we’ve defined the drawCrust() function, we’re ready to draw a crust on our <canvas>. However, when the web browser loads index.html, it’s going to run the drawPizza() function, which—in turn—will call a bunch of other functions, including drawCrust() and drawSauce(). While the drawCrust() function exists, the other functions do not, so drawPizza() won’t finish running. We can fix this by “commenting out” the functions that don’t exist yet.

If we type // on a line in our script, the web browser will ignore any text on the line after that point. We use this feature to put comments in our scripts or to temporarily disable parts of our scripts. Let’s comment out everything after we draw the crust.

function drawPizza() {
ctx = document.getElementById("pizzaCanvas").getContext("2d");
ctx.save();
ctx.translate(200, 200);
 
drawCrust();
//drawSauce();
//drawCheese();
//drawToppings(16, drawMushroom);
//drawToppings(16, drawPepperoni);
//drawToppings(32, drawGreenPepper);
 
ctx.restore();
}

Instead of commenting out each line one at a time, we can also comment out a block of lines:

function drawPizza() {
ctx = document.getElementById("pizzaCanvas").getContext("2d");
ctx.save();
ctx.translate(200, 200);
 
drawCrust();
/*
drawSauce();
drawCheese();
drawToppings(16, drawMushroom);
drawToppings(16, drawPepperoni);
drawToppings(32, drawGreenPepper);
*/
 
ctx.restore();
}

However, since we’ll be adding the lines back one at a time, I think it’s easier just to comment out the lines individually. Now when the web browser loads index.html, the drawPizza() function will draw just the crust.

Draw the sauce

If you think the drawSauce() function looks an awful lot like the drawCrust() function, you’re not mistaken. The sauce is another circle with a different color and a radius of 180.

function drawSauce() {
ctx.fillStyle = "rgba(200, 55, 62, 1)";
ctx.beginPath();
ctx.arc(0, 0, 180, 0, 2 * Math.PI, false);
ctx.fill();
}

To draw the sauce on top of the crust, go back to the drawPizza() function and uncomment out the drawSauce() function.

Draw the cheese

The cheese is another circle with a different color and a radius of 170. How am I getting these colors? I downloaded a bunch of pizza photos, opened them up in a photo-editing and drawing program, and used the eyedropper tool to find the RGB values of different colors.

function drawCheese() {
ctx.fillStyle = "rgba(251, 246, 242, 1)";
ctx.beginPath();
ctx.arc(0, 0, 170, 0, 2 * Math.PI, false);
ctx.fill();
}

I should also mention that the order in which functions are listed in scripts.js doesn’t matter. The draw order in the drawPizza() function matters—we have to draw the crust before drawing the sauce—but we can define the drawSauce() function before or after the drawCrust() function. As long as the web browser can find the function somewhere in scripts.js, the script will run.

Draw a pepperoni

The next two functions called in drawPizza() are drawToppings() and drawMushroom(). However, since those two functions are slightly complicated, let’s work on drawPepperoni() and drawGreenPepper() first.

function drawPepperoni() {
ctx.fillStyle = "rgba(162, 53, 58, 1)";
ctx.beginPath();
ctx.arc(0, 0, 20, 0, 2 * Math.PI, false);
ctx.fill();
}

All drawPepperoni() does is draw one pepperoni at the origin of the ctx. Drawing more than one pepperoni and placing them on the pizza is the job of drawToppings(). To see what one pepperoni looks like, just add a call to drawPepperoni() in drawPizza().

function drawPizza() {
ctx = document.getElementById("pizzaCanvas").getContext("2d");
ctx.save();
ctx.translate(200, 200);
 
drawCrust();
drawSauce();
drawCheese();
drawPepperoni();
//drawToppings(16, drawMushroom);
//drawToppings(16, drawPepperoni);
//drawToppings(32, drawGreenPepper);
 
ctx.restore();
}

Draw a green pepper

Like drawPepperoni(), all drawGreenPepper() does is draw one green pepper at the origin of the ctx. However, there are two important differences. First, the green pepper is a square, not a circle. The ctx has a shortcut for drawing rectangles. Instead of beginning a new path, drawing the rectangle, and then filling it with the fill color, we can fill in rectangles by calling ctx.fillRect(). The ctx.fillRect() function takes four parameters: the x- and y-coordinates of the rectangle’s top left corner, and the rectangle’s width and height. The width and the height are both 12. To center the square at the origin, the top left corner has to be at (-6, -6).

function drawGreenPepper() {
ctx.save();
ctx.rotate(2 * Math.random() * Math.PI);
ctx.fillStyle = "rgba(139, 195, 60, 1)";
ctx.fillRect(-6, -6, 12, 12);
ctx.restore();
}

The second difference is that we’re rotating the ctx before drawing the green pepper. If we don’t do that, all of the green peppers will be facing the same direction. We don’t have to rotate the pepperoni since the pepperoni are circles.

To get a random number in JavaScript, we use the Math.random() function to generate a random number between 0 and 1. If we then multiply that random number by 2π, we’ll have a random number between 0 and 2π. So, by passing 2 * Math.random() * Math.PI into ctx.rotate(), we are rotating the green pepper by a random angle between 0° and 360°. And—because we are rotating the ctx—we save the ctx before the rotation and restore it afterward. That way, drawGreenPepper() does not leave the ctx in a different state.

Draw a mushroom

So far we’ve drawn everything with circles and rectangles. To draw a mushroom, we’re going to draw our first complex shape. Since a mushroom is so small, we’re going to scale it up so that we’ll have a better view of it as we draw.

function drawPizza() {
ctx = document.getElementById("pizzaCanvas").getContext("2d");
ctx.save();
ctx.translate(200, 200);
 
drawCrust();
drawSauce();
drawCheese();
ctx.scale(4, 4);
drawMushroom();
//drawToppings(16, drawMushroom);
//drawToppings(16, drawPepperoni);
//drawToppings(32, drawGreenPepper);
 
ctx.restore();
}

This will draw a mushroom in the center of the pizza and increase its size by a scale factor of 4 in both the x- and y-dimensions.

function drawMushroom() {
ctx.save();
//ctx.rotate(2 * Math.random() * Math.PI);
//ctx.translate(0, 10);
ctx.fillStyle = "rgba(133, 114, 84, 1)";
 
ctx.beginPath();
ctx.arc(0, 0, 30, 1.25 * Math.PI, 1.75 * Math.PI, false);
ctx.lineTo(20, -10);
ctx.lineTo(8, -10);
ctx.lineTo(11, 10);
ctx.lineTo(-11, 10);
ctx.lineTo(-8, -10);
ctx.lineTo(-20, -10);
ctx.closePath();
ctx.fill();
 
ctx.rotate(-0.25 * Math.PI);
ctx.beginPath();
ctx.arc(0, -20, 10, 0, 2 * Math.PI, false);
ctx.fill();
 
ctx.rotate(0.5 * Math.PI);
ctx.beginPath();
ctx.arc(0, -20, 10, 0, 2 * Math.PI, false);
ctx.fill();
ctx.restore();
}

The first thing we do is save the ctx at the beginning of the function and restore it at the end. That way, drawMushroom() does not leave the ctx in a different state. We will also temporarily comment out the rotation and the translation. The rotation will place the mushroom at a random angle and the translation will center the mushroom at the origin of the ctx—but understanding how the mushroom is drawn is easier if we skip those transformations for now.

Use the number field to step through the drawing process. The red outline represents a path we will fill with the mushroom color. We start by drawing an arc with a radius of 30 from 1.25 * Math.PI (135°) to 1.25 * Math.PI (225°). An angle of 0° is pointing in the 3 o’clock direction. We then draw a series of lines. The ctx.closePath() function closes the path to create a closed shape, which is then filled with the mushroom color.

To complete the mushroom, we draw two smaller circles. I positioned those circles by doing a little math and rotating the ctx. However, we also could have positioned the circles just by tweaking their x- and y-coordinates until they were in the correct position.

Notice how the mushroom is a bit off-center? Its center is above the origin. If we uncomment out the rotation and the translation at the beginning of the function, the mushroom will be centered and at a random angle.

Place and draw toppings on the pizza

Step:

The last step is writing a function to place and draw the toppings on the pizza. To be efficient, we’re going to use the same function, drawToppings(), to place all three types of toppings. Whenever drawToppings() is called, it expects to receive two inputs: a number and a function. The function is what drawToppings() uses to draw the actual topping. If we pass it drawMushroom(), it will draw mushrooms. If we define and pass it drawBlackOlive(), it will draw black olives. The number tells drawToppings() how many times to place and draw the topping.

function drawToppings(n, drawTopping) {
// Do stuff
}

The function drawToppings() stores the number it receives in a variable named n and stores the drawing function in a variable named drawTopping. These variables are known as parameters. It then saves the ctx, rotates the ctx to a random angle, does some more stuff, and restores the ctx back to the way it was when it’s done.

function drawToppings(n, drawTopping) {
ctx.save();
ctx.rotate(2 * Math.random() * Math.PI);
 
// Do more stuff
 
ctx.restore();
}

In between, we have a for loop. A for loop has four parts. At the start of this for loop, it sets a variable i = 0. Then it runs the script enclosed in its curly brackets. After it runs the script, it runs i++, which increments i by 1. Then it checks to see if i < n. Remember, n is the number that was passed to drawToppings(). If i < n, then it runs the script between the curly brackets again, increments i, and checks again. It keeps doing this until i < n is false. Basically, this for loop runs the script between its curly brackets n times, incrementing i from 0 to n - 1.

function drawToppings(n, drawTopping) {
ctx.save();
ctx.rotate(2 * Math.random() * Math.PI);
 
for (var i = 0; i < n; i++) {
// Do this stuff n times
}
 
ctx.restore();
}

Draw .

The script inside the for loop is responsible for figuring out where to draw the toppings. Now, we could place each topping manually, but that would be a lot of work. Instead, we’ll use a script to place the toppings randomly for us. But we don’t want to place the toppings completely randomly—we also want them spread out fairly evenly.

Before looking at the script that makes this happen, let’s talk strategy. One way to place a topping randomly is to rotate the ctx to a random angle, and then pick a random distance from the center of the pizza. Since the cheese has a radius of 170, we could use a random distance between 0 and 150. But that might be too random. To ensure a more even distribution, we could randomize the distance but keep the angles evenly distributed.

Unfortunately, randomizing the distance but keeping the angles evenly distributed is still too random. The toppings have a tendency to clump toward the center and leave the edges of the pizza uncovered. To address those issues, I came up with two ideas. First, we could place the odd-numbered toppings in the for loop 80-150 pixels from the center and the even-numbered toppings 10-80 pixels from the center. This would improve coverage. Second, instead of using a random number between 0 and 1 to calculate the random distances, we could use the square root of a random number between 0 and 1 instead.

This second idea sounds a little strange, but let’s think about it. A random number generator should give us numbers that are evenly distributed between 0 and 1. But there’s more area to cover as we go from the center of the pizza to the outer edge. We actually want a random number generator skewed toward 1, which we can simulate by taking the square root. For example, the square root of 0.49 is 0.7, which means that over half of our “random numbers” will be greater than 0.7. The square root of 0.25 is 0.5, which means that 75% of our numbers will be greater than 0.5.

Now that we understand the basic strategy that we’ll use to place the toppings on the pizza, it’s time to look at the script inside of the for loop.

Completely random:

Random distance, even angle:

function drawToppings(n, drawTopping) {
ctx.save();
ctx.rotate(2 * Math.random() * Math.PI);
 
for (var i = 0; i < n; i++) {
ctx.save();
ctx.translate(0, 70*(Math.sqrt(Math.random()) + i % 2) + 10);
drawTopping();
ctx.restore();
ctx.rotate(2 * Math.PI / n);
}
 
ctx.restore();
}

In the first line, we save the ctx. In the second line, we translate the origin of the ctx out from the center of the pizza. The second line is a little complicated, so let’s come back to it later. In the third line, we draw the topping at the new origin. Remember, drawTopping() stores a reference to the function that was passed to drawToppings() when it was called. If this was drawPepperoni(), then drawTopping() is drawPepperoni(). In line 4, we restore the ctx, returning the ctx to its last save point, which is at the center of the pizza. In line 5, we rotate the ctx. The angles of these rotations add up to 2 * Math.PI, or 360°, after n turns.

The trickiest part of the script is the second line, where we’re translating the ctx along the y-axis a distance of 70*(Math.sqrt(Math.random()) + i % 2) + 10. But all we’re doing is following the strategy we outlined above. To understand how this distance expression works, we need to apply order of operations.

We certainly could have drawn our pizza without doing all this math by placing our toppings manually and hard-coding those positions in our script. We also could have placed our toppings randomly using a less effective algorithm. But good topping placement is critical in a good pizza, and figuring out how to do that in a script was a nice challenge.

Now all we have left to do is put everything together and build our pizza.

Go to stage 2.

Our strategy:

index.html

<!DOCTYPE html>
<html>
<head>
<title>Pizza</title>
<script type="text/javascript" src="scripts.js"></script>
</head>
<body>
<div style="width:400px; margin:20px auto">
<canvas id="pizzaCanvas" width="400" height="400"></canvas>
</div>
</body>
<script>
drawPizza();
</script>
</html>

scripts.js

var ctx;
 
function drawPizza() {
ctx = document.getElementById("pizzaCanvas").getContext("2d");
ctx.save();
ctx.translate(200, 200);
 
drawCrust();
drawSauce();
drawCheese();
drawToppings(16, drawMushroom);
drawToppings(16, drawPepperoni);
drawToppings(32, drawGreenPepper);
 
ctx.restore();
}
 
function drawCrust() {
ctx.fillStyle = "rgba(230, 192, 117, 1)";
ctx.beginPath();
ctx.arc(0, 0, 200, 0, 2 * Math.PI, false);
ctx.fill();
}
 
function drawSauce() {
ctx.fillStyle = "rgba(200, 55, 62, 1)";
ctx.beginPath();
ctx.arc(0, 0, 180, 0, 2 * Math.PI, false);
ctx.fill();
}
 
function drawCheese() {
ctx.fillStyle = "rgba(251, 246, 242, 1)";
ctx.beginPath();
ctx.arc(0, 0, 170, 0, 2 * Math.PI, false);
ctx.fill();
}
 
function drawToppings(n, drawTopping) {
ctx.save();
ctx.rotate(2 * Math.random() * Math.PI);
 
for (var i = 0; i < n; i++) {
ctx.save();
ctx.translate(0, 70 * (Math.sqrt(Math.random()) + i % 2) + 10);
drawTopping();
ctx.restore();
ctx.rotate(2 * Math.PI / n);
}
 
ctx.restore();
}
 
function drawPepperoni() {
ctx.fillStyle = "rgba(162, 53, 58, 1)";
ctx.beginPath();
ctx.arc(0, 0, 20, 0, 2 * Math.PI, false);
ctx.fill();
}
 
function drawGreenPepper() {
ctx.save();
ctx.rotate(2 * Math.random() * Math.PI);
ctx.fillStyle = "rgba(139, 195, 60, 1)";
ctx.fillRect(-6, -6, 12, 12);
ctx.restore();
}
 
function drawMushroom() {
ctx.save();
ctx.rotate(2 * Math.random() * Math.PI);
ctx.translate(0, 10);
ctx.fillStyle = "rgba(133, 114, 84, 1)";
 
ctx.beginPath();
ctx.arc(0, 0, 30, 1.25 * Math.PI, 1.75 * Math.PI, false);
ctx.lineTo(20, -10);
ctx.lineTo(8, -10);
ctx.lineTo(11, 10);
ctx.lineTo(-11, 10);
ctx.lineTo(-8, -10);
ctx.lineTo(-20, -10);
ctx.closePath();
ctx.fill();
 
ctx.rotate(-0.25 * Math.PI);
ctx.beginPath();
ctx.arc(0, -20, 10, 0, 2 * Math.PI, false);
ctx.fill();
 
ctx.rotate(0.5 * Math.PI);
ctx.beginPath();
ctx.arc(0, -20, 10, 0, 2 * Math.PI, false);
ctx.fill();
ctx.restore();
}