HTML5 Canvas drawing shapes 2

Using HTML5 Canvas part 2

Drawing HTML5 Canvas shapes using repetition statements

So far, we have looked at most of the programming constructs that are used for nearly any type of programming. The programming constructs we have used so far are:

  • Input

  • Output

  • Selection Statements

  • Instructions that perform calculations on input data or somehow modify input data

Now, let’s add a fifth programming construct, Repetition Statements. These include while loops and for loops. Both while loops and for loops are called repetition statements because they allow a block of instructions to be repeated. The main difference between them, is that a while loop repeats that block for a variable number of times, and a for loop repeats that block for a set amount of times. You can actually use a while loop instead of a for loop, and vice versa. But, generally you use a while loop for code that can run a different amount of times each the the containing program is run. And, you generally use a for loop for code that repeats a set amount of time, each time the containing program is run.

Using loops

One common reason to use a while loop is to help gather user input. This is often done when you want the user to be able to control the amount of times the while loop is repeated. However, when you are using programs that use a GUI (Graphical User Interface), like a web page, the main program itself represents a while loop. This is essentially like a while loop that runs, until you close the program. So, although we will use while loops in our lessons, it will be to perform tasks like removing HTML elements from a container element until no child elements exist.

So, we will make use of a while loop, and then also make use of a for loop. We can start where we left off from the previous lesson: HTML5 Canvas part 1.

Starting where we left off

Not using CodeSandbox

If you are not on CodeSandbox, you can go to your ~/Documents folder and use the starter template for vite:

$ cd ~/Documents
$ npx degit takebayashiv-cmd/vite-template-my-project html5_canvas_2
$ cd html5_canvas_2
$ npm install

This will not have the correct starting files, but you can replace index.html and index.mjs with the ones we left off with last time. Here are those files:

index.html
<!DOCTYPE html>
<html>
  <head>
    <title>HTML5 Canvas shapes part 1</title>
    <meta charset="UTF-8" />
    <script src="./index.mjs" type="module"></script>
  </head>
  <body>
    <canvas id="mycanvas" width="300" height="80"></canvas>
    <br />
    Choose shape:
    <select id="shape_select">
      <option>rectangle</option>
      <option>circle</option>
      <option>ellipse</option>
      <option>diamond</option>
    </select>
    <br>
    Choose color:
    <select id="color_select">
      <option>red</option>
      <option>blue</option>
      <option>green</option>
      <option>orange</option>
      <option>#ccffcc</option>
    </select>
    <br /><br />
    <button id="ok_button">Ok</button>
  </body>
</html>
index.mjs
import { Rectangle, Circle, Ellipse, Diamond } from "./shapes.mjs";

if (document.readyState === "loading") {
  document.addEventListener("DOMContentLoaded", init);
} else {
  init();
}

var mycanvas;
var ctx;

function handleOk() {
  const shape_select = document.getElementById("shape_select");
  const shape = shape_select.value;
  const color_select = document.getElementById("color_select");
  const color = color_select.value;
  ctx.clearRect(0, 0, mycanvas.width, mycanvas.height);
  if (shape === "rectangle") {
    const rect1 = new Rectangle(20, 30, 40, 20, color);
    rect1.draw(ctx);
  } else if (shape === "circle") {
    const circle1 = new Circle(40, 40, 20, color);
    circle1.draw(ctx);
  } else if (shape === "ellipse") {
    const ellipse1 = new Ellipse(40, 40, 20, 10, 0, 0, 2 * Math.PI, color);
    ellipse1.draw(ctx);
  } else if (shape === "diamond") {
    const diamond1 = new Diamond(20, 40, 40, 20, 60, 40, 40, 60, color);
    diamond1.draw(ctx);
  }
}

function init() {
  mycanvas = document.getElementById("mycanvas");
  ctx = mycanvas.getContext("2d");
  const ok_button = document.getElementById("ok_button");
  ok_button.addEventListener("click", handleOk);
}

Start up Visual Studio Code (VS Code) and create a new file in that html5_canvas_2 folder called shapes.mjs. Here are the contents for that file from the last lesson:

shapes.mjs
export class Rectangle {
  constructor(x, y, width, height, color) {
    this.x = x;
    this.y = y;
    this.width = width;
    this.height = height;
    this.color = color;
  }

  draw(ctx) {
    ctx.fillStyle = this.color;
    ctx.fillRect(this.x, this.y, this.width, this.height);
  }
}

export class Circle {
  constructor(x, y, radius, color) {
    this.x = x;
    this.y = y;
    this.radius = radius;
    this.color = color;
  }

  draw(ctx) {
    ctx.beginPath();
    ctx.arc(this.x, this.y, this.radius, 0, 2 * Math.PI);
    ctx.closePath();
    ctx.fillStyle = this.color;
    ctx.fill();
  }
}

export class Ellipse {
  constructor(x, y, radiusX, radiusY, rotAngle, startAngle, endAngle, color) {
    this.x = x;
    this.y = y;
    this.radiusX = radiusX;
    this.radiusY = radiusY;
    this.rotAngle = rotAngle;
    this.startAngle = startAngle;
    this.endAngle = endAngle;
    this.color = color;
  }

