Карта на 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

ссылка на оригинал статьи http://habrahabr.ru/post/186672/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *