Время идет, технологии меняются, набитые шишки копятся, настала пора обновить материалы по SVG-анимациям. Тем более, что тема для многих фронтендеров все еще остается странной и запутанной. В этой статье мы рассмотрим SVG-анимации с разных сторон, посмотрим на актуальное состояние дел, возможности и сопутствующие инструменты. Мы не будем разбирать каждое свойство и каждый хак. Слишком большой объем материала получится. Для этого есть MDN и ему подобные сайты. Задача текущей статьи — дать общее представление о том, что бывает, и от чего можно оттолкнуться, если вы решили изучать эту тему, а у вас полная каша в голове.


SVG в контексте фронтенда существует уже довольно давно. Можно даже сказать, что "оно было всегда". Хотя еще несколько лет назад мало кто активно использовал SVG в контексте интерфейсов, каких-то визуальных эффектов, анимаций и.т.д. В основном все крутилось вокруг векторных иконок. Это было время, когда люди вообще еще плохо понимали, как верстать что-то нестандартное, в JS еще не было классов, а проблемы кроссбраузерности стояли очень остро. Тогда на CodePen можно было выложить простой пример с каким-то техническим приемом использования того или иного инструмента и он моментально расходился в народе — все учились делать такую же фишку. Эх, вот были времена! Но ничего не стоит на месте, flash закончился, IE переобулся, направление креативной верстки более-менее устаканивается на новом стеке, и мы можем собрать какие-то списки типовых штук, которые хорошо уметь делать. Текущий материал будет полностью посвящен одной из тем — анимированию SVG во фронтенде. Посмотрим, какие есть варианты анимирования, какие возможности самого формата часто применяются, и какие есть вспомогательные инструменты.


Сразу небольшой дисклеймер. Мы не будем затрагивать вопросы UI/UX. Только техническую сторону, как можно реализовать функционал в коде. Так что если вы считаете, что анимации во фронтенде не нужны, что ими порой злоупотребляют, то можно не писать об этом в комментариях. Проблема такая есть, но к теме статьи не относится. Возможно мы ее затронем в будущем.

А теперь приступим.


Векторная графика = инструкции по рисованию


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


Для того, чтобы описывать содержимое SVG-картинки есть свой синтаксис, очень похожий на HTML: теги, атрибуты — все как всегда. Собственно XML-основа у них одна и та же. Как мы потом увидим — можно вставлять одно в другое. Например вот так мы описываем, что на картинке есть четыре круга с определенными координатами, радиусом и цветом:


<svg viewBox='0 0 100 100'>
    <circle cx='25' cy='50' r='5' fill='#14ff6e' stroke='#14ff6e' />
    <circle cx='75' cy='50' r='5' fill='#14ff6e' stroke='#14ff6e' />
    <circle cx='50' cy='25' r='5' fill='#14ff6e' stroke='#14ff6e' />
    <circle cx='50' cy='75' r='5' fill='#14ff6e' stroke='#14ff6e' />
</svg>

В целом по степени читаемости очищенный от мусора SVG код сравним с HTML. Тут можно организовать все элементы в группы, сделать осмысленные id, классы "как в БЭМ", и.т.д.


Более подробно со списками тегов и атрибутов можно познакомиться на MDN. Там слишком много всего, чтобы описывать каждый момент в рамках статьи — https://developer.mozilla.org/en-US/docs/Web/SVG. Мы же сосредоточимся на практической стороне вопроса, и посмотрим, а как этот теоретический функционал вписывается в реальность.


Интегрируем SVG в страницу


Но для начала нужно интегрировать SVG в страницу. Мы не в вакууме работаем. И есть два направления, как это можно сделать. Первое — код SVG можно вставить прямо в HTML.


<div class='my-svg-component'>
    <svg>
        <!-- содержимое svg -->
    </svg>
</div>

Вы можете спросить: а это получается, что можно использовать CSS и JS и работать с SVG-элементами так же, как и с любыми другими элементами на странице? — Да. Именно так. Правда с некоторыми оговорками. Иногда внутри SVG что-то все же будет работать не так, как на остальной странице. К этому еще вернемся. Такой подход в контексте анимаций удобен прежде всего тем, что мы можем все контролировать: мы можем легко связать любые события и любые стили на странице с тем, что происходит в SVG, так как это та же самая страница.


Разумеется, есть инструменты для любых сборщиков, которые подставляют код SVG картинок в нужные места страниц при их сборке, так что можно работать с картинками как с отдельными файлами, а на выходе иметь все в одном. Они гуглятся на любой вкус и цвет по запросу "название сборщика + inline svg". А многие шаблонизаторы и сами все умеют, ничего дополнительно прикручивать не нужно.


Важный момент: при вставке SVG-картинок в страницу, они все попадают в одну область видимости. Если мы используем id в них, то будет шанс, что они продублируются и начнется магия. За этим стоит следить при подготовке картинок к анимированию и добавлять какие-то префиксы к id. Или придумывать что-то с shadow DOM, чтобы все изолировать. Тут уже все в зависимости от процессов в проекте.

Второй вариант — использовать SVG как отдельный файл. Это может быть картинка


<img src='my-vector-image.svg' />

А может быть и не картинка. Можно вставить SVG с помощью тегов object или даже iframe. Она может быть и фоном в CSS. Но во всех этих случаях речь будет идти про сторонний файл. Отдельный от нашей страницы. И доступ к нему будет в любом случае урезанным.


В нашем контексте, как вы уже, наверное, предположили, сложно связать происходящее в стороннем файле с событиями на странице. Нельзя просто так в него залезть из скриптов или из CSS и что-то сделать. Всегда будут какие-то ограничения. Плюс у браузеров есть вопросы по части безопасности использования скриптов и растровых картинок в сторонних SVG файлах. И, как мы увидим в дальнейшем, иногда нужно буквально перезагрузить картинку по сети, чтобы анимация в ней перезапустилась. Это делает работу со сторонними файлами довольно неудобной во многих ситуациях. Мы в примерах будем использовать первый подход.


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

CSS + SMIL + JS


Вся суть анимирования SVG сводится к плавным изменениям значений атрибутов у тегов. Время идет — атрибуты меняются. Вот и вся анимация. Всем спасибо, увидимся в следующий раз.


На этом можно было бы и закончить, но мы раскроем тему неного шире, т.к. определенно есть люди, которым это будет полезно. В контексте фронтенда есть три технологии, которые дают нам возможность анимировать SVG: CSS, JS и SMIL. У каждой есть свои возможности и ограничения, так что мы познакомимся со всеми.


Анимирование SVG с помощью CSS


CSS-анимации — это самый простой вариант. Все мы с ними работали в "обычной" верстке. Они хорошо подходят для простых движений или изменений цвета элементов на :hover/:focus. Или по навешиванию классов-состояний. В основном в контексте SVG они используются именно для иконок или каких-то небольших деталей в элементах интерфейса. Там как раз что-то такое незамысловатое обычно и нужно. Простая задача — простой инструмент.


В CSS у нас есть два варианта анимирования свойств элементов — transition и keyframes. Если вы не знаете, что такое transition и keyframes, то лучше притормозить, сходить на MDN и почитать про них — раз и два.



В целом логика работы CSS-анимаций в контексте SVG принципиально не отличается от HTML, просто добавляются специфичные для SVG свойства у элементов.


Как и в случае с HTML, в SVG не все атрибуты являются анимируемыми со стороны CSS, поэтому упомянутые выше технологии для анимирования SVG не являются полностью взаимозаменяемыми даже в условно простых задачах. Хотя если большую часть времени придерживаться стандартной для CSS логики "меняем только transform и opacity", то можно этого и не заметить.

Но есть нюанс. На самом деле в обсуждениях и черновиках W3C по поводу интегрирования CSS анимаций и SVG было много самых разных вопросов, но этот конкретный момент особенно важен в нашем контексте. В мире CSS и в мире SVG есть штуки, которые по сути дублируют друг друга, но в деталях поведение отличается. И браузеры пытаются все как-то между собой состыковать. Причем на деле каждый это делает по-своему, в соответствии со своими тараканами в голове. Чтобы уменьшить количество нестыковок, хорошо использовать атрибут xmlns:


<svg version="1.0" xmlns='http://www.w3.org/2000/svg'>...</svg>

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


Поначалу ломают мозг новые единицы измерения. Точнее безразмерные координаты в SVG, которые добавляются к привычным единицам измерения в CSS. Но к этому быстро привыкаешь. Это не проблема. Действительно хороший пример такой двойственности, который путает новичков в анимировании — это трансформации. Да и не только новичков он путает. Создатели стандартов до сих пор не могут определиться со всеми деталями, хотя уже лет десять обсуждают, если не больше. В CSS есть transform, и в SVG есть transform. И там, и там, есть набор базовых трансформаций — translate, rotate, scale и skew. И интуитивно кажется, что все там должно работать одинаково, не так ли?


Здесь есть два основных вопроса. Первый состоит в том, что transform-origin в мире SVG всегда находится в координатах [0,0] всей координатной плоскости картинки. Не самого элемента, к которому трансформация применяется, а именно всей плоскости. Соответственно для поворота какого-то элемента в SVG сцене вокруг произвольной точки, нужно все сдвинуть в центр координат, совместив точку с ним, повернуть, и потом сдвинуть обратно. Если вы хоть немного касались области компьютерной графики, то, вероятно, видели нечно похожее, только другими словами, а вот для простых верстальщиков эта концепция оказывается чем-то новым и непонятным. В CSS все это скрыто от наших глаз:



Некоторые браузеры могут применить transform-origin из мира CSS, считая его более приоритетным, и на первый взгляд покажется, что все ок. Но другие браузеры этого делать не будут, и там все будет не ок.


И еще трансформации на SVG-элементах складываются в стек. Можно несколько раз применить к элементу одни и те же трансформации. Подвинуть, повернуть, еще подвинуть, еще повернуть, и.т.д. А потом они применяются в обратном порядке. А CSS так не умеет. Там можно применить только по одной трансформации каждого базового типа к элементу. Иногда это приводит к странному поведению элементов, так что нужно быть внимательным с этой нестыковкой и не нагромождать разную логику трансформаций на один элемент.


В целом можно вынести мораль: если какие-то штуки в мире CSS и в мире SVG выглядят одинаково, и в каких-то черновиках W3C написано, что они по идее должны работать одинаково — это еще не значит, что они на самом деле в реальных браузерах работают одинаково. Тут нужно быть аккуратным и проверять все не только в Chrome.

Инструменты для отладки CSS анимаций в браузерах работают с SVG. Если нужно отладить движения, замедлить, покрутить анимацию туда-сюда — все есть:



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


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


Анимирование SVG с помощью SMIL


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


SMIL описывается в отдельной от SVG спецификации, так что если вы пользуетесь W3C-валидатором, то не забывайте выбирать, на какие документы из списка document type ему нужно ориентироваться. Если не указать, то будет много ложных сообщений об ошибках из-за "несуществующих" тегов и атрибутов.

Основных тегов здесь три:


  1. animate — для плавных изменений значений атрибутов.
  2. animateTransform — по сути то же самое, но только для трансформаций.
  3. animateMotion — для движения объектов по кривым.

Еще есть тег set, с помощью которого можно резко заменить какое-то значение на другое. Это не совсем про анимации, но такое тоже есть.


Наличие в каком-то смысле дублирующих друг друга тегов animate и animateTransform, а еще устаревшего animateColor, само по себе намекает на то, что технология прошла долгий и тернистый путь в своем развитии, и содержит в себе то наследие из времен, когда flash был жив. Но несмотря на это использовать SMIL в простых задачах не так уж и сложно, разве что копипасты обычно получается довольно много:



Мы не будем здесь разбирать все возможные атрибуты у этих тегов, если кому-то интересно, то есть хорошее введение в SMIL на CSS-tricks: https://css-tricks.com/guide-svg-animations-smil/.


Если обобщить, то можно делать:


  • Все то же, что и в CSS.
  • Морфинг — изменение формы кривых.
  • Движения по заготовленным траекториям, как в примере выше.
  • Плюс есть встроенный механизм синхронизации времени начала анимаций. Можно время начала одной анимации задать в какой-то зависимости от времени начала или завершения другой анимации. Это действительно удобно в ситуациях, когда в сцене много движений связаны какой-то логикой.

Еще там есть пользовательские события, вроде клика мышкой. По сравнению с CSS возможностей больше, но по сравнению с тем, что можно сделать с помощью JS, набор сильно урезанный и не особо практичный, если уж все по-честному сравнивать. Но есть, и хорошо.


Идея всего этого сама по себе была неплохая. Но что-то пошло не так. Я не знаю всех деталей, думаю никто не знает, но технология долго находилась в каком-то непонятно-экспериментальном статусе и сдохла несколько лет назад. Тогда ее не поддерживал IE, в Firefox и Safari была куча проблем, в Chrome вроде что-то было, но стало deprecated. Появились рекомендации от разработчиков разных браузеров на тему перехода со SMIL на CSS или JS. Многие пророчили, что SMIL отправится по пути flash на свалку истории. А потом все забили. А потом IE превратился в Edge, Edge сменил движок, и внезапно оказалось, что SMIL “работает” во всех вечнозеленых браузерах. Вечнозеленый зомби, если можно так сказать.


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


Во-первых — это кроссбраузерность. Технология, конечно, работает, но проблемы на этом поле никто не исправляет. Например не все браузеры умеют использовать значения больше 1 в кривых Безье. Да, в 2022 году. И таких глупостей там предостаточно. И есть большой затык в том, что инструменты разработчика в этих самых браузерах тоже не имеют нормальной связи с этой технологией. У нас даже нет адекватных линтеров, а от формального W3C-валидатора пользы не очень то и много. Поэтому отладка порой становится очень болезненной — нужно заранее знать, куда смотреть, и высматривать ошибки глазами в коде. Это занятие очень на любителя, особенно в крупных сценах.


Если мы почему-то используем SVG в виде стороннего файла, то возникает вопрос кеширования. Обычно люди считают, что кеширование — это хорошо. Но загвоздка в том, что состояния анимаций тоже кешируются в браузере. Если анимация закончилась, то при перезагрузке страницы есть ненулевая вероятность, что она не начнется заного. Не раз видел верстальщиков, у которых при верстке с вебпаком все работает, а потом при раскатывании сайта на серьезном сервере — начинаются какие-то залипания анимаций. Это в теории решается со стороны сервера, на уровне заголовков, запрещающих кеширование, но на деле оно как-то не всегда работает. Плюс мы завязываем какие-то визуальные штуки на конфигурацию сервера, что само по себе выглядит костылем. Можно все решить на клиенте добавлением случайных параметров в url картинки, чтобы браузер ее перезагружал каждый раз. Но это тоже костыль. Плюс все это создает проблемы синхронизации анимаций с действиями пользователя, т.к. картинка по сети загружается не мгновенно. Каждый раз ее загружать не удобно. Все это нарастает как снежный ком. Так что использование SVG со SMIL-анимациями в виде отдельной картинки в целом идея спорная. Подойдет не всегда. Если вы знаете, как это можно красиво приготовить — напишите в комментариях.


С inline svg все немного проще — там можно запускать SMIL-анимации с помощью атрибута begin='indefinite' и метода beginElement, есть доступ ко всему из скриптов, но при этом по мере развития проектов растет и количество подставок и прослоек, чтобы все работало с постепенной загрузкой контента в рамках условного SPA. Тоже не идеальный вариант на самом деле, но он явно удобнее, чем с отдельными файлами. По крайней мере все надежно и предсказуемо.


Использование SMIL может быть оправдано в ситуациях, когда нужно сделать простой морфинг или движение объектов по кривым в иконках. Это вещи, которые в CSS или совсем невозможно сделать, или речь идет о страшных костылях. Также есть программы для анимаций и JS-библиотеки, которые могут генерировать SMIL-анимации, и иногда можно встретить такой машинно-сгенерированный код, требующий поддержки. В остальных случаях использование SMIL мне кажется нецелесообразным из-за большого количества вопросов и издержек, возникающих при работе с кодом, особенно если что-то идет не так.


Анимирование SVG с помощью JS


Следующий вариант анимирования — это скрипты. В целом здесь, как и в случае с CSS, все очень похоже на анимирование обычных HTML-элементов. Мы получаем их через методы вроде querySelector и используем setAttribute чтобы менять значения атрибутов у тегов. Ради каких-то оптимизаций возможны игры с innerHTML или еще что-то такое. Но в целом ничего сверхъестественного. Это все и не удивительно, т.к. речь идет о привычных нам встроенных во все браузеры инструментах.


Важное отличие от использования CSS/SMIL состоит в том, что со скриптами мы имеем полную свободу в плане использования сложных алгоритмов расчета траекторий передвижения элементов или каких-то других параметров. Мы можем использовать физические формулы, рандомизацию, расчеты каких-то параметров в реальном времени в зависимости от действий пользователя, и при этом код выглядит как код. Как набор человеко-понятных инструкций. Безусловно и на CSS можно делать сложные keyframes и использовать разные хаки со скрытыми интерактивными элементами, но код на JS в таких задачах и писать проще, и поддерживать. Про производительность мы еще поговорим дальше, но забегая вперед можно сказать, что все не так уж и плохо, как может показаться на первый взгляд.


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


Инструменты для интерполяции значений во времени


Главный класс инструментов, который используется в 99.9% случаев анимирования SVG с помощью скриптов — это инструменты для интерполяции значений. Это логично: если у нас есть задача менять со временем атрибуты элементов, то нужно иметь какой-то способ описывать логику этих изменений, и, в частности, логику расчета промежуточных значений между начальным и конечным состоянием элементов в анимациях.


В рамках CSS и SMIL у нас есть встроенные возможности для создания временных функций, есть способы описать длительность анимаций, время начала, в каких-то случаях логику следования одних за другими, а в JS из коробки такого нет. Во всяком случае пока нет. Мы бегло посмотрим принцип работы некоторых штук, чтобы примерно понимать, что происходит внутри готовых библиотек, на которые будем ссылаться. Какие-то детали там могут отличаться, но идеи в корне везде одни и те же.


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


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


function updateElement() {
    // обновляем атрибуты элементов

    requestAnimationFrame(updateElement);
}

Как пример:



На сегодняшний день стандартным FPS в браузерах является 60 кадров в секунду. Или 120, для экранов, которые это умеют. Это не то, чтобы константа. Зависит от монитора. Для нас важно, что при использовании requestAnimationFrame наша функция будет вызываться эти самые 60, или 120, или какая там будет частота у монитора, раз в секунду. Но не больше. Это важно. Если мы сделаем бесконечный цикл, то он будет крутиться с такой скоростью, на которую способен процессор в компьютере, а requestAnimationFrame — это своего рода тормозилка, снимающая нагрузку с железа. Если же наша функция будет тяжелой, будет что-то делать слишком долго, то следующие кадры будут пропускаться до тех пор, пока наши расчеты не закончатся. Будет видимая просадка FPS, но все будет работать в штатном режиме, по крайней мере устройство пользователя не повесится. Скорее всего.


Экраны с высокой частотой обновления обычно предполагают хорошее железо, которое скорее всего и не заметит наших анимаций. Но. Если поделить 1000ms на условные 60 кадров в секунду для типовых экранов среднестатистических ноутбуков, то получится 16.5ms на кадр. Это время, за которое наша страница должна обновляться, чтобы все было ок. Не стоит забывать, что компьютер пользователя вообще-то говоря занят и другими делами, помимо наших анимаций. Поэтому при оценке алгоритмической сложности происходящего и тестах на слабом железе не стоит думать, что у реальных пользователей все ресурсы компьютера будут на самом деле доступны все 16.5ms. Что-то будет занято другими процессами. Так что близко к этой границе лучше не подходить.

Функция requestAnimationFrame при вызове колбека передает туда в качестве параметра текущее время. Сложно сказать, насколько это время точное, т.к. в современных браузерах из соображений безопасности даже performance.now() вроде как имеет случайную погрешность, но в контексте анимаций можно считать, что это плюс-минус текущее время. Соответственно можно использовать его и время начала анимации, чтобы произвести нехитрые расчеты прогресса конечной анимации, у которой есть начало и конец (тут пример условного JS-псевдокода, чтобы не раздувать примеры в статье, полный код будет на гитхабе):


function updateElement(time) {
    const timeFraction = (time - startTime) / duration;

    if (timeFraction >= 1) {
        // анимация закончилась
    } else {
        const progress = easingFunction(timeFraction);
        // обновляем атрибуты элементов,
        // используя прогресс анимации от 0 до 1
        requestAnimationFrame(updateElement);
    }
}

В целом расчет прогресса анимации не представляет из себя ничего сложного, если иметь культуру использования значений от 0 до 1. Часть пройденного времени анимации — всегда от 0 до 1. Прогресс — от 0 до 1. Временные функции принимают на вход значения от 0 до 1 и возвращают значения от 0 до 1. Если это сложные функции, то они должны по крайней мере возвращать 0 для 0 на входе, и, соответственно, 1 для 1 на входе. Когда весь код построен на такой логике, в нем появляется возможность подставлять что угодно куда угодно, и становится проще его воспринимать, т.к. мы всегда знаем, какие примерно значения, в каких пределах используются. В CSS у нас все временные функции именно такие, если вы не задумывались. Как примеры временных функций на JS, подчиняющихся этой логике:


function easeLinear(x) {
    return x;
}

function easeInCubic(x) {
    return x * x * x;
}

function easeInExpo(x) {
    if (x === 0) {
        return 0;
    } else {
        return Math.pow(2, 10 * x - 10);
    }
}

Неплохая подборка стандартных временных функций с примерами есть на https://easings.net/. Если вам эта тема интересна, то можно посмотреть примеры в ней.


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


Библиотеки


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



GSAP появился уже лет 10 назад, и в то время он в контексте креативной веб-разработки конкурировал с технологией flash. Чем больше на него смотришь, тем больше понимаешь, почему он такой, какой есть. Яростный маркетинг про новый стандарт анимации в вебе со всякими перлами для новичков в духе "анимация на GSAP работает быстрее анимации на JS” (хотя GSAP сам написан на JS и не может работать быстрее самого себя), форум с платной техподдержкой и обучением, монструозная документация, куча готовых функций и плагинов на любые обстоятельства, из которых в повседневной работе в рядовых интерфейсах можно применить от силы 10% — это все наследие из тех времен. Но нужно отдать должное — за столько лет инструмент вылизали, что-то убрали, где-то упростили, и работает он очень даже хорошо. Хотя все еще часто кажется слишком большим относительно решаемых задач.


Но есть также поколение более молодых инструментов, которые появилсь лет на 5 после GSAP, когда flash уже заканчивался, старые IE постепенно отмирали, появлялся ES6, SPA-фреймворки, и становилось понятно, как будет выглядеть верстка будущего. Что в ней нужно. Эти инструменты сохраняют в себе только то, что востребовано в повседневной работе, и это позволяет им оставаться компактными по сравнению с GSAP. Они могут быть хорошим выбором в проектах с более-менее типовой версткой, где нет потребности в каких-то извращениях. Похоже, что самый популярный инструмент в этом поколении — это Anime.js.



Эти инструменты, как вы уже поняли, универсальные. Их можно использовать для любых анимаций на странице. Можно атрибуты элементов менять, можно какие-то переменные в скриптах пересчитывать и где-то потом использовать. В WebGL контексте, например. Но есть класс изначально более узкоспециализированных инструментов, которые предназначались для работы с SVG. Мы ведь про это в первую очередь говорим. Например Snap.svg. Подобных инструментов можно много загуглить. Там есть более объемные, есть менее. Какие-то решают только одну задачу, например движение по кривой. Некоторые из них, вроде Raphaël, уходят корнями во времена IE6. Можете себе представить, какой адъ там внутри.


Удобно все подобные инструменты рассматривать как jQuery для SVG. Они дают разные обертки над стандартными методами работы с элементами на странице, закрывают какие-то древние проблемы кроссбраузерности (иногда там внутри генерируется SMIL-разметка), есть какой-то еще относительно часто нужный функционал, который когда-то было неудобно писать. Актуальность этих инструментов в наше время — такая же, как и у jQuery. Есть много готовых решений для разных задач, которые когда-то сделали с их использованием. Если какое-то из них подходит под наши задачи — ок, берем. Но если речь про новый проект с чистого листа, и у нас есть условный GSAP для всего остального на странице — может и не нужны они. Вопросы кроссбраузерности сейчас уже не стоят так же остро, как когда-то, а в сложных задачах сама сложность обычно состоит в хитрых расчетах, а синтаксические сахарные обертки не особо с этим помогают.


Но есть еще один класс инструментов, которые вообще-то решают перпендикулярные анимациям задачи, хотя где-то в своих недрах умеют что-то там анимировать. И почему-то часто всплывает вопрос в духе "а не использовать ли этот инструмент, как основной инструмент для анимаций на странице?". Хороший пример — D3.js.



Очень модная штука была когда-то, про нее на всех собеседованиях спрашивали. Этот конкретный инструмент — для визуализации данных. Схожие по идеологии инструменты есть и для других задач, но этот у всех на слуху, так что будем ссылаться на него. Наверное не очень корректно сваливать все в одну кучу, но нам удобно представить, что суть всех этих инструментов в том, что это комбайны, в которых собрали все, что в теории может понадобиться в какой-то узкой специализации в плане задач. Если наши задачи находятся только в этой области, например мы только визуализируем данные, то это может быть очень хорошим выбором. Там есть куча штук, которые могут понадобиться для этой работы. И функционал анимаций тоже есть из коробки. Удобно. Но если наши задачи за пределами этой области — то… Ну такое. Если сравнить гибкость и удобство настройки анимаций в том же GSAP и в D3.js, то разница будет заметна. Все же есть инструменты, которые на этом специализируются, а есть те, для которых анимации — это побочный функционал.


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

Как-то так. Можно кратко подытожить, что простые задачи анимирования можно решать на чистом JS, для сложных — есть универсальные библиотеки для анимаций всего и вся. Динозавры — всегда под вопросом, но скорее нет. Специализированные инструменты, которые умеют что-то анимировать помимо своего основного функционала — выбираем по ситуации, но отдаем себе отчет, что нельзя все делать хорошо и одновременно.


А закончить тему сторонних инструментов можно грубым, но важным моментом, про который часто умалчивают маркетологи и евангелисты:


Никакие скрипты не расширяют фундаментальные возможности и ограничения формата SVG. Никак. Они позволяют более удобно организовать код. Возможно закрывают какие-то вопросы кроссбраузерности. Возможно есть готовые алгоритмы для чего-нибудь, которые можно взять и поиспользовать. Это удобно. Но сам формат SVG не меняется, и принципы рендеринга его браузером не меняются. Это важно осознавать, чтобы не попадаться на маркетинговую фигню. Зачастую в рекламе какие-то базовые возможности SVG, про которые просто редко вспоминают, выдаются за расширение возможностей формата с помощью чудесной библиотеки. Всегда думайте своей головой!

Кроссбраузерность SVG-анимаций


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


В плане CSS почти все ок. Бывают баги, связанные с рендерингом, но они обычно одинаковые со всеми вариантами анимирования, и скорее связаны с самим форматом SVG, чем со способом изменения атрибутов. Они во многом похожи на проблемы, которые возникают с HTML-элементами. Округления значений, странные сглаживания — вот эти штуки. В первом примере с девушкой в шляпе можно увидеть пример такого бага с прозрачностью на границах контуров. Это не случайность, так задумано. Такие баги и без анимаций иногда появляются. Единственный вопрос, который действительно путает — это противоречащие стандарты для трансформаций.


В контексте SMIL все несколько хуже. Да, IE теперь молодец, но поскольку технология SMIL не особо развивается, мы имеем дело с разными нестыковками, которые когда-то там исторически сложились. Иногда что-то работает в одном браузере, но не работает в другом. Причем то и дело всплывают какие-то новые нестыковки, когда какая-то новая верстка снаружи SVG влияет на работоспособность внутри. Флексы и гриды могут давать неочевидные комбинации фич и багов, ломающих SVG. В целом практика показывает, что хорошо тестировать SMIL-анимации сразу в Firefox, двигаясь небольшими шагами, чтобы отслеживать, на каком моменте что-то пошло не так. В большинстве случаев то, что работает там, будет работать и в остальных браузерах. Но проверять стоит везде. Это не то место, где можно что-то утверждать не глядя.


С JS все просто: все ок. Что написали — то и получили. Было бы странно, если бы было по-другому.


Вопросы производительности


Одна из самых нелюбимых тем у фронтендеров, да? Хуже только доступность. Но мы поговорим. В целом ситуация с производительностью SVG-анимаций похожа на происходящее в мире HTML+CSS, знакомом любому верстальщику.


Можно выделить 4 основных момента, на которые определенно стоит обращать внимание:


  1. Количество элементов в анимации: чем их больше, тем сильнее все будет тормозить. Ничего нового. Если вам зачем-то нужно анимировать 100000 постаматов на SVG карте — думайте про кластеризацию. Это еще и на удобстве использования хорошо скажется.
  2. Размер SVG на экране: чем больше места на экране занимает SVG, чем больше пикселей, тем больше ресурсов нужно, чтобы ее рендерить. Собственно с CSS-анимациями все так же.
  3. Маски: одна маска не окажет сильного влияния на анимацию, но если их будет много, то нагрузка на железо начнет расти. Тут мы, видимо, вступаем на какую-то скользкую дорожку внутренних оптимизаций в браузерах. Количество масок влияет явно сильнее, чем количество анимируемых элементов, но степень влияния сложно предсказать. Она явно не линейная. Нужно все проверять в конкретных анимациях.
  4. Фильтры: тут нужно быть очень осторожным. Если фильтры, связанные с цветами, не окажут сильного влияния, то штуки, связанные с blur и разного рода displacement map могут убить производительность страницы полностью. Почему это происходит, мы уже разбирали в статье про матрицы из серии "Математика верстальщику не нужна". Если вам интересно, как эти фильтры работают, и почему они требуют много ресурсов — полистайте на досуге.

Инструменты разработчика в браузерах обычно показывают что-то не очень полезное. Обычно в действительно "тяжелых" SVG-анимациях, когда уже идут множественные пропущенные кадры, мы видим много чего в разделе про GPU, но там традиционно нет каких-то явных рекомендаций о том, что делать:



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


К слову про GPU: в сети часто можно встретить утверждение, что SVG-анимации задействуют исключительно CPU, а canvas — исключительно GPU. И поэтому canvas по производительности выигрывает. Но нетрудно заметить, что это не совсем так. GPU используется и с SVG.

Отдельно стоит сказать, что светлое будущее еще не наступило на рынке в целом. У разработчиков часто присутствует когнитивное искажение, что если у них топовый i7 и видеокарта на 8GB видеопамяти, то и у пользователей все так же. А у кого не так — тот и не нужен. Неплатежеспособный клиентъ, так сказать. В реальности все куда прозаичнее — и в бюджетных ноутбуках, и в модных ультрабуках мы часто будем видеть низковольтные процессоры и встроенные видеокарты с сомнительной производительностью. Их достаточно для офисной работы, но не для серьезных задач из мира компьютерной графики. Ваши модные макбуки тоже взлетают от некоторых сайтов. И цена им никак не помогает. Ну а телефоны — это отдельная песня. Там производительность — понятие в целом растяжимое. Так что не злоупотребляйте.


Это все приводит нас к такой мысли, что не всегда разумно до последнего держаться за SVG, даже если задача поначалу кажется подходящей под этот формат. Иногда стоит выбрать что-то другое — перейти на WebGL, оптимизировав каким-то образом отрисовку элементов в своем частном случае, и на этом получив какой-то выигрыш по производительности, или может быть вообще заменить всю анимацию на видео. Иногда даже экспортированная через lottie анимация на 2D-канвасе может работать шустрее, чем нативный вариант с SVG. Опять же, за счет использования каких-то алгоритмов для частных случаев отрисовки на экране каких-то штук. Разумеется, заранее заготовленное видео не везде подойдет, а реализация анимации на WebGL может потребовать дополнительных ресурсов на разработку, но если качество имеет значение, то не стоит слепо исключать альтернативные подходы к решению задач. В конце концов, если все люто тормозит, дедлайн на носу, а в команде никто вообще не понимает, как работает код, то можно пойти по пути постепенной деградации интерфейса. Только планку поднять, и отключить все тяжелые эффекты вообще для всех пользователей. Про это не принято говорить, но с точки зрения пользователя лучше иметь более простой, но хорошо работающий интерфейс, чем тормозящую красоту.


Подготовка SVG от дизайнера к анимированию


Помимо навыков кодирования для работы с SVG-анимациями понадобятся базовые навыки работы с векторными графическими редакторами. Конечно, хороший дизайнер, который все подготовит и принесет на блюдечке, а еще лучше, если сам заанимирует — это круто. Но не у всех он есть. А если и есть, то не всегда он свободен. Поэтому какие-то вещи нужно делать самому. И прежде, чем бросаться писать CSS/JS/SMIL-код, было бы хорошо навести порядок в файлах. Чистый и структурированный код может и время сэкономить, и нервы.


Очень часто в файлах присутствует дополнительная информация от графического редактора, вроде такой:


<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
   xmlns:dc="http://purl.org/dc/elements/1.1/"
   xmlns:cc="http://creativecommons.org/ns#"
   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
   xmlns:svg="http://www.w3.org/2000/svg"
   xmlns="http://www.w3.org/2000/svg"
   xmlns:xlink="http://www.w3.org/1999/xlink"
   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
   inkscape:version="0.48.2 r9819"
   sodipodi:docname="gears.svg">
  <sodipodi:namedview
     objecttolerance="10"
     gridtolerance="10"
     guidetolerance="10"
     inkscape:window-width="1739"
     inkscape:window-height="838"

Она не нужна для конечного отображения SVG на странице, поэтому было бы хорошо ее сразу убрать. Или вот еще пример с другой стороны:


