There is no necessary connection between the canvas and objects but in some use cases objects may come in very handy. In order to make a connection I'll ask some questions, and suggest some answers.
What if you need more than one canvas on a page?
Whenever you create a canvas, you need to tell the HTML5 interpreter where, what is it's width and height, and what is it's background color for example. Repetitive code hints at functions, and if there is more than behaviour, meaning properties, an object, a canvas object should be able to help us.
What if you need to move the things you have drawn on the canvas?
Once drawn, a shape is committed to the canvas
exactly where it was drawn. There is no effect from
changing it's x, and y
coordinates. You may do that, but the shape will
visibly not move to another location.
In order to move a drawn object,
you must wipe it from the canvas,
and then redraw it at it's new location. The code
for each time you need to draw is the same. Only
the coordinates change. It looks as if a function
or method would be able to do that economically.
A shape object with appropriate coordinates as
properties and a draw method might come in handy.
What if you have many shapes on your canvas?
If you buy the answer to the previous question you make several instances of shape objects, and in stead of storing them as individual object reference variables, you might see the benefit of storing them in arrays. Arrays cater to easy and uniform treatment of their elements through looping, and above all, they are flexible as to the number of elements stored in them.
In a previous chapter you have already seen the
code for creation of a canvas on a page. It consists
of an HTML5 canvas element, or
more than one, in the page, and then some JavaScript
code to put some magic into the canvas.
First the canvas in the JavaScript quasi class format as described earlier.
import {$} from './nQuery.js';
/**
* Canvas object
*/
export class Canvas {
constructor(canvasId, color) {
this.canvas = $(canvasId);
this.context = this.canvas.getContext("2d");
this.color = color;
this.prep();
}
prep() {
this.context.fillStyle = this.color;
this.context.fillRect(0, 0, this.canvas.width, this.canvas.height);
}
clear() {
this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
}
getContext() {
return this.context;
}
getHeight() {
return this.canvas.height;
}
getWidth() {
return this.canvas.width;
}
};
The code to invoke the JavaScript at the load event.
'use strict';
import {Canvas} from './nmlCanvas.js';
/*
* nmlObject71.js
*/
let initialize = function () {
// create canvas object
let mycv0 = new Canvas('myCanvas0', 'yellow');
let mycv1 = new Canvas('myCanvas1', 'transparent');
mycv0.prep();
draw(mycv0.context, 1);
draw(mycv1.context, 2);
}
let draw = function (ctx, nr) {
if (nr === 1) {
ctx.fillStyle = "#088"; // fill color to 088
ctx.fillRect(20, 10, 120, 40); // fill rectangle
} else {
ctx.beginPath(); // begin new path
ctx.moveTo(75, 90);
ctx.arc(75, 90, 50, 0, Math.PI * 0.5, false);
// describe arc
ctx.strokeStyle = 'red'; // stroke color
ctx.fillStyle = '#cc0'; // set fill color
ctx.closePath(); // close the path
ctx.fill(); // fill the path
ctx.stroke(); // draw circumference
}
}
window.addEventListener('load', initialize);
The page is spookily similar to the other examples' pages.
<!doctype html>
<html language="en">
<!-- nmlObject71.html -->
<head>
<meta charset="utf-8"/>
<title>Canvas Experiment LXXI</title>
<script type='module' src="nmlObject71.js"></script>
</head>
<body>
<h1>Page with two canvasses and two shapes.</h1>
<p>
Two canvas objects. A shape in each.
</p>
<canvas id="myCanvas0" width="200" height="200"
style="outline: 1px solid magenta;">
<p>Powered by &html; canvas</p>
</canvas>
<canvas id="myCanvas1" width="300" height="150"
style="outline: 1px solid blue;">
<p>Powered by &html; canvas</p>
</canvas>
</body>
</html>
The canvas you have seen already. Here is the
the shape object.
'use strict';
/**
* Shape object, simple
*/
export class Shape {
constructor(cv, x, y, width, height, color) {
this.ctx = cv.context;
this.x = x;
this.y = y;
this.width = width;
this.height = height;
this.color = color;
}
draw() {
this.ctx.fillStyle = this.color;
this.ctx.fillRect(this.x, this.y, this.width, this.height);
}
};
The code to invoke the JavaScript at the load event.
/*globals document, window */
'use strict';
import {Canvas} from './nmlCanvas.js';
import {Shape} from './nmlShape.js';
/*
* nmlCanvas75.js
*/
let initialize = function () {
// create canvas object
let mycv = new Canvas('myCanvas', 'transparent');
// create objects
// put in array
let shape1 = new Shape(mycv, 20, 10, 120, 40, 'blue');
var shape2 = new Shape(mycv, 200, 100, 80, 60, 'green');
shapes.push(shape1);
shapes.push(shape2);
paint(mycv, shapes);
}
let paint = function (cv, arr) {
// loop through array of shapes and draw
for (let shape of arr) {
shape.draw();
}
}
let shapes = [];
window.addEventListener('load', initialize);
The page is spookily similar to the other examples' pages.
<!doctype html>
<html language="en">
<!-- nmlCanvas75.html -->
<head>
<meta charset="utf-8"/>
<script type='module' src="nmlCanvas75.js"></script>
</head>
<body>
<h1>Canvas Experiment LXXV</h1>
<p>
Create two shape objects. Put them into array.
Draw from array.
</p>
<canvas id="myCanvas" width="400" height="400"
style="outline: 1px solid magenta;">
<p>Powered by &html;5 canvas</p>
</canvas>
</body>
</html>
The canvas, again, you have seen already. Here is the
the shape object with a new method,
move added.
'use strict';
/**
* Shape object, with added move method
*/
export class Shape {
constructor(cv, x, y, width, height, color) {
this.ctx = cv.context;
this.x = x;
this.y = y;
this.width = width;
this.height = height;
this.color = color;
}
draw() {
this.ctx.fillStyle = this.color;
this.ctx.fillRect(this.x, this.y, this.width, this.height);
}
move(dx, dy) {
this.x += dx;
this.y += dy;
}
};
The code to invoke the JavaScript at the load event.
'use strict';
import {Canvas} from './nmlCanvas.js';
import {Shape} from './nmlShape1.js';
import {$} from './nQuery.js';
/*
* nmlCanvas76.js
*/
const initialize = function () {
// a couple of button eventlisteners
$('b1').addEventListener('click', moveShapes);
$('b2').addEventListener('click', moveShapes);
// create canvas object
mycv = new Canvas('myCanvas', 'transparent');
// create objects
// put in array
let shape1 = new Shape(mycv, 20, 10, 120, 40, 'blue');
let shape2 = new Shape(mycv, 200, 100, 80, 60, 'green');
shapes.push(shape1);
shapes.push(shape2);
redraw(mycv, shapes);
}
const redraw = function (cv, arr) {
cv.clear();
cv.prep();
// loop through array of shapes and draw
for (let shape of arr) {
shape.draw();
}
}
const moveShapes = function (ev) {
// which button was hit
if (ev.target.id === 'b1') {
shapes[0].move(2, 4);
} else {
shapes[1].move(3, -3);
}
redraw(mycv, shapes);
}
var shapes = [];
var mycv;
window.addEventListener('load', initialize);
The page has been given buttons to move the shapes.
<!doctype html>
<html language="en">
<!-- nmlCanvas76.html -->
<head>
<meta charset="utf-8"/>
<title>Canvas Experiment LVXXI</title>
<script type='module' src="./nmlCanvas76.js"></script>
</head>
<body>
<h1>Canvas Experiment LXXVI</h1>
<p>
Create two shape objects. Put them into array.
Draw from array.
</p>
<canvas id="myCanvas" width="400" height="400"
style="outline: 1px solid magenta;">
<p>Powered by &html;5 canvas</p>
</canvas>
<div>
<button id="b1">Move one</button>
<button id="b2">Move the other</button>
</body>
</html>
What you just saw was the movement of drawn shapes. The movement must be done by erasing the shapes, recreating the canvas, and redrawing the shapes. In this case it was done with attaching an eventlistener to a button, and then connecting each shape with a button. Rather tedious.
Wouldn't it be much more convenient if you could just use the mouse? Point at a shape with the mouse, hold the mouse button down, and move the shape with it? This was rethorical question. The answer, of course, is yes.
In order to do that we must prove that we can attach
an eventhandler to exactly one shape on the canvas.
Attaching eventhandlers to HTML5 elements is easy.
We do it all the time. Check how we attached click
events to the buttons in the previous example.
The problem in this case is that although the canvas
is an HTML5 element, the shapes are not. The
drawn shapes are not elements you can attach an
id attribute to, and then add an
eventlistener. My next example finds a way to circumvent
this, and attach a click event to a
particular shape. This will be
proof of concept if it works.
If this is possible, you should then be able to exchange the event with any other.
You could then create an eventhandler to make the shape follow the mouse
coordinates on a mousedown event in
stead.
You will be asked to do exactly that in one of the exercises following this chapter.
The canvas "class" code you have seen already.
No need to repeat it here. The code for the shape
"class" likewise. In the previous example we
added a move method. Please refer to
that code. Here we start with the code using these
two objects.
'use strict';
import {Canvas} from './nmlCanvas.js';
import {Shape} from './nmlShape1.js';
/*
* nmlCanvas77.js
*/
let initialize = function () {
let mycv = new Canvas('myCanvas', 'transparent');
mycv.canvas.addEventListener('click', hittest);
// create objects
// put in array
let shape1 = new Shape(mycv, 20, 10, 120, 40, 'blue');
let shape2 = new Shape(mycv, 200, 100, 80, 60, 'green');
shapes.push(shape1);
shapes.push(shape2);
repeater(mycv, shapes);
}
let redraw = function (cv, arr) {
cv.clear();
cv.prep();
// loop through array and draw
for (let shape of arr) {
shape.draw();
}
}
let repeater = function (cv, arr) {
// if this is an animation build a setInterval loop here
// if not, just draw
redraw(cv, arr);
}
let hittest = function (ev) {
for (let shape of shapes) {
let cx = shape.ctx;
cx.beginPath();
cx.rect(shape.x, shape.y, shape.width, shape.height);
cx.closePath();
let bb = this.getBoundingClientRect(); // canvas size and pos
// mouse to canvas coordinates
let x = (ev.clientX - bb.left) * (this.width / bb.width);
let y = (ev.clientY - bb.top) * (this.height / bb.height);
if (cx.isPointInPath(x, y)) {
cx.fillStyle = (cx.fillStyle === "#ffff00") ? "green" : "yellow";
cx.fill();
shape.color = cx.fillStyle;
// window.alert("hit: "+x+","+y);
} else {
// window.alert("nohit: "+x+","+y);
}
// console.log(shape);
}
}
let shapes = [];
window.addEventListener('load', initialize);
Let me take you through the pseudo code, point by point of this initialization process.
click event
on the canvas must pinpoint the shape under
the mouse pointer, and when done, perform
som change to exactly this, and no other
shape. This will be the proof of concept.
As action a change of colour has been chosen
simply because it is easily visible.
Looking at the code you should recognize statements
doing just that. There's a small detour. The
repeater method could draw the
shapes itself, but in case we want to introduce
animation or manual movement, we need to
clear, and prep the
canvas, if not the first time, then after the
first change of position. The code implemented
here leaves place in the repeater to
alter coordinates of all or any shape in the array.
The hittest method is handling the
mouse clicks. The code, not being intuitively
understandable, should be explained by it's
pseudo code:
for each element in the shapes array do the following:
<!doctype html>
<html language="en">
<!-- nmlCanvas77.html -->
<head>
<meta charset="utf-8"/>
<title>Canvas Experiment LVXXII</title>
<script type='module' src="./nmlCanvas77.js"></script>
</head>
<body>
<h1>Canvas Experiment LXXVII</h1>
<p>
Draw two shapes. Put them into array.
Click on one to color it.
</p>
<canvas id="myCanvas" width="400" height="400"
style="outline: 1px solid magenta;">
<p>Powered by &html;5 canvas</p>
</canvas>
</body>
</html>