Pizza: Stage 2

If you haven’t done so already, you should go through stage 1 of this tutorial. The work that we do in stage 2 builds off the drawing in stage 1, and you’ll need the index.html and scripts.js files from stage 1 to get started. Copies of the finished text files for stage 2 are here: stage02.zip.

Set up the webpage

To update our drawing, we don’t have to touch the index.html file. All of the actual drawing happens in the scripts.js file—index.html just creates a webpage with a blank <canvas> on it. We are going to update our drawing just by making changes to the existing drawing functions in scripts.js.

Brown the crust

In stage 1, we drew the crust by filling a circle with a solid color. We’re going to brown the crust by filling the circle with a gradient. This way, the color of the crust will get darker and redder toward the outer edge.

function drawCrust() {
var crustGradient = ctx.createRadialGradient(0, 0, 180, 0, 0, 200);
 
crustGradient.addColorStop(0, "rgba(230, 192, 117, 1)");
crustGradient.addColorStop(1, "rgba(208, 113, 65, 1)");
 
ctx.fillStyle = crustGradient;
ctx.beginPath();
ctx.arc(0, 0, 200, 0, 2 * Math.PI, false);
ctx.fill();
}

We create a gradient by calling ctx.createRadialGradient() and storing the gradient in a variable named crustGradient. To fill a shape with the gradient, all we do is set ctx.fillStyle = crustGradient. Any shape we fill after that will be filled by the gradient.

When creating a gradient, we need to define two circles. For crustGradient, circle0 is centered at (0, 0) and it has a radius of 180; circle1 is centered at (0, 0) and it has a radius of 200.

var crustGradient = ctx.createRadialGradient(0, 0, 180, 0, 0, 200);

Then we need to define two or more color stops.

crustGradient.addColorStop(0, "rgba(230, 192, 117, 1)");
crustGradient.addColorStop(1, "rgba(208, 113, 65, 1)");

Each color stop has a position and a color. The position of the first color stop is 0, which means it’s on circle0. The position of the second color stop is 1, which means it’s on circle1. If a color stop was at position 0.5, it would be halfway between circle0 and circle1. Anything inside of circle0 will have the color of circle0. Anything outside of circle1 will have the color of circle1. Anything between circle0 and circle1 will have a color between those two colors.

Update the mushrooms

We aren’t changing the sauce or the cheese at all in stage 2, but we are updating all of our toppings, starting with the mushrooms. Like the crust, the mushrooms in stage 1 were filled by a solid color. To give our mushrooms a bit more texture, we’re going to add some radial gradients.

function drawMushroom() {
var mushroomGradient;
 
ctx.save();
ctx.rotate(2 * Math.random() * Math.PI);
ctx.translate(0, 10);
 
ctx.fillStyle = "rgba(211, 169, 101, 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(10, 10);
ctx.lineTo(-10, 10);
ctx.lineTo(-8, -10);
ctx.lineTo(-20, -10);
ctx.closePath();
ctx.fill();
 
ctx.save();
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();
 
mushroomGradient = ctx.createRadialGradient(-10, -9, 3, -10, -11, 6);
mushroomGradient.addColorStop(0, "rgba(107, 93, 67, 1)");
mushroomGradient.addColorStop(1, "rgba(211, 169, 101, 1)");
 
ctx.fillStyle = mushroomGradient;
ctx.beginPath();
ctx.arc(-10, -11, 6, 0.4 * Math.PI, 1.65 * Math.PI, false);
ctx.fill();
 
mushroomGradient = ctx.createRadialGradient(10, -9, 3, 10, -11, 6);
mushroomGradient.addColorStop(0, "rgba(107, 93, 67, 1)");
mushroomGradient.addColorStop(1, "rgba(211, 169, 101, 1)");
 
ctx.fillStyle = mushroomGradient;
ctx.beginPath();
ctx.arc(10, -11, 6, -0.65 * Math.PI, 0.6 * Math.PI, false);
ctx.fill();
 
ctx.restore();
}

The first half of drawMushroom() is virtually unchanged from stage 1. We are drawing the exact same shape and filling it with a solid color. The only changes we’ve made are declaring our mushroomGradient variable in the first line, changing the ctx.fillStyle to a more golden brown color, and adding a ctx.save() and ctx.restore() to undo the two rotations we use to draw the mushroom shape.

After restoring the ctx, we draw two arcs and fill them with radial gradients. I’m going to start by using ctx.arc() to draw two full circles so that we can see what the radial gradients look like. Use the number field below the drawing to go to step 2.

The circle on the left is centered at (-10, -11) and the circle on the right is centered at (10, -11). Both circles have a radius of 6. There are two things to notice about the gradients filling these two circles. First, circle0 and circle1 have different centers. Circle0, the inner darker circle, is lower than circle1. Second, we are defining two gradients, one for the left circle and another for the right circle. We can’t use the exact same gradient for both circles because we need one gradient to be centered on the left and the other to be centered on the right. The coordinates of a gradient are based on the ctx and not on the shape being filled. While this seems like a pain, it’s actually really helpful in stage 3 when we puff the crust.

