Pizza: Stage 3

If you haven’t done so already, you should go through stage 2 of this tutorial. The work that we do in stage 3 builds off the drawing in stage 2, and you’ll need the index.html and scripts.js files from stage 2 to get started. Copies of the finished text files for stage 3 are here: stage03.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.

Update the pizza

In stage 2, we didn’t have to update drawPizza() at all. We improved our drawing just by updating drawCrust(), drawMushroom(), drawPepperoni(), and drawGreenPepper(). This time, we need to make some major changes to drawPizza() for stage 3.

function drawPizza() {
ctx = document.getElementById("pizzaCanvas").getContext("2d");
 
ctx.save();
ctx.translate(200, 200);
ctx.beginPath();
ctx.arc(0, 0, 200, 0, 2 * Math.PI, false);
ctx.clip();
 
drawCrust();
drawSauce();
//drawToppings(256, 4, 196, drawHerb);
drawCrustCaramelization();
//drawToppings(4, 50, 50, drawCheese);
//drawToppings(8, 120, 120, drawCheese);
//drawToppings(16, 10, 150, drawMushroom);
//drawToppings(16, 10, 150, drawPepperoni);
//drawToppings(16, 10, 150, drawGreenPepper);
 
ctx.restore();
}

First, we’re adding a clipping path. What’s a clipping path? We’ll go over that shortly. Second, we’re using drawToppings() to draw herbs and cheese on our pizza. Third, drawToppings() now has two more parameters in its definition. These parameters will make drawToppings() more flexible, but we’ll have to comment out drawToppings() until after we’ve updated it. Fourth, there’s a new function named drawCrustCaramelization().

Caramelize and puff the crust

In stage 2, we drew the crust using a radial gradient. We’re still using a radial gradient in stage 3, but the crust now has a more complex shape and it caramelizes unevenly in spots.

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

Since we’ll be adding in the caramelization later, we change the crustGradient in drawCrust() so that the outside of the crust is less red. Then we draw the sauce before puffing and caramelizing the crust. If we drew the puffs before adding the sauce, the sauce would cover the puffs.

Use the number field below the drawing to go to step 3. To create the puffy crust, we draw 16 circles around the edge of the pizza. The circles are outlined in red so that they’re easier to see.

function drawCrustCaramelization() {
var i, y, r, s;
var crustGradient = ctx.createRadialGradient(0, 0, 180, 0, 0, 200);
 
crustGradient.addColorStop(0, "rgba(230, 192, 117, 1)");
crustGradient.addColorStop(1, "rgba(233, 157, 73, 1)");
 
ctx.save();
ctx.fillStyle = crustGradient;
for (i = 0; i < 16; i++) {
ctx.rotate(0.125 * Math.PI);
ctx.beginPath();
r = 40 + 60 * Math.random();
y = 170 + r;
ctx.arc(0, y, r, 0, 2 * Math.PI, false);
ctx.fill();
}
ctx.restore();

We draw the 16 circles in drawCrustCaramelization() using a for loop. Each time through the for loop, the ctx is turned 22.5° (0.125 * Math.PI) and a circle is drawn with a random radius of 40-100 pixels. Each circle is drawn a distance of 170 + r pixels from the center of the pizza, where r is the circle’s radius. This means that the edge of the circle will always be 170 pixels from the center of the pizza—and since the sauce has a radius of 180 pixels, the circle will always overlap the edge of the sauce by 10 pixels.

The circles are filled with the same gradient used to draw the crust in the first place, giving the inner edge of the crust a nice puffy shape. Use the number field to go to step 4. However, the outer edge of the crust is kind of ruined. To fix this, we’re going to draw the pizza with a clipping path. A clipping path creates a mask, and anything drawn outside of the path is “clipped” or hidden from view. To use a clipping path on <canvas>, we have to create the clipping path first. Clipping paths don’t mask any drawings that already exist on the <canvas>, only drawings made afterward.

ctx.beginPath();
ctx.arc(0, 0, 200, 0, 2 * Math.PI, false);
ctx.clip();

This is why the clipping path is created in drawPizza() before drawCrustCaramelization() is called. Use the number field to go to step 5 to see the effect of the clipping path. The clipping path is a circle centered at the center of the pizza with a radius of 200 pixels.

Once the crust has its puffy shape, drawCrustCaramelization() draws dozens of tiny spots of caramelization on top of it. Use the number field to go to step 6. Because we don’t know exactly how many spots we’re drawing on the crust (the number varies every time), we’re using a while loop to draw the spots instead of a for loop.

i = 0;
while (i < 2 * Math.PI) {
ctx.save();
i = i + 0.05 * Math.random();
ctx.rotate(i);
ctx.translate(0, 182 + 16 * Math.random());
s = 0.2 + 0.8 * Math.random();
ctx.scale(s, s);
drawCaramelization();
ctx.restore();
}
}

Before the while loop begins, we set our angle i = 0. Then we add a random angle of 0-0.05 radians (≈0-3°) to the angle each time through the loop, and continue drawing spots as long as the angle is less than 360° (2 * Math.PI). Each spot is drawn a random distance of 182-198 pixels from the center of the pizza, and with a random scale of 0.2-1.