<path id='path5395' d='m809.53 158.72c-19.34-0.19-38.91-0.09-58.47 0.25-78.23 1.37-156.08 6.92-218.18 16.41-31.06 4.74-58.12 10.39-79.97 17.43-20.77 5.39-38.86 13.67-51.35 30.97-25.08 34.76-48.42 91.02-66.37 147.1-17.96 56.07-30.35 110.6-21.41 152.31 4.73 22.05 19.01 38.78 34.94 48.5 15.93 9.71 33.16 14.12 49.25 17.65 16.09 3.54 31.3 6.12 42.19 9.47s15.63 7.1 16.81 9.03c0.09 0.15 0.18 0.3 0.28 0.44 8.26 12.98 17.32 43.93 23.87 75.19 6.56 31.26 11.38 62.63 18.07 84.69 5.08 16.76 16.86 22.09 27.9 27.25 11.04 5.15 23.97 9.13 38.82 12.81 29.69 7.36 66.87 13.14 104.5 17.9 37.62 4.77 75.59 8.44 105.93 11.13 30.35 2.69 54.29 4.58 59.32 5.19h0.09c14.45 1.66 24.62-6.48 30.84-13.22 6.23-6.74 10.78-14.27 15.35-22.75 9.14-16.96 17.67-37.88 26.4-58.63 8.74-20.74 17.67-41.25 25.78-55.43 4.06-7.09 7.98-12.55 10.57-15.25 1.11-1.17 1.59-1.41 1.78-1.54 19.75-1.83 54.93-5.71 89.63-11.4 17.9-2.92 35.3-6.31 50.5-10.22s27.4-7.19 39-15.44c8-5.69 9.9-11.14 12.1-16.22 2.1-5.07 3.5-10.3 4.9-16.18 2.7-11.78 4.7-26.13 6.2-42.85 3.1-33.44 4.3-76.11 3-120.59-1.2-44.48-4.8-90.63-11.6-130.91-6.7-39-15.1-72.03-32-95.84-0.5-0.8-1-1.57-1.5-2.31-0.1-0.04-0.1-0.07-0.1-0.1 0-0.05-0.1-0.1-0.1-0.15v-0.03c-0.7-0.84-1.4-1.64-2.1-2.38-12.6-14.8-30.1-21.22-51.6-27.53-22.6-6.66-50.3-11.62-81.92-15.44-47.39-5.72-103.34-8.74-161.35-9.31zm-0.44 45.69c56.76 0.57 111.31 3.5 156.32 8.93 30 3.63 55.79 8.43 74.59 13.94 18.6 5.46 29.6 13.2 30.2 13.84 8.1 10.91 18.6 41.2 24.9 78.35 6.3 37.2 9.9 81.65 11.1 124.5s0 84.21-2.9 115.12c-1.4 15.46-3.3 28.37-5.2 36.88-0.7 3.25-0.7 5.68-2 6.69-2.6 2.12-10.5 4.44-20.9 7.12-13 3.36-29.5 6.59-46.4 9.38-34.03 5.57-70.32 9.52-88.61 11.18-12.86 1.17-21.76 8.2-28.5 15.25-6.74 7.06-12.09 15.17-17.22 24.13-10.26 17.92-19.37 39.48-28.19 60.4-8.81 20.93-17.33 41.26-24.56 54.69-2.97 5.51-5.59 9.32-7.41 11.66-10.56-1.08-27.2-2.32-53.93-4.69-30.05-2.66-67.51-6.29-104.22-10.94-36.72-4.65-72.84-10.42-99.25-16.96-13.21-3.28-24.05-6.85-30.47-9.85-2.66-1.24-3.91-2.4-4.53-3-4.52-16.6-9.7-46.44-16.1-76.94-6.85-32.66-14.32-65.65-30.03-90.34-10.24-16.9-26.78-23.85-42.22-28.59-15.43-4.75-31.33-7.24-45.78-10.41s-27.17-7.12-35.28-12.06c-8.11-4.95-11.94-9.15-14.06-19.07-4.76-22.21 3.39-76.16 20.25-128.81s41.3-106.36 60.87-135.72c-0.75 1.11 9.26-6.98 27.35-12.81 18.08-5.83 43.3-11.27 72.84-15.78 59.07-9.02 135.54-14.53 212.13-15.88 19.14-0.33 38.29-0.4 57.21-0.21z'/>

Вы уже догадались, какой формы этот path? Что тут нарисовано. Ну хотя бы примерно?


Нет? Ну ничего. Не все сразу. Практика решает. Ну а если ваша интуиция вас подводит, или вы не хотите вечно играть в угадайку, то SVG все же стоит готовить перед анимированием. Даже если страшно.


Для работы с SVG у нас есть векторные графические редакторы. Странно, но многие мидл+ фронтенд-разработчики в модных компаниях не умеют с ними работать. Или даже боятся их. Или, что для меня было открытием, не знают, что это такое. Если вы их еще не пробовали — самое время пойти и скачать себе какой-нибудь. Все знать не нужно, но базовые приемы работы на самом деле могут упростить жизнь и сэкономить кучу времени. Есть действия, которые натыкать мышкой куда быстрее, чем что-то изобретать в суровой программистской IDE.


Мы посмотрим примеры в Inkscape, но все векторные графические редакторы по сути одинаковые в своем базовом функционале. Они работают с одним и тем же форматом SVG, и мы уже знаем, что он и в африке остается SVG.


В целом выглядит оно примерно так:



У нас есть поле, где мы визуально видим картинку и можем тыкать мышкой, двигать, группировать элементы и.т.д. Есть поле, где мы видим структуру документа, которая потом превратится в код. На этапе редактирования в ней может быть мусор в виде префиксов, метаданных и неиспользованных id. Это неважно, все потом подчистится. И есть поле, прямо под деревом, где можно редактировать атрибуты выбранного элемента. Это три основных места, куда мы смотрим при подготовке к анимированию. И есть ряд типовых действий, которые хорошо уметь делать. Мы их кратко обозначим, чтобы было понятно, куда посмотреть в документации к вашему редактору, если возникнет необходимость что-то такое сделать. Кнопки везде в разных местах, но суть одна и та же.


Структура документа


Первая группа действий — это просмотр и структурирование элементов в SVG. Сюда войдут такие действия, как группировка элементов в теги g, сокращенно от group, и задание атрибутов для них.


В Inkscape можно тыкать мышкой с Shift по визуальному отображению, выделяя много элементов и делать Gtrl+G, чтобы их быстро группировать. В меню это находится в Object > Group. Дерево тегов открывается на Ctrl+Shift+X или Edit > XML Editor, если вы его вдруг закрыли. В тем можно посмотреть, все ли правильно сгруппировалось. Атрибуты у тегов редактируются в дополнительной панели там же:



Полезно взять на заметку, что при работе с CSS/JS удобно использовать классы для разных элементов в SVG. Не обязательно использовать только id. Уникальные идентификаторы хороши для ссылок, но не для описания логики. Если сцена сложная — думайте о всех этих объектах, как о условных БЭМ-компонентах, и создавайте логику в сценах по тому же принципу. Иногда это будет слишком громоздким решением, но иногда это будет экономить очень много времени — картинки будут человеко-понятными на уровне кода. С такими работать — одно удовольствие.


Работа с кривыми


