...

понедельник, 15 июля 2013 г.

[Из песочницы] Карта на Canvas

Не так давно, для одного проекта потребовалось написать карту, которая будет отвечать следующим требованиям:


  • Плавная прокрутка

  • Подгрузка областей карты


Мне пришлось потратить несколько дней на то, чтобы определиться в том, как лучше всего решить данную задачу.

В итоге я остановился на 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, без использования

сторонних библиотек.

Надеюсь вам поможет моя статья измежать таких же ошибок, как допустил я в первой версии.

Исходники




BitBucket

Demo

This entry passed through the Full-Text RSS service — if this is your content and you're reading it on someone else's site, please read the FAQ at fivefilters.org/content-only/faq.php#publishers. Five Filters recommends: 'You Say What You Like, Because They Like What You Say' - http://www.medialens.org/index.php/alerts/alert-archive/alerts-2013/731-you-say-what-you-like-because-they-like-what-you-say.html


Комментариев нет:

Отправить комментарий