  draw(ctx) {
    ctx.beginPath();
    ctx.ellipse(this.x, this.y, this.radiusX, this.radiusY, this.rotAngle, this.startAngle, this.endAngle);
    ctx.closePath();
    ctx.fillStyle = this.color;
    ctx.fill();
  }
}

export class Diamond {
  constructor(leftX, leftY, topX, topY, rightX, rightY, bottomX, bottomY, color) {
    this.leftX = leftX;
    this.leftY = leftY;
    this.topX = topX;
    this.topY = topY;
    this.rightX = rightX;
    this.rightY = rightY;
    this.bottomX = bottomX;
    this.bottomY = bottomY;
    this.color = color;
  }

  draw(ctx) {
    ctx.beginPath();
    ctx.moveTo(this.leftX, this.leftY);
    ctx.lineTo(this.topX, this.topY);
    ctx.lineTo(this.rightX, this.rightY);
    ctx.lineTo(this.bottomX, this.bottomY);
    ctx.closePath();
    ctx.fillStyle = this.color;
    ctx.fill();
  }
}

You can also delete the file myclasses.js, as we won’t need that file. Go back to the terminal and start up the vite server:

$ cd ~/Documents/html5_canvas_2
$ npm run dev

After this is running, you can open a browser to http://localhost:5173, to see the app running. When you make changes to any of the source scripts, the browser will automatically update and show the changes.

Using CodeSandbox

If you are using CodeSandbox, connect to your account. In the Recent area, look for your project for the previous lesson. I named my project html5_canvas_shapes_1. Click on the ellipsis for Sandbox Actions:

sandbox actions

When, you click on Sandbox Actions, select Fork.

fork project

This will create a fork and open up this project.

newly forked

Note the name of the project. We want to change this, so click on the square icon on the top left and select Dashboard. Locate the fork of your project and click on the Sandbox Actions ellipsis. Select Rename.

rename fork

Just backspace to start editing. I renamed my project to html5_canvas_shapes_2.

renamed project

Click on this project to open it. When you do so, you will that only index.html is visible in the editor area. To open up more than two files at once, you can use the mouse to drag the filename, next to an opened tab. When you see the opened tab’s edges darken, let go with the mouse and the new file will be open for editing. Do this for both index.mjs and shapes.mjs. The next animated gif file illustrates this.

test3

For any animated gif file, I will include a button labelled Reload gif below the image. You can click on this to rerun the animation.

Changing the code

For this lesson, we want to make it so that the user of the web page can enter a number of shapes and have the app keep track of the shapes that were entered. Right now, the app will allow the user to enter as many shapes as she/he wants. But, let’s add a feature that keeps track of those shapes in the order that they were entered. In JavaScript, a common way to store more than one instance of some kind of data, is to use an array.

JavaScript arrays

An array is used to store more than one of the same kind of data. Arrays are used in just about all programming languages, and JavaScript is no exception. Let’s learn a little about JavaScript arrays by using the Console. To open up the Console in the CodeSandbox sandbox, you need to right-click on the Preview area and choose Inspect. If you right-click on an image, you will just see a short menu with Inspect on the bottom. If you right-click on some area of the preview that does not have an image, the menu will be longer, but Inspect will still be on the bottom of that menu. Here is a screen shot showing the menu when you right-click on an image.

short menu inspect

On the other hand, if you right-click somewhere else in the Preview window, you will see a longer list. Inspect will still be on the bottom of that list.

long menu inspect

Here is a screen shot showing the creation of an empty array called numbers. You need to select Console, and clear away any initial messages.

console numbers array

Note that the line after initializing numbers to be an empty list shows undefined. That is because that initializing instruction, does not return anything. The Console will display the returned value (if any) on the next line. If the instruction carried out does not return anything, undefined will be displayed.

The following screen shot shows the same thing in the browser displaying the vite server output. Note that hitting the F12 key, toggles the DevTools console on and off. Also, note that the Console was cleared before entering in the instruction to create the numbers array.

devtools console numbers array

Let’s add a number to the numbers array. This requires the use of the push() method. Here is a screen shot showing adding the number 5 to numbers and then displaying the numbers array after this is done.

numbers push 5

Note the [] brackets symbols denoting an array. An empty [] denotes an empty array. In this case, just entering the name of that variable (numbers) does return that array. So, no undefined message. We can add another number to the array, and display it after that. Here is a screen shot showing this:

pushed second number

Note the (2) in front of the array shows that the array contains 2 elements. Also, note that using the push() method places the element at the end of the array. So, if you use push() repeatedly to add to an array, the elements will show up in the same order as they were pushed on to the array. Also you can see that each time you call the push() instruction, the number of elements in the array is returned.

Arrays and for loops

The most common way to manipulate an array, is to use a for loop. This is true in any programming language. Let’s look at some simple uses of a for loop to print out the elements of a JavaScript array. We can use the Console to do this. Here is a screen shot showing the Console that shows some things that we can do:

names array console

We first define an array called names that is initialized to hold the names that are shown. If we want to access an individual array element, we need to use the index notation. That is, you start with the name of the array, followed by square brackets ([]), with an index inside the brackets. Like most programming languages, JavaScript is zero-index based. So, that is why names[1] refers to the second element. The first element would be referred to by names[0].

Now, let’s look at the for loop. This is what is called an index-based for loop, as it makes use of the indices of the array to process the array. As you can see, the result is that all the names stored in the array are printed out. There is a simpler variant of the for loop that is often called the for-each style. The for-each style has a couple of variants in JavaScript. The following shows those variants:

for each variants

The arrow style function is a shorthand way of writing an anonymous function. That is, a function that has no name. So the following are basically equivalent:

names.forEach(function(name) {
  console.log(name);
});

// is roughly equivalent to

names.forEach((name) => { console.log(name); })

There are some scoping rules in terms of variable visibility and the this object that are different. But, those differences are not important for this example.

The for(let .. of ..) style is also a for-each style of for loop, as it processes all the objects stored in the array. Any of these variants can be used when you want to do the same thing to all elements stored in the array. These for-each style of for loops are preferred over an index-based for loop if you are going to process all the elements in the array the same way. However, the index-based for loop is easier to use when you want to process the elements in the array differently. The following screen shot shows some examples.

index based examples

In the first example, we can start the loop with i (the index) starting at 1 instead of 0. This skips the first element. In the second example, we use a selection statement that is true if i is an even number. So, only the elements with an even number for the index will be printed out.

Modifying index.mjs

Let’s start by modifying index.mjs so that each time a shape is selected, we store that shape inside an array called shapes_array. For this first step, we will just display the contents of shapes_array to the Console.

index.mjs
import { Rectangle, Circle, Ellipse, Diamond } from "./shapes.mjs";

if (document.readyState === "loading") {
  document.addEventListener("DOMContentLoaded", init);
} else {
  init();
}

var mycanvas;
var ctx;
var shapes_array = [];

function handleOk() {
  const shape_select = document.getElementById("shape_select");
  const shape = shape_select.value;
  const color_select = document.getElementById("color_select");
  const color = color_select.value;
  ctx.clearRect(0, 0, mycanvas.width, mycanvas.height);
  let shape_obj;
  if (shape === "rectangle") {
    shape_obj = new Rectangle(20, 30, 40, 20, color);
    shape_obj.draw(ctx);
  } else if (shape === "circle") {
    shape_obj = new Circle(40, 40, 20, color);
    shape_obj.draw(ctx);
  } else if (shape === "ellipse") {
    shape_obj = new Ellipse(40, 40, 20, 10, 0, 0, 2 * Math.PI, color);
    shape_obj.draw(ctx);
  } else if (shape === "diamond") {
    shape_obj = new Diamond(20, 40, 40, 20, 60, 40, 40, 60, color);
    shape_obj.draw(ctx);
  }
  shapes_array.push(shape_obj);
  console.log(shapes_array);
}

function init() {
  mycanvas = document.getElementById("mycanvas");
  ctx = mycanvas.getContext("2d");
  const ok_button = document.getElementById("ok_button");
  ok_button.addEventListener("click", handleOk);
}

The new lines are lines 11, 19, 21-22, 24-25, 27-28, 30-31 and 33-34. Line 11 adds the variable, shapes_array, to store the shapes in. Line 19 defines the variable, shape_obj, to store the instances of the various shape classes within. So, lines 21-22, 24-25, 27-28 and 30-31 have substituted in shape_obj for the names for each of the shape classes. This allows storing any shape object inside the shapes_array as is done using the push() method on line 33. Line 34 prints that array out each time a new shape object is stored.

The following screen shot shows the Console after three shape objects have been created and stored. You can see by looking at the contents of shapes_array, that the first shape object created was a red Rectangle, the second shape object was a blue Circle, and the third shape object created was a green Ellipse. Note that I had to expand the third shape object to display the color.

three shapes stored

Outlining the next steps

Now that we can store the shape objects in an array that keeps a kind of history of the shape objects created, let’s go back and think of the programming constructs that we want to use to complete our app.

  • Gathering user input. This part is already taken care of from the previous lesson using the <select> elements.

  • Manipulating the data. Since we wanted to keep a record of the shape objects in terms of which shape was created and in which order, we needed to manipulate the data. Previously, we just created a shape and then displayed it on the screen. But, to keep this record, we need to store the shape objects in an array. To do that, we needed to use the same variable, shape_obj, to hold the shape object regardless of the type and color of the object. That allowed storing any shape object in the shapes_array array.

  • Output. We already temporarily display the shape object on the screen. But, if we want to display something that shows the history of all the shape objects entered, we need something more. One way to display this history, is to display the shape objects in an unordered list <ul>. Another way, is to display the shapes on the canvas in a chain showing the order. We will work on the unordered list first.

  • Repetition statements. As you might have guessed, we can use a for loop to take the shape objects in the array to populate the unordered list. We could also use a for loop to display a chain of the shapes on the canvas. Although it is probably not obvious yet, we need to have a way of clearing out the unordered list each time we add list items to the unordered list. To clear out the unordered list, we will use a while loop.

