A Basic canvas map scrolling example

Time now for another one of my canvas examples, this time I think I will make a basic example of a scrolling map. This is something that will come into play for many projects that are some kind of game that involves a large world map of cells or tiles.

This will be a basic getting started canvas example of a map, and moving around such a map. So it might not be the best solution for large maps, as I have not put a lot of time into this to improve performance.

1 - The map module

So lets start out with the map module, I tired to make this module a little more functional rather that just making it a class. Not all the methods are pure functions as some of them will return references to objects within a gird object, but that is still the direction I started going with this module.

So it is used my calling a methods that will create a grid object, and then that grid object can be passed to many other methods in the module that will return various useful values.

1.1 - The start of the module and parse grid properties

I start off the module with just an object literal, all the methods are public so this kind of pattern will work for now. I then start off with a methods that can be used to parse options for other methods that will be used to create a grid object.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var g = {};
// CREATE A GRID OBJECT
// parse grid properties
g.parseGridProps = function (grid) {
var a = {};
a.width = grid.width || 64;
a.height = grid.height || 16;
a.cellSize = grid.cellSize || 32;
a.xOffset = grid.xOffset === undefined ? 0 : grid.xOffset;
a.yOffset = grid.yOffset === undefined ? 0 : grid.yOffset;
a.bufferSize = grid.bufferSize === undefined ? 32 : grid.bufferSize;
a.selectedCellIndex = grid.selectedCellIndex || -1;
a.cells = [];
return a;
};

1.2 - Create grid object methods

I then have methods that I use to create a grid object.

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
// make and return a new grid object by just passing width and height values
g.createGridObject = function (w, h) {
var a = g.parseGridProps({
width: w,
height: h
});
return g.createClearCellGrid(a);
};
// create a new grid object with blank cells by passing a given grid Object
g.createClearCellGrid = function (grid) {
var a = g.parseGridProps(grid);
// create clean cells
var i = 0,
x,
y,
len = a.width * a.height;
while (i < len) {
a.cells.push({
i: i,
x: i % a.width,
y: Math.floor(i / a.width),
type: 0, // type index (0 = sand , 1-5 = grass, 6-10 = wood),
worth: 0
});
i += 1;
}
return a;
};

1.3 - Set bounds

I made one methods that can be used to set bounds for a grid object. This works by returning a set of new offset values only that can then be used to update a grid objects offset values outside of the module without mutating the given grid object.

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
// BOUNDS
// return a set of clamped offset values for the given grid
g.clampedOffsets = function (grid, canvas) {
canvas = canvas || {
width: 320,
height: 120
};
var w = grid.width * grid.cellSize,
h = grid.height * grid.cellSize,
bufferSize = grid.bufferSize,
xMin = bufferSize,
yMin = bufferSize,
xMax = (w - canvas.width + bufferSize) * -1,
yMax = (h - canvas.height + bufferSize) * -1,
x = grid.xOffset,
y = grid.yOffset;
// rules
x = x > xMin ? xMin : x;
y = y > yMin ? yMin : y;
x = x < xMax ? xMax : x;
y = y < yMax ? yMax : y;
// return offset values
return {
xOffset: x,
yOffset: y
};
};

1.4 - Get cell helpers

I then have a number of helpers that can be used to get a cell in the grid, or a cell index value.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// GET CELL
// get a cell from the given cell position
g.get = function (grid, x, y) {
if (x < 0 || y < 0 || x >= grid.width || y >= grid.height) {
return {};
}
return grid.cells[y * grid.width + x];
};
// get a cell position by way of a point on a canvas
g.getCellPositionFromCanvasPoint = function (grid, x, y) {
return {
x: Math.floor((x - grid.xOffset) / grid.cellSize),
y: Math.floor((y - grid.yOffset) / grid.cellSize)
};
};
// get a cell position by way of a point on a canvas
g.getCellFromCanvasPoint = function (grid, x, y) {
var pos = g.getCellPositionFromCanvasPoint(grid, x, y);
return g.get(grid, pos.x, pos.y);
};

