Stage 4

Overview

Overviewvisual1

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

 

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.

var x = 0;

while (x < 400) {
  var s = randomInteger(10, 50);
  context.save();
  context.translate(x, 0);
  drawSquare(s);
  context.restore();

  x = x + s;
}

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

Inside the while loop, we generate a random integer between 10 and 50 to use as the side length of the next square. We translate to the x-coordinate stored in the variable x, draw the square, and then restore the coordinate system. Finally, we add the square's side length to x to get the x-coordinate of the next square.

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. To learn more about while loops, visit the While Loops lesson.

Quick Reference: Coordinates Variables While Loops fillRect() random() round() / floor() / ceil()

Editor (write code below)
var canvas = document.getElementById('basic_cityscape_stage4_example1'); var context = canvas.getContext('2d'); context.save(); context.fillStyle = '#FFFFFF'; context.fillRect(0, 0, 400, 100); context.strokeStyle = '#FF1493'; context.translate(0, 100); var x = 0; while (x < 400) { var s = randomInteger(10, 50); context.save(); context.translate(x, 0); drawSquare(s); context.restore(); x = x + s; } context.restore(); function drawSquare(s) { context.save(); context.translate(0, -s); context.strokeRect(0, 0, s, s); context.restore(); } function randomInteger(min, max) { var i = min + Math.floor((max - min + 1) * Math.random()); return i; }
Message Log
This is a lesson, not a challenge, the code runs automatically.

But change it! Play with it! Click "Run" to see your changes.

Run
Run and Focus Canvas
Reset
Canvas (your drawing will display here)

Challenge 1

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

function drawBuildingRow(rowX, groundY) {
  context.save();
  context.translate(rowX, groundY);

  // YOUR CODE FOR DRAWING A ROW OF RANDOM BUILDINGS HERE

  context.restore();
}

Inside the drawBuildingRow() function, set up a while loop to draw the row of buildings. Draw the first building at (0, 0) and keep drawing buildings as long as the x-coordinate of the next building is less than 800.

Inside the while loop, generate random integers for the number of office units per floor, the number of floors, the type of windows, and the type of roof for the building. Draw the building, and then calculate the x-coordinate of the next building so there is a 12-pixel space between buildings.

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, in the drawBuildingRow() function, we need the width of the building stored in the variable w to calculate the x-coordinate of the next building, but we can't access w inside the while loop because it is only defined locally inside the drawBuilding() function.

To fix this, move the width and height calculations out of the drawBuilding() function and put them into the while loop after generating random integers for the number of units per floor and the number of floors. Then, pass those values into the drawBuilding() function as parameters.

The definition for the drawBuilding() function should now look like this:

function drawBuilding(leftX, groundY, w, h, units, floors, windowType, roofType) {

  // YOUR CODE FOR DRAWING A BUILDING HERE

}

And because the width of the building is now stored in the variable w defined inside the while loop, we can use it to calculate the x-coordinate of the next building. Make sure to update the x-coordinate for the next building at the end of the while loop or the loop will never end.

Challenge1visual1

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".

If you press "Run" and your program seems stuck, you may have forgotten to update the x-coordinate for the next building at the end of the while loop. To end the while loop, you will need to close this entire page in your browser and then re-open it again.

To learn more about variable scope, visit the Variables lesson.

