Делаем схему выбора мест в кинозале на React: о canvas, красивом дизайне и оптимизации

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

В богатой экосистеме Тинькофф есть лайфстайл-сервисы. Купить билеты на различные мероприятия - в кино, театры, на концерты, спортивные события можно на https://www.tinkoff.ru/entertainment/, а также в мобильном приложении.

Меня зовут Вадим и я расскажу вам, как мы это делали в команде Развлечений в Тинькофф Банке.


Что нужно, чтобы купить билет в кино?

Когда вы определились с фильмом и кинотеатром, вам нужно выбрать место в кинозале - изучить красивую схему выбора мест и купить самые лучшие билеты.

дизайн схемы выбора мест
дизайн схемы выбора мест

Мы придумали три варианта реализации такой схемы.

1. Сделать все на старом добром HTML

Схема на HTML. Найдено на просторах Интернета
Схема на HTML. Найдено на просторах Интернета

Плюсы:

  • Удобно стилизовать.

  • Удобно работать в React.

  • Все доступно (A11Y).

Минусы:

  • Растет количество DOM-нод и глубина DOM-дерева (пример — на изображении выше).

  • Проблемы с производительностью при взаимодействии с пользователем (перемещение схемы).

2. Использовать SVG

Плюсы и минусы примерно такие же, как и с HTML.

Получилось найти только схему метро на SVG
Получилось найти только схему метро на SVG

3. Canvas

Стильно. Ярко. Производительно.
Стильно. Ярко. Производительно.

Плюсы:

  • Удобно стилизовать (можно нарисовать что угодно).

  • Меньше проблем с производительностью.

Минусы:

  • Не получится совместить с Server Side Rendering.

  • Проблемы с A11Y (нет «из коробки»).

Мы решили делать схему на canvas, потому что нам важно, чтобы все было красиво и с приятным UX для пользователя. Также с технической стороны у нас пропадают проблемы с глубиной DOM-дерева и количеством нод в нем. Тем более что canvas без проблем работает даже в Internet Explorer 11.

Конечно, на наше решение повлияло и то, что использовать canvas намного интереснее, чем просто работать с SVG- и HTML-решениями.

Экосистема вокруг canvas

Итак, мы отправились выбирать библиотеку для более удобной работы с canvas. Как оказалось, их существует достаточно большое количество, из самых популярных — Konva, PixiJS и Phaser.

Из этого многообразия мы выбрали PixiJS. Ключевые особенности Pixi: он быстрый, гибкий и производительный. Также наши коллеги активно рекомендовали использовать именно его.

Простой код на PixiJS выглядит так:

Простой код на PixiJS. Мы инстанцируем Pixi.Appс заданным конфигом (например, ширину, высоту, цвет фона, разрешение). Добавляем объекты на сцену (Stage в терминологии Pixi), пишем простой цикл и получаем сетку 5 × 5 из кроликов, которые вращаются вокруг своей оси — пример с официального сайта Pixi

Структура и читаемость

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

Выше не слишком крупная программа, но мы уже дошли до 100 строк кода и, просто глядя на этот код, тяжело понять, что же происходит.

Вписываем в React

Кроме сложности понимания кода возникает другой вопрос: как это вписать в парадигму React?

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

Одно из решений, которое понравилось нам, — библиотека react-pixi-fiber. Ее плюс в том, что мы пишем привычный нам JSX, а под капотом происходит взаимодействие с Pixi и мы получаем наш canvas. 

В этой библиотеке у нас уже есть обертки для всех нативных объектов Pixi. К примеру, вместо инстанцирования класса Pixi.Textмы используем react-элемент <Text />

Также есть удобное АПИ для создания своих объектов — CustomPIXIComponent

Приблизительно так теперь выглядит код для нашей схемы выбора мест. Здесь уже нет никаких инстансов Pixi, у нас обычный JSX: компоненты Stage, Container, посадочные места, привычный маппинг данных на react-компоненты.

А вот как выглядит создание своего компонента. Он немного отличается от привычных react-компонентов, но, если разобраться, по сути тут все то же самое. У нас есть ссылка на отображаемый компонент graphics и привычное слово props. Также почти привычным образом мы можем использовать обработчики событий, например ховер, клик и так далее.

Применяем все на практике

Какие у нас были вводные для отрисовки кресел?

У нас была информация в виде массива объектов. В каждом — данные, необходимые для отрисовки сиденья: размеры, координаты, номер места и ряда.

Наша задача — сделать так, чтобы в любых сочетаниях кресла смотрелись красиво.

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

Вариант с загрузкой кресла как простой текстуры мы сразу отбросили: были проблемы с отображением на retina-экранах и в целом с изменением размеров без визуальной деформации. А с SVG в то время были проблемы у PixiJS: некорректно работала подгрузка ассетов в SVG.

Поэтому мы решили сами рисовать каждое кресло.

Рисуем кресло на PixiJS

Для удобства мы разделили кресло на сектора:

A — полукруглые края подлокотников.
B — подлокотник.
C — кривая от подлокотников до спинки кресла.
D — спинка кресла.
E — верхняя часть кресла.
F — средняя часть кресла.
G — нижняя часть кресла. 

Ширина одной клетки — width / 22. 
Высота одной клетки — height / 16.
Кресло в макете у нас имеет размер 22 пикселя на 16, таким образом, каждая черточка или буковка — это пиксель в сетке.

Затем мы разделили эту сетку на зоны: подлокотники, спинка и так далее. И отрисовали все по частям, используя PixiJS и CustomPIXIComponent.

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

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

Схемы секторов

Когда вы покупаете билет на крупное мероприятие, например на хоккей или на концерт в «Олимпийском», скорее всего, сначала вы выбираете сектор, в котором хотите сидеть, а потом уже — места в этом секторе. С появлением задач по концертам нам тоже нужно было это реализовать.

От наших партнеров приходила такая схема секторов.
Собственно массив секторов в поле sectors с информацией о каждом секторе, название площадки, а также строка hallScheme, которая занимает почти 236 килобайт.

 Как оказалось, это схема секторов площадки в SVG и закодирована в base64.

Что же нам с этим делать?

Первым нашим решением было парсить этот SVG и как-то перевести на PixiJS.

Второй вариант — просто вставить это как HTML, повесить обработчики через стандартные методы.

Рассмотрев эти варианты и взвесив плюсы и минусы, мы решили пойти дальше и сделать третий вариант — парсить эту SVG и превращать ее в react-элементы. 

Выбором для парсера стал html-react-parser. Эта библиотека парсит любой валидный HTML в react-элементы. Работает как на стороне Node.js, так и на стороне браузера. Но решающим стало то, что любой элемент из оригинальной разметки можно заменить на что угодно.

Передаем всю разметку схемы в функцию parseHtmlToReact а также через опции задаем функцию, которая будет заменять элементы на наши.

Здесь уже опять привычный нам JSX и полный контроль над всем, что нужно: обработчики событий, стилизация и так далее.

Вот так теперь выглядит ВТБ Арена.
Вот так теперь выглядит ВТБ Арена.

Поговорим об оптимизации

Во время работы над схемами мы заметили, что даже для маленьких схем загрузка процессора держится на 20%, а в больших достигает 80—90%. Визуально это незаметно для пользователя, но может привести к проблемам на слабых мобильных устройствах и быстрому разряду батареи.

Используя инструменты разработчика, мы видим, что даже при простое приблизительно каждые 16 мс вызывается одна и та же таска. Сразу видно некий Ticker_tick

В замечательной документации по Pixi можно найти упоминание об этом тикере. Как понятно из описания, это некий цикл, который выполняет что-то за некий интервал времени, в нашем случае — приблизительно каждые 16 мс.

Но почему именно 16 миллисекунд?

Вспомним понятие «60 кадров в секунду» - это нужно, чтобы обеспечить плавность анимации и перемещений. Также, новые фильмы снимают в 60 кадров в секунду, что дает более плавную картинку.

Чтобы получить такую частоту обновления, нужно каждую секунду обновлять изображение 60 раз: 1000 мс ÷ 60 = 16,6666 мс.

Как раз этот цикл из класса Pixi.Tickerобеспечивает обновление 60 раз в секунду всего canvas, и у нас все плавно и красиво. В нашем случае при большом количестве объектов перерисовка выходит достаточно дорогой. При этом чаще всего схема абсолютно статичная, а плавность нужна только при взаимодействии.

Так как мы не работаем напрямую с Pixi, получить доступ к регулированию цикла обновления мы не могли.

Исходный код компонента Stage из react-pixi-fiber
Исходный код компонента Stage из react-pixi-fiber

Как видно, вся работа с Pixi происходит внутри компонента Stage от react-pixi-library. К сожалению, официальных способов от создателей react-pixi-library по работе с Ticker нет.

В нашем случае выходом стало применение опции sharedTickerдля Pixi. По сути, эта опция включает использование всех инстансов pixi-приложений общего Ticker. Общий Ticker доступен простым импортом из пакета.

Мы сразу отключили автоматический старт цикл обновлений Ticker с инициализацией приложения. А дальше мы связали это с ререндером react-компонента, так как при взаимодействии пользователя со схемой меняются props данного компонента.

Соответственно, цикл обновления запускается, только когда нужно. В остальных случаях canvas статичен, выглядит как картинка и не нагружает ресурсы.

Пока мы все это изучали, обнаружили, что у Pixi на Github есть целая wiki, где очень много интересной информации:

  • Gotchas

  • Cоветы по улучшению производительности

  • Различные трюки

Забавно, что на оффициальном сайте Pixi ссылку на эту wiki не найти.

Главный совет по оптимизации заключается в том, что инстансы объектов Pixi.Graphics стоят дорого и не кэшируются, в отличие от текстур, спрайтов и так далее. А наши кресла, как сложные объекты, как раз и являются инстансами Pixi.Graphics.

Выводы

Какие выводы из этого всего можно сделать? 

  1. Чем меньше оберток — тем более гибко мы можем оптимизировать приложение.

  2. Работа с canvas отличается от обычных рутинных задач.

  3. Pixi заточен под более интерактивные вещи, например игры.

  4. При разработке желательно сразу иметь большой объем данных. Например, в нашем случае надо было сразу получить схему какого-нибудь огромного концертного зала вместо зала кинотеатра.

Источник: https://habr.com/ru/company/tinkoff/blog/535046/


Интересные статьи

Интересные статьи

Всем привет, меня зовут Паша, я дизайнер и сегодня расскажу о том, почему решил переехать на работу в Испанию, как живется в Мадриде, в чем особенности рабочей культуры в стране, ско...
Сегодня подвели с женой итоги моего первого года без работы. Последняя, на данный момент, моя официальная должность — заведующий лаборатории по разработке прикладного программного обеспечения. ...
История по оптимизации образов для java приложений началась с выхода статьи в блоге спринга — Spring Boot in a Container. В ней обсуждались различные аспекты по созданию docker образов для spring...
Уже три года я живу и работаю в Германии. В декабре прошлого года в нашем кабинете перегорела одна из ламп дневного света. Но не просто перестала светить, а как это часто бывает у люминесцентных ...
Практически все коммерческие интернет-ресурсы создаются на уникальных платформах соответствующего типа. Среди них наибольшее распространение получил Битрикс24.