Stage 2

Overview

Overviewvisual1

In the second stage of the Animated Cityscape Challenge, we draw the buildings in front of the sky. Because we have to redraw the buildings every time the sky is redrawn, we store the data needed to redraw each building in an object and each row of buildings in an array.

Store Data in Objects

In this example, we draw three blue squares on top of a yellow background.

We start by declaring four global variables: counter, squareA, squareB, and squareC. We use counter to keep track of the number of times we have updated the scene and the three square variables to store the data needed to redraw the squares after each update.

var counter;
var squareA, squareB, squareC;

When the page first loads, the program calls the initScene() function and adds an event listener to the canvas. The event listener automatically calls the updateScene() function whenever a mouse click is detected on the canvas.

initScene();
canvas.addEventListener('click', updateScene);

Inside the initScene() function, we initialize the four variables and then call the drawScene() function.

function initScene() {
  counter = 0;
  squareA = {x: 200, y:  40, s: 60};
  squareB = {x:  80, y: 120, s: 40};
  squareC = {x: 240, y: 220, s: 80};
  
  drawScene();
}

The variable squareA is initialized as an object with three properties: x, y, and s. An object is simply a collection of properties, where each property has a name (or key) and a value. In this case, the x property in object squareA is assigned the value 200.

Inside the drawScene() function, we draw the yellow background (covering the previous scene) with the frame number and then draw the three squares. We draw each square by passing the object storing its data into the drawSquare() function as a parameter.

function drawScene() {
  drawBackground();
  drawSquare(squareA);
  drawSquare(squareB);
  drawSquare(squareC);
}

Inside the drawSquare() function, the object passed into the function is stored in the parameter square. We access the values stored in the object's properties using dot notation. For example, to access the value stored in the object's x property, we use square.x.

function drawSquare(square) {
  context.save();
  context.fillStyle = 'RoyalBlue';
  context.fillRect(square.x, square.y, square.s, square.s); // Use the data in the object square to draw the square
  context.restore();
}

The updateScene() function is called automatically whenever a mouse click is detected on the canvas. Inside the updateScene() function, we increase the value stored in the counter by 1, decrease the values stored in the squareA.x and squareC.y properties by 5, and increase the value stored in the squareB.x property by 5 before redrawing the scene. Because we are drawing the yellow background over the previous scene, the squares look like they are moving.

function updateScene() {
  counter += 1;
  squareA.x -= 5; // This is a shorter way of writing squareA.x = squareA.x - 5
  squareB.x += 5;
  squareC.y -= 5;
  drawScene();
}

Click on the canvas to see the scene update. Change the values stored in the objects to change the size and position of the squares. To learn more about objects, visit the Objects lesson.

Quick Reference: Coordinates Variables Functions Objects fillRect() fillStyle

