Stage 4

Overview

In the fourth stage of the Cityscape Challenge, we will create three rows of buildings. The buildings in the foreground will be larger and lighter in color. The buildings in the background will be smaller and darker in color. This will create the illusion of 3D depth.

Lesson: Draw a Row of Buildings

When we wanted to draw a row of windows in Stage 2 Challenge 2, we used a for loop. A for loop is great when we want to draw something over and over again a specific number of times.

The difference between drawing a row of windows and drawing a row of buildings is: we know exactly how many windows to draw, but we don't know how many buildings to draw because every building has a different width.

So, to draw a row of buildings, we are going to use a while loop instead of a for loop.

In this example, we draw a white rectangle that is 400 pixels wide, and then use a while loop to draw pink squares along the bottom of the white rectangle until the last square reaches the end of the rectangle.

Before the while loop, we assign the variable x = 0. We are using x to calculate the x-coordinate of the next building, and we will keep running through the while loop as long as x < 400. Once the x-coordinate of the next building is greater than or equal to 400, the while loop stops.

To calculate the x-coordinate of the next building, we use x = x + w. So, if x = 0 at the start of the first loop and we draw a 32x32 square, we add 32 to x and at the start of the next loop, x = 32.

Press "Run" a few times and count the number of squares drawn. The number of squares will change depending on the size of the squares.

Quick Reference:

Message Log

Challenge 1

Create a drawBuildingRow() function to draw a row of random buildings.

Because we are drawing a row of buildings, we will set groundY for the entire row and then position each building at (x, 0).

To set the groundY for the entire row, we can either apply context.translate(x, y) before calling drawBuildingRow(), or we can call drawBuildingRow(x, y) and apply the context.translate(x, y) inside of the function. Either approach works.

Inside the drawBuildingRow() function, draw the first building at x = 0, add a 12-pixel space between buildings, and keep drawing buildings while the x-coordinate of the next building is less than 800.

Note: In the example above with the random squares, we use x = x + s to update the value of x for the next loop. This works because both x and s are defined within the "scope" of the while loop. However, when drawing buildings, we can't use the variable w inside the while loop because w is only defined inside of the drawBuilding() function. What we can do instead is use the variable units, which is the number of office units on a floor in the building, to calculate the value of x for the next loop.

When your drawBuildingRow() function is ready, draw a row of buildings starting at (0, 320). Press "Run" multiple times to make sure it is working.

The row of buildings should cover the length of the red line on the canvas and look similar to the image above. Obviously, the buildings will be random. If your drawBuildingRow() function seems to be working, mark the challenge as complete by selecting "Yes, it looks good".

Quick Reference:

Previous Challenge: View your code from Stage 3 Challenge 4 to use on this challenge.

Code Missing: You have not yet entered any code in to the previous challenge: Stage 3 Challenge 4
Stage 3 Challenge 4
Message Log
Canvas (your drawing will display here)

A Solution: Here's the code I wrote to complete this challenge. View One Possible Solution

