Анимация уже давно стала стандартным инструментом в дизайне и способна выполнять самые разные задачи: обеспечивает плавность и приятную отзывчивость — как в современных ОС (например, дизайн-система Android Material 3 или Apple, которые используют принципы Springs), транслирует голос бренда и становится частью айдентики (CashMoney, Instagram, Plex), добавляет продуктовым сценариям эмоциональность и значимость. Недавно и перед нашей командой встала задача «оживить» некоторые из них.
Меня зовут Саша Гончаров, я моушен-дизайнер в 2ГИС. По просьбе продуктовых дизайнеров я подключился к работе над анимацией интерфейсов. В этой статье вместе с разработчиком Сергеем Львовым расскажем, на что стоит обращать внимание при создании интерфейсных анимаций, какие технические нюансы важно учитывать и как мы достигаем желаемых эффектов. Материал будет полезен моушен-дизайнерам, которые хотят работать с интерфейсами, а также разработчикам, интересующимся особенностями рендеринга объектов на карте.
Общие вводные
Давайте бегло пробежимся по фреймворкам, с которыми можно работать в моушен-дизайне. Известных вариантов не очень много — это Lottie, GSAP, Framer и Rive. Из-за простоты интеграции, возможности делать сложные комплексные анимации в привычном для моушен-дизайнеров софте и хорошей производительности (при должной оптимизации), наш выбор пал на Lottie.
Если же вашему продукту просто необходимо быть ярким и двигающимся — присмотритесь к Rive. У него довольно широки возможности, и есть крутые кейсы использования, например, от Duolingo.
У Lottie есть свои минусы. Моушен-дизайнер, двигающий шейпы в Adobe After Effects, вынужден работать с самыми примитивными инструментами — с шейпами и градиентами. Примерно 90% функционала софта оказывается не у дел.
Едем дальше. У анимации всегда должна быть цель. И я руководствуюсь двумя критериями: утилитарность и эмоциональность.
1. Утилитарный сценарий — это про функциональность. Чаще всего он базовый — ради него и скачивают приложение. Ничто не должно отвлекать внимание пользователя, а анимация помогает пользоваться продуктом.


2. Эмоциональный сценарий — это про придание событию особого значения. Момент, когда хочется наградить пользователя проработанным, эффектным и редким контентом за достижение.


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


Часть для разработчиков
Передаю слово моему коллеге Сергею Львову. Он расскажет про выбор библиотеки растеризации анимированных элементов на карте и оптимизацию памяти.
На момент внедрения Lottie в карту библиотека rlottie была, пожалуй, единственным растеризатором, написанным на С/С++. Компактная, без зависимостей, кроссплатформенная — её не составило особого труда внедрить в существующий проект и завести на всех поддерживаемых платформах. Отсутствие поддержки текста не было проблемой: у нас просто не было анимированных иконок с текстом на тот момент времени. А вот что было проблемой — rlottie позволяет растеризовывать иконки только на CPU. Это мучительно медленно, поэтому не может быть и речи о том, чтобы растеризовывать кадры анимации «на лету». Не хотелось, чтобы fps карты просел из-за того, что на экране проигрывается какая-то особенно сложная анимированная иконка. Поэтому мы стали использовать схему, когда все кадры анимации растеризуются заранее и складываются в текстуры, которые называют «атласы». Выглядит такая текстура примерно так:

В современных графических API есть возможность аллоцировать гигантские текстуры, но наш графический движок ориентирован на широкий круг устройств, в том числе на весьма старые модели телефонов, поэтому мы аллоцируем атласы скромного размера — не более 1024x1024 текселей. Как можно заметить, на текстуру из схематического рисунка выше влезло всего 6 кадров анимации. Обычно анимации длятся гораздо больше 6 кадров, поэтому нам понадобится больше текстур, чтобы сохранить все кадры анимации. Поэтому схематично полностью растеризованная Lottie-иконка будет выглядеть так:

Это чертовски много памяти! Если предположить, что мы используем атласы размером 1к, то каждая текстура занимает 4 Мб видеопамяти. Разумеется, иконки бывают разного размера, но в среднем на одну иконку уходит примерно 4–5 атласов, то есть около 20 Мб. А на устройства с высоким DPI — и того больше!
Не меньшая проблема — это то, что мы не можем взять и так вот просто изменить размер иконки на экране. Допустим, мы хотели сначала рисовать иконку размером 200x100 пикселей, а потом передумали и решили рисовать иконку размером 210x105 пикселей. А атласы-то у нас уже нарисованы с расчётом на размер 200x100! Можно, конечно, растянуть растеризованные кадры, используя линейную фильтрацию, но эксперименты показали, что результат получается неудовлетворительный (по крайней мере для нашего случая). Картинка получается слишком мыльной. Поэтому приходилось выкидывать готовые атласы и рисовать новые, с новым размером иконки.
Очевидно, что так продолжаться не могло — иконки нужно было рисовать на стороне GPU. Как раз в это время появились библиотеки, которые могут это делать. Если вы ориентируетесь на C/C++ и вам нужно рисовать Lottie-анимации на GPU, то сейчас у вас есть из чего выбрать: это либо SKIA, которая используется в том числе и в Google Chrome, либо активно развивающаяся и весьма интересная библиотека ThorVG, у которой также внушительный список внедрений. Примечательно, что обе библиотеки умеют рисовать не только Lottie, но и SVG, что весьма приятно и удобно.
Если вырезать из Skia всё лишнее и оставить только рисовалку для SVG и Lottie, то размер бинаря вырастет примерно на 3 Мб — и это без учёта поддержки GPU и зависимостей наподобие freetype, ICU и HarfBuzz. ThorVG же позиционируется как легковесная альтернатива: он весит в несколько раз меньше. С другой стороны в Skia есть готовая поддержка всех трёх графических API, которые нам были нужны: OpenGL, Vulkan и Metal. Это и стало решающим фактором при выборе библиотеки. Сейчас, оглядываясь назад, кажется, что стоило бы хотя бы попробовать ThorVG — дописать поддержку недостающих render API не так сложно.
Итак, теперь, когда появилась возможность рисовать кадры Lottie-анимации «на лету» прямо на GPU, кажется, можно избавиться от атласов. Правда ведь? Правда? В целом — да, но на деле лучше атласы оставить, пусть и не в том виде, в каком они были раньше.
Суть вот в чём: FPS у Lottie-анимаций обычно ниже, чем у основного приложения. Допустим, у иконки 30 FPS, а у карты — 60 (нам хотелось бы, чтобы там было 60). Тогда, если рисовать иконку сразу на экран, то каждый второй раз мы будем рисовать один и тот же кадр. Не лучше ли его закэшировать в атлас?
Или представим, что анимация закончилась, но нам надо отображать на экране последний кадр Lottie-анимации ещё какое-то время. Не хотелось бы рисовать один и тот же кард иконки снова и снова, особенно, если это какая-то сложная иконка. Поэтому мы оставили схему, при которой иконка сначала рисуется в атлас, а из атласа — на экран.
В таком случае атлас выглядит немного иначе:

На одном атласе уживаются кадры от разных иконок! Когда нужно нарисовать какую-то иконку — сначала ищем свободное место на текстуре. Обычно всё помещаются на одну текстуру, но в некоторых случаях требуется ещё 2–3 дополнительные. В любом случае, это на порядок меньше памяти, чем раньше, потому что в атласе хранится только один кадр иконки — тот, который нужен прямо сейчас.
Когда иконка исчезает с экрана, то место в атласе помечается «свободным», и его может занять уже другая иконка. Чтобы быстро находить свободное место подходящего размера на текстуре, используется один из алгоритмов texture packing. В интернете есть статья от команды Mozilla — Improving texture atlas allocation in WebRender — где они подробно рассказывают, как искали оптимальный алгоритм для похожей задачи.
Важно, что нам нужен именно динамический алгоритм, поскольку мы то добавляем, то удаляем что-то с текстуры. Такой алгоритм не очень компактно упаковывает иконки в текстуру, но зато работает быстро. Немного офтопа: когда речь заходит о компактности упаковки атласов, то мне всё время вспоминается доклад от команды EA games, в котором ребята рассказывают, как добились плотности упаковки атласа в 96% — потрясающий результат!
Стоит упомянуть, что у растеризации иконок в промежуточную текстуру есть ещё одно преимущество: простота интеграции графических движков друг с другом. У нас свой графический движок, который развивался годами, у SKIA — свой (на самом деле их там два: старенький Ganesh и более современный Graphite). У нас не было и нет никакого желания объяснять SKIA, как работает наш движок, и наоборот — переделывать свой движок в угоду SKIA. Гораздо проще позволить им работать по очереди.
Сначала запускаем SKIA и говорим: «Положи результат своей работы вот в эту область памяти на GPU». Затем запускается наш движок, которому мы говорим: «Иконки лежат вот здесь, возьми их, они уже отрисованы». В итоге движкам не обязательно знать друг о друге.
Часть для моушен-дизайнеров
Дели на два
Довольно часто моушен-дизайнеры переходят в интерфейсы из сфер, где анимация должна быть яркой и эффектной: реклама, игры, развлекательные интерактивные проекты и многое другое. И когда перед таким специалистов встаёт задача анимации в интерфейсах, он по привычке продолжает жить по лекалам прошлых форматов. Мне захотелось выработать для себя правило, которое быстрее бы приближало меня к результатам, пригодным для использования в интерфейсах. И, мне кажется, такое правило было найдено. Звучит оно так: «Дели на два».
Есть 12 принципов анимации, но в интерфейсах они работают не в полную силу. Поэтому условные эффекты, такие как Squash and Stretch и Anticipation, можно смело делить как минимум на два. Нужно соблюсти баланс между функцией и эффектностью.

Разрабатывай стандарты
Интерфейсы — очень чувствительная среда, которая нуждается в предсказуемости, а значит в единообразии элементов и их поведении. Если речь идёт про один компонент, стоит разработать для него стандарт длительности, основных «ударных» точек, который будет поддерживать единообразие и при этом выполнять задачу контраста между разными вариантами компонента.

Учитывай контекст
Жизнь анимаций не заканчивается во вьюпорте софта, будь то After Effects или Rive. Нужно обязательно примерять результаты на прототипах и согласовывать стандарты с разработчиками. Под стандартами я подразумеваю размер, разрешение, длительность, ориентацию и прочее.
Пример: когда мы разрабатывали стандарт для «хвостов» в функцию «Друзья на карте», я сделал для себя вертикальную композицию — так было проще видеть все элементы и не крутить головой из стороны в стороны (шея ещё пригодится), хотя на хвосты привычнее смотреть в горизонтальном виде. Сначала хвосты были сделаны из абстрактных форм, что позволяло не акцентировать внимание на ориентацию объектов в пространстве, но когда появились первые хвосты с не абстрактными объектами, то стало понятно, что нужно учитывать, что анимация будет проигрываться «вверх ногами» и смотреться она должна хорошо и в таком положении. По той же причине мы стараемся не использовать текст в хвостах, не факт что его прочитают, если пользователь будет ехать «вверх ногами».
Также было важно соблюсти характер движения. В этом случае работает логика «пламени»: движения объектов происходят равномерно без явных рывков, постоянно и бесшовно.


Вектор > Растр
Lottie поддерживает растровые ассеты и позволяет включать их прямо в .json-файл. Но нужно учитывать, что площадь растрового ассета должна быть небольшой, иначе есть риск столкнуться с неприятными просадками и фризами. Таким образом нам пришлось перерисовывать все погодные эффекты в векторный вид. iOS чувствовал себя нормально, но старые устройства на Android не тянули.

Также нужно договариваться с продактами и иллюстраторами, если они хотят сложной анимации. С растром моушен-дизайнер не сделает ничего, а с вектором — почти что угодно. По схожей схеме пришлось перерисовывать эмодзи для нашей маленькой пасхалки во время экстремальных 11-балльных пробок.