Adding an unordered list (<ul>)

We can start by modifying index.html to add in the <ul> element. Here is the new version of index.html:

index.html
<!DOCTYPE html>
<html>
  <head>
    <title>HTML5 Canvas shapes part 2</title>
    <meta charset="UTF-8" />
    <script src="./index.mjs" type="module"></script>
  </head>
  <body>
    <canvas id="mycanvas" width="300" height="80"></canvas>
    <br />
    Choose shape:
    <select id="shape_select">
      <option>rectangle</option>
      <option>circle</option>
      <option>ellipse</option>
      <option>diamond</option>
    </select>
    <br />
    Choose color:
    <select id="color_select">
      <option>red</option>
      <option>blue</option>
      <option>green</option>
      <option>orange</option>
      <option>#ccffcc</option>
    </select>
    <br /><br />
    <button id="ok_button">Ok</button>
    <h2>Shapes List</h2>
    <ul id="shapes_list"></ul>
  </body>
</html>

The new lines are line 4 and 30-31. Line 4 just changes the <title> contents to "HTML5 Canvas shapes part 2". Line 30 adds in a <h2> element that places a title above the shapes list. Line 31 adds in an <ul> element with an id=shapes_list attribute. This is where we will write our list of shape objects.

Next, we modify index.mjs to create the list items for the <ul> element by processing the shapes_array array with a for loop. To do that, we need to get a reference to the <ul> element. Here is the new version of index.mjs:

index.mjs
import { Rectangle, Circle, Ellipse, Diamond } from "./shapes.mjs";

if (document.readyState === "loading") {
  document.addEventListener("DOMContentLoaded", init);
} else {
  init();
}

var mycanvas;
var ctx;
var shapes_array = [];
var shapes_list;

function handleOk() {
  const shape_select = document.getElementById("shape_select");
  const shape = shape_select.value;
  const color_select = document.getElementById("color_select");
  const color = color_select.value;
  ctx.clearRect(0, 0, mycanvas.width, mycanvas.height);
  let shape_obj;
  if (shape === "rectangle") {
    shape_obj = new Rectangle(20, 30, 40, 20, color);
    shape_obj.draw(ctx);
  } else if (shape === "circle") {
    shape_obj = new Circle(40, 40, 20, color);
    shape_obj.draw(ctx);
  } else if (shape === "ellipse") {
    shape_obj = new Ellipse(40, 40, 20, 10, 0, 0, 2 * Math.PI, color);
    shape_obj.draw(ctx);
  } else if (shape === "diamond") {
    shape_obj = new Diamond(20, 40, 40, 20, 60, 40, 40, 60, color);
    shape_obj.draw(ctx);
  }
  shapes_array.push(shape_obj);
  console.log(shapes_array);
  createList();
}

function createList() {
  for (let shape of shapes_array) {
    let li = document.createElement('li');
    const contents = document.createTextNode(`${shape.color} ${shape.constructor.name}`);
    li.appendChild(contents);
    shapes_list.appendChild(li);
  }
}

function init() {
  mycanvas = document.getElementById("mycanvas");
  ctx = mycanvas.getContext("2d");
  const ok_button = document.getElementById("ok_button");
  ok_button.addEventListener("click", handleOk);
  shapes_list = document.getElementById("shapes_list");
}

The new lines are 12, 36, 39-46 and 53. Line 12 creates the variable shapes_list that will be used to store a reference the the <ul id="shapes_list"> element. Line 53, gets that reference.

Lines 39-46 define the createList() function. Lines 40-45 define a for loop of the for-each style, that will create a list item (<li>) element on line 41. Then, on line 42 the contents for the <li> element are created. This is worth taking a closer look. Here is the right side of the statement on line 42

document.createTextNode(`${shape.color} ${shape.constructor.name}`)

