Canvas Module JavaScript example

Many of my projects that I make involve working with canvas elements, and I also like to make vanilla javaScript projects where most if not all of the code is my own. Still I would like to stop making everything all over again each time I start a new project, so in todays JavaScript example post I will be going over a kind of canvas module that so far works okay for what I want to use such a module for.

There are at least a few basic features that a canvas module should have and one of them is to create and return not just one canvas element, but a collection of canvas elements. So the main create method of the canvas module should have an array of canvas elements as one of the properties. There should also be some additional features that have to do with attaching pointer event handers to at least one of the container of the canvas elements, or one of the canvas elements. There are a whole bunch of other features that might be a good idea also, for example one of my older JavaScript examples posts was on a draw points method, which I think should be adding into this kind of module.

1 - The canvas module

In this section I will be going over the source code of the canvas module itself before moving on to some additional code examples that make use of the module. Like many of my vanilla javaScript projects I went with a module design that packs all of the code in a single IIFE, this might not always be the best option in all situations but for now it is still how I make my modules.

The canvas module is designed in a way in which I have a single private object called FEATURES inside the body of the IIFE. This FEATURES object contains a number of built in features for creating an array of points, and drawing to a canvas layer in a stack of layers created with the main create layer stack method of this module. Also as mentioned with the create canvas layers method there are a number of public methods, for creating an array of points, and loading additional features. So on top of the built in features there is also a load method of the canvas module that can be used to add on top of the built in features.

1.1 - The beginning of the module, and built in draw methods