function drawCaramelization() {
var caramelColor = "rgba(217, 102, 35";
var caramelizationGradient = ctx.createRadialGradient(0, 0, 0, 0, 0, 6);
 
caramelizationGradient.addColorStop(0, caramelColor + ", 1)");
caramelizationGradient.addColorStop(1, caramelColor + ", 0)");
 
ctx.fillStyle = caramelizationGradient;
ctx.beginPath()
ctx.arc(0, 0, 6, 0, 2 * Math.PI, false);
ctx.fill();
}

To draw the spots, we call drawCaramelization(), which draws a circle with a radius of 6 pixels and fills it with a caramelizationGradient. Use the number field to go to step 6.

Update how toppings are placed

Step:

Back in stage 2, we used drawToppings() to place and draw pockets of fat on our pepperoni, but we had to use a scale factor because drawToppings() is hard-coded to place toppings 10-150 pixels from the center of a circle—and the pepperoni only have a radius of 20 pixels. Using the scale factor worked for the pepperoni, but since we’re going to use drawToppings() to place herbs in our sauce and mozzarella slices on top of our pizza in stage 3, it would be nice if drawToppings() was not hard-coded with distances.

To make drawToppings() more flexible, it will now have two additional parameters as input: rMin and rMax.

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

When drawToppings() was hard-coded, rMin was always 10 and rMax was always 150—but now those values can be whatever we need them to be.

Draw the herbs

drawToppings(16, , , drawPepperoni)

With drawToppings() updated, we’re finally ready to add herbs to our sauce. In drawPizza(), we call drawToppings(256, 4, 196, drawHerb) to draw 256 herbs between rMin = 4 and rMax = 196. To draw the actual herb, we pass it drawHerb().

function drawHerb() {
ctx.save();
ctx.rotate(2 * Math.random() * Math.PI);
ctx.fillStyle = "rgba(0, 102, 0, 1)";
ctx.fillRect(-2, -1, 4, 2);
ctx.restore();
}

The drawHerb() function is fairly simple. A herb is a solid green rectangle that’s randomly rotated.

Update the cheese

Instead of drawing a single layer of cheese, we’re going to draw 12 slices of melted mozzarella that overlap. Each slice has a random radius 40-50 pixels, and we change the color of the cheese slightly and give it an alpha value of 0.95 so that we can see the slices overlapping.

function drawCheese() {
ctx.fillStyle = "rgba(246, 246, 232, 0.95)";
ctx.beginPath();
ctx.arc(0, 0, 40 + 10 * Math.random(), 0, 2 * Math.PI, false);
ctx.fill();
}

Then, in drawPizza(), we draw four slices 50 pixels from the center of the pizza and eight more slices 120 pixels from the center.

drawToppings(4, 50, 50, drawCheese);
drawToppings(8, 120, 120, drawCheese);

Update the green peppers

Since green peppers typically have dark green skins, we’re going to draw dark green skins on our peppers, too. To do this, we’ll draw a dark green pepper shifted up one pixel, and then draw the exact same green pepper with the light green color shifted down one pixel. This will give our green peppers a 2-pixel thick dark green skin.

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(109, 155, 40, 1)";
 
ctx.beginPath();
ctx.arc(r1 - length / 2, -1 + r1 / 2, r1, -Math.PI, -0.5 * Math.PI, false);
ctx.arc(length / 2 - r2, -1 + r2 / 2, r2, -0.5 * Math.PI, 0, false);
ctx.stroke();
 
ctx.strokeStyle = "rgba(139, 195, 60, 1)";
ctx.beginPath();
ctx.arc(r1 - length / 2, 1 + r1 / 2, r1, -Math.PI, -0.5 * Math.PI, false);
ctx.arc(length / 2 - r2, 1 + r2 / 2, r2, -0.5 * Math.PI, 0, false);
ctx.stroke();
 
ctx.restore();
}

Since we’re drawing the same shape twice, we should really create a function to draw the shape and then call the function twice instead of writing the same script twice—but this was fairly simple.

Randomize the mushrooms

Step:

The last function that we’ll update in stage 3 is drawMushroom(). The mushrooms in stage 2 were all identical; we’d like the mushrooms in stage 3 to be randomized at least a little bit. We can do this by scaling each mushroom randomly and varying the length of their stems.

function drawMushroom() {
var mushroomGradient;
var y = 8 + 12 * Math.random();
var s = 0.8 + 0.2 * Math.random();

We start by generating a random stem length of 8-20 pixels and a random scale factor of 0.8-1. The mushrooms in stage 2 all had stem lengths of 20 pixels and a scale factor of 1.

ctx.save();
ctx.rotate(2 * Math.random() * Math.PI);
ctx.translate(0, 10);
ctx.scale(s, s);

We apply the scale factor to the ctx before drawing the mushroom.

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(8 + 0.1 * y, -10 + y);
ctx.lineTo(-(8 + 0.1 * y), -10 + y);
ctx.lineTo(-8, -10);
ctx.lineTo(-20, -10);
ctx.closePath();
ctx.fill();

The random stem length, y, is used to calculate the shape of the mushroom. Some mushrooms will have shorter stems and others will have longer stems.

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();
}

