Быстрая интерактивная схема зала на canvas

Быстрая интерактивная схема зала на canvas

Разрабатываем библиотеку для отображения больших интерактивных схем залов на canvas без фреймворков и заставляем хорошо работать в ie и мобильных устройствах. Попутно разбираемся с особенностями работы canvas.

Постановка задачи

Первым делом сформируем требования:

  • Производительность: 5-10 тысяч объектов не должны смущать нашу библиотеку даже в ie
  • На каждый объект можно навести курсор/кликнуть, и объект должен иметь возможность это обработать
  • Схема должна масштабироваться и двигаться
  • Адаптивность под размеры контейнера
  • Поддержка touch устройств

Введение

Не будем тянуть и сразу посмотрим демо, так будет понятнее о чем речь.

В статье я буду вставлять только небольшие участки кода, остальное можно посмотреть на GitHub

Вспоминаем, что canvas по сути картинка с api, поэтому обработка ховеров и кликов на нашей совести: нужно самим считать координаты с учетом масштаба и скролла, искать объекты по их координатам. Но в тоже время мы полностью контролируем производительность и рисуем только то, что нужно.

Постоянно перебирать все объекты на схеме и сверять их координаты не оптимально. Хотя это и будет происходить достаточно быстро, мы все равно сделаем лучше: построим деревья поиска, разбив карту на сектора.

Кроме оптимизации поиска, постараемся следовать следующим правилам работы с canvas:

requestAnimationFrame

У браузера есть свой таймер отрисовки, и с помощью метода requestAnimationFrame можно попросить браузер отрисовать наш кадр вместе с остальными анимациями, — это позволит избежать двойной работы браузера. Для отмены анимации есть cancelAnimationFrame. Полифил.

Кеширование сложных объектов

Не обязательно постоянно перерисовывать сложные объекты, если они не изменяются. Можно отрисовать их заранее на скрытом canvas, а потом брать оттуда.

Отрисовывать только видимые объекты

Даже если элемент выходит за границы холста, на его отрисовку все равно тратится время. Особенно это заметно в ie, он честно отрисовывает все, в то время в хроме это оптимизировано, и на это времени тратится намного меньше.

Перерисовывать только изменившиеся объекты

Нет смысла перерисовывать всю сцену, если изменился один элемент.

Меньше текста

Отрисовка текста для canvas тяжелая задача, поэтому нужно избегать большого количества объектов с текстом. Даже если хочется на каждое место поставить цифру — лучше ограничить отображение этой цифры масштабом: например, показывать цифру только при определенном приближении, когда эта информация будет полезна.

Архитектура

Scheme — основной класс. View — класс знает canvas, на котором нужно рисовать, и его параметры (у нас их будет два). SchemeObject — класс объекта схемы знает свое местоположение, как себя отрисовать и как обрабатывать события. Может содержать дополнительные параметры, например, цену. EventManager — класс обработки и создания событий. При получении события передает его нужному классу. ScrollManager — класс, отвечающий за скролл схемы. ZoomManager — класс, отвечающий за зум схемы. StorageManager — класс, отвечающий за хранение объектов схемы, создание дерева поиска и поиск объектов по координатам. Polyfill — класс с набором поллифилов для кроссбраузерности. Tools — класс с различными функциями, типа определения пересечения квадратов. ImageStorage — класс создания канвасов для хранения изображений

Конфигурация

Очень хочется, чтобы у схемы были гибкие настройки. Для этого создадим такой нехитрый метод конфигурации объекта:

Теперь можно конфигурировать объекты так:

Это удобно: нужно только создать сеттеры у объектов, которые могут не просто установить значение в свойство, но и свалидировать или изменить значение при необходимости.

Хранение и отображение объектов

Первым делом нужно научиться просто размещать объекты на схеме. Но для этого нужно понять, какие объекты сейчас находятся в зоне видимости. Мы договорились, не перебирать постоянно все объекты, а построить дерево поиска.

Для построения дерева нужно разделять схему зала на части, записывать одну часть в левый узел дерева, а другую — в правый. Ключом узла будет являться прямоугольник, ограничивающий область схемы. Т.к. объект представляет плоскость, а не точку, он может оказаться сразу в нескольких узлах дерева — не страшно. Вопрос: как разбивать схему? Для достижения максимального профита, дерево должно быть сбалансировано, т.е. количество элементов в узлах должно быть примерно одинаковое. В нашем случае можно особо не заморачиваться, т.к. обычно объекты на схеме расположены практически равномерно. Просто делим пополам поочередно по ширине и высоте. Вот такое разбиение будет для дерева глубиной 8:

TreeNode — класс узла дерева знает своего родителя, своих детей и координаты квадрата содержащихся в нем объектов:

Теперь нужно рекурсивно создать дерево, заполняя его объектами. Это выглядит так: берем очередной узел, если глубина меньше установленной в конфигах — разбиваем объекты этого узла по разделяющей линии и создаем два дочерних узла, помещаем в них объекты.

Теперь нам очень просто найти желаемые объекты как по квадрату, так и по координатам. Здесь уже есть поправки на скролл и зум, про них чуть ниже поговорим.

Еще мы можем легко определить, какие объекты лежат в зоне видимости и требуют отрисовки без перебора всех объектов:

Масштабирование

Зум — это просто. У canvas есть метод scale, который трансформирует сетку координат. Но нам нужно не просто зумить, нам нужно зумить в точку, в которой находится курсор или центр.

Для зума в точку нужно всего лишь знать две точки: старый центр зума (при старом масштабе) и новый, и добавить их разницу к смещению схемы:

Но мы же хотим поддерживать тач устройства, поэтому нужно обработать движение двух пальцев и запретить нативный зум:

В айфонах 6 и старше была найдена неприятная особенность: при быстром двойном касании возникал нативный зум с фокусом на канвасе, причем в таком режиме канвас начинал жутко тормозить. На viewport никакой реакции. Лечится так:

Класс, отвечающий за масштабирование: src/managers/ZoomManager.ts

Перемещение схемы

Я решил просто прибавлять к координатам смещение слева и сверху. Правда есть метод translate, который смещает сетку координат. На момент разработки он показался мне не очень удобным, но, возможно, я им еще воспользуюсь. Но это все мелочи, больше всего нас интересуют вопросы обработки событий.

Некоторые люди при клике могут немного смещать курсор, это мы должны учесть:

Оптимизация

Вот вроде бы уже есть рабочий вариант схемы, но нас ждет неприятный сюрприз: наша схема сейчас быстро работает только в хроме. Проблема в том, что при перемещении схемы в полном размере и зуме из этого полного размера, перерисовываются все объекты. А когда уже в масштабе помещается только часть объектов — работает нормально.

Сначала я хотел объединить ближайшие места в кластеры, чтобы место сотни объектов рисовать один при мелком масштабе. Но не смог найти/придумать алгоритм, который бы делал это за разумное время и был бы устойчивым, т.к. объекты на карте могут быть расположены как угодно.

Потом я вспомнил правило, которое написано на каждом заборе (и в начале этой статьи) при работе с canvas: не перерисовывать неизменяющиеся части. Действительно, при перемещении и зуме сама схема не изменяется, поэтому нам нужно просто иметь «снимок» схемы в n раз больше начального масштаба и, при перемещении/зуме не рендерить объекты, а просто подставлять нашу картинку, пока разрешение карты не превысило разрешение снимка. А потом уже и оставшиеся реальные объекты будут быстро рисоваться в виду своего количества.

Но эта картинка тоже должна иногда меняться. Например, при выборе места оно меняет вид и мы не хотим, чтобы выбранные места исчезали на время перемещения схемы. Перерисовывать весь снимок (в n раз больше начального размера карты) при клике дорого, но в тоже время мы можем позволить себе на снимке не сильно заботиться о пересечении объектов и обновлять только квадрат, в котором находиться измененный объект.

Таким вроде бы нехитрым способом мы очень сильно повысили скорость работы схемы.

Спасибо, что дочитали до конца. В процессе работы над схемой я подглядывал в исходники fabricjs и chartjs чтобы меньше велосипедить.

📎📎📎📎📎📎📎📎📎📎