В богатой экосистеме Тинькофф есть лайфстайл-сервисы. Купить
билеты на различные мероприятия - в кино, театры, на концерты,
спортивные события можно на https://www.tinkoff.ru/entertainment/,
а также в мобильном приложении.
Меня зовут Вадим и я расскажу вам, как мы это делали в команде
Развлечений в Тинькофф Банке.
Что нужно, чтобы купить билет в кино?
Когда вы определились с фильмом и кинотеатром, вам нужно выбрать
место в кинозале - изучить красивую схему выбора мест и купить
самые лучшие билеты.
дизайн схемы выбора мест
Мы придумали три варианта реализации такой схемы.
1. Сделать все на старом добром HTML
Схема на HTML. Найдено на просторах Интернета
Плюсы:
-
Удобно стилизовать.
-
Удобно работать в React.
-
Все доступно (A11Y).
Минусы:
2. Использовать SVG
Плюсы и минусы примерно такие же, как и с HTML.
Получилось найти только схему метро на SVG
3. Canvas
Стильно. Ярко. Производительно.
Плюсы:
Минусы:
Мы решили делать схему на canvas, потому что нам важно, чтобы
все было красиво и с приятным UX для пользователя. Также с
технической стороны у нас пропадают проблемы с глубиной DOM-дерева
и количеством нод в нем. Тем более что canvas без проблем работает
даже в Internet Explorer 11.
Конечно, на наше решение повлияло и то, что использовать canvas
намного интереснее, чем просто работать с SVG- и
HTML-решениями.
Экосистема вокруг canvas
Итак, мы отправились выбирать библиотеку для более удобной
работы с canvas. Как оказалось, их существует достаточно большое
количество, из самых популярных Konva, PixiJS,
Fabric.js и
Phaser.
Из этого многообразия мы выбрали PixiJS. Ключевые особенности
Pixi: он быстрый, гибкий и производительный. Также наши коллеги
активно рекомендовали использовать именно его.
Простой код на 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%, а в больших достигает
8090%. Визуально это незаметно для пользователя, но может привести
к проблемам на слабых мобильных устройствах и быстрому разряду
батареи.
Используя инструменты разработчика, мы видим, что даже при
простое приблизительно каждые 16 мс вызывается одна и та же таска.
Сразу видно некий Ticker_tick
В замечательной документации по Pixi можно найти
упоминание об этом тикере. Как понятно из описания, это
некий цикл, который выполняет что-то за некий интервал времени, в
нашем случае приблизительно каждые 16 мс.
Но почему именно 16 миллисекунд?
Вспомним понятие 60 кадров в секунду - это нужно, чтобы
обеспечить плавность анимации и перемещений. Также, новые фильмы
снимают в 60 кадров в секунду, что дает более плавную картинку.
Чтобы получить такую частоту обновления, нужно каждую секунду
обновлять изображение 60 раз: 1000 мс 60 = 16,6666 мс.
Как раз этот цикл из класса Pixi.Ticker
обеспечивает
обновление 60 раз в секунду всего canvas, и у нас все плавно и
красиво. В нашем случае при большом количестве объектов перерисовка
выходит достаточно дорогой. При этом чаще всего схема абсолютно
статичная, а плавность нужна только при взаимодействии.
Так как мы не работаем напрямую с Pixi, получить доступ к
регулированию цикла обновления мы не могли.
Исходный код компонента 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, где очень много интересной информации:
Забавно, что на оффициальном сайте Pixi ссылку на эту wiki не
найти.
Главный совет по оптимизации заключается в том, что инстансы
объектов Pixi.Graphics стоят дорого и не кэшируются, в отличие от
текстур, спрайтов и так далее. А наши кресла, как сложные объекты,
как раз и являются инстансами Pixi.Graphics.
Выводы
Какие выводы из этого всего можно сделать?
-
Чем меньше оберток тем более гибко мы можем оптимизировать
приложение.
-
Работа с canvas отличается от обычных рутинных задач.
-
Pixi заточен под более интерактивные вещи, например игры.
-
При разработке желательно сразу иметь большой объем данных.
Например, в нашем случае надо было сразу получить схему
какого-нибудь огромного концертного зала вместо зала
кинотеатра.