At the top of the module I create the FEATURES object as just a single object with the object literal syntax. I will then be appending additional objects to the FEATURES object in the module itself for draw methods and, and methods for creating a collection of points. The first such object is the draw methods object which will contain the built in draw methods that I can use with the public draw methods of this module that I will be getting to later in this section when it comes to getting into the public methods. When it comes to the built in draw methods I have one for clearing a canvas layer, another for drawing a background, and one for drawing a collection of points created with one of the points methods in the points methods object that I will be getting to next.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
(function (api) {
var FEATURES = {};
/********* ********** *********
Draw Methods
********** ********** *********/
var drawMethods = FEATURES.drawMethods = {};
// clear a layer
drawMethods.clear = function(stack, ctx, canvas, layerObj){
ctx.clearRect(-1, -1, canvas.width + 1, canvas.height + 1);
};
// draw a background
drawMethods.background = function (stack, ctx, canvas, layerObj, background) {
ctx.fillStyle = background || stack.background || 'black';
ctx.fillRect(0, 0, canvas.width, canvas.height);
};
// draw a points collection
drawMethods.points = function (stack, ctx, canvas, layerObj, points, cx, cy, opt) {
opt = opt || {};
ctx.save();
ctx.translate(cx, cy);
points.forEach(function (pointArray) {
var len = pointArray.length,
close = opt.close === undefined ? true : opt.close,
fill = opt.fill === undefined ? 'black' : opt.fill,
stroke = opt.stroke === undefined ? 'white' : opt.stroke,
lineWidth = opt.lineWidth === undefined ? 3 : opt.lineWidth,
el,
i = 2;
ctx.beginPath();
ctx.moveTo(pointArray[0], pointArray[1]);
while (i < len) {
el = pointArray[i];
if (typeof el === 'number') {
ctx.lineTo(el, pointArray[i + 1]);
i += 2;
} else {
var parts = el.split(':');
if (parts[0] === 'close') {
close = parts[1] === 'true' ? true : false;
}
if (parts[0] === 'stroke') {
stroke = parts[1] || false;
}
if (parts[0] === 'fill') {
fill = parts[1] || false;
}
if (parts[0] === 'lineWidth') {
lineWidth = parts[1] || 1;
}
i += 1;
}
}
ctx.lineWidth = lineWidth;
if (close) {
ctx.closePath();
}
if (fill) {
ctx.fillStyle = fill;
ctx.fill();
}
if (stroke) {
ctx.strokeStyle = stroke;
ctx.stroke();
}
});
ctx.restore();
};

1.2 - The Built in points method

On top of the draw methods object of the FEATURES object I also have a points methods object that can be used to create a collection of points to be used with the points draw methods that I covered above in this section. One built in method to start off with is a box method that will create one of my points collections that is just a simple basic box. When it comes to this method alone I might all ready want to add a few features, and even pull it out of here into an external file actually. In many projects I might not need to use this kind of method as I would draw box areas by another means. However never the less I needed to start out with something at least.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/********* ********** *********
Points Methods
********** ********** *********/
var pointsMethods = FEATURES.pointsMethods = {};
// create a box
pointsMethods.box = function(sx, sy, w, h){
var x = sx - w / 4,
y = sy - h / 4;
var points = [[
x, y, x + w / 2, y,
x + w / 2, y + h / 2, x, y + h / 2
]];
return points;
};

1.3 - Helper functions

I have a number of helper methods that will be used in the create stack public method for now, but if I keep working on this I am sure the collection of helper functions will expand as is usually the case. A number of these methods are often methods that are found in one of my many general utility type libraries for my various canvas projects. However I often use that kind of module as a dumping ground for methods that do not have a more appropriate place to live. Sense I am making a full canvas library now I am placing many of these methods here, and as a result will have a more minimal utility library for demos and projects when and if I need to use one.

So many of the helper functions thus far have to do with attaching events to a canvas element, however I have one additional helper that is used to create just a single layer object. This method is used in my create layer stack public method to create a stack of layer objects each of which contains a single canvas element, along with additional properties.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
/********* ********** *********
HELPERS
********** ********** *********/
// get a canvas relative position that is adjusted for scale
var getCanvasRelative = function (e) {
var canvas = e.target,
bx = canvas.getBoundingClientRect(),
pos = {
x: (e.changedTouches ? e.changedTouches[0].clientX : e.clientX) - bx.left,
y: (e.changedTouches ? e.changedTouches[0].clientY : e.clientY) - bx.top,
bx: bx
};
// adjust for native canvas matrix size
pos.x = Math.floor((pos.x / canvas.scrollWidth) * canvas.width);
pos.y = Math.floor((pos.y / canvas.scrollHeight) * canvas.height);
return pos;
};
// create and return a canvas pointer event handler for a stack
var canvasPointerEventHandler = function (stack, state, events) {
return function (e) {
var pos = getCanvasRelative(e),
handler = null;
e.preventDefault();
if (e.type === 'mousedown' || e.type === 'touchstart') {
handler = events['pointerStart'];
}
if (e.type === 'mousemove' || e.type === 'touchmove') {
handler = events['pointerMove'];
}
if (e.type === 'mouseup' || e.type === 'touchend') {
handler = events['pointerEnd'];
}
if (handler) {
handler.call(state, e, pos, state, stack);
}
};
};
// attach canvas pointer events
var attachCanvasPointerEvents = function (stack) {
var handler = canvasPointerEventHandler(stack, stack.state, stack.events),
canvas = stack[stack.length - 1].canvas,
options = {
passive: false
}
canvas.addEventListener('mousedown', handler, options);
canvas.addEventListener('mousemove', handler, options);
canvas.addEventListener('mouseup', handler, options);
canvas.addEventListener('touchstart', handler, options);
canvas.addEventListener('touchmove', handler, options);
canvas.addEventListener('touchend', handler, options);
};
// create a single layer object
var createLayer = function (opt) {
opt = opt || {};
opt.append = opt.append === undefined ? true : opt.append;
var layer = {};
// a layer should have a container
layer.container = opt.container || document.getElementById('canvas-app') || document.body;
if (typeof layer.container === 'string') {
layer.container = document.querySelector(layer.container);
}
layer.canvas = document.createElement('canvas');
layer.ctx = layer.canvas.getContext('2d');
// assign the 'canvas_layer' className
layer.canvas.className = 'canvas_layer';
// set native width
layer.canvas.width = opt.width === undefined ? 320 : opt.width;
layer.canvas.height = opt.height === undefined ? 240 : opt.height;
// translate by 0.5, 0.5
layer.ctx.translate(0.5, 0.5);
// disable default action for onselectstart
layer.canvas.onselectstart = function () {
return false;
}
// append canvas to container
if (opt.append) {
layer.container.appendChild(layer.canvas);
}
return layer;
};

1.4 - The public API

So now that I have my build in FEATURES object with draw methods, and points methods as well as some helper functions I will now want to get to the public methods that will be used outside of this module. The main method of interest when it comes to using this canvas module is to start out by calling the create layer stack method. When I call this method what is returned is an array like object with a length property and each numbed key in the object is a reference to a layer object, created with the create layer helper method.

Another main pubic method of interest is the main draw method of the module, which I use by calling the draw method and pass a stack of layers, along with the name of the draw method I want to use, and the layer index I want to draw to. As I covered before there are a few built in methods to start off with, but often I will want to add at least a few custom methods when it comes to making an actual demo or project with this module.

There is then the public draw points method that is like the draw method only it is used to create a collection of points using one of the create points methods, rather than drawing to the canvas. I also have the load method that can be used as a way to extend the built in draw and create points methods with optional additional files that add features that I might not want or need for all projects.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
/********* ********** *********
PUBLIC API
********** ********** *********/
// create a stack of layers as an 'Array Like' Object
api.createLayerStack = function (opt) {
opt = opt || {};
// creating an array like object
var stack = {
length: opt.length === undefined ? 2 : opt.length,
container: opt.container || document.getElementById('canvas-app') || document.body,
events: opt.events || {},
state: opt.state || {},
background: opt.background || 'blue'
};
if (typeof stack.container === 'string') {
stack.container = document.querySelector(stack.container);
}
// layer options
var layerOpt = {
container: stack.container,
append: true
};
// create layers for the stack
var i = 0;
while (i < stack.length) {
stack[i] = createLayer(layerOpt);
i += 1;
}
attachCanvasPointerEvents(stack);
return stack;
};
// main draw method
api.draw = function (stack, key, layerIndex) {
// layer object
var layerObj = stack[layerIndex];
// CORE ARGUMENTS created from stack, and layerIndex arguments of canvasMod.draw
var coreArgu = [stack, layerObj.ctx, layerObj.canvas, layerObj];
// ADDITIONAL ARGUMNETS that will change depending on the draw method used with key argument of canvasMod.draw
var addArgu = [];
if (arguments.length > 3) {
addArgu = Array.prototype.slice.call(arguments, 3, arguments.length);
}
FEATURES.drawMethods[key].apply(stack, coreArgu.concat(addArgu));
};
// create points
api.createPoints = function (stack, key) {
var coreArgu = Array.prototype.slice.call(arguments, 2, arguments.length);
var points = pointsMethods[key].apply(stack, coreArgu);
return points;
};
// load additional FEATURES
api.load = function(plugObj){
Object.keys(plugObj).forEach(function(featuresKey){
var featureArray = plugObj[featuresKey];
featureArray.forEach(function(feature){
FEATURES[featuresKey][feature.name] = feature.method;
})
});
console.log(FEATURES);
};
}
(this['canvasMod'] = {}));

