В богатой экосистеме Тинькофф есть лайфстайл-сервисы. Купить билеты на различные мероприятия - в кино, театры, на концерты, спортивные события можно на https://www.tinkoff.ru/entertainment/, а также в мобильном приложении.
Меня зовут Вадим и я расскажу вам, как мы это делали в команде Развлечений в Тинькофф.
Что нужно, чтобы купить билет в кино?
Когда вы определились с фильмом и кинотеатром, вам нужно выбрать место в кинозале - изучить красивую схему выбора мест и купить самые лучшие билеты.
Мы придумали три варианта реализации такой схемы.
1. Сделать все на старом добром HTML
Плюсы:
Удобно стилизовать.
Удобно работать в React.
Все доступно (A11Y).
Минусы:
Растет количество DOM-нод и глубина DOM-дерева (пример — на изображении выше).
Проблемы с производительностью при взаимодействии с пользователем (перемещение схемы).
2. Использовать SVG
Плюсы и минусы примерно такие же, как и с HTML.
3. Canvas
Плюсы:
Удобно стилизовать (можно нарисовать что угодно).
Меньше проблем с производительностью.
Минусы:
Не получится совместить с Server Side Rendering.
Проблемы с A11Y (нет «из коробки»).
Мы решили делать схему на 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%, а в больших достигает 80—90%. Визуально это незаметно для пользователя, но может привести к проблемам на слабых мобильных устройствах и быстрому разряду батареи.
Используя инструменты разработчика, мы видим, что даже при простое приблизительно каждые 16 мс вызывается одна и та же таска. Сразу видно некий Ticker_tick
В замечательной документации по Pixi можно найти упоминание об этом тикере. Как понятно из описания, это некий цикл, который выполняет что-то за некий интервал времени, в нашем случае — приблизительно каждые 16 мс.
Но почему именно 16 миллисекунд?
Вспомним понятие «60 кадров в секунду» - это нужно, чтобы обеспечить плавность анимации и перемещений. Также, новые фильмы снимают в 60 кадров в секунду, что дает более плавную картинку.
Чтобы получить такую частоту обновления, нужно каждую секунду обновлять изображение 60 раз: 1000 мс ÷ 60 = 16,6666 мс.
Как раз этот цикл из класса Pixi.Ticker
обеспечивает обновление 60 раз в секунду всего canvas, и у нас все плавно и красиво. В нашем случае при большом количестве объектов перерисовка выходит достаточно дорогой. При этом чаще всего схема абсолютно статичная, а плавность нужна только при взаимодействии.
Так как мы не работаем напрямую с Pixi, получить доступ к регулированию цикла обновления мы не могли.
Как видно, вся работа с 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 заточен под более интерактивные вещи, например игры.
При разработке желательно сразу иметь большой объем данных. Например, в нашем случае надо было сразу получить схему какого-нибудь огромного концертного зала вместо зала кинотеатра.