Canvas and Objects

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.

Example 11.4. A Page With Two Canvases and a Shape in Each

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>

        

View in browser.


Example 11.5. A Page With a Canvas and Several Shapes

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>

        

View in browser.


Example 11.6. Same as Previous, but the Shapes Move with Buttons

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>

        

View in browser.


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.

Example 11.7. Events on Individual Shapes from Array, LXXVII

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.

  • Create a canvas object for the designated canvas element.
  • Attach an eventlistener to the canvas. It's handler will do the essentials.
  • Create two or more shapes for the canvas. Push them onto an array for shapes.
  • Iterate through the shapes array printing the shapes on the canvas.
  • The handler for the 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:

  • Get the shape's context from the appropriate property.
  • Create a context path for the shape. This mimics the drawing the shape again without the actual putting pen to paper.
  • Get the getBoundingClientRect ????
  • Because the canvas coordinates, and the mouse click coordinates are relative to the browser window, they must be recalculated to use of the canvas as a coordinate system. Do that.
  • If this reveals that the click is inside the shape of the current iteration of the loop, bingo, change it's color.
  • If not, leave it a continue with the next shape of the array.
<!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>

        

View in browser.