We don’t want the radial gradients to cover the stem of the mushroom; we actually want it to look like the stem is covering the radial gradients. Luckily, we can accomplish this by drawing arcs instead of full circles for the radial gradients. Use the number field to go to step 3. Now the circle on the left is an arc that goes clockwise from an angle of 72° (0.4 * Math.PI) to 197° (1.65 * Math.PI); and the circle on the right is now an arc that goes clockwise from an angle of -117° (-0.65 * Math.PI) to 108° (0.6 * Math.PI).

Update the pepperoni

Step:

Next, we’re going to give our pepperoni a little texture, too. Right now the pepperoni are solid red circles, but real pepperoni have pockets of fat running through them. To draw a fat pocket, we will create a drawFat() function. Then, to draw a bunch of fat pockets on each pepperoni, we will pass the drawFat() function to drawToppings(). Using the drawToppings() function for this job only makes sense because we designed it to place things randomly but evenly on a circle.

There is, however, a small problem with this idea: drawToppings() is hard-coded to put toppings on a layer of cheese with a radius of 170 pixels while our pepperoni have a radius of 20 pixels. To get this to work, we’re going to apply a scale factor of 0.1 to the ctx when drawing the fat pockets. This solution is a complete hack, but it’s easy to implement, and we’ll replace it with a better solution in stage 3.

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

The only changes we’re making to drawPepperoni() are changing the scale of the ctx and then using drawToppings() to draw 16 fat pockets with drawFat(). Of course, now we have to define the drawFat() function.

function drawFat() {
ctx.fillStyle = "rgba(255, 255, 255, " + 0.5 * Math.random() + ")";
ctx.beginPath();
ctx.arc(0, 0, 10 + 10 * Math.random(), 0, 2 * Math.PI, false);
ctx.fill();
}

Each fat pocket is a white circle with a random alpha value between 0 and 0.5 and a random radius between 10 and 20. Because we’re drawing them when the ctx has a scale factor of 0.1, the radius of a fat pocket in the outside view will actually be between 1 and 2 pixels.

To draw a circle with a radius between 10 and 20, we pass the ctx.arc() function a radius of 10 + 10 * Math.random(). Math.random() generates a random number between 0 and 1; multiplying it by 10 gives us a random number between 0 and 10; and adding 10 to it gives us a random number between 10 and 20.

To get a random alpha value between 0 and 0.5, we just multiply 0.5 * Math.random(). However, since ctx.fillStyle is set by assigning it a string of text, we need to embed this value into a text string. Luckily, in JavaScript, we can combine text strings using the “+“ operator; and if we attempt to add a number to a text string, the number will be automatically converted to a string. This means that, if 0.5 * Math.random() = 0.3, then:

ctx.fillStyle = "rgba(255, 255, 255, " + 0.5 * Math.random() + ")"
= "rgba(255, 255, 255, " + 0.3 + ")"
= "rgba(255, 255, 255, " + "0.3" + ")"
= "rgba(255, 255, 255, 0.3)"

First, 0.5 * Math.random() is evaluated. Then, the number 0.3 is converted into a string. Finally, the three strings are combined into one string, and this string is assigned to ctx.fillStyle.

Update the green peppers

The final change in stage 2 is to update the green peppers from diced cubes to long strips. Since long strips are larger than diced cubes, we’re also going to reduce the number of green peppers from 32 to 16.

function drawGreenPepper() {
var r1 = 14 + 6 * Math.random();
var r2 = 14 + 6 * Math.random();
var length = 40 + 20 * Math.random();
 
ctx.save();
ctx.rotate(2 * Math.random() * Math.PI);
ctx.lineWidth = 6;
ctx.strokeStyle = "rgba(139, 195, 60, 1)";
 
ctx.beginPath();
ctx.arc(r1 - length / 2, r1 / 2, r1, -Math.PI, -0.5 * Math.PI, false);
ctx.arc(length / 2 - r2, r2 / 2, r2, -0.5 * Math.PI, 0, false);
ctx.stroke();
 
ctx.restore();
}

The basic shape of one of the green pepper strips is two quarter-circles joined by a straight line. The quarter-circles have a random radius between 14 and 20 pixels, and the length of the green pepper in the x-dimension is a random length between 40 and 60 pixels. We aren’t filling a shape with color to draw our green pepper strips; we’re drawing a green line that is 6 pixels thick. Because of that, we are setting the ctx.strokeStyle instead of the ctx.fillStyle.

To see how drawGreenPepper() works, let’s generate some random numbers and draw one.

var r1 = ;
var r2 = ;
var length = ;
 
ctx.beginPath();
ctx.arc(, , , -Math.PI, -0.5 * Math.PI, false);
ctx.arc(, , , -0.5 * Math.PI, 0, false);
ctx.stroke();