1.5 - Movement

I made a get pointer movement deltas methods that will return a set of deltas that can be used to update offsets. This works by passing a grid object, along with a canvas, and an x and y pointer position.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// MAP MOVEMENT
// get a set of deltas
g.getPointerMovementDeltas = function (grid, canvas, px, py) {
var cx = canvas.width / 2,
cy = canvas.height / 2,
a = Math.atan2(py - cy, px - cx),
d = Math.sqrt(Math.pow(px - cx, 2) + Math.pow(py - cy, 2)),
per,
dMax = canvas.height / 2,
delta
d = d >= dMax ? dMax : d;
per = d / dMax;
delta = (0.5 + per * 2.5) * -1;
return {
x: Math.cos(a) * delta,
y: Math.sin(a) * delta
};
};

2 - The draw map method

I made a single draw methods that I used to draw the current state of the map.

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
var drawMap = function (grid, ctx, canvas) {
var colors = ['yellow', 'green'],
cellSize = grid.cellSize || 10,
x,
y,
xOffset = grid.xOffset,
yOffset = grid.yOffset;
grid.cells.forEach(function (cell) {
ctx.fillStyle = colors[cell.type] || 'white';
x = cell.x * cellSize + xOffset;
y = cell.y * cellSize + yOffset;
ctx.fillRect(x, y, cellSize, cellSize);
ctx.strokeStyle = 'white';
ctx.strokeRect(x, y, cellSize, cellSize);
});
if (grid.selectedCellIndex > -1) {
ctx.strokeStyle = 'red';
var cell = grid.cells[grid.selectedCellIndex],
x = cell.x * cellSize + xOffset,
y = cell.y * cellSize + yOffset;
ctx.strokeStyle = 'red';
ctx.strokeRect(x, y, cellSize, cellSize);
}
};

3 - The main.js file

Now for the main javaScript file that makes use of the map module, and my draw 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
// CANVAS
var canvas = document.createElement('canvas'),
ctx = canvas.getContext('2d'),
container = document.getElementById('gamearea') || document.body;
container.appendChild(canvas);
canvas.width = 320;
canvas.height = 120;
// CREATE GRID
var grid = g.createGridObject(16, 8);
grid.xOffset = canvas.width / 2 - grid.width * grid.cellSize / 2;
grid.yOffset = 0;
var mousedown = false,
gridDelta = {
x: 0,
y: 0
};
// MAIN APP LOOP
var loop = function () {
requestAnimationFrame(loop);
grid.xOffset += gridDelta.x;
grid.yOffset += gridDelta.y;
var offsets = g.clampedOffsets(grid, canvas);
grid.xOffset = offsets.xOffset;
grid.yOffset = offsets.yOffset;
// fill black
ctx.fillStyle = 'black';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// draw map
drawMap(grid, ctx, canvas);
};
loop();
// EVENTS
canvas.addEventListener('mousedown', function (e) {
var canvas = e.target,
bx = canvas.getBoundingClientRect(),
x = e.clientX - bx.left,
y = e.clientY - bx.top;
e.preventDefault();
mousedown = true;
var cell = g.getCellFromCanvasPoint(grid, x, y);
if (cell.i === grid.selectedCellIndex) {
grid.selectedCellIndex = -1;
} else {
if (cell.i >= 0) {
grid.selectedCellIndex = cell.i;
}
}
});
canvas.addEventListener('mouseup', function (e) {
e.preventDefault();
mousedown = false;
gridDelta.x = 0;
gridDelta.y = 0;
});
canvas.addEventListener('mousemove', function (e) {
var canvas = e.target,
bx = canvas.getBoundingClientRect(),
x = e.clientX - bx.left,
y = e.clientY - bx.top,
deltas = g.getPointerMovementDeltas(grid, canvas, x, y);
if (mousedown) {
gridDelta.x = deltas.x;
gridDelta.y = deltas.y;
}
});