Вторая группа действий — эта работа с кривыми. Тут есть несколько вещей, которые могут пригодиться:


  • Редактирование кривых. Двигаем точки мышкой и получаем новые кривые. Это куда проще самому попробовать, чем описать словами.
  • Смена направления контура через Path > Reverse. Это, в определенных случаях, определяет, что является "внутренностью" и "наружностью" у замкнутого контура, а также может быть полезно в анимациях рисовании линий.
  • Заливка цветом. За это отвечает атрибут fill. Тут не забываем, что прозрачные контуры можно заливать почти прозрачным цветом, чтобы во всех браузерах с ними работал штатный браузерный :hover.

Дальше мы еще посмотрим, зачем все это может понадобиться.


Мусор


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



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


В случае Inkscape стоит зайти в Edit > Preferences > Input/Output > SVG Output и выбрать формат, в котором будут сохраняться кривые. Можно выбрать абсолютные координаты или относительные (по умолчанию там выбран загадочный optimized формат, логику которого я так и не понял) и заставить его повторять избыточные команды, ничего не удаляя. В обычных ситуациях нам это не важно, но при подготовке контуров для морфинга мы должны четко понимать, какая у них структура, и не позволять редактору ее менять ради уменьшения размера картинки.


В конечном счете мы получим что-то такое:


<svg version="1.0" viewBox="0 0 600 480" xmlns="http://www.w3.org/2000/svg">
  <g id="skull" fill="#ff214f">
    <path id="skull-border" d="..."/>
    <g id="mouth">
      <path d="..."/>
      <path d="..."/>
      <path d="..."/>
      <path d="..."/>
    </g>
    <g id="eyes">
      <path d="..." fill-rule="evenodd"/>
      <path d="..."/>
      <path id="eye-left" d="..."/>
      <path id="eye-right" d="..." fill-rule="evenodd"/>
    </g>
    <g id="nose">
      <path d="..." fill-rule="evenodd"/>
      <path d="..."/>
    </g>
    <g id="bones">
      <path id="bone-bottom-right" d="..."/>
      <path id="bone-bonnom-left" d="..."/>
      <path id="bone-top-right" d="..."/>
      <path id="bone-top-left" d="..."/>
    </g>
  </g>
</svg>

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


Некоторые технические детали


Теперь, когда мы примерно определились с инструментами и почистили свои SVG, можно посмотреть примеры использования некоторых штук, которые мы упоминали, и которые нельзя просто так взять и сделать на HTML+CSS. Как показывает практика, тут есть вещи не совсем очевидные для фронтендеров. Возникают разные вопросы. Так что мы бегло пройдемся по самым популярным из них, чтобы развеять магию, чтобы вы примерно представляли, откуда все берется, и что гуглить, если что.


Морфинг


Главная магия векторных картинок. CSS так не умеет. Идея морфинга в том, что мы берем два path, две кривых, и плавно трансформируем одну кривую в другую. Здесь есть важный момент, что эти два path должны иметь одинаковую структуру, чтобы все работало.


Там есть свой синтаксис описания кривых с помощью чисел-координат и букв, которые определяют, как провести линии между ними. Там есть прямые линии "L = line", есть кривые Безье "C = curve", можно просто подвинуть кисть и ничего не рисовать "M = move", и.т.д. Структура path — это по сути инструкция, как его рисовать кистью, только все слова сокращены для краткости. M 10 10 будет буквально означать "подвинуть кисть в координаты 10 10". L 50 50 будет буквально "нарисовать линию до 50 50". Чего-то принципиально сложного в этом нет. Большие буквы — используем абсолютные координаты, маленькие буквы — координаты будут относительно текущего положения кисти. Это как с absolute и relative в CSS.


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

Так вот. Если эта синтаксическая структура кривой будет разной, например "M 0 0 L 10 10 L 20 20" и "M 0 0 L 10 10 M 10 20 L 20 20" — то будет совершенно не понятно, как плавно перевести одну кривую в другую. Там разный набор инструкций для рисования. Что хотел сказать автор? А вот если они одинаковые, то все становится предельно просто.


Посмотрим пример, чтобы развеять всю техническую магию. Если у нас есть кривая:


<svg viewBox='0 0 100 100'>
    <path id='magic-path' d='M 10 10 L 90 10 L 10 90 L 10 10' />
</svg>

Треугольник, если быть точнее. Нетрудно заметить, что инструкция содержит в себе три линии. И мы сделаем себе шаблон этой кривой в JS:


const d = `M ${A.x} ${A.y} L ${B.x} ${B.y} L ${C.x} ${C.y} L ${A.x} ${A.y}`;

То что будет, если мы начнем плавно менять координаты точек? Правильно — получится плавное изменение формы. Тот самый страшный-престрашный морфинг:



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


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


В текущем контексте может возникнуть соблазн взять магические инструменты, которые адаптируют контуры сами. Например ShapeShifter, или MorphSVGPlugin для GSAP. В Inkscape эта штука для интерполяций тоже есть. В Extensions. Все они действительно могут сгенерировать новые кривые с одинаковой структурой. Но они ничего не знают о том, как должны перемещаться точки при морфинге. А это может быть важно.


Автоматически-магические преобразовалки контуров не знают ничего о дизайне и могут портить внешний вид анимаций. Это проблема не какого-то конкретного, инструмента, а всего класса инструментов.

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


Генеративные плавные кривые


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


Еще из мира CSS мы знаем, что кривые Безье можно рисовать по точкам. Из соображений производительности в мире фронтенда мы не используем кривые высших степеней, только кубические. А их мы рисуем по четырем точкам. При создании визуально длинных кривых мы имеем последовательность кривых Безье. Структуру, в которой последняя точка одной кривой Безье — это первая точка другой кривой. А последняя ее точка может быть первой точкой следующей. И так до бесконечности. В векторном графическом редакторе нарисовать такую штуку не сложно — там при рисовании линий их составляющие сами именно таким образом и соединяются. Но когда мы хотим анимировать эту штуковину, нам нужно следить за тем, чтобы контур не ломался:



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


Лайфхак: viewBox='0 0 100 100' особенно удобен в таких генеративных штуках. Да и вообще, думать в процентах от квадрата всегда проще, чем в 1784 безразмерных единицах из 2823.

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


Рисование линий


Рисование линий — это тема, тесно связанная с предыдущей. Строго говоря в SVG нет прямо вот API для рисования линий. Но на деле этот визуальный эффект можно имитировать с помощью свойств stroke-dasharray и stroke-dashoffset. Оба свойства анимируемые:



В целом логика достаточно простая. Свойство stroke-dasharray — это перечисление линий и дырок в пунктире. Первое число — линия. Второе — дырка. Третье — линия. Четвертое — дырка. и.т.д… Когда числа заканчиваются — все идет по кругу. И тут уж на что первое число попало, то и будет. Попало на линию — будет линия. Попало на дырку — будет дырка.


Если изначально сделать набор из линии нулевой длины и дырки длиной во весь контур, а потом анимировать его в набор из линии длиной во весь контур и нулевую дырку — получится классическое рисование линии, как мы его себе визуально привыкли представлять. Как в примере с карандашом. Если сдвигать все это с помощью stroke-dashoffset — то можно рисовать в другую сторону.


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


Маски


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