2 - Some css to use with the module

I have some css together that should be used with the canvas module. When I create a stack the default id name for a container is canvas-app, and I also assign a canvas_layer class name to every canvas element that is part of a stack of layers created with the canvas module create layer stack method. I might want to adjust the css a little now and then here and there, but in the project folder I have this css worked out that seems to work out well thus far.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#canvas-app{
position: relative;
}
.canvas_layer {
position:absolute;
}
@media (max-width: 599px) {
#canvas-app {
margin: -10px;
width: 240px;
height: 180px;
margin-left: auto;
margin-right: auto;
}
.canvas_layer {
width: 240px;
height: 180px;
}
}
@media (min-width: 600px) and (max-width: 999px) {
#canvas-app {
margin: 0px;
width: 320px;
height: 240px;
margin-left: auto;
margin-right: auto;
}
.canvas_layer {
width: 320px;
height: 240px;
}
}
@media (min-width: 1000px) {
#canvas-app {
margin: 0px;
width: 640px;
height: 480px;
margin-left: auto;
margin-right: auto;
}
.canvas_layer {
width: 640px;
height: 480px;
}
}

3 - A box demo of the canvas module

Now to work out at least one if not a few basic examples of this canvas module just for the sake of making sure the project works the way that it should. For this example the goal was to just make a quick project that just makes use of the built in box method in the create points object. I wanted to still test out all the basic features of the module though so even though this is a basic example, it is still the beginnings of something that is not so basic. Many of my canvas project prototype examples make use of a state machine, in fact I have one canvas example where a state machine that was the focus of the example.

Anyway for this demo I worked out just a single update game method that takes the place of what would otherwise be a full game module. In this update game module I am not really updating the state of a game but I am created the state of a collection of points that I will then be using with the built in draw points method.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
// helpers
var updateGame = function(sm, secs){
sm.game.size += 32 * secs;
if(sm.game.size > 128){
sm.game.size = 64;
}
sm.game.w = sm.game.size;
sm.game.h = sm.game.size;
sm.game.points = canvasMod.createPoints(sm.layers, 'box', sm.game.x, sm.game.y, sm.game.w, sm.game.h);
};
// state object
var sm = {
secs: 0,
fps: 30,
lt: new Date(),
currentState: 'game',
game: {},
layers: {},
events: {
pointerStart: function (e, pos, sm) {
sm.states[sm.currentState].events.pointerStart.call(sm, e, pos, sm);
},
pointerMove: function (e, pos, sm) {
sm.states[sm.currentState].events.pointerMove.call(sm, e, pos, sm);
},
pointerEnd: function (e, pos, sm) {
sm.states[sm.currentState].events.pointerEnd.call(sm, e, pos, sm);
}
},
states: {}
};
sm.layers = canvasMod.createLayerStack({
container: '#canvas-app',
events: sm.events,
state: sm
});
sm.game = {
x: 160,
y: 120,
w: 256,
h: 256,
size: 32,
points: []
};
updateGame(sm, 0);
// game state
sm.states.game = {
// this start hook will be called just once
// here I can draw a static background to the canvas just once
start: function(sm){
canvasMod.draw(sm.layers, 'background', 0, 'red');
},
update: function (sm, secs) {
updateGame(sm, secs);
},
draw: function (sm, stack) {
canvasMod.draw(stack, 'clear', 1);
canvasMod.draw(stack, 'points', 1, sm.game.points, 0, 0);
},
events: {
pointerStart: function (e, pos, sm) {
// change loction of box
sm.game.x = pos.x;
sm.game.y = pos.y;
},
pointerMove: function (e, pos, sm) {},
pointerEnd: function (e, pos, sm) {}
}
};
// loop
// call start just once
sm.states.game.start(sm);
var loop = function () {
var now = new Date(),
secs = (now - sm.lt) / 1000,
state = sm.states[sm.currentState];
requestAnimationFrame(loop);
if (secs >= 1 / sm.fps) {
state.update(sm, secs);
state.draw(sm, sm.layers);
sm.lt = now;
}
};
loop();