Quick Reference: Coordinates Variables Functions While Loops random()

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
Editor (write code below)
var canvas = document.getElementById('basic_cityscape_stage4_challenge1'); var context = canvas.getContext('2d'); function drawBuilding(leftX, groundY, w, h, units, floors, windowType, roofType) { // YOUR CODE FOR DRAWING A BUILDING HERE } function drawWindow(windowType) { // YOUR CODE FOR DRAWING THE FOUR DIFFERENT TYPES OF WINDOWS HERE } function drawRoof(w, roofType) { // YOUR CODE FOR DRAWING THE FOUR DIFFERENT TYPES OF ROOFS HERE } function randomInteger(min, max) { // YOUR CODE FOR GENERATING A RANDOM INTEGER BETWEEN MIN AND MAX, INCLUDING MIN AND MAX, HERE } function drawBuildingRow(rowX, groundY) { context.save(); context.translate(rowX, groundY); // YOUR CODE FOR DRAWING A ROW OF RANDOM BUILDINGS HERE context.restore(); } // DRAW A ROW OF RANDOM BUILDINGS SITTING ON THE GROUND STARTING AT (0, 320) HERE
Message Log
This is a lesson, not a challenge, the code runs automatically.

But change it! Play with it! Click "Run" to see your changes.

Run
Run and Focus Canvas
Reset
Canvas (your drawing will display here)
Challenge1
 

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() method.

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

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

Try context.scale(1.5, 1.5) to 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 the context.save() and context.restore() methods.

To learn more about scaling the coordinate system, visit the scale() lesson.

Quick Reference: Coordinates Variables For Loops Switch Statements save() / restore() translate() random() scale()

Editor (write code below)
var canvas = document.getElementById('basic_cityscape_stage4_example2'); var context = canvas.getContext('2d'); context.save(); context.translate(20, 20); context.scale(0.6, 0.6); // Changes the scale of the coordinate system for (var i = 0; i < 4; i = i + 1) { var flagType = randomInteger(0, 2); drawFlag(flagType); context.translate(100, 30); } context.restore(); function drawFlag(country) { context.save(); switch (country) { case 0: context.fillStyle = '#0055A4'; context.fillRect(0, 0, 30, 60); context.fillStyle = '#FFFFFF'; context.fillRect(30, 0, 30, 60); context.fillStyle = '#EF4135'; context.fillRect(60, 0, 30, 60); break; case 1: context.fillStyle = '#FCD116'; context.fillRect(0, 0, 90, 30); context.fillStyle = '#003893'; context.fillRect(0, 30, 90, 15); context.fillStyle = '#CE1126'; context.fillRect(0, 45, 90, 15); break; case 2: context.fillStyle = '#ED1C24'; context.fillRect(0, 0, 90, 60); context.fillStyle = '#FFFFFF'; context.fillRect(0, 10, 90, 40); context.fillStyle = '#241D4F'; context.fillRect(0, 20, 90, 20); break; } context.restore(); } function randomInteger(min, max) { var i = min + Math.floor((max - min + 1) * Math.random()); return i; }
Message Log
This is a lesson, not a challenge, the code runs automatically.

But change it! Play with it! Click "Run" to see your changes.

Run
Run and Focus Canvas
Reset
Canvas (your drawing will display here)

Challenge 2

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

In the definition of the drawBuildingRow() function, add a new parameter named scale:

function drawBuildingRow(rowX, groundY, scale) {
  context.save();
  context.translate(rowX, groundY);

  // YOUR CODE FOR DRAWING A ROW OF RANDOM BUILDINGS HERE

  context.restore();
}

Then, inside the drawBuildingRow() function, after saving the drawing state and translating the coordinate system to the start of the row, change the scale of the coordinate system in both the x- and y-directions to the value stored in the scale parameter using the context.scale() method. We want to scale the coordinate system after translating it because the coordinates (rowX, groundY) are at normal 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 800 pixels long on the canvas. While the row of buildings is 800 pixels long in the coordinate system, it's only 480 pixels long on the canvas 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 using x < 800, we should keep drawing buildings as long as x < 800 / scale. At 0.6 scale, that means drawing buildings as long as x < 1333.

Challenge2visual1

Draw a row of buildings at 0.6 scale where the first building is sitting on the ground at (0, 280) and the row 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".

If you press "Run" and your program seems stuck, you may have forgotten to update the x-coordinate for the next building at the end of the while loop. To end the while loop, you will need to close this entire page in your browser and then re-open it again.

Quick Reference: Coordinates Variables Functions While Loops save() / restore() translate() scale()

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
Editor (write code below)
var canvas = document.getElementById('basic_cityscape_stage4_challenge2'); var context = canvas.getContext('2d'); function drawBuilding(leftX, groundY, w, h, units, floors, windowType, roofType) { // YOUR CODE FOR DRAWING A BUILDING HERE } function drawWindow(windowType) { // YOUR CODE FOR DRAWING THE FOUR DIFFERENT TYPES OF WINDOWS HERE } function drawRoof(w, roofType) { // YOUR CODE FOR DRAWING THE FOUR DIFFERENT TYPES OF ROOFS HERE } function randomInteger(min, max) { // YOUR CODE FOR GENERATING A RANDOM INTEGER BETWEEN MIN AND MAX, INCLUDING MIN AND MAX, HERE } function drawBuildingRow(rowX, groundY) { // YOUR CODE FOR DRAWING A ROW OF RANDOM BUILDINGS HERE } // DRAW A ROW OF RANDOM BUILDINGS AT 0.6 SCALE SITTING ON THE GROUND STARTING AT (0, 280) HERE
Message Log
This is a lesson, not a challenge, the code runs automatically.

But change it! Play with it! Click "Run" to see your changes.

Run
Run and Focus Canvas
Reset
Canvas (your drawing will display here)
Challenge2
 

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 the context.fillStyle property. So far, we have used the color '#999999' to draw our buildings. Another way to describe the color '#999999' is 'rgb(153, 153, 153)'. The number 153 in base 10 is actually 99 in base 16.

When using RGB values 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 the maximum value is 255. For example, the color black is 'rgb(0, 0, 0)', which is no red, no green, and no blue; and the color 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 values to define a color. First, the red, green, and blue values have to be integers between 0 and 255. No decimals. Second, the 'rgb()' format is a string of text. It may look like a function call, but it isn't. To build a string of text from the three random integers stored in variables r, g, and b, we use the + operator to combine bits of text:

var color = 'rgb(' + r + ', ' + g + ', ' + b + ')'; // Combine the red, green, and blue in a text string

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

To learn more about describing fill colors and loose-typing of variables, visit the fillStyle and Variables lessons.

Quick Reference: Variables fillRect() fillStyle random() round() / floor() / ceil()

Editor (write code below)
var canvas = document.getElementById('basic_cityscape_stage4_example3'); var context = canvas.getContext('2d'); var r = randomInteger(0, 255) // Choose a random amount of red var g = randomInteger(0, 255) // Choose a random amount of green var b = randomInteger(0, 255) // Choose a random amount of blue var color = 'rgb(' + r + ', ' + g + ', ' + b + ')'; // Combine the red, green, and blue in a text string context.save(); context.fillStyle = color; // Use the color as the context's fillStyle context.fillRect(40, 40, 360, 240); drawText(color, 220, 300); // Print the text string stored in the variable color context.restore(); function drawText(text, x, y) { context.save(); context.fillStyle = 'rgb(0, 0, 0)'; context.font = '16px sans-serif'; context.textAlign = 'center'; context.fillText(text, x, y); context.restore(); } function randomInteger(min, max) { var i = min + Math.floor((max - min + 1) * Math.random()); return i; }
Message Log
This is a lesson, not a challenge, the code runs automatically.

But change it! Play with it! Click "Run" to see your changes.

Run
Run and Focus Canvas
Reset
Canvas (your drawing will display here)

Challenge 3

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

Inside the drawBuildingRow() function, use the expression 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 named buildingColor. Store the text string 'rgb(102, 102, 102)' in another variable named windowColor. The window color for all three rows is the same.

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

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

Challenge3visual1

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 both smaller and darker than the buildings in Challenge 1, and the row 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".

If you press "Run" and your program seems stuck, you may have forgotten to update the x-coordinate for the next building at the end of the while loop. To end the while loop, you will need to close this entire page in your browser and then re-open it again.

Quick Reference: Variables Functions While Loops fillStyle save() / restore() translate() scale()

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
Editor (write code below)
var canvas = document.getElementById('basic_cityscape_stage4_challenge3'); var context = canvas.getContext('2d'); function drawBuilding(leftX, groundY, w, h, units, floors, windowType, roofType) { // YOUR CODE FOR DRAWING A BUILDING HERE } function drawWindow(windowType) { // YOUR CODE FOR DRAWING THE FOUR DIFFERENT TYPES OF WINDOWS HERE } function drawRoof(w, roofType) { // YOUR CODE FOR DRAWING THE FOUR DIFFERENT TYPES OF ROOFS HERE } function randomInteger(min, max) { // YOUR CODE FOR GENERATING A RANDOM INTEGER BETWEEN MIN AND MAX, INCLUDING MIN AND MAX, HERE } function drawBuildingRow(rowX, groundY, scale) { // YOUR CODE FOR DRAWING A ROW OF RANDOM BUILDINGS HERE } // DRAW A ROW OF RANDOM BUILDINGS AT 0.6 SCALE SITTING ON THE GROUND STARTING AT (0, 280) HERE
Message Log
This is a lesson, not a challenge, the code runs automatically.

But change it! Play with it! Click "Run" to see your changes.

Run
Run and Focus Canvas
Reset
Canvas (your drawing will display here)
Challenge3
 

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: Functions For Loops Switch Statements fillRect() fillStyle random()

Editor (write code below)
var canvas = document.getElementById('basic_cityscape_stage4_example4'); var context = canvas.getContext('2d'); function randomInteger(min, max) { var i = min + Math.floor((max - min + 1) * Math.random()); return i; } function color(i) { switch (i) { case 0: c = '#90EE90'; break; case 1: c = '#E6E6FA'; break; case 2: c = '#FFFACD'; break; case 3: c = '#FFB6C1'; //c = '#FFE4E1'; break; case 4: c = '#B0E0E6'; break; case 5: c = '#1E90FF'; break; case 6: c = '#9932CC'; break; case 7: c = '#BA55D3'; break; case 8: c = '#FF1493'; break; case 9: c = '#3CB371'; break; } return c; } function drawCandle() { context.save(); context.translate(0, -80); context.fillStyle = '#FFFFFF'; context.fillRect(-5, 30, 10, 50); context.fillStyle = '#FFFF00'; context.fillRect(-6, 0, 12, 30); context.fillStyle = '#FF8C00'; context.fillRect(-5, 10, 10, 20); context.fillStyle = '#FF0000'; context.fillRect(-4, 20, 8, 10); context.restore(); } function drawDecoration(icingColor, accentColor) { context.save(); context.rotate(0.25 * Math.PI); context.fillStyle = accentColor; context.fillRect(-8, -8, 16, 16); context.rotate(0.25 * Math.PI); context.fillStyle = icingColor; context.fillRect(-8, -8, 16, 16); context.rotate(0.25 * Math.PI); context.fillStyle = accentColor; context.fillRect(-3, -3, 6, 6); context.restore(); } function drawTier(cakeColor, icingColor) { context.save(); context.fillStyle = cakeColor; context.translate(-100, 0); context.fillRect(0, 0, 200, 50); context.translate(1, 45); context.fillStyle = icingColor; for (var i = 0; i < 19; i = i + 1) { context.save(); context.rotate(0.25 * Math.PI); context.fillRect(0, 0, 10, 10); context.restore(); context.translate(11, 0); } context.restore(); } function drawCake() { var cakeColor = color(randomInteger(0, 4)); var icingColor = color(randomInteger(5, 9)); context.save(); context.fillStyle = '#000000'; context.fillRect(0, 0, canvas.width, canvas.height); context.translate(200, 300); for (var i = 0; i < 4; i = i + 1) { context.translate(0, -50); drawTier(cakeColor, icingColor); } context.translate(-80, 0); for (var j = 0; j < 5; j = j + 1) { drawCandle(); context.translate(40, 0); } context.restore(); } drawCake();
Message Log
This is a lesson, not a challenge, the code runs automatically.

But change it! Play with it! Click "Run" to see your changes.

Run
Run and Focus Canvas
Reset
Canvas (your drawing will display here)

Challenge 4

In the drawBuildingRow() function, change the while loop to run as long as x < canvas.width. We have stored a reference to the canvas in the canvas variable, and the width of the canvas is stored in the canvas.width property. This will ensure each row of buildings covers the entire width of the canvas.

Challenge4visual1

Draw a gray rectangle (color '#CCCCCC') at (0, 220) with a width equal to canvas.width and a height of canvas.height - 220. The canvas.height property stores the height of the canvas. This will draw a rectangle for the ground from y = 220 down to the bottom of the canvas.

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: Coordinates Variables Functions fillRect() fillStyle scale()

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
Editor (write code below)
var canvas = document.getElementById('basic_cityscape_stage4_challenge4'); var context = canvas.getContext('2d'); function drawBuilding(leftX, groundY, w, h, units, floors, windowType, roofType, buildingColor, windowColor) { // YOUR CODE FOR DRAWING A BUILDING HERE } function drawWindow(windowType) { // YOUR CODE FOR DRAWING THE FOUR DIFFERENT TYPES OF WINDOWS HERE } function drawRoof(w, roofType) { // YOUR CODE FOR DRAWING THE FOUR DIFFERENT TYPES OF ROOFS HERE } function randomInteger(min, max) { // YOUR CODE FOR GENERATING A RANDOM INTEGER BETWEEN MIN AND MAX, INCLUDING MIN AND MAX, HERE } function drawBuildingRow(rowX, groundY, scale) { // YOUR CODE FOR DRAWING A ROW OF RANDOM BUILDINGS HERE } // DRAW A RECTANGLE WITH THE COLOR #CCCCCC FROM (0, 220) TO THE BOTTOM RIGHT CORNER OF THE CANVAS // DRAW A ROW OF RANDOM BUILDINGS AT 0.6 SCALE SITTING ON THE GROUND STARTING AT (0, 280) HERE // DRAW A ROW OF RANDOM BUILDINGS AT 0.8 SCALE SITTING ON THE GROUND STARTING AT (0, 300) HERE // DRAW A ROW OF RANDOM BUILDINGS AT 1.0 SCALE SITTING ON THE GROUND STARTING AT (0, 320) HERE
Message Log
This is a lesson, not a challenge, the code runs automatically.

But change it! Play with it! Click "Run" to see your changes.

Run
Run and Focus Canvas
Reset
Canvas (your drawing will display here)