Not so long ago, for one project, it was necessary to write a card that would meet the following requirements:
- Smooth scrolling
- Loading map areas
I had to spend several days trying to figure out how to best solve this problem.
In the end, I settled on the canvas.
I spent a long time searching the Internet for similar solutions, but to my surprise nothing like this was found.
As a result, I decided to write everything myself, from scratch.
Unfortunately, the first version was
slowed down too much, the map movement, in some browsers, was spasmodic.
In the new version, I took into account all the errors, and in the end I managed to ensure that the card complied with the stated requirements.
Training
I will not describe the preparatory stages, they have already been described many times in Habré, so I will pay attention to where I have problems.
The basis, the core of the map, lies in the
core.js file, I have a separate
canvas.js file for working with canvas.
')
To initialize the map, in the
index.html file, I create an object to which I transfer the map size and starting coordinates.
var map = new Zig.Map.Core($('body').width(), $('body').height(), 100, 100); map.addEventListener('change', function(data){ $('#coord').html(' : ' + data.x + ':' + data.y); });
The initialization process creates an object responsible for working with the canvas. At the moment, all functions for working with it are public,
but in the future I plan to make most of the functions private so that no one can paint on my canvas.
I create an array of canvass, where the first one is the main one, located on the screen, and all the rest are buffers, later I will explain why there are so many of them.
Immediately after initialization, the transition function to certain coordinates
goto (x, y, callback) is called, which loads the map area around the requested coordinates.
Due to the fact that this is a prototype, I did not make a full-fledged receipt of ajax card, replacing it with some kind of analog:
_get_ajax_map : function(coords, callback) { setTimeout(function(){
Using
setTimeout, I emulate the receipt of a response asynchronously.
Rendering
The rendering is split into several parts, the call is then
drawn to the screen in
canvas.js , and the main work related
with all sorts of computations produced in
core.js. render : function(buffer, buffer2, mouse) { this._checkMoveMap(mouse); if (this._rebuild_buffer) {
First of all, I’m filling 2 buffers, assigned to the variable
this._rebuild_buffer = false; which indicates that in
The next clock cycle does not need to update the buffer.
If this variable becomes
true , the buffer will be rebuilt at the next clock cycle. I did it then, so as not to burden the brazer once again
unnecessary work.
After reorganizing the execution of this function, I simply clean the main buffer, and draw 2 buffers on top of it, with some offset, which I received in response.
Catching mouse events
In the first version of the map, I had a big problem. Immediately after receiving the event that there was a movement around the window with the mouse button pressed,
I ran a bunch of recalculations, and even rebuild buffers. I think it is not necessary to say that mouse events can come more than 60 times per second.
In the new version, I took into account the error, and began to memorize all the actions of the mouse, and pick them up when rendering. In the end, no matter how many events happen,
processing will still occur no more than 60 times per second.
This is how I memorize mouse movement around the screen:
_move: function(e) { var x = e.offsetX || e.layerX, y = e.offsetY || e.layerY; this.diff.x += Math.abs(this.pos.x - x); this.diff.y += Math.abs(this.pos.y - y); if (this.pressed) { this._addToAction('drag', this.pos.x - x, this.pos.y - y); } else { this._action.move = {x : x, y : y}; } this.pos.x = x; this.pos.y = y; }, _addToAction : function(key, x, y) { if (typeof this._action[key] == 'undefined') { this._action[key] = {x : 0, y : 0}; } this._action[key].x += x; this._action[key].y += y; }
As you can see, I have two
drag and
move events so that I can distinguish where the card is dragged and where it is simply driven.
Taking these events, the variable is cleared:
getAction : function() { var action = this._action; this._action = {}; return action; }
Motion card
First, a little theory.
I have a canvas on the screen, the dimensions of which I specified during initialization, as well as in memory there are 3 more buffers, the dimensions of which are twice the size of the main one.
This is done in order not to rebuild the buffer at the slightest movement of the map. So the buffer is built with a margin, and can safely move around.
In order to place them correctly, I use offset. Those. where the main canvas is 0: 0, the buffers will have some value, say 512: 512.

In the picture, the yellow square is the main canvas, the red is the buffer, the black dot is the requested coordinates.
To move the card sideways, you just need to slightly move the buffer.
In order to know exactly how much the map is shifted, I have 2 variables that are equal by default:
offset : { x : _ * 4, y : _ * 4 }
In fact, the default offset is equal to the distance between the upper left corners of the red and yellow square.
When moving a map, I simply add the delta to these values:
this._options.pos.offset.x += act.drag.x; this._options.pos.offset.y += act.drag.y;
And I also change the position of the upper left square:
this._options.pos.px.x += act.drag.x; this._options.pos.px.y += act.drag.y;
This is done so that I can always, without any problems, calculate over which square a mouse is, just adding to it
the coordinate value of the mouse.
Thus, I always know where to draw the buffer, so that the visible points remain in their places.
But, if you move the card away, the buffer will end. To prevent this from happening, you need to update the buffer in time, i.e. rebuild it so
so that visible cells remain in their places.
And in order to achieve this, I do not just assign the default value to the offset, but also perform the formula calculation to find out
how much and in which direction you need to change the default offset value, so that the visible cells remain in place.
In order to understand this clearly, let's remember that, the “angle” I will call the visible upper left corner of the main canvas, a
"Square" is the square to which the point in the "corner" belongs, i.e. the coordinates of the "angle" are somewhere inside this "square".
The chances that the coordinates of the "angle" coincide with the coordinates of the upper left corner of the "square" are close to zero.
And in this regard, we simply calculate the difference between them, which we then add to the default offset.
this._options.pos.offset.x = w * 4 + (p.px.x - (xy.x + 4) * w); this._options.pos.offset.y = h * 4 + (p.px.y - (xy.y + 4) * h);
Where
- w, h - the width and height of the square
- p.px.x, p.px.y - pixel coordinates, which are located in the upper left corner of the main canvas
- (xy.x + 4), (xy.y + 4) - internal coordinates of the square, which is tied to the upper left corner of the canvas
Third buffer
The third buffer is currently not used by me, but I created it in order not to update the buffer completely, when
the map is moving. I plan to make the first buffer not cleaned all, but inserted into the third one with offset,
and only the displacement void was filled.
This will work even faster.
Conclusion
I was interested in doing this project. It was interesting in practice to study the canvas in JavaScript, without using
third-party libraries.
I hope my article will help you to make the same mistakes as I made in the first version.
Sources
BitbucketDemo