Null — это больше, чем Object
Часто ассеты, которые нужно задвигать, приходят к аниматору только в виде финального кадра. С этим можно жить, но для ускорения работы полезно получать ассет в дефолтном положении, с прямым ракурсом, без искажений. Или же с контрольными позами и точками, для удобства перехода между ними.
Null Objects — наши лучшие друзья. Можно назначить их на любые трансформации: от Scale для иллюзии трёхмерного поворота до элементарных наклонов шейпов для придания большей любви и жизни. Классно, что поддерживается результат работы плагина Points To Null Objects. В некоторых ситуациях он незаменим, особенно тогда, когда две точки двух разных шейпов нужно держать в одном месте.

Упрощай, запекай, удаляй
Lottie требует оптимизации для использования в продукте. На размер влияют количество точек в шейпах, количество ключевых кадров в анимации, размер растровых ассетов в композициях, а также наличие масок. Можно использовать Expressions, но лучше сразу запекать ключи через выделение заскриптованного параметра и прожатия Keyframe Assistant → Convert Expression To Keyframes. Если вы анимируете с настройками 50 FPS, то на момент запекания ключей стоит поменять этот параметр на 25 FPS, а потом вернуться назад до 50 при необходимости. Так можно убрать половину лишних ключевых кадров, которые никак не повлияют на саму анимацию.

Градиент — не просто цвет
Стилизация шейпов в Lottie — нетривиальная задача, особенно с учётом того, что использовать нужно только самые примитивные инструменты. Когда сталкиваешься с подобными ограничениями, порой удивляешься, насколько изобретательно можно использовать такой простой инструмент как градиентная заливка.
Градиенты фактически становятся главным инструментом для формирования иллюзий объёма, теней, эффектов и многого другого. Условный Drop Shadow вполне можно организовать градиентами. Также ими можно заменить некоторые эффекты Layer Styles, например, Inner Shadow — кстати, именно так мне пришлось сделать в нашем онбординге.
Количество градиентов в слое может быть двузначным, они мало влияют на вес и позволяют достигать классного результата. Сложно? О, да. Практически нет слоя, на котором не надо будет двигать точки начала и окончания градиентной растяжки, но чего только не сделаешь ради классной анимации.

Обманывай зрителя (немножко)
Технические ограничения часто дают возможности для разных ухищрений. Например, наш онбординг, когда пользователь после скачивания приложения открывает приложение в первый раз. Продакты хотели, чтобы мы сделали его анимированным и ярким и при этом задействовать механику сторис, которая уже есть в нашем приложении, чтобы не сильно тратить ресурсы разработки.
В то же время мне хотелось другого жанра. Сторис — понятная история, и она подразумевает быстрый и резкий переход от слайда к слайду, а в онбординге хочется плавности, бесшовности и дружелюбности. Поэтому я предложил, чтобы последний кадр первой сторис был первым кадром следующей. Таким образом механика резкого переключения слайдов стала абсолютно бесшовной, а с помощью Lottie её было недорого реализовать.

Что ещё почитать:
Lottie Supported Features — список поддерживаемых Lottie функций.
Телеграм-канал Pototsky — канал Саши Пототского, моушен-дизайнера, который активно анимирует Lottie для Telegram и делится хитростями.
Телеграм-канал «Идейный Аниматор» — классный канал неизвестного мне автора с тонкостями и хитростями анимации Lottie.
YouTube-канал SVGenius — много классных уроков по использованию Lottie, в том числе интерактивных в веб-среде.
Если будут вопросы, смело спрашивайте — с радостью отвечу. А если захотите поработать в 2ГИС — у нас открыты вакансии продуктовых дизайнеров, заглядывайте.
Ivnika
Мне, как пользователю, больше важно качество основной функции - навигация, а она (в частности Беларусь) мягко говоря плоха. В итоге я рад что вы развиваетесь (искренне), но приходится пользоваться яндексом у которого для меня много недостатков (даже критических), но с навигацией он более менее справляется.
p.s. Да-да, я понимаю что статья не о том, но я оцениваю продукт целиком, а не частями (едва ли вы скажете что вино прекрасно если пробка верх искусства, а по вкусу уксус)