В CSS у нас есть clip-path и mix-blend-mode с помощью которых можно нахимичить что-то похожее с некоторыми оговорками. А в SVG есть полноценные маски, которые удобно использовать. В контексте анимаций — можно скрывать какие-то части анимации, можно анимировать саму маску. Можно ее делать полупрозрачной, для каких-то плавных переходов. Большой простор для творчества. С масками определенно стоит поэкспериментировать, если вы раньше с ними не сталкивались.



Что-то конкретное про маски сложно рассказать. Нужно брать и использовать. Пожалуй есть только один момент, который может вызвать недоумение: если работаем с трансформациями, нужно следить за тем, чтобы маски применялись туда, куда нужно. Тут валидатор не подскажет, если что-то не так. Если к одному и тому же элементу применить и маску, и трансформацию, то он уедет в сторону вместе с маской и будет казаться, что ничего не работает. Хотя на самом деле все работает. Просто высокоточный робот делает какую-то фигню, которую мы, глупые люди, сказали сделать.


И еще с масками хорошо сочетается дублирование элементов. Можно делать ненастоящее 3D и анимации, визуально противоречащие логике работы z-index на странице:



Фильтры


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


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


SVG vs. 2D-canvas vs. WebGL


Итак, мы разобрались со многими вещами. Посмотрели, как готовить SVG, как внедрять в страницу, какие есть варианты анимирования, инструменты, и популярные технические приемы. Напоследок будет хорошо сравнить все это с альтернативами, чтобы как-то устаканить новые знания в мире фронтенда в целом. Обычно люди изучают все в последовательности HTML > CSS > SVG > 2D canvas > WebGL. Вот по ней и пройдем.


SVG-анимации привносят в мир HTML+CSS новые возможности в виде работы с кривыми. Здесь мы можем генерировать кривые нужной формы, делать морфинг, делать движение объектов по кривым. Можем делать более хитрые по сравнению с CSS маски и фильтры. Но аккуратно, не забывая о производительности. А еще можем просто использовать SVG как более гибкий вариант Image Map для разных карт и схем, который еще и анимируется.


По сравнению с 2D-канвасом мы имеем векторную штуковину, которую легко адаптировать под разные размеры экрана. Морфинг, движение по кривым, фильтры — все это есть из коробки. Есть стандартные события вроде hover, да и в целом можно делать все, что мы делаем с HTML элементами на странице. Легко все интегрировать. Но мы не можем работать с отдельными пикселями и не можем экспортировать какие-то промежуточные состояния в виде готовых картинок. Так что если нужны пиксели — выбора не остается. Только канвас. Еще в теории у канваса может быть лучше производительность на тех же задачах. Есть разные синтетические тесты, и вроде что-то можно насчитать в пользу канваса. Но это все дело такое — нужно смотреть, какие там конечные алгоритмы будут применяться для рендеринга. Скорее всего то, что действительно тормозит в виде SVG, никакой 2D-канвас уже не спасет.


И где-то еще дальше находится WebGL. Тут у нас полная свобода в 3D, так что сравнивать это с миром SVG будет не совсем корректно. SVG в 3D не умеет. Тем не менее некоторые задачи, которые обычно делаются на SVG, можно сделать и с помощью WebGL, написав все алгоритмы своими руками. Маски и фильтры для картинок часто делают на WebGL. В этом контексте, в отдельных случаях можно получить лучшую производительность, написав абсолютно топорный аглоритм для своего частного решения какой-то задачи. Отбросив все проверки и убив универсальность действительно можно порой выиграть по части производительности. Но тут нужно понимать, что и зачем мы делаем. Потому что можно как выиграть, так и наговнокодить что-то, что потом вообще с трудом будет работать на ноутбуках. Это та область, где уже действительно нужны те самые базовые знания из мира computer science, которыми все пугают новичков в IT.


Заключение


Ух. Много чего обсудили, пора подвести какие-то итоги. SVG-анимации, как мы поняли, не являются чем-то принципиально новым и чуждым для мира фронтенда. Многие подходы здесь такие же, как и при работе с HTML. Но есть и свои фишки, которыми можно расширить свой инструментарий. Конечно не получится в рамках одной статьи охватить абсолютно все свойства и технические приемы, которые могут пригодиться, но я надеюсь, что мы охватили достаточный объем, чтобы погрузиться в контекст, в котором уже понятно, что делать дальше.


Если у вас есть какие-то полезные ссылки, или есть релевантный опыт — не стесняйтесь поделиться в комментариях. Возможно в вашем опыте есть успешные примеры применения того же SMIL, и вы можете рассказать, как же вы смогли все приготовить? А может что-то пошло не так — тоже расскажите. Людям, которые будут это читать в будущем, любая дополнительная информация будет полезна.

Комментарии (6)


  1. Spaceoddity
    22.05.2022 20:44

    Статья как-то и обо всём, и при этом очень поверхностно.

    Лучше бы рассказали про "насущные вещи" - про адаптивность и масштабирование, и как на это влияют атрибуты width, height и viewBox.


    1. sfi0zy Автор
      22.05.2022 21:04
      +7

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


  1. itelmen
    23.05.2022 08:00

    Пару лет назад делал svg анимацию на этом сайте, интересный опыт mienoty.ru В те времена, в хроме до 88 версии, не было GPU ускорения для SVG анимации, приходилось многое упрощать и оптимизировать, но и сейчас не все так гладко как хотелось бы.


  1. defolt0
    23.05.2022 08:00

    Я недавно начал изучать svg и поэтому хотелось бы уточнить. Есть ли какие-то библиотеки, позволяющие рисовать svg линии от руки?


    1. sfi0zy Автор
      23.05.2022 08:52

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


      Есть и полноценные SVG-редакторы, если нужно. Тут стоит вопрос уточнить, какие линии и как вы хотите рисовать.


  1. olegbarabanov
    23.05.2022 15:34
    +2

    Спасибо за статью.

    Кстати, на тему оптимизации SVG. Когда нет под боком редактора, часто выручает SVGOMG, который можно попробовать в виде веб-версии jakearchibald.github.io/svgomg/. Неплохо так чистит. Но и как и в других оптимизаторах, могут "упрощаться" кривые, что в свою очередь может сказаться на анимации.

    Что касается самой SVG анимации, то с ней, откровенно говоря, встречал много проблем и то же аппаратное ускорение может не работать (например потому что GPU или драйвер в blacklist) или может работать с багами, особенно если еще и присутствуют тяжелые SVG фильтры.

    Что касается сложной анимации, то на слабых системах (в т.ч. старые, но активно используемые Android устройства), с браузерами на основе Chromium при работе с Canvas (и WebGL в частности) вычисления можно выполнять в WebWorkers с отрисовкой в OffscreenCanvas. Это помогает разгрузить основной поток, что благотворно влияет на общую производительность. Это хорошее решение, если очень надо отображать постоянную, сложную, контролируемую анимацию, даже на слабых системах. Хотя конечно и у WebGL тоже багов хватает.

    Поэтому для простых анимаций пока отдаю предпочтение CSS и JS через WebAnimation API, ибо это куда стабильнее и более контролируемо.