While varying the stem length and scaling the mushroom is not a very large change, having a little variation is better than none. And, of course, this is simply the beginning. What can we do in stage 4? How about stage 10 or stage 20? Little changes add up to large changes… and the sky’s the limit.

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);
ctx.beginPath();
ctx.arc(0, 0, 200, 0, 2 * Math.PI, false);
ctx.clip();
 
drawCrust();
drawSauce();
drawToppings(256, 4, 196, drawHerb);
drawCrustCaramelization();
drawToppings(4, 50, 50, drawCheese);
drawToppings(8, 120, 120, drawCheese);
drawToppings(16, 10, 150, drawMushroom);
drawToppings(16, 10, 150, drawPepperoni);
drawToppings(16, 10, 150, 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(233, 157, 73, 1)");
 
ctx.fillStyle = crustGradient;
ctx.beginPath();
ctx.arc(0, 0, 200, 0, 2 * Math.PI, false);
ctx.fill();
}
 
function drawCrustCaramelization() {
var i, y, r, s;
var crustGradient = ctx.createRadialGradient(0, 0, 180, 0, 0, 200);
 
crustGradient.addColorStop(0, "rgba(230, 192, 117, 1)");
crustGradient.addColorStop(1, "rgba(233, 157, 73, 1)");
 
ctx.save();
ctx.fillStyle = crustGradient;
for (i = 0; i < 16; i++) {
ctx.rotate(0.125 * Math.PI);
ctx.beginPath();
r = 40 + 60 * Math.random();
y = 170 + r;
ctx.arc(0, y, r, 0, 2 * Math.PI, false);
ctx.fill();
}
ctx.restore();
 
i = 0;
while (i < 2 * Math.PI) {
ctx.save();
i = i + 0.05 * Math.random();
ctx.rotate(i);
ctx.translate(0, 182 + 16 * Math.random());
s = 0.2 + 0.8 * Math.random();
ctx.scale(s, s);
drawCaramelization();
ctx.restore();
}
}
 
function drawCaramelization() {
var caramelColor = "rgba(217, 102, 35";
var caramelizationGradient = ctx.createRadialGradient(0, 0, 0, 0, 0, 6);
 
caramelizationGradient.addColorStop(0, caramelColor + ", 1)");
caramelizationGradient.addColorStop(1, caramelColor + ", 0)");
 
ctx.fillStyle = caramelizationGradient;
ctx.beginPath()
ctx.arc(0, 0, 6, 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 drawHerb() {
ctx.save();
ctx.rotate(2 * Math.random() * Math.PI);
ctx.fillStyle = "rgba(0, 102, 0, 1)";
ctx.fillRect(-2, -1, 4, 2);
ctx.restore();
}
 
function drawCheese() {
ctx.fillStyle = "rgba(246, 246, 232, 0.95)";
ctx.beginPath();
ctx.arc(0, 0, 40 + 10 * Math.random(), 0, 2 * Math.PI, false);
ctx.fill();
}
 
function drawToppings(n, rMin, rMax, drawTopping) {
ctx.save();
ctx.rotate(2 * Math.random() * Math.PI);
 
for (var i = 0; i < n; i++) {
ctx.save();
ctx.translate(0, 0.5 * (rMax - rMin) * (Math.sqrt(Math.random()) + i % 2) + rMin);
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();
 
drawToppings(16, 1, 15, drawFat);
ctx.restore();
}
 
function drawFat() {
ctx.fillStyle = "rgba(255, 255, 255, " + 0.5 * Math.random() + ")";
ctx.beginPath();
ctx.arc(0, 0, 1 + 1 * 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(109, 155, 40, 1)";
 
ctx.beginPath();
ctx.arc(r1 - length / 2, -1 + r1 / 2, r1, -Math.PI, -0.5 * Math.PI, false);
ctx.arc(length / 2 - r2, -1 + r2 / 2, r2, -0.5 * Math.PI, 0, false);
ctx.stroke();
 
ctx.strokeStyle = "rgba(139, 195, 60, 1)";
ctx.beginPath();
ctx.arc(r1 - length / 2, 1 + r1 / 2, r1, -Math.PI, -0.5 * Math.PI, false);
ctx.arc(length / 2 - r2, 1 + r2 / 2, r2, -0.5 * Math.PI, 0, false);
ctx.stroke();
 
ctx.restore();
}
 
function drawMushroom() {
var mushroomGradient;
var y = 8 + 12 * Math.random();
var s = 0.8 + 0.2 * Math.random();
 
ctx.save();
ctx.rotate(2 * Math.random() * Math.PI);
ctx.translate(0, 10);
ctx.scale(s, s);
 
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(8 + 0.1 * y, -10 + y);
ctx.lineTo(-(8 + 0.1 * y), -10 + y);
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();
}