Editor (write code below)
var canvas = document.getElementById('animated_cityscape_stage2_example1'); var context = canvas.getContext('2d'); var counter; var squareA, squareB, squareC; initScene(); canvas.addEventListener('click', updateScene); function initScene() { counter = 0; squareA = {x: 200, y: 40, s: 60}; squareB = {x: 80, y: 120, s: 40}; squareC = {x: 240, y: 220, s: 80}; drawScene(); } function drawScene() { drawBackground(); drawSquare(squareA); drawSquare(squareB); drawSquare(squareC); } function updateScene() { counter += 1; squareA.x -= 5; // This is a shorter way of writing squareA.x = squareA.x - 5 squareB.x += 5; squareC.y -= 5; drawScene(); } function drawSquare(square) { context.save(); context.fillStyle = 'RoyalBlue'; context.fillRect(square.x, square.y, square.s, square.s); // Use the data in the object square to draw the square context.restore(); } function drawBackground() { context.save(); context.fillStyle = 'Cornsilk'; context.fillRect(0, 0, canvas.width, canvas.height); // Draw a rectangle over the entire canvas context.fillStyle = '#CCCCCC'; context.font = '48px Arial'; context.fillText('Frame: ' + counter, 10, 48); // Print the number of the frame on the background context.restore(); }
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

Challenge1visual1
What your drawing should look like

Even though we aren't moving the buildings in the Animated Cityscape Challenge, we still need to redraw them every time we redraw the sky because the new sky will cover over everything previously drawn on the canvas. And to redraw the buildings, we need to store the data used to draw each building.

Inside the initScene() function, initialize the three variables, buildingA, buildingB, and buildingC, as objects with the property names and values listed below. Then, draw the scene.

buildingA:
  • leftX: 6,
  • groundY: 300,
  • w: 88,
  • h: 200,
  • units: 5,
  • floors: 12,
  • windowType: 3,
  • roofType: 2
buildingB:
  • leftX: 106,
  • groundY: 300,
  • w: 168,
  • h: 136,
  • units: 10,
  • floors: 8,
  • windowType: 2,
  • roofType: 1
buildingC:
  • leftX: 286,
  • groundY: 300,
  • w: 72,
  • h: 248,
  • units: 4,
  • floors: 15,
  • windowType: 0,
  • roofType: 0

Note how we are declaring buildingA, buildingB, and buildingC as global variables. This is important because, if we declared the three variables inside the initScene() function, they would no longer exist once the function ends. By declaring them globally, they persist and can be accessed from within other functions.

Inside the drawScene() function, declare and initialize the variables buildingColor and windowColor with the colors 'rgb(153, 153, 153)' and 'rgb(102, 102, 102)', respectively. Then, draw the three buildings by passing the object that contains each building's data into the drawBuilding() function along with buildingColor and windowColor.

Finally, change the drawBuilding() function's definition so it now accepts three parameters instead of ten. Instead of passing values for the building's left x-coordinate, ground y-coordinate, width, height, number of office units, number of floors, window type, and roof type into the function separately, we are going to pass all of those values as properties stored in a single object, b.

function drawBuilding(b, buildingColor, windowColor) {
  
  // code block
  
}

Then, go through the drawBuilding() function and replace any references to the deleted parameters with references to the building object's properties. For example, the building's left x-coordinate is now stored in the b.leftX property, not in the leftX parameter, which no longer exists.

Quick Reference: Coordinates Variables Functions For Loops Objects fillRect() fillStyle

Editor (write code below)
var canvas = document.getElementById('animated_cityscape_stage2_challenge1'); var context = canvas.getContext('2d'); var buildingA, buildingB, buildingC; initScene(); function initScene() { // Initialize the three building variables // Draw the scene } function drawScene() { // Declare and intialize the buildingColor and windowColor variables // Draw the three buildings by passing the data for each building in the drawBuilding() function } function drawBuilding(leftX, groundY, w, h, units, floors, windowType, roofType, buildingColor, windowColor) { var x = leftX; var y = groundY - h; context.save(); context.translate(x, y); context.fillStyle = buildingColor; context.fillRect(0, 0, w, h); drawRoof(w, roofType); context.translate(4, 4); context.fillStyle = windowColor; for (var i = 0; i < floors; i += 1) { context.save(); for (var j = 0; j < units; j += 1) { drawWindow(windowType); context.translate(16, 0); } context.restore(); context.translate(0, 16); } context.restore(); } function drawWindow(windowType) { context.save(); 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; } context.restore(); } function drawRoof(w, roofType) { context.save(); switch(roofType) { case 0: // draw nothing break; case 1: context.fillRect(8, -16, w - 16, 16); break; case 2: context.fillRect(8, -24, w - 16, 24); context.fillRect((w - 32) / 2, -48, 32, 24); context.fillRect((w - 8) / 2, -80, 8, 56); break; case 3: context.beginPath(); context.moveTo(w / 2, -80); context.lineTo(w / 2 + 16, -16); context.lineTo(w / 2 - 16, -16); context.closePath(); context.fill(); context.fillRect((w - 64) / 2, -16, 64, 16); break; } context.restore(); }
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

Store Objects in an Array

When storing the data used to draw three squares, it's okay to declare three separate variables named squareA, squareB, and squareC, one for each square. But it becomes a problem if we need to store the data to draw a hundred squares or if we don't know how many buildings we'll be drawing ahead of time, as in the Animated Cityscape Challenge.

One way to store an arbitrary number of values in a variable is by using an array. An array is basically a list with an index starting at 0. Imagine a list of books stored in an array named bookArray. We can access the first book in the list at bookArray[0] and the second book at bookArray[1]. To add a new book at the end of the list, we use the array's push() method, and to find the number of books in the list, we access the array's length property.

In this example, we update the program from the previous example to store each square's data in an array.

Instead of declaring the three separate variables squareA, squareB, and squareC, we start by declaring the global variable squareArray:

var counter;
var squareArray;

Inside the initScene() method, we initialize squareArray as an empty array, []. We have to initialize squareArray as an array before we can use the push() method to add data to the end of the array.

function initScene() {
  counter = 0;
  
  squareArray = []; // Initialize the variable as an empty array
  squareArray.push( {x: 200, y:  40, s: 60} ); // Add the data for the first square to the end of the array
  squareArray.push( {x:  80, y: 120, s: 40} ); // Add the data for the second square to the end of the array
  squareArray.push( {x: 240, y: 220, s: 80} ); // Add the data for the third square to the end of the array
  
  drawScene();
}

At this point, we have pushed the data for three squares onto the end of the array, and we can access the data for the first square at squareArray[0], the data for the second square at squareArray[1], and the data for the third square at squareArray[2]. By using an array, we can easily store the data for a hundred squares in a single variable.

When working with objects and arrays, it's important to understand where values are stored and how to access them. Keep this reference in mind when working with the data stored in squareArray:

squareArray // Used to access the array containing the data for all the squares
squareArray[0] // Used to access the data for the first square in the array
squareArray[0].x // Used to access the x-coordinate in the data for the first square in the array

Inside the drawScene() function, we could draw each square by passing its data into the drawSquare() method manually like this:

function drawScene() {
  drawBackground();
  
  drawSquare(squareArray[0]);
  drawSquare(squareArray[1]);
  drawSquare(squareArray[2]);
}

But we are going to automate the process using a for loop. This way, if we add the data for a fourth square to the array, the drawScene() function will automatically draw it, too.

function drawScene() {
  drawBackground();
  
  for (var i = 0; i < squareArray.length; i += 1) {
    drawSquare(squareArray[i]);
  }
}

Note that the for loop runs as long as i < squareArray.length. This is because, while the length of the array is 3, the last square's data is at index 2. Remember, the index of an array starts at 0, so the last item in an array is always at index length - 1.

Finally, we update the updateScene() function to use squareArray instead of squareA, squareB, and squareC.

function updateScene() {
  counter += 1;
  squareArray[0].x -= 5;
  squareArray[1].x += 5;
  squareArray[2].y -= 5;
  drawScene();
}

Click on the canvas to see the scene update. Change the values stored in the objects to change the size and position of the squares and push another set of data into the array to add another square. To learn more about arrays, visit the Arrays lesson.

Quick Reference: Coordinates Variables Functions For Loops Objects Arrays fillRect() fillStyle

Editor (write code below)
var canvas = document.getElementById('animated_cityscape_stage2_example2'); var context = canvas.getContext('2d'); var counter; var squareArray; initScene(); canvas.addEventListener('click', updateScene); function initScene() { counter = 0; squareArray = []; // Initialize the variable as an empty array squareArray.push( {x: 200, y: 40, s: 60} ); // Add the data for the first square to the end of the array squareArray.push( {x: 80, y: 120, s: 40} ); // Add the data for the second square to the end of the array squareArray.push( {x: 240, y: 220, s: 80} ); // Add the data for the third square to the end of the array drawScene(); } function drawScene() { drawBackground(); for (var i = 0; i < squareArray.length; i += 1) { drawSquare(squareArray[i]); } } function updateScene() { counter += 1; squareArray[0].x -= 5; squareArray[1].x += 5; squareArray[2].y -= 5; drawScene(); } function drawSquare(square) { context.save(); context.fillStyle = 'RoyalBlue'; context.fillRect(square.x, square.y, square.s, square.s); // Use the data in the object square to draw the square context.restore(); } function drawBackground() { context.save(); context.fillStyle = 'Cornsilk'; context.fillRect(0, 0, canvas.width, canvas.height); // Draw a rectangle over the entire canvas context.fillStyle = '#CCCCCC'; context.font = '48px Arial'; context.fillText('Frame: ' + counter, 10, 48); // Print the number of the frame on the background context.restore(); }
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

Challenge1visual1
What your drawing should look like

Update the program from Challenge 1 to store the data for each building in an array instead of in separate variables. Start by copying your program from Challenge 1 into the editor below.

Instead of declaring the variables buildingA, buildingB, and buildingC, declare only one global variable: buildingRow.

Inside the initScene() function, initialize buildingRow with an empty array. Then, push the data for the three buildings onto the end of the array using the array's push() method.

Inside the drawScene() function, use a for loop to pass the data for each building into the drawBuilding() function along with the building and window colors. Remember, the data for the first building is stored in the object at buildingRow[0], the data for the second building in the object at buildingRow[1], and the data for the third building in the object at buildingRow[2].

Quick Reference: Coordinates Variables Functions For Loops Objects Arrays fillRect() fillStyle

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

Code Missing: You have not yet entered any code in to the previous challenge: Stage 1 Challenge 1
Stage 1 Challenge 1
Editor (write code below)
var canvas = document.getElementById('animated_cityscape_stage2_challenge2'); var context = canvas.getContext('2d'); // COPY YOUR PROGRAM FROM CHALLENGE 1 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

Initialize and Draw Building Rows Separately

In the Basic Cityscape Challenge, we created the data for a building row and then drew the building row at the same time using the drawBuildingRow() function. This meant that, if we wanted to draw the building row again, we would recreate the data. But since the data is generated randomly, the buildings would be different each time and it would look like we were drawing a brand new scene instead of updating the existing scene.

We are currently in the process of separating those two steps. The goal is to create the data for a building row using the initBuildingRow() function when the page first loads and then use that data to redraw the buildings using the drawBuildingRow() function when we update the scene. That way, it will look like the same scene changing over time.

Since we are separating the current drawBuildingRow() function into two separate functions, you should review how the drawBuildingRow() function works. In this example, we position a building row at (0, 280) and draw it with a scale factor of 0.6. We use the scale factor to calculate the color of the buildings in the row and a while loop to create and draw just enough buildings to cover the width of the canvas. Each building is randomly generated using the randomInteger() function.

Quick Reference: Coordinates Variables Functions While Loops If Statements fillStyle translate() scale() random() round() / floor() / ceil()

Editor (write code below)
var canvas = document.getElementById('animated_cityscape_stage2_example3'); var context = canvas.getContext('2d'); drawBuildingRow(0, 280, 0.6); function drawBuildingRow(rowX, groundY, scale) { context.save(); context.translate(rowX, groundY); context.scale(scale, scale); var c = Math.round(153 * scale); var buildingColor = 'rgb(' + c + ', ' + c + ', ' + c + ')'; var windowColor = 'rgb(102, 102, 102)'; var x = 0; while (x < canvas.width / scale) { var units = randomInteger(4, 10); var floors = randomInteger(6, 20); var windowType = randomInteger(0, 3); var roofType; if (units > 8) { roofType = randomInteger(0, 1); } else if (units > 6) { roofType = randomInteger(0, 2); } else { roofType = randomInteger(0, 3); } var w = 16 * units + 8; var h = 16 * floors + 8; drawBuilding(x, 0, w, h, units, floors, windowType, roofType, buildingColor, windowColor); x = x + w + 12; } context.restore(); } function drawBuilding(leftX, groundY, w, h, units, floors, windowType, roofType, buildingColor, windowColor) { var x = leftX; var y = groundY - h; context.save(); context.translate(x, y); context.fillStyle = buildingColor; context.fillRect(0, 0, w, h); drawRoof(w, roofType); context.translate(4, 4); context.fillStyle = windowColor; for (var i = 0; i < floors; i += 1) { context.save(); for (var j = 0; j < units; j += 1) { drawWindow(windowType); context.translate(16, 0); } context.restore(); context.translate(0, 16); } context.restore(); } function drawWindow(windowType) { context.save(); 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; } context.restore(); } function drawRoof(w, roofType) { context.save(); switch(roofType) { case 0: // draw nothing break; case 1: context.fillRect(8, -16, w - 16, 16); break; case 2: context.fillRect(8, -24, w - 16, 24); context.fillRect((w - 32) / 2, -48, 32, 24); context.fillRect((w - 8) / 2, -80, 8, 56); break; case 3: context.beginPath(); context.moveTo(w / 2, -80); context.lineTo(w / 2 + 16, -16); context.lineTo(w / 2 - 16, -16); context.closePath(); context.fill(); context.fillRect((w - 64) / 2, -16, 64, 16); break; } context.restore(); } function randomInteger(min, max) { return (min + Math.floor((max - min + 1) * Math.random())); }
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

Challenge3visual1
What your drawing should look like

Update the program from Challenge 2 to draw a building row at a set position and scale. Set the building color based on the scale of the row. Start by copying your program from Challenge 2 into the editor below.

Since we are going to position the row of buildings in the drawBuildingRow() function, change the groundY property for all three buildings in the row to 0.

Define a function named drawBuildingRow() which accepts three parameters: rowX, groundY, and scale. Use the context.translate() method to position the row and the context.scale() method to scale the row. Don't forget to save and restore the drawing state. Then, use the scale to calculate the building color. Look at the program in the example above to see how.

Finally, move the for loop used to draw the buildings from the drawScene() function into the drawBuildingRow() function and call the drawBuildingRow() function inside the drawScene() function. Draw the row of buildings stored in the buildingRow array at (0, 280) with a scale factor of 0.6.

Quick Reference: Coordinates Variables Functions For Loops Objects Arrays fillStyle save() / restore() translate() scale()

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

Code Missing: You have not yet entered any code in to the previous challenge: Stage 1 Challenge 2
Stage 1 Challenge 2
Editor (write code below)
var canvas = document.getElementById('animated_cityscape_stage2_challenge3'); var context = canvas.getContext('2d'); // COPY YOUR PROGRAM FROM CHALLENGE 2 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

Challenge 4

Challenge4visual1
What your drawing should look like

To turn the sky along the horizon red as the sun sets, we need to add a third color stop between positions 0 and 1. The third color stop will confine the red color to a narrow band.

Start by setting horizonY, the y-coordinate of the horizon, and copying the definitions for the rgbColor() and drawSky() functions from Challenge 3.

Inside the drawSky() function, declare the variables pMiddle, rMiddle, gMiddle, and bMiddle for the middle color stop. We are using pMiddle to set the position of the color stop. By starting at position 1 and moving down to position 0, it will confine the red color to a narrower and narrower band until it disappears completely.

If time < 5 or time > 7, set pMiddle to -1.

Why are we setting pMiddle to -1? Before 5:00 pm and after 7:00 pm, we only need to add color stops at positions 0 and 1; we aren't adding a third color stop at position pMiddle. By setting pMiddle to -1, we are letting ourselves know not to add the third color stop later on.

In the code block for the else clause, change the calculations for the color at position 0 and add the calculations for the color at position pMiddle. The calculations for the color at position 1 stay the same.

  • r0: 255
  • g0: 255 → 0
  • b0: 255 → 0
  • pMiddle: 1 → 0
  • rMiddle: 102
  • gMiddle: 153 → 102
  • bMiddle: 255 → 102

This changes the color of the sky at position 0 from white to red and the color of the sky at position 1 from blue to dark gray. We use the middle color stop at position pMiddle to make the red band at the horizon narrower. The color of the sky at the middle color stop changes from blue to gray, but it also moves closer to the horizon as pMiddle changes from 1 to 0.

After creating the linear gradient and adding color stops at positions 0 and 1, add the middle color stop only if pMiddle >= 0. If pMiddle is -1, the program will skip this step and the middle color stop won't be added.

if (pMiddle >= 0) {
  gradient.addColorStop(pMiddle, rgbColor(rMiddle, gMiddle, bMiddle));
}

The program should now draw slices of the sky as it gets darker and redder along the horizon at 5:00, 5:30, 6:00, 6:30, and 7:00 pm. If you need help with if statements and linear gradients, visit the If Statements and createLinearGradient() lessons.

Quick Reference: Coordinates Variables Functions If Statements fillRect() fillStyle save() / restore() translate() createLinearGradient()

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

Code Missing: You have not yet entered any code in to the previous challenge: Stage 1 Challenge 3
Stage 1 Challenge 3
Editor (write code below)
var canvas = document.getElementById('animated_cityscape_stage2_challenge4'); var context = canvas.getContext('2d'); // COPY YOUR PROGRAM FROM CHALLENGE 3 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)

Update the Time and Redraw the Scene with a Mouse Click

At this point, we can draw the sky and ground at any time of day by manually setting the time variable. The next step is writing a program which updates the time variable and redraws the scene automatically.

A JavaScript program is triggered by events. So far, we've done all of our drawing immediately when the page first loads. But we can also tell our program to listen for events and run specific functions when those events occur.

In this example, we run the following code when the page loads:

var canvas = document.getElementById('animated_cityscape_stage2_example4');
var context = canvas.getContext('2d');

var squareX, squareY;

initScene();
canvas.addEventListener('click', updateScene);

We start by storing a reference to the canvas and the canvas's context in the variables canvas and context. Then, we declare the global variables squareX and squareY. We need to declare these variables globally so all of our functions can use them and the values stored in them are persistent and aren't deleted once a function ends. To learn more about variables and scope, visit the Variables lesson. Finally, we call the initScene() function and use the addEventListener() method to register an event listener on the canvas object. Note: "init" is short for initialize.

The initScene() function sets the initial values of squareX and squareY and calls the drawScene() function:

function initScene() {
  squareX = 10;
  squareY = 10;
  
  drawScene();
}

The drawScene() function draws a square filled with the color 'RoyalBlue' at the coordinates (squareX, squareY):

function drawScene() {
  context.save();
  context.fillStyle = 'RoyalBlue';
  context.fillRect(squareX, squareY, 20, 20);
  context.restore();
}

By registering to listen for 'click' events, the program will automatically call the updateScene() function whenever a mouse click is detected on the canvas. Note: Because we are passing the updateScene() function into the addEventListener() method as a variable, we don't include parentheses after the function name. To learn more about passing functions as variables and registering event listeners, visit the Functions and Event Listeners lessons.

The updateScene() method adds 30 to squareX and 20 to squareY before calling the drawScene() function and drawing another square:

function updateScene() {
  squareX += 30; // This is a shorter way of writing squareX = squareX + 30
  squareY += 20; // This is a shorter way of writing squareY = squareY + 20
  
  drawScene();
}

Now we can automatically update the drawing on the canvas simply by clicking on it. Note that the new scene is drawn on top of the existing scene. If we wanted to redraw the square and make it look like the square is moving, we'd have to clear the canvas first.

Quick Reference: Coordinates Variables Functions Event Listeners fillRect() fillStyle

Editor (write code below)
var canvas = document.getElementById('animated_cityscape_stage2_example4'); var context = canvas.getContext('2d'); var squareX, squareY; initScene(); canvas.addEventListener('click', updateScene); function initScene() { squareX = 10; squareY = 10; drawScene(); } function drawScene() { context.save(); context.fillStyle = 'RoyalBlue'; context.fillRect(squareX, squareY, 20, 20); context.restore(); } function updateScene() { squareX += 30; // This is a shorter way of writing squareX = squareX + 30 squareY += 20; // This is a shorter way of writing squareY = squareY + 20 drawScene(); }
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 5

Challenge5visual1
What your drawing should look like at 6:12 pm

Write a program that increases the time and redraws the scene when a mouse click is detected on the canvas.

Start by copying the definitions for the drawSky(), drawGround(), and rgbColor() functions from Challenges 2 and 4.

Inside the initScene() function, set time to 5, set horizonY so the horizon is 100 pixels above the bottom of the canvas, and then call the drawScene() function.

Inside the drawScene() function, draw the sky and ground. Then, call the drawTime() function to automatically draw the time in the bottom left corner of the canvas.

Inside the updateScene() function, increase the time by 0.2 if time < 7, else reset the time back to 5. Then, call the drawScene() function to draw a new scene on top of the current one.

Press "Run" and click on the canvas enough times to see the sky and ground change color between 5:00 and 7:00 pm. Once you feel satisfied that the scene is updating and drawing correctly, mark the challenge as complete by selecting "Yes, it looks good".

Quick Reference: Coordinates Variables Functions Event Listeners

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

Code Missing: You have not yet entered any code in to the previous challenge: Stage 1 Challenge 4
Stage 1 Challenge 4
Editor (write code below)
var canvas = document.getElementById('animated_cityscape_stage2_challenge5'); var context = canvas.getContext('2d'); var time; var horizonY; initScene(); canvas.addEventListener('click', updateScene); function initScene() { // Set the time to 5 // Set horizonY so the horizon is 100 pixels above the bottom of the canvas // Draw the scene } function drawScene() { // Draw the sky // Draw the ground drawTime(); // Draw the time in the bottom left corner } function updateScene() { // If time < 7, increase the time by 0.2, else reset the time back to 5 // Draw the scene } function drawSky() { // Copy the function definition from Challenge 4 } function drawGround() { // Copy the function definition from Challenge 2 } function rgbColor(r, g, b) { // Copy the function definition from Challenge 4 } function drawTime() { var t; var h = Math.floor(time); var m = Math.round(60 * (time - h)); if (m >= 10) { t = h + ':' + m; } else { t = h + ':0' + m; } context.save(); context.fillStyle = 'White'; context.font = '16px Arial'; context.fillText(t, 10, canvas.height - 10); context.restore(); }
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)

Update the Time and Redraw the Scene with a Timer

Instead of using a mouse click to update the time and redraw the scene, we are going to use a timer.

In this example, we set up a timer to update the scene from the previous example. We start by declaring the global variable timer. Then, instead of adding an event listener to the canvas object to detect mouse clicks, we use the setInterval() method to create a timer that will automatically call the updateScene() function every 25 milliseconds. We store a reference to the timer in the variable timer.

var timer;

timer = setInterval(updateScene, 25); // Call the updateScene() function every 25 milliseconds

Again, because we are passing the updateScene() function into the setInterval() method as a variable, we don't include parentheses at the end of the function name. The number 25 tells the timer how long to wait before calling the updateScene() function again. The setInterval() method measures time in milliseconds and there are 1000 milliseconds in one second. So, if the timer calls the updateScene() function every 25 milliseconds, it will update the scene 40 times per second (1000 ÷ 25 = 40).

Inside the updateScene() function, we increase squareX and squareY by 3 and 2, instead of 30 and 20, so the square doesn't move as far in each update. Then, if squareX > canvas.width, which means the latest square is positioned past the right edge of the canvas, we cancel the timer and stop updating the scene by passing the reference stored in the timer variable into the clearInterval() method.

function updateScene() {
  squareX += 3; // This is a shorter way of writing squareX = squareX + 3
  squareY += 2; // This is a shorter way of writing squareY = squareY + 2
  
  drawScene();
  
  if (squareX > canvas.width) {
    clearInterval(timer); // Cancel the timer and stop updating the scene
  }
}

Finally, inside the drawScene() function, before drawing the square, we draw a rectangle filled with the color 'Cornsilk' covering the entire canvas. This will cover the previous scene, so now it looks like one square is moving instead of a new square being drawn each time.

function drawScene() {
  context.save();
  context.fillStyle = 'Cornsilk';
  context.fillRect(0, 0, canvas.width, canvas.height); // Draw a rectangle over the entire canvas
  context.fillStyle = 'RoyalBlue';
  context.fillRect(squareX, squareY, 20, 20);
  context.restore();
}

Press "Run" to reload the page and start the animation.

See what happens if you don't draw a rectangle over the previous scene or if you change how far the square moves or the time interval between updates. Note: if you try to update the scene too quickly, the program won't be able to keep up and the animation may stutter. To learn more setting timers, visit the setInterval() lesson.

Quick Reference: Coordinates Variables Functions fillRect() fillStyle setInterval()

Editor (write code below)
var canvas = document.getElementById('animated_cityscape_stage2_example5'); var context = canvas.getContext('2d'); var timer; var squareX, squareY; initScene(); timer = setInterval(updateScene, 25); function initScene() { squareX = 10; squareY = 10; drawScene(); } function drawScene() { context.save(); context.fillStyle = 'Cornsilk'; context.fillRect(0, 0, canvas.width, canvas.height); // Draw a rectangle over the entire canvas context.fillStyle = 'RoyalBlue'; context.fillRect(squareX, squareY, 20, 20); context.restore(); } function updateScene() { squareX += 3; // This is a shorter way of writing squareX = squareX + 3 squareY += 2; // This is a shorter way of writing squareY = squareY + 2 drawScene(); if (squareX > canvas.width) { clearInterval(timer); // Cancel the timer and stop updating the scene } }
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 6

Copy the program from Challenge 5.

Then, change the program so that, instead of listening for mouse clicks to update the scene, a timer automatically calls the updateScene() function every 50 milliseconds.

Inside the updateScene() function, increase the time by 0.05 and, instead of resetting the time back to 5, cancel the timer and stop updating the scene if time > 7.

Press "Run" and watch the sky and ground change color between 5:00 and 7:00 pm. If you need to slow the animation down to get a better look, increase the time interval passed into the setInterval() method. Once you feel satisfied that the scene is updating and drawing correctly, mark the challenge as complete by selecting "Yes, it looks good".

Quick Reference: Variables Functions setInterval()

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

Code Missing: You have not yet entered any code in to the previous challenge: Stage 1 Challenge 5
Stage 1 Challenge 5
Editor (write code below)
var canvas = document.getElementById('animated_cityscape_stage2_challenge6'); var context = canvas.getContext('2d'); var time; var horizonY; initScene(); canvas.addEventListener('click', updateScene); function initScene() { // Copy the function definition from Challenge 5 } function drawScene() { // Copy the function definition from Challenge 5 } function updateScene() { // Copy the function definition from Challenge 5 // Increase the time by 0.05 // If time >= 7, cancel the timer and stop updating the scene } function drawSky() { // Copy the function definition from Challenge 5 } function drawGround() { // Copy the function definition from Challenge 5 } function rgbColor(r, g, b) { // Copy the function definition from Challenge 5 } function drawTime() { var t; var h = Math.floor(time); var m = Math.round(60 * (time - h)); if (m >= 10) { t = h + ':' + m; } else { t = h + ':0' + m; } context.save(); context.fillStyle = 'White'; context.font = '16px Arial'; context.fillText(t, 10, canvas.height - 10); context.restore(); }
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)

Play and Pause the Scene

In this example, we add event listeners to the previous example's program to play and pause the scene.

We can register our program to listen for more than mouse click events. On a web page, only one HTML element can have the focus at a time. The focus determines which element is actively receiving keyboard events. On this page alone there are thirteen editors. It would get kind of crazy if you were typing into more than one editor at a time.

Note: A canvas element does not normally receive the focus. We have enabled each canvas to receive the focus by setting its tabindex attribute. That's not something you need to know about until you start creating the HTML for your own web pages.

Because we have enabled each canvas to receive the focus, clicking on a canvas selects it and triggers a 'focus' event. Clicking on a different part of the page deselects it and triggers a 'blur' event. We start by adding event listeners to play the scene when the canvas receives a 'focus' event and pause the scene when the canvas receives a 'blur' event.

canvas.addEventListener('focus', playScene);
canvas.addEventListener('blur', pauseScene);

Instead of starting the timer when the page first loads, we start the timer inside the playScene() function. We also check the position of the last square before starting the timer. If the last square is off the canvas, we reset it's position.

function playScene() {
  if (squareX > canvas.width) {
    squareX = 10;
    squareY = 10; // Reset the square's position
  }
  
  timer = setInterval(updateScene, 25);
}

Inside the pauseScene() function, we cancel the timer and draw the text string 'Click to Play' in the center of the canvas so the user knows how to start the animation.

function pauseScene() {
  clearInterval(timer);
  
  context.save();
  context.fillStyle = 'Black';
  context.font = '16px Arial';
  context.textAlign = 'center';
  context.fillText('Click to Play', canvas.width / 2, canvas.height / 2);
  context.restore();
}

We also call the pauseScene() function when the page first loads so the 'Click to Play' text string is drawn right away.

Then, inside the updateScene() function, instead of canceling the timer if squareX > canvas.width, we use the blur() method to blur the canvas. When the canvas loses the focus, it receives a blur event and the pauseScene() function is called.

function updateScene() {
  squareX += 3; // This is a shorter way of writing squareX = squareX + 3
  squareY += 2; // This is a shorter way of writing squareY = squareY + 2
  
  drawScene();
  
  if (squareX > canvas.width) {
    canvas.blur(); // Remove the focus from the canvas
  }
}

Click on the canvas to start the animation. The animation will continue running until you click on a different part of the page or until the latest square is positioned past the right edge of the canvas. If you restart the animation and the latest square was positioned past the right edge of the canvas, squareX and squareY are reset and the animation starts again from the beginning.

Quick Reference: Coordinates Variables Functions Event Listeners fillRect() fillStyle setInterval()

Editor (write code below)
var canvas = document.getElementById('animated_cityscape_stage2_example6'); var context = canvas.getContext('2d'); var timer; var squareX, squareY; initScene(); pauseScene(); canvas.addEventListener('focus', playScene); canvas.addEventListener('blur', pauseScene); function initScene() { squareX = 10; squareY = 10; drawScene(); } function drawScene() { context.save(); context.fillStyle = 'Cornsilk'; context.fillRect(0, 0, canvas.width, canvas.height); // Draw a rectangle over the entire canvas context.fillStyle = 'RoyalBlue'; context.fillRect(squareX, squareY, 20, 20); context.restore(); } function updateScene() { squareX += 3; // This is a shorter way of writing squareX = squareX + 3 squareY += 2; // This is a shorter way of writing squareY = squareY + 2 drawScene(); if (squareX > canvas.width) { canvas.blur(); // Remove the focus from the canvas } } function playScene() { if (squareX > canvas.width) { squareX = 10; squareY = 10; // Reset the square's position } timer = setInterval(updateScene, 25); } function pauseScene() { clearInterval(timer); context.save(); context.fillStyle = 'Black'; context.font = '16px Arial'; context.textAlign = 'center'; context.fillText('Click to Play', canvas.width / 2, canvas.height / 2); context.restore(); }
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 7

Copy the program from Challenge 6.

Then, change the program so the scene is initially paused and the time set to 2. Play the scene when the canvas receives the focus and continue playing the scene until the canvas loses the focus or until time > 12. If the canvas receives the focus and the time is already greater than 12, reset the time back to 2 and replay the scene from the beginning.

Press "Run" and click on the canvas to start playing the scene. Once you feel satisfied that the scene is playing and pausing correctly, mark the challenge as complete by selecting "Yes, it looks good".

Quick Reference: Variables Functions Event Listeners setInterval()

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

Code Missing: You have not yet entered any code in to the previous challenge: Stage 1 Challenge 6
Stage 1 Challenge 6
Editor (write code below)
var canvas = document.getElementById('animated_cityscape_stage2_challenge7'); var context = canvas.getContext('2d'); // Copy the program from Challenge 6 here function playScene() { } function pauseScene() { // Cancel the timer context.save(); context.fillStyle = 'rgba(0, 0, 0, 0.2)'; context.fillRect(canvas.width / 2 - 55, canvas.height / 2 - 18, 110, 24); context.fillStyle = 'White'; context.font = '16px Arial'; context.textAlign = 'center'; context.fillText('Click to Play', canvas.width / 2, canvas.height / 2); context.restore(); }
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)

Ready for the next lesson?

Next up, the "Stage: 3" lesson >