4 - Testing out the plug in system with a circle create points method

I am going to want to have at least one additional demo in which I am testing out the load method of this canvas module. When it comes to using this module in any kind of project I am going to want to define some custom draw methods, and also some methods that will create an array of points also. In this example I am adding a circle create points method, along with an additional oval create points method. When creating a plug in object for the canvas module I can define more than one new method when it comes to points methods, as well as draw methods, so I just wanted to quickly make sure that this feature is working as it should with this demo basically.

4.1 - points-circle.js method

Here I have the points circle javaScript file of this demo in which I am extending the features of the canvas module by adding a circle and over method for creating an array of points. In addition to this I also have added a single new draw method that can be sued to draw text to a layer of a stack.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
canvasMod.load({
// points methods to add
pointsMethods : [
// a circle method
{
name: 'circle',
method: function(cx, cy, radius, pointCount){
pointCount = pointCount === undefined ? 100 : pointCount;
var points = [[]];
var i = 0, x, y, radian;
while(i < pointCount){
radian = Math.PI * 2 / pointCount * i;
x = cx + Math.cos(radian) * radius;
y = cy + Math.sin(radian) * radius;
points[0].push(x, y);
i += 1;
}
return points;
}
},
// an oval method
{
name: 'oval',
method: function(cx, cy, radius1, radius2, pointCount){
pointCount = pointCount === undefined ? 100 : pointCount;
var points = [[]];
var i = 0, x, y, radian;
while(i < pointCount){
radian = Math.PI * 2 / pointCount * i;
x = cx + Math.cos(radian) * radius1;
y = cy + Math.sin(radian) * radius2;
points[0].push(x, y);
i += 1;
}
return points;
}
}
],
drawMethods: [
{
name: 'print',
method: function(stack, ctx, canvas, layerObj, text, x, y, opt){
opt = opt || {};
opt.fontSize = opt.fontSize || 10;
ctx.fillStyle = opt.fillStyle || 'black';
ctx.textBaseline = 'top';
ctx.font = opt.fontSize + 'px arial';
ctx.fillText(text, x, y);
}
}
]
});

4.2 - The main.js file for this points circle demo

When I test this out everything seems to work the way it should. So then I can create all kinds of additional files that I might reuse from one project to the next with this. Also when it comes to just about any kind of project there will need to be at least a few custom draw methods for various kinds of things. So then I can use the canvas modules load methods as a way to add these draw methods in.

1
2
3
4
5
6
7
8
9
10
11
12
var sm = {};
// testing out oval
sm.points = canvasMod.createPoints(sm.layers, 'oval', 0, 0, 150, 75, 20)
sm.stack = canvasMod.createLayerStack({
container: '#canvas-app',
state: sm
});
canvasMod.draw(sm.stack, 'background', 0, 'red');
canvasMod.draw(sm.stack, 'clear', 1);
canvasMod.draw(sm.stack, 'points', 1, sm.points, 160, 120);
// custom draw method works
canvasMod.draw(sm.stack, 'print', 1, 'hello world', 5, 5, {fillStyle: 'white', fontSize: 20 });

5 - Conclusion

The current state of the canvas module seems to work okay thus far, but there are a few more features that I would like to add. However I think most of this functionality should be added in the form of optional plug ins rather than making the module itself more bloated. In my canvas example posts I have one example that is just drawing stars with a canvas element and a little javaScript code, I think I would like to have that to work with as a draw points method but I do not think I want to hard code it into the canvas module itself. There is also another canvas example where I worked out a standard for making sprite sheets without loading external images that is another project that I might want to work into this also.