Не так давно, для одного проекта потребовалось написать карту, которая будет отвечать следующим требованиям:
- Плавная прокрутка
- Подгрузка областей карты
Мне пришлось потратить несколько дней на то, чтобы определиться в том, как лучше всего решить данную задачу.
В итоге я остановился на canvas.
Я потратил долгое время на поиски в интернете аналогичных решений, но к моему удивлению ничего подобного не нашлось.
В результате я решил написать все сам, с нуля.
К сожалению первая версия оказалась тормознутой слишком медленной, движения карты, в некоторых браузерах, были скачкообразными.
В новой версии я учел все ошибки, и в итоге мне удалось добиться того, что карта соответсвовала заявленным требования.
Подготовка
Я не буду описывать подготовительные этапы, они уже много раз описывались на хабре, по этому я уделю внимание тому, где у меня возникли проблемы.
Основа, ядро карты, лежит в файле core.js, для работы с canvas у меня имеется отдельный файл canvas.js.
Для инициализации карты, в файле index.html я создаю объект, в который передаю размер карты, и начальные координаты.
var map = new Zig.Map.Core($('body').width(), $('body').height(), 100, 100);
map.addEventListener('change', function(data){
$('#coord').html('Выбранные координаты: ' + data.x + ':' + data.y);
});
В процессе инициализации создается объект, отвечающий за работу с canvas. На данный момент, все функции для работы с ним публичные,
но в дальнейшем я планирую сделать большинство функций приватными, дабы никто не мог рисовать на моем холсте.
У меня создается массив canvas-ов, где первый это основной, расположенный на экране, а все остальные это буфера, позже я объясню зачем их так много.
Сразу после инициализации, вызывается функция перехода на определенные координаты goto(x, y, callback), которая подгружает область карты вокруг запрошенных координат.
В связи с тем что это прототип, я не стал делать полноценное получение карты по ajax, заменив неким аналогом:
_get_ajax_map : function(coords, callback) {
setTimeout(function(){
// Генегируем ответ аякса
var map = {};
for(var x = Math.min(coords.x1, coords.x2); x <= Math.max(coords.x1, coords.x2); x++) {
for(var y = Math.min(coords.y1, coords.y2); y <= Math.max(coords.y1, coords.y2); y++) {
if (typeof map[x] == 'undefined') {
map[x] = {};
}
if (x < 0 || y < 0) {
// пустота (море, пустыня, космос, на ваше усмотрение)
map[x][y] = { image : null };
} else {
map[x][y] = { image : 'img/' + (((y * 200 + x) % 7 + 2) + '.png') };
}
}
}
callback && callback(map);
}.bind(this), 0);
}
Используя setTimeout я эмулирую получения ответа асинхронно.
Рендеринг
Рендеринг разбит на несколько частей, вызов последующая отрисовка на экран происходит в canvas.js, а оснонная работа, связанныя
со всевозможными вычислениями производится в core.js.
render : function(buffer, buffer2, mouse) {
this._checkMoveMap(mouse);
if (this._rebuild_buffer) {
// Перестраиваем буфер
this._rebuild_buffer = false;
this._rebuild_buffer2 = false;
this._rebuildBuffer(buffer);
this._rebuildBuffer2(buffer2);
} else if (this._rebuild_buffer2) {
this._rebuild_buffer2 = false;
this._rebuildBuffer2(buffer2);
}
return this._options.pos.offset;
}
Первым делом у меня заполняются 2 буфера, присваивается переменной this._rebuild_buffer = false;, которая указывает на то, что в
следующем такте не нужно обновлять буфера.
В случае, если эта переменная, станет true, при следующем такте перестроится буфер. Сделал я это затем, чтобы не нагружать лишний раз бразуер
ненужной работой.
После перестройки выполнения этой функции, я просто чищу основной буфер, и рисую поверх него 2 буфера, с некоторым смещением, которое получил в ответ.
Отлов событий мыши
В первой версии карты, у меня была большая проблема. Сразу после получения события о том что произошло движение по окну с нажатой кнопкой мыши,
я запускал кучу пересчетов, и даже перестроение буферов. Я думаю не нужно говорить, что события от мыши, могут приходить чаще чем 60 раз в сукунду.
В новой версии я учел ошибку, и стал запоминать все действия мыши, и забирать их при рендеринге. В итоге сколько бы событий не произошло,
обработка все равно будет происходить не чаще чем 60 раз в секунду.
Вот так я запоминаю движение мыши по экрану:
_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;
}
Как видите, у меня есть два события drag и move, чтобы я мог отличать где таскают карту, а где просто водят мышкой.
Забирая эти события, переменная чистится:
getAction : function() {
var action = this._action;
this._action = {};
return action;
}
Движение карты
Сначала немного теории.
У меня на экране имеется canvas, размеры которого я задал при инициализации, а так же в памяти имеется еще 3 буфера, размеры которых в два раза больше основного.
Сделано это для того, чтобы не перестраивать буфера при малейшем движении карты. Так буфера построены с запасом, и могут спокойно двигаться по сторонам.
Для того чтобы их разместить правильно, я использую смещение. Т.е. там где у основного canvas-а 0:0, у буферов будет какое-то значение, допустим 512:512.
На картинке, желтый квадрат это основной canvas, красный — буфер, черная точка — запрошенные координаты.
Чтобы сдвинуть карту вбок, на нужно просто буфер немного передвинуть.
Для того, чтобы точно знать насколько смещена карта, у меня имеется 2 переменные, которые по умолчанию равны:
offset : {
x : ШИРИНА_КВАДРАТА * 4,
y : ВЫСОТА_КВАДРАТА * 4
}
Фактически, дефолтное смещение равно расстоянию между верхними левыми углами красного и желтого квадрата.
При движении карты, я к этим значений просто добавляю дельту:
this._options.pos.offset.x += act.drag.x;
this._options.pos.offset.y += act.drag.y;
А так же изменяю положение верхнего левого квадрата:
this._options.pos.px.x += act.drag.x;
this._options.pos.px.y += act.drag.y;
Делается это для того, чтобы я всегда, без проблем, мог вычислить над каким квадратом находится мышка, просто добавив к его
значению координаты мыши.
Таким образом я всегда знаю, где рисовать буфер, так чтобы видимые точки оставались на своих местах.
Но, если двинуть карту далеко, буфер кончится. И чтобы этого не произошло, нужно вовремя обновить буфер, т.е. перестроить его так,
чтобы видимые клетки внешне остались на своих местах.
И чтобы этого добиться, я не просто присваиваю смещению дефолтное значение, но и выполняю расчет по формуле, чтобы узнать
насколько и в какую сторону нужно изменить дефолтное значение смещения, так, чтобы видимые клетки остались на своих местах.
Для того, чтобы понятно это объяснить, давай запомним что, «углом» я буду называть видимый верхний левый угол основного canvas-a, а
«квадратом» — квадрат, которому принадлежит точка, лежащая в «углу», т.е. координаты «угла», находятся где-то внутри этого «квадрата».
Шансов, что координаты «угла» совпадут с координатами верхнего левого угола «квадрата», близки к нулю.
И в связи с этим мы просто вычисляем разницу между ними, которую затем прибавляем к дефолтному смещению.
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);
где
- w, h — ширина и высота квадрата
- p.px.x, p.px.y — пиксельные координаты, которые расположены в верхнем левом углу основного канваса
- (xy.x + 4), (xy.y + 4) — внутренние координаты квадрата, который косается верхнего левого угла канваса
Третий буфер
Третий буфер на данный момент у меня не используется, но создал я его для того, чтобы не обновлять буфер полностью, когда
происходит перемещение карты. Я планирую сделать, чтобы первый буфер не чистился весь, а вставлялся в третий со смещением,
и только пустота смещения заполнялась.
Так будет работать еще быстрее.
Заключение
Мне было интересно заниматься данным проектом. Интересно было на практике изучить canvas в JavaScript, без использования
сторонних библиотек.
Надеюсь вам поможет моя статья измежать таких же ошибок, как допустил я в первой версии.
Исходники
Автор: zig1375