This will create a text node, which is used for the contents of the <li> element. The back ticks (`) enclose a string that uses variable substitution. Within the back ticks, using ${} around a variable name will cause the value of that variable to be substituted into the string. So, using `${shape.color} ${shape.constructor.name} will result in a string that starts with color of the shape object followed by a space, and then ends with the name of the class. So, for the Rectangle class, the constructor.name will be Rectangle. This means that if the shape object is a green diamond, the string that is created on line 42 would be "green Diamond".

Line 43 will place the contents string inside the <li> element. That is what appendChild() is used for; to place an element or contents inside another element. Finally, on line 44, the <li> element is appended to the <ul>.

Finally, on line 36, the createList() function is called to process the shapes_array and append to the <ul> element.

When we run the program, you will see that the <ul> element does not work as we want. That is because each time createList() is called, it appends <li> elements to the existing <ul> element. Here is a screen shot showing what happened when a red Rectangle was added, then a blue Circle added and finally a green Ellipse was added.

list has repeats

So, what we need to do is remove all the elements from within the <ul> element before we use the for loop to append the <li> elements. Here is a version of index.mjs that does that.

index.mjs
import { Rectangle, Circle, Ellipse, Diamond } from "./shapes.mjs";

if (document.readyState === "loading") {
  document.addEventListener("DOMContentLoaded", init);
} else {
  init();
}

var mycanvas;
var ctx;
var shapes_array = [];
var shapes_list;

function handleOk() {
  const shape_select = document.getElementById("shape_select");
  const shape = shape_select.value;
  const color_select = document.getElementById("color_select");
  const color = color_select.value;
  ctx.clearRect(0, 0, mycanvas.width, mycanvas.height);
  let shape_obj;
  if (shape === "rectangle") {
    shape_obj = new Rectangle(20, 30, 40, 20, color);
    shape_obj.draw(ctx);
  } else if (shape === "circle") {
    shape_obj = new Circle(40, 40, 20, color);
    shape_obj.draw(ctx);
  } else if (shape === "ellipse") {
    shape_obj = new Ellipse(40, 40, 20, 10, 0, 0, 2 * Math.PI, color);
    shape_obj.draw(ctx);
  } else if (shape === "diamond") {
    shape_obj = new Diamond(20, 40, 40, 20, 60, 40, 40, 60, color);
    shape_obj.draw(ctx);
  }
  shapes_array.push(shape_obj);
  console.log(shapes_array);
  createList();
}

function removeChildren(elem) {
  while (elem.childNodes.length > 0) {
    elem.removeChild(elem.childNodes[0]);
  }
}

function createList() {
  removeChildren(shapes_list);
  for (let shape of shapes_array) {
    let li = document.createElement('li');
    const contents = document.createTextNode(`${shape.color} ${shape.constructor.name}`);
    li.appendChild(contents);
    shapes_list.appendChild(li);
  }
}

function init() {
  mycanvas = document.getElementById("mycanvas");
  ctx = mycanvas.getContext("2d");
  const ok_button = document.getElementById("ok_button");
  ok_button.addEventListener("click", handleOk);
  shapes_list = document.getElementById("shapes_list");
}

The new lines are 39-43 and 46. Lines 39-43 define the removeChildren() function. This consists of a while loop that runs as long as elem (the element) has children. If the element has children, then elem.childNodes.length will be greater than zero. On line 41, we use the removeChild() function to remove the first element from the childNodes array. As long as childNodes is not empty, it must have a first child (i.e. a value at index 0). So, by the time the while loop completes, the element that removeChildren() is called on will be empty. This is a good example of using a while loop. We don’t know beforehand how many children a HTML container element may have. This while loop will run as long as is needed to remove all the children. Therefore, this shows how while loops are repetition statements that can run a variable amount of times, depending on what the while loop is operating on.

Line 46 just calls the removeChildren() function to clear out the <ul> element before the for loop appends the <li> elements to the <ul> element. As can be seen in the next screen shot, now the <ul> element works correctly:

list fixed

Displaying a chain of the shapes on the Canvas

We can start by modifying index.html so that it has a button that will draw the chain of shape objects when that button is clicked on. Here is the new version of index.html:

index.html
<!DOCTYPE html>
<html>
  <head>
    <title>HTML5 Canvas shapes part 2</title>
    <meta charset="UTF-8" />
    <script src="./index.mjs" type="module"></script>
  </head>
  <body>
    <canvas id="mycanvas" width="300" height="80"></canvas>
    <br />
    Choose shape:
    <select id="shape_select">
      <option>rectangle</option>
      <option>circle</option>
      <option>ellipse</option>
      <option>diamond</option>
    </select>
    <br />
    Choose color:
    <select id="color_select">
      <option>red</option>
      <option>blue</option>
      <option>green</option>
      <option>orange</option>
      <option>#ccffcc</option>
    </select>
    <br /><br />
    <button id="ok_button">Ok</button>
    <button id="draw_chain_button">Draw chain</button>
    <h2>Shapes List</h2>
    <ul id="shapes_list"></ul>
  </body>
</html>

The new line is line 29. This just adds a <button id="draw_chain_button"> right next to the Ok button. The draw_chain_button will be labelled "Draw chain".

Next, we modify index.mjs. We will need to get a reference to the Draw chain button and make it so that a function called drawChain() is called to draw the chain of shape objects. We will start with just a few changes to make sure that the button works and that we can figure out which shape object to draw. Here is index.mjs:

index.mjs
import { Rectangle, Circle, Ellipse, Diamond } from "./shapes.mjs";

if (document.readyState === "loading") {
  document.addEventListener("DOMContentLoaded", init);
} else {
  init();
}

var mycanvas;
var ctx;
var shapes_array = [];
var shapes_list;

function handleOk() {
  const shape_select = document.getElementById("shape_select");
  const shape = shape_select.value;
  const color_select = document.getElementById("color_select");
  const color = color_select.value;
  ctx.clearRect(0, 0, mycanvas.width, mycanvas.height);
  let shape_obj;
  if (shape === "rectangle") {
    shape_obj = new Rectangle(20, 30, 40, 20, color);
    shape_obj.draw(ctx);
  } else if (shape === "circle") {
    shape_obj = new Circle(40, 40, 20, color);
    shape_obj.draw(ctx);
  } else if (shape === "ellipse") {
    shape_obj = new Ellipse(40, 40, 20, 10, 0, 0, 2 * Math.PI, color);
    shape_obj.draw(ctx);
  } else if (shape === "diamond") {
    shape_obj = new Diamond(20, 40, 40, 20, 60, 40, 40, 60, color);
    shape_obj.draw(ctx);
  }
  shapes_array.push(shape_obj);
  console.log(shapes_array);
  createList();
}

function removeChildren(elem) {
  while (elem.childNodes.length > 0) {
    elem.removeChild(elem.childNodes[0]);
  }
}

function drawChain() {
  for (let shape of shapes_array) {
    const shape_class = shape.constructor.name;
    console.log('shape_class', shape_class);
  }
}

function createList() {
  removeChildren(shapes_list);
  for (let shape of shapes_array) {
    let li = document.createElement('li');
    const contents = document.createTextNode(`${shape.color} ${shape.constructor.name}`);
    li.appendChild(contents);
    shapes_list.appendChild(li);
  }
}

function init() {
  mycanvas = document.getElementById("mycanvas");
  ctx = mycanvas.getContext("2d");
  const ok_button = document.getElementById("ok_button");
  ok_button.addEventListener("click", handleOk);
  shapes_list = document.getElementById("shapes_list");
  const draw_chain_button = document.getElementById("draw_chain_button");
  draw_chain_button.addEventListener('click', drawChain);
}

The new lines are 45-50 and 68-69. Line 68 gets a reference to the Draw chain button. Line 69 makes it so that the drawChain() function is called when the Draw chain button is clicked. Lines 45-50 define the drawChain() function. This consists of a for-each style for loop that iterates over all the shape objects. On line 47 we get the name of the shape class. Line 48 just prints this out, so that we can check to see that we are getting the correct class names.

Here is a screen shot showing the result of entering some shapes and then clicking the Draw chain button.

draw chain1

As you can see, the class of the shape objects is being recognized correctly. Knowing the class is important because as we draw the chain of shapes, we need to know what kind of shape is being drawn to position the shape object correctly. One way to create the chain is to keep track of the x-coordinate of the center. If you look at how each shape object is constructed, each shape object is 40 pixels wide. So, if we make the center of the next shape 40 pixels away from the previous shape, this will make the shapes line up nicely. We need to look back at the shapes.mjs to see how each class is set up. Here is the shapes.mjs file again:

Revisiting shapes.mjs

shapes.mjs
export class Rectangle {
  constructor(x, y, width, height, color) {
    this.x = x;
    this.y = y;
    this.width = width;
    this.height = height;
    this.color = color;
  }

  draw(ctx) {
    ctx.fillStyle = this.color;
    ctx.fillRect(this.x, this.y, this.width, this.height);
  }
}

export class Circle {
  constructor(x, y, radius, color) {
    this.x = x;
    this.y = y;
    this.radius = radius;
    this.color = color;
  }

  draw(ctx) {
    ctx.beginPath();
    ctx.arc(this.x, this.y, this.radius, 0, 2 * Math.PI);
    ctx.closePath();
    ctx.fillStyle = this.color;
    ctx.fill();
  }
}

export class Ellipse {
  constructor(x, y, radiusX, radiusY, rotAngle, startAngle, endAngle, color) {
    this.x = x;
    this.y = y;
    this.radiusX = radiusX;
    this.radiusY = radiusY;
    this.rotAngle = rotAngle;
    this.startAngle = startAngle;
    this.endAngle = endAngle;
    this.color = color;
  }

  draw(ctx) {
    ctx.beginPath();
    ctx.ellipse(this.x, this.y, this.radiusX, this.radiusY, this.rotAngle, this.startAngle, this.endAngle);
    ctx.closePath();
    ctx.fillStyle = this.color;
    ctx.fill();
  }
}

export class Diamond {
  constructor(leftX, leftY, topX, topY, rightX, rightY, bottomX, bottomY, color) {
    this.leftX = leftX;
    this.leftY = leftY;
    this.topX = topX;
    this.topY = topY;
    this.rightX = rightX;
    this.rightY = rightY;
    this.bottomX = bottomX;
    this.bottomY = bottomY;
    this.color = color;
  }

  draw(ctx) {
    ctx.beginPath();
    ctx.moveTo(this.leftX, this.leftY);
    ctx.lineTo(this.topX, this.topY);
    ctx.lineTo(this.rightX, this.rightY);
    ctx.lineTo(this.bottomX, this.bottomY);
    ctx.closePath();
    ctx.fillStyle = this.color;
    ctx.fill();
  }
}

Looking at the constructor for the Rectangle class, we see that the x-coordinate of the left of the rectangle is used to construct a Rectangle. So, this x-coordinate will be at 20 pixels to the left of the center of the rectangle. So, the x value will be specified as (centerX - 20).

If you look at the constructor for the Circle class, the x-coordinate used to construct a Circle is at the center. So, this x-coordinate is already at the center of the circle. So, the x value will be specified as (centerX).

Looking at the constructor for the Ellipse class, the x-coordinate is at the center, just as it is for Circle class. So, this x-coordinate = centerX.

Finally, the Diamond class uses x-coordinates for the left point, the top point, the right point and the bottom point. This means leftX = centerX - 20, topX = centerX, rightX = centerX + 20 and bottomX = centerX.

We make use of these to draw the shapes in a uniform chain. Here is the new version of index.mjs:

index.mjs
import { Rectangle, Circle, Ellipse, Diamond } from "./shapes.mjs";

if (document.readyState === "loading") {
  document.addEventListener("DOMContentLoaded", init);
} else {
  init();
}

var mycanvas;
var ctx;
var shapes_array = [];
var shapes_list;

function handleOk() {
  const shape_select = document.getElementById("shape_select");
  const shape = shape_select.value;
  const color_select = document.getElementById("color_select");
  const color = color_select.value;
  ctx.clearRect(0, 0, mycanvas.width, mycanvas.height);
  let shape_obj;
  if (shape === "rectangle") {
    shape_obj = new Rectangle(20, 30, 40, 20, color);
    shape_obj.draw(ctx);
  } else if (shape === "circle") {
    shape_obj = new Circle(40, 40, 20, color);
    shape_obj.draw(ctx);
  } else if (shape === "ellipse") {
    shape_obj = new Ellipse(40, 40, 20, 10, 0, 0, 2 * Math.PI, color);
    shape_obj.draw(ctx);
  } else if (shape === "diamond") {
    shape_obj = new Diamond(20, 40, 40, 20, 60, 40, 40, 60, color);
    shape_obj.draw(ctx);
  }
  shapes_array.push(shape_obj);
  console.log(shapes_array);
  createList();
}

function removeChildren(elem) {
  while (elem.childNodes.length > 0) {
    elem.removeChild(elem.childNodes[0]);
  }
}

function drawChain() {
  ctx.clearRect(0, 0, mycanvas.width, mycanvas.height);
  let centerX = 40;
  for (let shape of shapes_array) {
    const shape_class = shape.constructor.name;
    if (shape_class === "Rectangle") {
       shape.x = centerX - 20;
    } else if (shape_class === "Circle") {
       shape.x = centerX;
    } else if (shape_class === "Ellipse") {
       shape.x = centerX;
    } else if (shape_class === "Diamond") {
       shape.leftX = centerX - 20;
       shape.topX = centerX;
       shape.rightX = centerX + 20;
       shape.bottomX = centerX;
    }
    shape.draw(ctx);
    centerX = centerX + 40;
  }
}

function createList() {
  removeChildren(shapes_list);
  for (let shape of shapes_array) {
    let li = document.createElement('li');
    const contents = document.createTextNode(`${shape.color} ${shape.constructor.name}`);
    li.appendChild(contents);
    shapes_list.appendChild(li);
  }
}

function init() {
  mycanvas = document.getElementById("mycanvas");
  ctx = mycanvas.getContext("2d");
  const ok_button = document.getElementById("ok_button");
  ok_button.addEventListener("click", handleOk);
  shapes_list = document.getElementById("shapes_list");
  const draw_chain_button = document.getElementById("draw_chain_button");
  draw_chain_button.addEventListener('click', drawChain);
}

The new lines are 46-47, 50-61 and 63. Line 46 clears the canvas before drawing the chain of shapes. Line 47 initializes centerX to be at 40. This means that the first shape in the chain will be centered at 40. Lines 50-61 define an if statement with three else if clauses. Lines 50-52 handle the Rectangle case, where the x-coordinate is 20 pixels to the left of the center of the rectangle. Lines 52-54 handles the Circle case, where the x-coordinate is at the center. Similarly, lines 54-56 do the same thing for the Ellipse case as is done for the Circle case. Lines 56-61 handle the Diamond case. This case has the most lines, because four x-coordinates must be specified. Line 63 increases the value of centerX by 40 before the next shape in the chain is drawn.

The screen shot below shows the chain of shapes drawn on the canvas.

draw chain2

Clearing the list

You can reload the page so that you can draw a new list instead of continuing with the current list. But, let’s add a Clear list button so that you can clear the list of shapes without having to reload the page. Here is the code for index.html

index.html
<!DOCTYPE html>
<html>
  <head>
    <title>HTML5 Canvas shapes part 2</title>
    <meta charset="UTF-8" />
    <script src="./index.mjs" type="module"></script>
  </head>
  <body>
    <canvas id="mycanvas" width="300" height="80"></canvas>
    <br />
    Choose shape:
    <select id="shape_select">
      <option>rectangle</option>
      <option>circle</option>
      <option>ellipse</option>
      <option>diamond</option>
    </select>
    <br />
    Choose color:
    <select id="color_select">
      <option>red</option>
      <option>blue</option>
      <option>green</option>
      <option>orange</option>
      <option>#ccffcc</option>
    </select>
    <br /><br />
    <button id="ok_button">Ok</button>
    <button id="draw_chain_button">Draw chain</button>
    <button id="clear_list_button">Clear list</button>
    <h2>Shapes List</h2>
    <ul id="shapes_list"></ul>
  </body>
</html>

The line is line 30. This just adds in the <button> element for Clear list.

Here is the code for index.mjs:

index.mjs
import { Rectangle, Circle, Ellipse, Diamond } from "./shapes.mjs";

if (document.readyState === "loading") {
  document.addEventListener("DOMContentLoaded", init);
} else {
  init();
}

var mycanvas;
var ctx;
var shapes_array = [];
var shapes_list;

function handleOk() {
  const shape_select = document.getElementById("shape_select");
  const shape = shape_select.value;
  const color_select = document.getElementById("color_select");
  const color = color_select.value;
  ctx.clearRect(0, 0, mycanvas.width, mycanvas.height);
  let shape_obj;
  if (shape === "rectangle") {
    shape_obj = new Rectangle(20, 30, 40, 20, color);
    shape_obj.draw(ctx);
  } else if (shape === "circle") {
    shape_obj = new Circle(40, 40, 20, color);
    shape_obj.draw(ctx);
  } else if (shape === "ellipse") {
    shape_obj = new Ellipse(40, 40, 20, 10, 0, 0, 2 * Math.PI, color);
    shape_obj.draw(ctx);
  } else if (shape === "diamond") {
    shape_obj = new Diamond(20, 40, 40, 20, 60, 40, 40, 60, color);
    shape_obj.draw(ctx);
  }
  shapes_array.push(shape_obj);
  console.log(shapes_array);
  createList();
}

function removeChildren(elem) {
  while (elem.childNodes.length > 0) {
    elem.removeChild(elem.childNodes[0]);
  }
}

function drawChain() {
  ctx.clearRect(0, 0, mycanvas.width, mycanvas.height);
  let centerX = 40;
  for (let shape of shapes_array) {
    const shape_class = shape.constructor.name;
    if (shape_class === "Rectangle") {
       shape.x = centerX - 20;
    } else if (shape_class === "Circle") {
       shape.x = centerX;
    } else if (shape_class === "Ellipse") {
       shape.x = centerX;
    } else if (shape_class === "Diamond") {
       shape.leftX = centerX - 20;
       shape.topX = centerX;
       shape.rightX = centerX + 20;
       shape.bottomX = centerX;
    }
    shape.draw(ctx);
    centerX = centerX + 40;
  }
}

function createList() {
  removeChildren(shapes_list);
  for (let shape of shapes_array) {
    let li = document.createElement('li');
    const contents = document.createTextNode(`${shape.color} ${shape.constructor.name}`);
    li.appendChild(contents);
    shapes_list.appendChild(li);
  }
}

function clearList() {
  shapes_array = [];
  createList();
  ctx.clearRect(0, 0, mycanvas.width, mycanvas.height);
}

function init() {
  mycanvas = document.getElementById("mycanvas");
  ctx = mycanvas.getContext("2d");
  const ok_button = document.getElementById("ok_button");
  ok_button.addEventListener("click", handleOk);
  shapes_list = document.getElementById("shapes_list");
  const draw_chain_button = document.getElementById("draw_chain_button");
  draw_chain_button.addEventListener('click', drawChain);
  const clear_list_button = document.getElementById("clear_list_button");
  clear_list_button.addEventListener('click', clearList);
}

The new lines are 77-81 and 91-92. Line 91 gets a reference to the Clear list button and line 92 makes it so that the clearList() function is called when that button is clicked. Lines 77-81 define the clearList() function. Line 78 empties the shapes_array array. When line 79 calls the createList() function, this will empty out the <ul> element since the shapes_array is empty. Line 80 clears the canvas.

Summary

  • We reused the shape classes created in the last lesson. This is one of the benefits of creating classes, as they can become reusable pieces of code for other projects.

  • This project already had the <select> elements in place for gathering user input. The additional work was mainly to get a "history" of the shape objects created. This made use of an array, as that can keep track of the order the objects. In addition, arrays are readily processed using for loops.

  • To handle removing HTML elements from a container element, we used a while loop. This allows the removal of any number of children elements.

  • To handle the display of the "history" of created shape objects, we used a for loop to place those objects in an unordered list (<ul>).

  • To handle the display of the chain of created shape objects on the canvas, we also used a for loop. Within this for loop, we used a selection statement that had an if with three else if clauses to handle the positioning for the different types of shapes.

  • The for loops we used were both of the for-each style, because we wanted to iterate over all of the shape objects in the array. The particular variant was the for (let .. of ..) style.