company_banner

Делаем схему выбора мест в кинозале на 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, 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, получить доступ к регулированию цикла обновления мы не могли.

Исходный код компонента 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, где очень много интересной информации:

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

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

Выводы

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

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

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

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

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

TINKOFF
IT’s Tinkoff — просто о сложном

Comments 30

    +2
    >>Konva, PixiJS и Phaser
    Можно еще упомянуть FabricJS (есть 4 статьи на хабре). Делал на нем графический калькулятор — для новичков в целом неплох. При выборе как раз отказался от Pixi, что также показалось, что заточен под игры.
    FabricJS:
    +: полно разных ответов/вопросов, потихоньку развивается, есть поясняющие статьи.
    -: документация «не очень».
      0
      Спасибо, интересная либка! добавил упоминание
      +3

      Вставляйте код текстом, пожалуйста!

        0

        Обязательно исправлюсь)

        0
        А почему не WebGL, разве там не должно быть еще больше производительность?
          0

          Pixi автоматически использует WebGL в современных браузерах. Canvas2d в старых.
          Но опять же, такое более актуально для игр где много чего происходит на экране. Или для красивых 3D анимаций. Для статичных схем как у нас вся мощь WebGL не раскрывается. Поэтому подкапотные оптимизации от Pixi — скорее приятный бонус)

          0
          Минусы:
          Не получится совместить с Server Side Rendering.

          Почему это минус?
          Выбор места на канвасе — это не контент. Это функционал. Ему не нужно индексироваться.
            0

            Привет. Сейчас поясню. Для нас тут дело не в индексации поисковиками.


            Есть случаи, когда пользователь сразу открывает страницу где должна быть схема. И при этом схема сразу во вьюпорте. Но, так как SSR на неё нет, то ему приходится ждать прогрузки js-кода и инициализации.


            Это, конечно, для кого-то и не минус, но мы на tinkoff.ru стараемся по максимуму отрисовывать на сервере то, что пользователь может увидеть сразу. Чтобы у пользователя был более плавный UX. Поэтому я и записал это в минусы.

              0
              Так что мешает на сервере нарисовать все на HTML/SVG, а когда загрузится JS, то зареплейсить HTML/SVG на Canvas? Если уж вы так сильно заморочены на этом и для вас это минус.
                0
                что мешает

                К сожалению, ресурсы разработки не бесконечные. Приходится искать компромиссы.
                В идеальном мире можно было сразу все идеально сделать)

                Возможно, это будет одной из доработок на будущее ;)
                  0
                  К сожалению, ресурсы разработки не бесконечные.

                  Ах ну понятно))
                  Значит SSR для вашего кейса все таки не нужен. И его отсутствие минусом то не является)
                    0
                    Я не понимаю как то, что мы решили не делать SSR для схемы должно отменять этот «минус»? По моему мнению, эти факты между собой не связаны и не взаимоисключающие. Можешь пояснить подробнее?
                      0
                      Ну смотрите, есть минус, как бы минус настоящий, то есть вещь серьезная, ощутимая, реальный недостаток.
                      А есть «минус», ну как бы такое, высосанное из пальца, и названо минусом так чисто ради придирки.

                      Допустим есть у меня машина и вот лично мой экземпляр кушает на 0.1 литра на 100 км больше, чем у остальных, ну вот прямо где-то просочился малюсенький такой дефект, так вот, это можно назвать «минусом» который именно в кавычках и высосанный из пальца. И если переложить ещё сильнее на вашу статью, то при этом, она быстрее чем у всех остальных скажем на 50% разгоняется, двигатель работает ровнее, масло не поджирает, но есть вот у нее «минус», на 0.1 литра она все таки больше съедает на 100ку.

                      И вот дилема, можно ли называть это минусом вообще?) Или же все таки это минус чисто по приколу, не имеющий отношения к реальному недостатку?)

                      А теперь представьте, что она бы теперь кушала в 3 раза больше бензина чем остальные, но при этом все так же на 50% разгонялась быстрее, и вот тут уже этот минус ощутим и осязаем по настоящему, т.к. 10 литров и 30 литров очень большая разница, даже с учетом выигрыша по разгону.

                      Ах да, вы же написали
                      Не получится совместить с Server Side Rendering.

                      Но самом деле то получается, ещё как, просто вам лень) Потому что есть SSR в вашем кейсе или нет, роли реальной вообще никак для пользователя не сыграет.
                        0

                        Я понял, с вашей точки зрения это не минус, но сравнения с машинами меня все равно не убедило убрать это из статьи.


                        просто вам лень

                        Я не знаю как тут на Хабре принято. Но это уже явно переход границы профессионального общения.


                        Если хочется ещё поговорить, я предлагаю созвониться и поболтать голосом)

                          –1
                          меня все равно не убедило убрать это из статьи.

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

                          Это утверждение не верно. Почему это вас не убеждает убрать это из статьи? Или изменить текст, на «совместить с Server Side Rendering можно в паре с HTML/SVG/Canvas to PNG и при загрузке JS реплейснуть на canvas для большей производительности и отзывчивости»
                          Но это уже явно переход границы профессионального общения.

                          Вас обидело слово «лень»? Ведь вам же аргументы приводят вполне конкретные, а не абстрактные взятые из потолка, а вы «переходит границы». Если не воспринимаете конструктивную критику зачем пишете статьи которые можно комментировать и критику высказывать?
                            +1

                            Человек делится результатами своей работы, а вы даете личную оценку человеку, это и есть нарушение этики профессионального общения. Личная оценка человека это уже точно не конструктивная критика к этой статье

                              –6
                              Какие мы нежные и находимся в королевском Английском обществе с зашкаливающем этикетом.
                              Так может лучше вообще статьи не писать и вообще ни с кем не разговаривать? А то вдруг кто-то не будет восхищаться тем, что ты пишешь или говоришь.
                                0
                                Специально хотите кого-то задеть? В чем ваша проблема? Не пишите комментарии тогда, а я не буду писать статьи. На том и порешим.
                    0
                    Можно на коленке «накостылять» SSR canvas в png безголовым браузером, как временное решение — особых человекочасов не понадобится, а потом уже написать генератор SVG по тем данным, по которым у вас рисуется зал.
                      0
                      Да, конечно, можно! Но, мы были ограничены в сроках и ресурсах, поэтому рассматривали больше именно варианты «из коробки», нежели собственную разработку.

                      Но, движения в сторону новых схем есть. Решим проблемы и обязательно напишем про это ещё одну статью)
              0
              Не получится совместить с Server Side Rendering

              Разве в такого рода специфических страницах/проектов это так необходимо и можно отнести к минусам?
              0

              А как в итоге задачу решили? Что видит пользователь, пока загружается pixi и react-pixi-fiber (это уже 150Кб минимум), плюс еще ваш код?

                0
                JS-код схемы (код компонента + либы) мы получаем отдельным асинхронным чанком и у нас отдельные бандлы на разные страницы, поэтому пока схема не понадобится для показа — браузер не грузит этот код. Пользователь видит скелетон схемы.
                +3

                За статью конечно спасибо! Давно ждал нечто подобного, что кто-то возьмет, и сделает это: скрестит React и Canvas. В добавок тема отрисовки концертных залов мне знакома (фулстек разработчик Myslo-кассы).


                Года 4 назад нужно было создать инструмент, для продажи билетов. Тоже был выбор SVG или canvas. Делать на чистом HTML не рассматривал как вариант, потому что там ожидали проблема с масштабированием. В тот момент как раз Павел Дуров устраивал как раз конкурс на отрисовку графиков, и там как раз в чатах была борьба, какой механизм отрисовки лучше: SVG или canvas. SVG проиграл. Поэтому я выбрал canvas.


                Я выбрал делать на canvas в нативе (canvas 2D). На тот момент образцом для подражания была афиша яндекса с монстуозной схемой. У нее был только один минус -все очень сильно тормозило. Одним из требований было то что это все должно работать очень быстро. В итоге, выбор сектора были сделан на SVG, а выбор мест на canvas (пример можно посмотреть тут как работает схема зала).


                Нюансы при отрисовки:


                1. Не отрисовывать места которые выходят за пределы области canvas
                2. Не делать лишних перерендеров, перерендер выполняется только по внешнему событию: перемещение схемы, изменение масштаба, изменение размера страницы.
                3. FPS при отрисовки схема должен быть 60 кадров в секунду!

                Пожалуй FPS было самым главным требованием! Да, схема зала получилась очень простой, но отрисовка мест происходит очень быстро.


                Выводы которые сделал я:


                1. SVG — это максимум 1000 элементов в DOM дереве — все остальное медленно.
                2. canvas — отрисовка в 5000 областей за раз не вызывает просидания FPS. Все что больше требует принятия дополнительных мер: дробить цикл на кадры, кеширование областей.
                3. Избегать отрисовки больше 1000 мест на одном экране.

                Я решил посмотреть как это работает на вашем сайте, на примере этого мероприятия. Сразу пройдусь по багам которые заметил:


                1. Шаг изменения масштаба зала происходит очень резко: есть вариант с выводом всего зала и сразу с супер увеличением.
                2. Из 2 вытекает проблема что если нужно выбрать место с краю, нужно drug&drop-ом двигать схему зала. И тут конечно заметно просидание FPS по отрисовки на Core i5! По ощущению, ну кадров 5 в секунду (Firefox, в хроме лучше).
                3. Меняем размер страницы и все зависает, а потом загружается начальная страница.
                4. Запаздывающий эффект наведения.
                5. Возможность выбора мест на начальной схеме зала без увеличения масштаба. Каждое место на экране занимает ровно 3 пикселя. Я не думаю что это удобный способ выбора мест. Тем более в этом режиме однозначно будут просидания в производительности

                Низкий FPS

                Запаздывающий эффект наведения

                Выбор мест на начальной схеме зала

                Выводы:


                1. В целом конечно видно, что все еще очень сырое. Вчера выкатили, а сегодня написали статью. Этот комментарий я хотел написать еще вчера, но я просто не мог не посмотреть как работает схема потому что она просто падала, с ошибкой связаной с booking. Схема зала не работала не в одном из опробованных браузеров.
                2. Мне кажется Pixi нужно менять на что-то другое (возможно свое). Возможно стоит делать отрисовку только на canvas 2D, раз 3D не используется. Canvas отрисовку нужно хорошенько кешировать, тогда она будет работать быстро.
                3. За React нужно конечно тщательно следить, потому что с ним очень просто наделать кучу лишних перерендеров.
                4. На счет максимального кол-ве мест в зале — нужно было выбирать олимпийский — и пытаться отрисовать их 3 на одном экране. Здесь конечно, мне кажется, нужно использовать какой то механизм создания контура секторов исходя из координат мест, и на большой схеме показывать только эти контуры, а при увеличении уже подгружать места для секторов. Тогда не будет проблемы отрисовки больше 1000 мест на одном экране и показ первого экрана станет быстрее. В своей реализации, я пытался тоже сделать механизм автоматического создания контуров, но потом счет это слишком сложной задачей.
                  0
                  У меня всё это дело вообще очень долго прогружается и нет возможности увеличить/уменьшить.
                    0
                    Напиши в личку что за браузер и ОС.
                    0
                    Спасибо за огромный комментарий! Кажется, тебе стоит и самому написать статью про такие схемы)

                    И спасибо большое за баг-репорты и советы!
                    0

                    Сам столкнулся с canvas в реакте. Нужно было сделать линейный график для криптовалют в реальном времени. В принципе сделал его на нативном канвасе, но переодически выскакивают проблемы производительностью возникают. Тоже задумался прикрутить библиотеку какую. Можете посоветовать?

                      0
                      Привет. Я не эксперт в области либ по канвасу)
                      Можешь посмотреть приведенные в статье, попробовать накидать по прототипу на каждой и сравнить. Линейный график — достаточно распространенная визуализация, возможно, у каждой либы есть уже готовый компонент/код для такого.

                      Вообще, раз уже есть написанное решение, я бы посоветовал сначала попрофилировать код. Возможно, всплывут узкие места, которые связаны не с самой отрисовкой на канвасе, а с чем нибудь другим.

                    Only users with full accounts can post comments. Log in, please.