var canvas = document.getElementById('flappy_square_stage4_challenge1'); var context = canvas.getContext('2d'); function drawBuilding(leftX, groundY, units, floors, windowType, roofType) { context.save(); var width = (units * 16) + (4 * 2); var height = (floors * 16) + (4 * 2); context.translate(leftX, groundY - height); context.fillStyle = '#999999'; context.fillRect(0, 0, width, height); context.save(); context.translate(4, 4); context.fillStyle = '#666666'; for (var j=0; j < floors; ++j) { context.save(); for (var i=0; i < units; ++i) { drawWindow(windowType); context.translate(16, 0); } context.restore(); context.translate(0, 16); } context.restore(); drawRoof(width, roofType); context.restore(); } function drawWindow(windowType) { switch(windowType) { case 0: context.fillRect(4, 2, 8, 10); break; case 1: context.fillRect(2, 3, 5, 8); context.fillRect(9, 3, 5, 8); break; case 2: context.fillRect(0, 3, 16, 8); break; case 3: context.fillRect(5, 1, 6, 14); break; } } function drawRoof(w, roofType) { context.save(); switch(roofType) { case 1: context.translate((16 / 2), -16); context.fillRect(0, 0, w - 16, 16); break; case 2: context.translate((16 / 2), -24); context.fillRect(0, 0, w - 16, 24); context.translate(((w - 16) / 2) - (32 / 2), -24); context.fillRect(0, 0, 32, 24); context.translate((32 / 2) - (8 / 2), -32); context.fillRect(0, 0, 8, 32); break; case 3: context.translate((w - 64) / 2, -16); context.fillRect(0, 0, 64, 16); context.translate((64 - 32) / 2, 0); context.beginPath(); context.moveTo(0, 0); context.lineTo(16, -64); context.lineTo(32, 0); context.closePath(); context.fill(); break; } context.restore(); } function randomInteger(min, max) { return min + Math.floor(Math.random() * (max - min)); } var x = 0; while (x < 800) { var units = randomInteger(4, 10); var floors = randomInteger(5, 18); var windowType = randomInteger(0, 4); var roofType = randomInteger(0, 4); drawBuilding(x, 320, units, floors, windowType, roofType); x += (units * 16) + (4 * 2) + 12; }

Lesson: Draw a Smaller Row of Random Buildings

To create a 3D effect, we are going to draw two more rows of buildings behind the first row. Because objects get smaller in the distance, we will draw the other rows of buildings slightly smaller using the context.scale() function.

In this example, we use the context.scale() function to change the size of four random flags.

By using context.scale(0.6, 0.6), we are drawing everything at 60% scale. If we use context.scale(1, 1), then we are drawing everything at normal size.

Try context.scale(1.5, 1.5) and see what happens. The first value changes the scale in the x-direction. The second value changes it in the y-direction. The two values do not have to be the same.

Note: The scale of the context gets saved and restored with context.save() and context.restore().

Quick Reference:

Message Log

Challenge 2

Update the drawBuildingRow() function so we can draw rows with different scales.

In the definition of the drawBuildingRow() function, add a parameter called scale: drawBuildingRow(scale).

Then, inside the drawBuildingRow() function, after saving the context for the first time, change the scale of the context using context.scale(scale, scale).

If you try to draw a row of buildings at 0.6 (or 60%) scale now, you will see something interesting. The buildings are smaller, but the row of buildings is not at least 800 pixels long on the canvas. In fact, the row of buildings is 800 pixels long in the context, but only 480 pixels long on the canvas. That's because 60% of 800 is 480.

To draw a row of buildings at 0.6 scale that is at least 800 pixels long on the canvas, we need to adjust the condition inside our while loop. Instead of x < 800, we should keep drawing buildings as long as x < 800 / scale. For 0.6 scale, that works out to x < 1333.

Draw a row of buildings at 0.6 scale where the first building is sitting on the ground at (0, 280) and the row of buildings is at least 800 pixels long on the canvas.

The row of buildings should cover the length of the red line on the canvas and look similar to the image above. Obviously, the buildings will be random. If your drawBuildingRow() function seems to be working, mark the challenge as complete by selecting "Yes, it looks good".

Quick Reference:

Previous Challenge: View your code from Stage 4 Challenge 1 to use on this challenge.

Code Missing: You have not yet entered any code in to the previous challenge: Stage 4 Challenge 1
Stage 4 Challenge 1
Message Log
Canvas (your drawing will display here)

A Solution: Here's the code I wrote to complete this challenge. View One Possible Solution