This green pepper will be pixels long. The arc on the left has a radius of pixels and the arc on the right has a radius of pixels. To find the center points of the two arcs so that the green pepper is centered at the origin of the ctx, we have to do a little math. If the length of the green pepper is , then the left edge of the green pepper is at x = . The left arc has a radius of , so its center point must be pixels from the left edge at x = . And since the height of the left arc is the same as its radius, we can center the arc vertically by positioning its center point at y = .

Use the number field below the drawing to go to step 2. This draws an arc at (, ) with a radius of , going clockwise from an angle of -180° (-Math.PI) to an angle of -90° (-0.5 * Math.PI).

Similarly, the right edge of the green pepper is at x = and the right arc has a radius of , so the center point of the right arc must be pixels from the right edge at x = . And since the height of the right arc is the same as its radius, we can center the arc vertically by positioning its center point at y = .

Use the number field to go to step 3. This draws an arc at (, ) with a radius of , going clockwise from an angle of -90° (-0.5 * Math.PI) to an angle of 0° (0).

But when the ctx draws the two arcs, we don’t just see two arcs; we see two arcs connected by a line. Use the number field to go to step 4. Why does the ctx draw a line when we didn’t tell it to? Well, actually, we did. After the ctx draws the left arc, its “pen” is sitting at the end of the arc. So when we tell it to draw the right arc, the pen will move from the end of the left arc to the start of the right arc, drawing a line. If we want the ctx to draw two arcs without connecting them with a line, then we need to draw each arc in its own path.

Paths are an important concept when drawing on <canvas>. We’re actually taking advantage of the fact that the ctx automatically connects parts of a path to draw our green pepper. What do you think would happen if we called ctx.closePath() before calling ctx.stroke? The ctx would connect the end of the right arc to the start of the left arc to create a closed shape.

Go to stage 3.

Step:

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(16, drawGreenPepper);
 
ctx.restore();
}
 
function drawCrust() {
var crustGradient = ctx.createRadialGradient(0, 0, 180, 0, 0, 200);
 
crustGradient.addColorStop(0, "rgba(230, 192, 117, 1)");
crustGradient.addColorStop(1, "rgba(208, 113, 65, 1)");
 
ctx.fillStyle = crustGradient;
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.save();
ctx.fillStyle = "rgba(162, 53, 58, 1)";
ctx.beginPath();
ctx.arc(0, 0, 20, 0, 2 * Math.PI, false);
ctx.fill();
 
ctx.scale(0.1, 0.1);
drawToppings(16, drawFat);
ctx.restore();
}
 
function drawFat() {
ctx.fillStyle = "rgba(255, 255, 255, " + 0.5 * Math.random() + ")";
ctx.beginPath();
ctx.arc(0, 0, 10 + 10 * Math.random(), 0, 2 * Math.PI, false);
ctx.fill();
}
 
function drawGreenPepper() {
var r1 = 14 + 6 * Math.random();
var r2 = 14 + 6 * Math.random();
var length = 40 + 20 * Math.random();
 
ctx.save();
ctx.rotate(2 * Math.random() * Math.PI);
ctx.lineWidth = 6;
ctx.strokeStyle = "rgba(139, 195, 60, 1)";
 
ctx.beginPath();
ctx.arc(r1 - length / 2, r1 / 2, r1, -Math.PI, -0.5 * Math.PI, false);
ctx.arc(length / 2 - r2, r2 / 2, r2, -0.5 * Math.PI, 0, false);
ctx.stroke();
 
ctx.restore();
}
 
function drawMushroom() {
var mushroomGradient;
 
ctx.save();
ctx.rotate(2 * Math.random() * Math.PI);
ctx.translate(0, 10);
 
ctx.fillStyle = "rgba(211, 169, 101, 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(10, 10);
ctx.lineTo(-10, 10);
ctx.lineTo(-8, -10);
ctx.lineTo(-20, -10);
ctx.closePath();
ctx.fill();
 
ctx.save();
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();
 
mushroomGradient = ctx.createRadialGradient(-10, -9, 3, -10, -11, 6);
mushroomGradient.addColorStop(0, "rgba(107, 93, 67, 1)");
mushroomGradient.addColorStop(1, "rgba(211, 169, 101, 1)");
 
ctx.fillStyle = mushroomGradient;
ctx.beginPath();
ctx.arc(-10, -11, 6, 0.4 * Math.PI, 1.65 * Math.PI, false);
ctx.fill();
 
mushroomGradient = ctx.createRadialGradient(10, -9, 3, 10, -11, 6);
mushroomGradient.addColorStop(0, "rgba(107, 93, 67, 1)");
mushroomGradient.addColorStop(1, "rgba(211, 169, 101, 1)");
 
ctx.fillStyle = mushroomGradient;
ctx.beginPath();
ctx.arc(10, -11, 6, -0.65 * Math.PI, 0.6 * Math.PI, false);
ctx.fill();
 
ctx.restore();
}