HTML5 Canvas drawing shapes 1
Using HTML5 Canvas part 1
Drawing shapes using the HTML5 Canvas
With HTML5, a new element called <canvas> became available. This is an area that can be used to draw simple graphics. It is like a paint area, and makes use of a graphics context like other paint applications. This graphics context will provide the ability to draw graphics in the <canvas>.
Starting the lesson in CodeSandbox
I will start by going over how to start this lesson using CodeSandbox. If you are not using CodeSandbox, the next section will explain how to start the lesson outside of CodeSandbox. Start by logging in to your CodeSandbox account. Click on the + Create button on the top right of the window and select the Browser templates. From those templates, select the JavaScript template.
Give the Sandbox an appropriate name like "html5_canvas_shapes_1" and create the Sandbox. The files index.html and index.mjs are created for you, but we will modify the contents of both of those files. Here is the contents for 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="100" height="80"></canvas>
</body>
</html>
Here are the contents for index.mjs:
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", init);
} else {
init();
}
function init() {
console.log("init called");
}
Starting the lesson outside of CodeSandbox using vite
To have a similar experience to developing using CodeSandbox, we will make use of a Node.js package called vite. Here are the steps to get started:
$ cd ~/Documents
$ mkdir html5_canvas_1
$ cd html5_canvas_1
$ npm create vite@latest html5_canvas_shapes_1
$ npm create vite@latest html5_canvas_shapes_1
Need to install the following packages:
create-vite@8.0.1
Ok to proceed? (y) y
> npx
> create-vite html5_canvas_shapes_1
│
◇ Select a framework:
│ Vanilla
│
◇ Select a variant:
│ JavaScript
│
◇ Use rolldown-vite (Experimental)?:
│ No
│
◆ Install with npm and start now?
│ ● Yes / ○ No
│
◇ Installing dependencies with npm...
added 13 packages, and audited 14 packages in 12s
5 packages are looking for funding
run `npm fund` for details
found 0 vulnerabilities
│
◇ Starting dev server...
> html5_canvas_shapes_1@0.0.0 dev
> vite
VITE v7.1.7 ready in 105 ms
➜ Local: http://localhost:5173/
➜ Network: use --host to expose
➜ press h + enter to show help
Stop the vite server by hitting CTRL+C. At this point, this is the directory structure created (I have omitted the node_modules directory):
.
└── html5_canvas_shapes_1
├── index.html
├── package.json
├── package-lock.json
├── public
│ └── vite.svg
└── src
├── counter.js
├── javascript.svg
├── main.js
└── style.css
Change to the html5_canvas_shapes_1 directory. Delete the src directory.
$ cd ~/Documents/html5_canvas_1/html5_canvas_shapes_1
$ rm -r src
Those commands should work for Linux and MacOS. If you are on Windows, you would do this:
> cd ~\Documents\html5_canvas_1\html5_canvas_shapes_1
> rmdir /q /s src
Open Visual Studio Code and open the html5_canvas_shapes_1 folder:
Name the new file index.mjs. Use the same code as in the previous section for index.mjs:
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", init);
} else {
init();
}
function init() {
console.log("init called");
}
Open up index.html and use the same code as in the previous section for this file:
<!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="100" height="80"></canvas>
</body>
</html>
The structure of the html5_canvas_shapes_1 directory will look like this now:
html5_canvas_shapes_1/
├── index.html
├── index.mjs
├── package.json
├── package-lock.json
└── public
└── vite.svg
Go to the terminal and restart vite using the following commands:
$ cd ~/Documents/html5_canvas_1/html5_canvas_shapes_1
$ npm run dev
VITE v7.1.7 ready in 101 ms
➜ Local: http://localhost:5173/
➜ Network: use --host to expose
➜ press h + enter to show help
You can now open a browser to localhost:5173 and hit F12 to see the DevTools console. Here is a screen shot:
In CodeSandbox, you can use the Preview area and open the console as shown in the next screen shot:
Adding shape classes
JavaScript is not an object-oriented programming language like Java. But, JavaScript does support the use of classes. We are going to make use of classes for this lesson. This will introduce you to the concept of a JavaScript class, and show how we import code from one JavaScript file into another JavaScript file. We will be doing this using the ES6 Modules syntax. This is the modern way of doing things, and this is a good way to get used to doing things with JavaScript. Note that CodeSandbox already assumes you are using the ES6 Modules syntax. For those of you outside of CodeSandbox, the vite server is used to do the same thing.
In the same folder that index.html and index.mjs are stored in, create a file called shapes.mjs. You can do this in Visual Studio Code (VSCode). In the Explorer, you can click on the New File icon or just right-click and select New File. Here are the contents to start with for 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;
}
}
Lines 2-8 define the constructor for this class. On line 1, the export directive is used so that the Rectangle class can be used outside of this file. In a little while, you will see that we will use the Rectangle class inside of index.mjs. On line 2, the values listed in the parentheses are called parameters. These are values that are passed to the constructor that the constructor needs to construct a Rectangle object. Lines 3-7 assign each of those parameters to instance variables for the class. In JavaScript, all instance variables are prefaced with the keyword this. The way that this works, is that this refers to the unnamed instance of the class. It is unnamed at this level, because the name of the instance is assigned when the constructor is actually used. For now, it is important to remember that every instance variable in JavaScript will start with this..
In this case, this.x will store the x-coordinate of the left edge of the rectangle. The variable, this.y will store the y-coordinate of the top of the rectangle. So, the coordinates (this.x, this.y) are the coordinates for the top left corner of the rectangle. The variables this.width and this.height specify the width and height of the rectangle, respectively. The variable this.color will be a string that represents the fill color of the rectangle.
Let’s make use of the Rectangle class, by importing it into index.mjs so we can use it there. Here is the new version of index.mjs:
import {Rectangle} from "./shapes.mjs";
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", init);
} else {
init();
}
function init() {
console.log("init called");
const rect1 = new Rectangle(20, 30, 40, 20, 'blue');
console.log(rect1);
}
The new lines are 11-12. Line 11 calls the Rectangle constructor using the keyword new. The values sent will be used to create a rectangle that has its top left corner at (20, 30), has width of 40, a height of 20 and a color of blue. Line 12 displays that object to the console. Here is a screen shot showing the Console with the rectangle object:
Of course, this is not very impressive as we want to see an actual rectangle in the main window. So, we need to modify the Rectangle class inside 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);
}
}
The new lines are 10-13. These lines add the draw() method. A method is a function that belongs to a class. So, methods are sometimes referred to as member functions. So, unlike most functions in JavaScript, there is no function keyword in front of draw().
The draw() method is used to actually draw something in the graphics context of the canvas. So, this is why the graphics context (ctx) is a parameter for this function. Line 11 sets the fillStyle to the color specified when the Rectangle is constructed. Then, line 12 calls the fillRect() function to actually draw the rectangle into the graphics context using the color set for fillStyle.
Next, we need to modify index.mjs so that the the draw() function is used:
import {Rectangle} from "./shapes.mjs";
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", init);
} else {
init();
}
var mycanvas;
var ctx;
function init() {
console.log("init called");
const rect1 = new Rectangle(20, 30, 40, 20, 'blue');
console.log(rect1);
mycanvas = document.getElementById("mycanvas");
ctx = mycanvas.getContext('2d');
rect1.draw(ctx);
}
The new lines are 9-10 and 16-18. Line 9 creates a global variable mycanvas that will be used to reference the <canvas> element. Line 10 creates a global variable that will be used to hold the graphics context for the canvas. Line 16 is where the reference to the <canvas> element is obtained. This is done inside the init() function so that we will have this reference for any other functions that we might use. Line 17 uses the canvas reference to call getContext('2d') to obtain the graphics context for this canvas. Finally, on line 19, we use rect1.draw(ctx) to draw the rectangle on to the canvas. Here is a screen shot showing
Adding a Circle class
Let’s add a Circle class to shapes.mjs. Here is the new code:
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();
}
}
The new lines are 16-22 and 24-31. Line 16 and line 31 mark the start and end of the Circle class, respectively. Lines 17-22 form the constructor for the class. Note that the parameters are the (x, y) coordinates of the center of the circle, the radius of the circle and the color of the circle.
Lines 24-30 define the draw() method. Line 25 calls beginPath() to start the path. Line 26 calls the arc() function to draw an arc that is centered around (this.x, this.y), has a radius of this.radius, starts at angle 0, and ends at 2*PI (so one full rotation). Line 27 closes the path using closePath(). Now that the path is closed, we can set the fillStyle on line 28 and then fill the enclosed path on line 29.
Next, we modify index.mjs to use the Circle class.
import {Rectangle, Circle} from "./shapes.mjs";
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", init);
} else {
init();
}
var mycanvas;
var ctx;
function init() {
console.log("init called");
const rect1 = new Rectangle(20, 30, 40, 20, 'blue');
console.log(rect1);
mycanvas = document.getElementById("mycanvas");
ctx = mycanvas.getContext('2d');
rect1.draw(ctx);
const circle1 = new Circle(90, 40, 20, 'red');
circle1.draw(ctx);
}
The lines are 19-20. Line 19 constructs a new Circle object that will be centered at (90, 40), have a radius of 20 and a color of 'red'. Line 20 causes this circle to be drawn on to the canvas.
Now that we are drawing more than one shape to the canvas, we need to modify index.html to make our <canvas> wider, so we can fit more shapes. Here is the new version of 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>
</body>
</html>
The modified line is 9. We have increased the width from 100 to 300 pixels.
With these changes, here is a screen shot showing both shapes being drawn to the canvas:
Adding an Ellipse class
Let’s add another shape, and ellipse. So, let’s add an Ellipse class to 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();
}
}
The new lines are 33-43 and 45-52. Line 33 and line 52 define the start and end of the Ellipse class, respectively. Lines 34-42 define the constructor for the Ellipse class. The parameters that are used for constructing an ellipse are the (x, y) coordinates of the center of the ellipse, radiusX (radius on x-axis), radiusY (radius on y-axis), rotAngle (angle of rotation of the ellipse major axis), startAngle, endAngle and color.
Lines 45-51 define the draw() method. Line 46 begins the path, line 47 creates the elliptical path, and line 48 closes the path. With the path closed, line 49 sets the fillStyle color and line 50 fills the ellipse using the fillStyle.
Next, we modify the index.mjs to make use of the Ellipse class. Here is the new version of index.mjs:
import {Rectangle, Circle, Ellipse} from "./shapes.mjs";
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", init);
} else {
init();
}
var mycanvas;
var ctx;
function init() {
console.log("init called");
const rect1 = new Rectangle(20, 30, 40, 20, 'blue');
console.log(rect1);
mycanvas = document.getElementById("mycanvas");
ctx = mycanvas.getContext('2d');
rect1.draw(ctx);
const circle1 = new Circle(90, 40, 20, 'red');
circle1.draw(ctx);
const ellipse1 = new Ellipse(140, 40, 20, 10, 0, 0, 2 * Math.PI, 'green');
ellipse1.draw(ctx);
}
The new lines are 21 and 22. Line 21 constructs a new Ellipse object with a center at (140, 40), a radiusX of 20, a radiusY of 10, a rotAngle of 0, a startAngle of 0, an endAngle of 2 * PI and a green color. Line 22, uses the draw() method to draw the ellipse on the canvas. Here is a screen shot showing the resulting page:
Adding a Diamond class
Let’s add one more shape, a diamond shape. Here is the new version of shapes.mjs that does this:
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();
}
}
The new lines are 54-65 and 67-77. Line 54 and line 77 define the start and end of the Diamond class, respectively. Lines 55-65 define the constructor for the Diamond class. The parameters include the (x,y) coordinates for the left point, the top point, the right point and the bottom point. The parameters also include the color to fill the diamond with.
Lines 67-76 define the draw() method for the Diamond class. Line 68 begins the diamond’s path. Line 69 moves to the left point, then line 70 draws a line to the top point. Line 71 adds a line to the right point, and line 72 adds a line to the bottom point. Line 73 closes the path by returning back to the left point. With the diamond’s path closed, we can set the fillStyle on line 74 and fill the diamond shape on line 75.
Next, we modify index.mjs to make use of the Diamond class:
import {Rectangle, Circle, Ellipse, Diamond} from "./shapes.mjs";
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", init);
} else {
init();
}
var mycanvas;
var ctx;
function init() {
console.log("init called");
const rect1 = new Rectangle(20, 30, 40, 20, 'blue');
console.log(rect1);
mycanvas = document.getElementById("mycanvas");
ctx = mycanvas.getContext('2d');
rect1.draw(ctx);
const circle1 = new Circle(90, 40, 20, 'red');
circle1.draw(ctx);
const ellipse1 = new Ellipse(140, 40, 20, 10, 0, 0, 2 * Math.PI, 'green');
ellipse1.draw(ctx);
const diamond1 = new Diamond(170, 40, 190, 20, 210, 40, 190, 60, 'orange');
diamond1.draw(ctx);
}
The new lines are 23 and 24. Line 23 constructs a new Diamond object with the left, top, right and bottom coordinates shown. Line 24 draws the orange filled diamond on to the canvas. Here is a screen shot showing the resulting page:
Letting the user choose the shape
Let’s modify our application so that the user can select the shape to draw on the canvas. When you create a program that allows the user to have a choice, you should be thinking that your program will contain a selection statement, such as an if statement. At the same time, since we are creating a web application, you should be planning on how you will gather the user input. We have used input text boxes (<input type="text">) and select (<select>) elements. An input text box is good when you don’t know what the user input will be. On the other hand, using a <select> element is good when you want to limit the options that the user must choose from for the input. For our application, since we only know how to draw four shapes, then it is reasonable that we would use a <select> element as our method for gathering user input.
Getting the user input
Since we want the user to be able to choose the shape to draw, but only have 4 shapes, using a <select> element is a good way to gather the user input. So, let’s start by modifying index.html to make use of a <select> element that allows choosing the shape:
<!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 /><br />
<button id="ok_button">Ok</button>
</body>
</html>
The new lines are 10-19. Line 10 just puts a <br /> (break) element after the <canvas> element. This will make the prompt on line 11 show up on a new line. Lines 12-17 define a <select> element with an id="shape_select" attribute. Lines 13-16 define the four options for this <select> element. Line 18 puts two break elements before the <button> element is displayed. This makes it so that there is a blank like between the <select> element and the <button> defined on line 19.
Here is a screen shot showing what the web page looks like as a result of these changes:
Next, we will modify index.mjs to handle the user’s choice. When doing this, you need to think about what the output will be like. To start, let’s make it so that the shapes will be drawn on the canvas with their center point at (40, 40). Let’s assume that we will make the shapes the same size as the shapes we already drew, using the same colors as we used for each shape. This means we will use:
-
new Rectangle(40 - width/2, 40 - height/2, width, height, 'blue') = new Rectangle (20, 30, 40, 20, 'blue')
-
new Circle(40, 40, 20, 'red')
-
new Ellipse(40, 40, 20, 10, 0, 0, 2 * Math.PI, 'green')
-
new Diamond(40 - 20, 40 - 0, 40 + 0, 40 - 20, 40 + 20, 40 + 0, 40 + 0, 40 + 20, 'orange') or
new Diamond(20, 40, 40, 20, 60, 40, 40, 60, 'orange')
So, those computations depended on how we are choosing to display the output. These choices will require an if statement that has 4 choices.
Here is the version of 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() {
console.log('handleOk clicked');
}
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 12-14 and 19-20. Actually, in the init() function most of the lines that were there have been deleted. Only lines 17 and 18 remain from before. Those lines are to get a reference to the canvas and a reference to the graphics context of that canvas, respectively. Line 19 gets a reference to the Ok button, and line 20 associates the handleOk() function with clicking on that Ok button.
Lines 12-14 define the handleOk() function called when clicking on the Ok button. Line 13 just causes 'handleOk clicked' to be displayed in the console.
Here is a screen shot showing what the web page and Console look like after clicking on the Ok button:
Note how we are practicing incremental development for this lesson. Instead of adding a lot of code at once, we add enough code to test, and then stop to test. Only when things work correctly, do we start coding again. And, when we do that, we again add only enough code to test. So, at this point, we know that we can click on the Ok button and call the correct function. Let’s further modify 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;
console.log('shape', shape);
}
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 13-15. Line 13 gets a reference to the <select> element with an id="shape_select" attribute. Line 14 gets the value that has been selected from that element. Line 15 just writes 'shape ' followed by the shape selected. Here is a screen shot showing the console after the circle and then the ellipse shapes were selected before clicking on the Ok button:
Now that we know that we can get the shape that was selected, we can modify index.mjs so that we actually draw the correct shape to the canvas:
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;
if (shape === "rectangle") {
const rect1 = new Rectangle(20, 30, 40, 20, 'blue');
rect1.draw(ctx);
} else if (shape === "circle") {
const circle1 = new Circle(40, 40, 20, 'red');
circle1.draw(ctx);
}
}
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 15-21. Lines 15-18 define the first case for the selection statement. This is the case where a blue rectangle will be drawn on to the canvas. Lines 18-21 define the second case for the if statement. This is the case where a red circle will be drawn on to the canvas. Practicing incremental development, we only put in a couple of cases and then test. The following screen shot shows what the page looks like if you select a rectangle, and then select a circle:
If you look carefully, you can see that the circle mostly covers up the rectangle. Actually, we only want to display one shape at a time, so we need to clear the canvas before drawing the next shape. Here is the next version of index.mjs that does this:
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;
ctx.clearRect(0, 0, mycanvas.width, mycanvas.height);
if (shape === "rectangle") {
const rect1 = new Rectangle(20, 30, 40, 20, 'blue');
rect1.draw(ctx);
} else if (shape === "circle") {
const circle1 = new Circle(40, 40, 20, 'red');
circle1.draw(ctx);
}
}
function init() {
mycanvas = document.getElementById("mycanvas");
ctx = mycanvas.getContext('2d');
const ok_button = document.getElementById("ok_button");
ok_button.addEventListener('click', handleOk);
}
The new line is line 15. This calls the clearRect() function to clear a rectangular area of the canvas. Since we want to clear the entire canvas, the first two arguments are the (x, y) coordinates at the top left corner. The last two arguments make the bottom right of the rectangular area to be cleared the bottom right corner of the canvas. If you go back and forth between selecting a rectangle and a circle, you will see that only one shape is drawn at a time. Next we add in the last two shape cases: ellipse and diamond.
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;
ctx.clearRect(0, 0, mycanvas.width, mycanvas.height);
if (shape === "rectangle") {
const rect1 = new Rectangle(20, 30, 40, 20, 'blue');
rect1.draw(ctx);
} else if (shape === "circle") {
const circle1 = new Circle(40, 40, 20, 'red');
circle1.draw(ctx);
} else if (shape === "ellipse") {
const ellipse1 = new Ellipse(40, 40, 20, 10, 0, 0, 2 * Math.PI, 'green');
ellipse1.draw(ctx);
} else if (shape === "diamond") {
const diamond1 = new Diamond(20, 40, 40, 20, 60, 40, 40, 60, 'orange');
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);
}
The new lines are 22-28. Lines 22-25 handle the case of drawing a green ellipse, and lines 25-28 handle the case of drawing an orange diamond. When you look at lines 16-28, you might think that these represent four selection statements. But, this is just one selection statement that handles four cases. In other words, we have a single if statement with three else if clauses.
Letting the user choose a color
Let’s make another modification to this application so that the user can specify the color of the shape. As before, let’s go through the various things that we need to address. We need a way to allow the user to enter input. Since the color is specified as a string, you could use an <input type="text"> text box. Or, you could choose to limit the colors, to colors that you know should work. In the latter case, you could use a <select> element to do this, or you could even use <input type="radio"> radio buttons. If you use a text box, the user could enter some string that does not represent a color, so that could cause problems. If you use radio buttons, you will need a for loop to iterate through all the choices to find the one that has been clicked. So, the easiest thing to do is to use a <select> element as we did for letting the user select a shape. In terms of output for the user’s choice, this will just be setting the fillStyle for the shape, so that is pretty straightforward. In terms of manipulating the input color so that it can be used in the output, all that needs to be done is to get the selected color before handling the selection of the shape. So, as long as this is done in the correct order, this should be easy enough to do. In terms of the code to deal with the color selected, all that needs to be done is to get a reference to the <select> element for the color, and get the value of the selected color.
With that analysis done, let’s start by modifying index.html by adding a <select> element for the color. Here is the new version of 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>
The new lines are 18-26. Line 18 is just break so that the prompt on line 19 is not on the same line as the previous <select>. Lines 20-26 create the select list that the user can use to select the color of the shape. Line 20 gives this <select> element an id="color_select" attribute. Lines 21-25 give the 5 color choices, that I chose to support. You could easily add more choices here if you wanted. If you look in the Preview on CodeSandbox or on localhost:5173 if you are using vite, you will see that now you have two <select> lists.
Next, we will modify index.mjs to handle the color input:
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);
}
The new lines are 15-16, 19, 22, 25 and 28. Line 15 gets a reference to the <select id="color_select"> element. Line 16 gets the selected color from that select list. Line 19, 22, 25 and 28 have the const color substituted in for the color for the various shapes. Here is a screen shot where the user has selected a green diamond:
Summary
-
The HTML5 Canvas was introduced as a way of painting graphics on to a web page.
-
JavaScript Classes were introduced. Note how creating the shape classes made the code for index.mjs shorter and easier to understand.
-
The <select> element was used to gather user input where the number of choices was set to be limited. This eliminates errors associated with <input type="text"> such as misspellings and/or strings that are not valid.
-
To handle the shape choices, an if statement with three else if clauses was used. Note that this is considered to be a single selection statement. That is because we are only selecting on the shape value, so all the choices have to do with the value of a single const. So, this lesson was another review on using selection statements.
-
The overall design of a program that includes input, output, selection statements, and data manipulation or computations, was discussed in planning the program. For the next lesson, one more programming construct, repetition statements, will be added.