var canvas = document.getElementById('flappy_square_stage4_challenge2'); var context = canvas.getContext('2d'); function drawBuilding(leftX, groundY, units, floors, windowType, roofType) { context.save(); var width = (units * 16) + (4 * 2); var height = (floors * 16) + (4 * 2); context.translate(leftX, groundY - height); context.fillStyle = '#999999'; context.fillRect(0, 0, width, height); context.save(); context.translate(4, 4); context.fillStyle = '#666666'; for (var j=0; j < floors; ++j) { context.save(); for (var i=0; i < units; ++i) { drawWindow(windowType); context.translate(16, 0); } context.restore(); context.translate(0, 16); } context.restore(); drawRoof(width, roofType); context.restore(); } function drawWindow(windowType) { switch(windowType) { case 0: context.fillRect(4, 2, 8, 10); break; case 1: context.fillRect(2, 3, 5, 8); context.fillRect(9, 3, 5, 8); break; case 2: context.fillRect(0, 3, 16, 8); break; case 3: context.fillRect(5, 1, 6, 14); break; } } function drawRoof(w, roofType) { context.save(); switch(roofType) { case 1: context.translate((16 / 2), -16); context.fillRect(0, 0, w - 16, 16); break; case 2: context.translate((16 / 2), -24); context.fillRect(0, 0, w - 16, 24); context.translate(((w - 16) / 2) - (32 / 2), -24); context.fillRect(0, 0, 32, 24); context.translate((32 / 2) - (8 / 2), -32); context.fillRect(0, 0, 8, 32); break; case 3: context.translate((w - 64) / 2, -16); context.fillRect(0, 0, 64, 16); context.translate((64 - 32) / 2, 0); context.beginPath(); context.moveTo(0, 0); context.lineTo(16, -64); context.lineTo(32, 0); context.closePath(); context.fill(); break; } context.restore(); } function randomInteger(min, max) { return min + Math.floor(Math.random() * (max - min)); } function drawBuildingRow(scale) { context.save(); context.scale(scale, scale); var x = 0; var end = (800 / scale); while (x < end) { var units = randomInteger(4, 10); var floors = randomInteger(5, 18); var windowType = randomInteger(0, 4); var roofType = randomInteger(0, 4); drawBuilding(x, (280 / scale), units, floors, windowType, roofType); x += (units * 16) + (4 * 2) + 12; } context.restore(); } drawBuildingRow(0.6);

Lesson: Draw a Smaller and Darker Row of Random Buildings

In addition to making the rows in the back smaller, we will also make them darker.

There are several ways to define colors when using context.fillStyle. So far, we have been using the color #999999 to draw our buildings. Another way to write #999999 is rgb(153, 153, 153). The number 153 in base 10 is actually 99 in base 16.

When using rgb() to define a color, we are describing the amount of red (r), green (g), and blue (b) in the color, where 0 is none and 255 is the maximum value. For example, black is rgb(0, 0, 0), which is no red, no green, and no blue. White is rgb(255, 255, 255), which is maximum red, maximum green, and maximum blue.

In this example, we draw a rectangle with a random color by selecting and combining random amounts of red, green, and blue.

There are a few things to keep in mind when using rgb() to define a color. First, the red, green, and blue values have to be integers between 0 and 255. No decimals. Second, the rgb() definition is a string of text. So, we add pieces of text together to get what we need.

Press "Run" to change the color of the rectangle.

Quick Reference:

Message Log

Challenge 3

Update the drawBuildingRow() and drawBuilding() functions to draw darker buildings as the scale gets smaller.

Inside the drawBuildingRow() function, use Math.round(153 * scale) to calculate the amount of red, green, and blue in the building's color. (For the building's color, the amount of red, green, and blue are all the same.) This will make the color darker as the scale gets smaller.

Create a text string from the red, green, and blue values. Store the text string in a variable called buildingColor. Store the text string 'rgb(102, 102, 102)' and store it in a variable called windowColor. The window color for all three rows is the same.

Pass the variables buildingColor and windowColor into the drawBuilding() function. Make sure you update the drawBuilding() function definition to include buildingColor and windowColor as parameters.

Inside the drawBuilding() function, set the context.fillStyle to buildingColor when drawing the building and roof, and to windowColor when drawing the windows.

Then, draw a row of buildings at 0.6 scale where the first building is sitting on the ground at (0, 280). The buildings should be smaller and darker than the row of buildings in Challenge 1.

The row of buildings should cover the length of the red line on the canvas and look similar to the image above, especially the building color. Obviously, the buildings will be random. If your drawBuildingRow() function seems to be working, mark the challenge as complete by selecting "Yes, it looks good".

Quick Reference:

Previous Challenge: View your code from Stage 4 Challenge 2 to use on this challenge.

Code Missing: You have not yet entered any code in to the previous challenge: Stage 4 Challenge 2
Stage 4 Challenge 2
Message Log
Canvas (your drawing will display here)

A Solution: Here's the code I wrote to complete this challenge. View One Possible Solution

var canvas = document.getElementById('flappy_square_stage4_challenge3'); var context = canvas.getContext('2d'); function drawBuilding(leftX, groundY, units, floors, windowType, roofType) { var windowColor = 'rgb(102, 102, 102)'; context.save(); var width = (units * 16) + (4 * 2); var height = (floors * 16) + (4 * 2); context.translate(leftX, groundY - height); context.fillRect(0, 0, width, height); context.save(); context.translate(4, 4); context.fillStyle = windowColor; for (var j=0; j < floors; ++j) { context.save(); for (var i=0; i < units; ++i) { drawWindow(windowType); context.translate(16, 0); } context.restore(); context.translate(0, 16); } context.restore(); drawRoof(width, roofType); context.restore(); } function drawWindow(windowType) { switch(windowType) { case 0: context.fillRect(4, 2, 8, 10); break; case 1: context.fillRect(2, 3, 5, 8); context.fillRect(9, 3, 5, 8); break; case 2: context.fillRect(0, 3, 16, 8); break; case 3: context.fillRect(5, 1, 6, 14); break; } } function drawRoof(w, roofType) { context.save(); switch(roofType) { case 1: context.translate((16 / 2), -16); context.fillRect(0, 0, w - 16, 16); break; case 2: context.translate((16 / 2), -24); context.fillRect(0, 0, w - 16, 24); context.translate(((w - 16) / 2) - (32 / 2), -24); context.fillRect(0, 0, 32, 24); context.translate((32 / 2) - (8 / 2), -32); context.fillRect(0, 0, 8, 32); break; case 3: context.translate((w - 64) / 2, -16); context.fillRect(0, 0, 64, 16); context.translate((64 - 32) / 2, 0); context.beginPath(); context.moveTo(0, 0); context.lineTo(16, -64); context.lineTo(32, 0); context.closePath(); context.fill(); break; } context.restore(); } function randomInteger(min, max) { return min + Math.floor(Math.random() * (max - min)); } function drawBuildingRow(scale) { context.save(); context.scale(scale, scale); var color = Math.round(153 * scale); var buildingColor = 'rgb(' + color + ',' + color + ',' + color + ')'; context.fillStyle = buildingColor; var x = 0; var end = (800 / scale); while (x < end) { var units = randomInteger(4, 10); var floors = randomInteger(5, 18); var windowType = randomInteger(0, 4); var roofType = randomInteger(0, 4); drawBuilding(x, (280 / scale), units, floors, windowType, roofType); x += (units * 16) + (4 * 2) + 12; } context.restore(); } drawBuildingRow(0.6);

Lesson: Draw Three Rows of Buildings and a Horizon

We are almost done. The last step is to assemble your final drawing.

In this example, we draw a cake for celebrating! Press "Run" to find your favorite cake.

Quick Reference:

Message Log

Challenge 4

In the drawBuildingRow() function, change the while loop to run as long as x < canvas.width. This will ensure each row of buildings will cover the entire width of the canvas.

Draw a gray rectangle (color #CCCCCC) at (0, 220) with a width equal to canvas.width and a height of 100. This rectangle is the ground.

Draw a row of buildings with a scale of 0.6 at (0, 280). This is the back row of buildings.

Draw a row of buildings with a scale of 0.8 at (0, 300). This is the middle row of buildings.

Draw a row of buildings with a scale of 1.0 at (0, 320). This is the front row of buildings.

Press "Run" multiple times to make sure you are drawing a random cityscape with three rows buildings. Each row should cover the width of the canvas, and as the rows get farther away, the buildings should get smaller and darker. Once you feel satisfied with your drawings, mark the challenge as complete by selecting "Yes, it looks good".

Quick Reference:

Previous Challenge: View your code from Stage 4 Challenge 3 to use on this challenge.

Code Missing: You have not yet entered any code in to the previous challenge: Stage 4 Challenge 3
Stage 4 Challenge 3
Message Log
Canvas (your drawing will display here)

A Solution: Here's the code I wrote to complete this challenge. View One Possible Solution

var canvas = document.getElementById('flappy_square_stage4_challenge4'); var context = canvas.getContext('2d'); function drawBuilding(leftX, groundY, units, floors, windowType, roofType) { var windowColor = 'rgb(102, 102, 102)'; context.save(); var width = (units * 16) + (4 * 2); var height = (floors * 16) + (4 * 2); context.translate(leftX, groundY - height); context.fillRect(0, 0, width, height); context.save(); context.translate(4, 4); context.fillStyle = windowColor; for (var j=0; j < floors; ++j) { context.save(); for (var i=0; i < units; ++i) { drawWindow(windowType); context.translate(16, 0); } context.restore(); context.translate(0, 16); } context.restore(); drawRoof(width, roofType); context.restore(); } function drawWindow(windowType) { switch(windowType) { case 0: context.fillRect(4, 2, 8, 10); break; case 1: context.fillRect(2, 3, 5, 8); context.fillRect(9, 3, 5, 8); break; case 2: context.fillRect(0, 3, 16, 8); break; case 3: context.fillRect(5, 1, 6, 14); break; } } function drawRoof(w, roofType) { context.save(); switch(roofType) { case 1: context.translate((16 / 2), -16); context.fillRect(0, 0, w - 16, 16); break; case 2: context.translate((16 / 2), -24); context.fillRect(0, 0, w - 16, 24); context.translate(((w - 16) / 2) - (32 / 2), -24); context.fillRect(0, 0, 32, 24); context.translate((32 / 2) - (8 / 2), -32); context.fillRect(0, 0, 8, 32); break; case 3: context.translate((w - 64) / 2, -16); context.fillRect(0, 0, 64, 16); context.translate((64 - 32) / 2, 0); context.beginPath(); context.moveTo(0, 0); context.lineTo(16, -64); context.lineTo(32, 0); context.closePath(); context.fill(); break; } context.restore(); } function randomInteger(min, max) { return min + Math.floor(Math.random() * (max - min)); } function drawBuildingRow(scale, ground) { context.save(); context.scale(scale, scale); var color = Math.round(153 * scale); var buildingColor = 'rgb(' + color + ',' + color + ',' + color + ')'; context.fillStyle = buildingColor; var x = 0; var end = (canvas.width / scale); while (x < end) { var units = randomInteger(4, 10); var floors = randomInteger(5, 18); var windowType = randomInteger(0, 4); var roofType = randomInteger(0, 4); drawBuilding(x, (ground / scale), units, floors, windowType, roofType); x += (units * 16) + (4 * 2) + 12; } context.restore(); } context.save(); context.fillStyle = '#CCCCCC'; context.fillRect(0, canvas.height - 100, canvas.width, 100); context.restore(); drawBuildingRow(0.6, 280); drawBuildingRow(0.8, 300); drawBuildingRow(1, 320);