July 16, 2022

Все об SVG анимации

В данной статье я хочу осветить тонкости работы с SVG-графикой, SVG анимацию (в том числе и path), проблемы и способы их решения, а также разнообразные подводные камни, коих в SVG огромное множество. Эту статью я позиционирую как подробное руководство.

Здесь не будет никаких плагинов, библиотек и прочего, речь пойдет только о чистом SVG.
Единственный инструмент, который я буду использовать, это Adobe Illustrator.

Предисловие

Все началось со скучной лекции и в надежде занять себя хоть чем-то, я решил изучить SVG графику, а именно анимацию. К моему удивлению, в интернете было совсем мало информации. Везде дублировалась информация, объясняющая основы, а про анимацию вообще от силы 2-3 ссылки с абсолютно идентичной информацией, являющейся переводом статьи A Guide to SVG Animations (SMIL) за авторством Сары Суэйдан.

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

Правильный экспорт SVG из Illustrator

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

Подготовить документ для анимации очень важный этап, пренебрежительное отношение к которому может обернуться очень неприятными последствиями. Учить тебя, как лучше рисовать в Illustrator, я не стану. Единственное, что я скажу – при отрисовке фигур следует следить за значениями, желательно, чтобы они имели лишь одно число после запятой, а лучше вообще были целыми. Следовать этому правилу не обязательно, но оно уменьшит размер файла, упростит дальнейшую анимацию и визуально сократит объем информации. Взгляни

<path d="M 17.7 29 C 28.2 12.9 47 5.6 62.8 10.4 c 28.2 8.5 30 50.5 24.8 53.1 c -2.6 1.3 -10.4 -6.1 -29.2 -34.6"/>
<path d="M 17.651 28.956 c 10.56 -16.04 29.351 -23.359 45.12 -18.589 c 28.151 8.516 29.957 50.5 24.841 53.063 c -2.631 1.318 -10.381 -6.148 -29.235 -34.643"/>

В примере одна и та же кривая, но в первом случае одна цифра после запятой, а во втором три. Эта кривая имеет всего 4 точки, а второй пример на треть длиннее первого. Представь, как много места займет кривая из 20 точек.

После того как каркас нарисован, нужно сохранить изображение как SVG файл. Для этого есть два пути – «Сохранить как» или «Экспортировать как». Но какой способ выбрать? Если доверяешь мне – лучше используй «сохранить как». Если хочешь знать «почему», то разворачивай спойлер.

Способы импорта SVG документа в HTML

Перед тем, как я приступлю непосредственно к анимации, я хочу рассказать про то, как встроить SVG на страничку. Каждый способ имеет свои «особенности», которые оказывают прямое влияние на анимацию. И если про них не рассказать, то статья будет неполной.
Предположим, что у тебя уже есть готовый SVG с интерактивной анимацией и осталось встроить этот документ на сайт. Как же это сделать?

Вариант номер раз – вспомнить, что SVG это тоже изображение и его можно импортировать стандартными средствами HTML. Можно создать тег <img> со ссылкой на документ

<img src="Hello_SVG.svg" />

Или задать SVG в качестве фонового изображения

#box { background-image: url("Hello_again.svg"); }

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

Вариант номер два – создать объект из SVG, использовав теги <object> или <embed>. Также есть возможность использовать <iframe>, чтобы создать фрейм, но этот способ я использовать не рекомендуют, т.к. требуется костыль для всех браузеров, чтобы этот вариант отображался корректно

<object data="My_SVG.svg" type="image/svg+xml"></object>
<embed src="My_SVG.svg" type="image/svg+xml" />
<iframe src="My_SVG.svg"></iframe>

Тут уже дела обстоят получше. Анимации получают возможность быть интерактивными, но только если объявлены внутри SVG документа, а содержимое доступно для внешнего JavaScript. Еще <object> и <iframe> могут показать заглушку, если вдруг изображение не загрузится.

Вариант номер три – просто вставить содержимое SVG документа прямо внутрь HTML. Да так можно. Поддержка SVG появилась в стандарте HTML5. Так как SVG по сути является частью самой странички, то доступ к нему есть везде. Анимации и стили элементов могут быть объявлены как внутри SVG, так и во внешних файлах. Минус заключается в том, что такие изображения просто так не кэшируются отдельно от страницы

<body>
...
<svg> <!-- Содержимое --> </svg>
</body>

SVG анимация

Есть два основных способа анимации SVG элемента:

  • CSS анимация
  • SMIL анимация, встроенная в SVG (на самом деле это SVG анимация, которая базируется на SMIL и расширяет его функционал)

Лично я разделяю их как анимацию «внешнюю» и «внутреннюю». Данное деление условно, но все же они имеют функциональные различия. Если говорить об отличиях в общем: CSS – имеет лучшую поддержку браузерами; SMIL – обладает большим функционалом. Трудно сказать, что использовать лучше, т.к. они во многом похожи. Выбор зависит от поставленной задачи, поэтому я просто скажу основные причины использовать SMIL вместо CSS

SMIL — когда нужно:

  1. Сделать то, что не смог CSS (анимировать неподдерживаемый атрибут и т.д.)
  2. Иметь более точный контроль над анимацией
  3. Сделать морфинг контура (анимация атрибута d у тега path)
  4. Синхронизировать анимации
  5. Сделать интерактивные анимации

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

Анимация средствами CSS

Тут ничего нового. Любой SVG элемент мы можем анимировать так же, как мы это делаем с HTML. Все анимации создаются с помощью @keyframes. Так как CSS-анимация это уже другая тема, я подробно останавливаться на этом пункте не буду, в сети полно документации и руководств на эту тему. Все, что там описывается применимо и к SVG, а я лишь приведу несколько примеров.

SVG документ имеет внутренние таблицы стилей, вот в них мы и будем писать анимацию

<svg>
    <style>
        <!-- Тут анимация -->
    </style>
    <!-- А здесь SVG элементы -->
</svg>

Анимировать SVG атрибут так же просто, как и CSS атрибуты

@keyframes reduce_radius {
    from { r: 10; }
    to { r: 3; }
}
@keyframes change_fill {
    0% { fill: #49549E; }
    75% { fill: #1bceb1; }
    100% { fill: #1bce4f; }
}

Можно задавать значения как в процентах, так и конструкцией from-to

Затем остается просто применить созданные анимации к нужному элементу

.circle { animation: change_fill 1s, popup 2s; }

Все, что я описывал выше – это статичные анимации, интерактивностью там и не пахнет. А что делать, если уже очень хочется? Ну кое-что все-таки можно сделать интерактивным и на CSS. Например, если использовать transition в сочетании с псевдоклассом hover

.circle { fill: #49549E; transition: .3s; }
.circle:hover { fill: #1bceb1; }

При наведении на элемент он изменит свой цвет с синего на голубой за 300ms

Анимация атрибутов и небольшой кусочек интерактивности – на этом особенности CSS-анимации заканчиваются. Но этого функционала предостаточно, ведь большинство задач сводятся к анимации какого-либо атрибута. Практически любой SVG атрибут можно анимировать. И когда я пишу «практически любой» я имею в виду, что если ты выберешь случайный атрибут и он окажется неанимируемым, то тебе ОЧЕНЬ повезло.

SMIL анимация

Сразу стоит сказать, что SMIL анимация стара как мир и она потихоньку вымирает, поддержка браузеров пусть и приличная, но все же меньше, чем у CSS Animation, однако есть причина, почему SMIL все еще привлекателен – он может то, что не может CSS.

Про SMIL я буду рассказывать подробнее, потому что тут есть множество подводных камней, про которые редко где пишут. Да и тема эта менее популярная, чем CSS. Основные теги для анимации это <animate>, <set>, <animateTransform>, <animateMotion>.

<animate>

Начнем с тяжёлой артиллерии. <animate> – используется для анимации любого атрибута и является основным инструментом. Остальные же теги узкоспециализированные, но о всем по порядку.

Как применить анимацию к элементу?

Указать элемент, к которому будет применена анимация, можно двумя способами

  1. Положить тег внутрь элемента. Этот способ позволяет инкапсулировать анимацию внутри объекта, что облегчает чтение кода
  2. <circle ...> <animate .../> </circle> В данном случае анимация будет применена к элементу circle
  3. Передать ссылку на элемент. Пригодится если хочется, чтобы все анимации были собраны в одно месте
  4. <svg xmlns:xlink="http://www.w3.org/1999/xlink"> <circle id="blue" .../> ... <animate xlink:href="#blue" .../> </svg> Здесь используется атрибут xlink:href, в котором мы указываем id элемента, к которому должна примениться анимация. Для того чтобы этот способ работал, необходимо определить пространство имен xlink. Это делается в теге <svg>

С SVG 2 атрибут xlink:href устарел, вместо него спецификация рекомендует использовать href, который не требует определять пространство имен xlink.

<circle id="blue" .../>
...
<animate href="#blue" .../>

Но и тут не все так гладко — href не поддерживается Safari. Получается патовая ситуация, один атрибут устарел, другой частично не поддерживается. Так что какой способ использовать решает каждый для себя сам.

Для тех, кто заметил сходство с CSS селекторами: спешу огорчить, обратиться к элементам по классу не получится

<circle class="blue_circle" .../>
<animate href=".blue_circle" .../>

Это не работает!

Как указать атрибут для анимации?

Для этого существует attributeName. В качестве значения выступает имя атрибута, которое мы будем анимировать.

<circle r="25" ...>
    <animate attributeName="r" ... />
</circle>

Указав в attributeName значение r, мы сообщаем, что собираемся анимировать радиус окружности

Что такое attributeType и почему он тебе не нужен?

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

Везде пишут примерно следующее:

Можно указать XMLNS префикс для атрибута, чтобы явно указать его пространство имен

Этот способ упоминается вскользь и без примеров. Вот и я не буду изменять традициям. (советую тебе тут остановиться, забыть про префиксы как про страшный сон и переходить к attributeType, я тебя предупредил)

«Я мазохист»

Чтобы явно указать, к чему принадлежит анимируемый атрибут, используется attributeType. Он принимает 3 значения: CSS, XML, auto. Если явно не указывать attributeType, то будет использоваться auto. В этом случае сначала проверяются CSS свойства и если нет совпадений, то проверяются атрибуты целевого элемента. В примере укажем, что собираемся анимировать именно CSS свойство

<animate attributeType="CSS" attributeName="opacity" .../>

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

Неожиданно, правда? Как я сказал в начале главы – SMIL вымирает и связано это с тем, что анимацию переводят на рельсы CSS. Большинство дублирующихся атрибутов абсолютно идентичны друг другу, т.е. неважно, принадлежит атрибут CSS или SMIL — результат будет один и тот же. А в сочетании со значением auto по умолчанию необходимость в явном определении attributeType отпадает.

Минутка интересных фактов: атрибут attributeType не поддерживается SVG. Откуда тогда он взялся? Он пришел к нам из SMIL Animation, на котором базируется SVG анимация. А еще attributeType удален после SVG 1.1 Second Edition. Все пруфы тут

Как определить значения анимации?

Указать атрибут для анимации недостаточно, необходимо определить его значения. Тут на сцену выходят from, to, by, values.

Начнем с парочки, которая всегда вместе: from и to. Смысл их существования очевиден, from указывает на начало, to на конец

<circle r="25" ...>
    <animate
        attributeName="r"
        from="10"
        to="45"
        .../>
</circle>

Результатом выполнения анимации будет плавное изменение радиуса окружности с 10 до 45

Пусть я и сказал, что они всегда вместе, to так же может использоваться и без явного объявления from. В таком случае from примет значение, определенное в целевом элементе. Для примера выше анимация будет начинаться с 25.

Если есть необходимость указать набор из нескольких значений, используется values. Значения перечисляются через точку с запятой

<circle r="25" ...>
    <animate
        attributeName="r"
        values="15;50;25"
        .../>
</circle>

Значение радиуса уменьшится до 15, после увеличится до 50 и затем вернется в начальное положение

Последний на очереди by. Ему не важно «откуда» и «куда», все, что его интересует, это «на сколько». Иначе говоря, вместо абсолютных значений он работает с относительными

<circle r="25" ...>
    <animate
        attributeName="r"
        by="15"
        .../>
</circle>

Как итог анимации – радиус увеличится на 15, то есть получится 25+15=40

По просторам руководств ходит легенда, что «by может использоваться для указания величины, на которую анимация должна продвинутся». Я понимаю это так: если from=20, to=50, и задан by=10, то этот путь должен преодолеваться «прыжками» по 10, т.е. 20, 30, 40, 50. Но как бы я не пытался, что с by, что без него, анимация ни капли не изменялась. Также я не нашел подтверждения в спецификации. Похоже, это просто ошибка.

Наибольшим приоритетом обладает values, затем идет from-to, последний by. Наименьший приоритет by объясняет, почему «легенда» не может работать в принципе. Однако by работает в связке с from, в этом варианте from просто переопределяет текущее положение элемента

<circle cy="50" ...>
    <animate
        attributeName="cy"
        from="70"
        by="30"
        .../>
</circle>

Тут анимация вместо 50 начнется с 70 и закончится на 100

Еще про относительные анимации

Можно заставить остальные атрибуты работать так же, как и by. Делается это с помощью атрибута additive, который имеет два положения – replace и sum. Первый стоит по умолчанию, поэтому нас интересует второй. При значении sum все атрибуты будут прибавляться к текущему значению целевого элемента, т.е. при анимации радиуса, равного 20, со значениями form=5 и to=15, анимация будет с 20+5 до 20+15

<circle r="20" ...>
    <animate attributeName="r" from="5" to="15" additive="sum" .../>
</circle>

При выполнении анимации произойдет резкий скачок в положение 25, что не есть хорошо (если, конечно, так не задумано). Этого можно избежать при form=0, но тогда теряется смысл использования sum, потому что тот же эффект можно получить и без additive используя by

<animate attributeName="r" from="0" to="15" additive="sum" .../>
<animate attributeName="r" by="15" .../>

Как по мне, второй способ гораздо понятнее и удобнее

Где указывать длительность анимации?

Остался последний обязательный атрибут, чтобы сделать рабочую анимацию – и это dur. Значение атрибута определяет длительность анимации, которое можно указывать как в секундах, так и в миллисекундах

<animate dur="0.5s" .../>
<animate dur="500ms" .../>
<animate dur="00:00:00.5" .../>

По последней строчке можно догадаться, что есть еще кое-что…

Также можно указывать значения в минутах и даже часах

<animate dur="1.2min" .../>
<animate dur="0.02h" .../>

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

Что сделать, чтобы анимация не возвращалась в начало?

Атрибут fill (не путайте этот атрибут с его тезкой) отвечает за поведение элемента после окончания анимации. Предусмотрено две опции:

  • remove (значение по умолчанию) – как только анимация достигает своего конца, все преобразования сбрасываются и элемент принимает состояние как до анимации
  • freeze – элемент застывает в конечном положении анимации

Можно ли зациклить анимацию?

Ответ – да. Для этого в атрибуте repeatCount указывается значение indefinite. Атрибут определяет число повторений анимации и по умолчанию имеет 1, но можно указать любое число

<animate repeatCount="indefinite" .../>
<animate repeatCount="3" .../>

Первая будет повторяться бесконечно, вторая отработает 3 раза

Теперь меня бесят бесконечные анимации, можно их выключить через время?

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

<animate dur="2s" repeatCount="indefinite" repeatDur="3s" .../>

Анимация прервется в середине второй итерации

А что если я хочу, чтобы анимация начиналась не сразу?

Тогда для тебя, мой друг, предусмотрен атрибут begin. Отвечает он за то, когда начнется анимация. Этот атрибут очень полезен, потому что также используется для синхронизации нескольких анимаций, но об этом чуть позже.

Если нужно указать обычную задержку запуска, то пишем, через какой промежуток времени должна начаться анимация после открытия документа

<animate begin="1.5s" .../>

Воспроизведение начнётся через 1,5 секунды

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

<animate begin="-2s" dur="4s" .../>

Анимация начнется с открытием документа, но будет воспроизводиться с середины

Делаем анимации интерактивными

В качестве значения begin можно указать событие, при котором начнется анимация, но без приставки «on». Например, если хочется сделать анимацию по клику, то вместо «onclick» пишем click

<circle ...>
    <animate begin="click" .../>
</circle>

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

<circle id="button" .../>
...
<animate begin="button.click" .../>

Еще можно указать несколько условий начала анимации. Для этого нужно перечислять их через точку с запятой

<animate begin="click; 0s" .../>

Анимация начнется при загрузке документа и по клику

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

Анимация перезапускается, не достигнув конца, как это починить?

Я приведу простой пример. Здесь анимация начинается по клику. Если пользователь так и не нажмет, то предусмотрен автоматический запуск через 3 секунды

<animate begin="click; 3s" dur="7s" .../>

Но появляется проблема: если пользователь нажмет до автоматического таймера, то, когда пройдет 3 секунды, анимация перезапустится, так и не дойдя до конца. На помощь придет атрибут restart в значении whenNotActive. Всего у него их три

  • always стоит по умолчанию – разрешает перезапускать анимацию в любой момент времени
  • whenNotActive – анимация может быть запущена, если она уже не воспроизводится
  • never – запрещает перезапуск анимации
<animate begin="click; 3s" dur="7s" restart="whenNotActive" .../>

Проблема решена, хотя в большинстве случаев можно обойтись без этого атрибута, просто грамотно строя зависимости

Синхронизация анимаций

Помимо стандартных событий, по типу клика, есть события начала, конца, повторения анимации. Для того, чтобы привязать событие, необходимо указать id анимации и через точку begin, end, repeat соответственно

<animate id="pop" begin="click" .../>
<animate begin="pop.begin" .../>
<animate begin="pop.end" .../>

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

<animate id="flip" repeatCount="5"  .../>
<animate begin="flip.repeat(2)"  .../>

Анимация запустится после двух повторений, а не каждые 2 повторения

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

<animate id="another" .../>
<animate begin="another.begin + 2s"  .../>

Или запустить анимацию за секунду до окончания другой

<animate begin="another.end - 1s"  .../>

На что еще способен begin...
хотел я назвать этот раздел, но правильнее его назвать «Что он должен уметь, но не умеет?». По уже моей любимой спецификации у begin должно быть еще два значения, которые он должен принимать. Первый это accessKey, который запускает анимацию по нажатию клавиши, указанной в формате Unicode. Второй — wallclock, определяющий начало анимации по реальному времени. И там можно указать не только часы, но даже месяц и год, в общем полный набор.

К сожалению, ни один из них не захотел работать. Хотя не велика потеря, ведь необходимость в них все равно сомнительная

<animate begin="accessKey(\u0077)" .../>
<animate begin="wallclock(2019-04-09T19:56:00.0Z);" .../>

Не знаю, в чем проблема, может, их не поддерживает мой браузер, а может что-то еще…

Могу ли я прервать анимацию?

Это можно сделать атрибутом end. По своему использованию он идентичен begin, также можно указывать время, события, и т.д. Как можно заметить, это уже не первый (и не последний) способ прерывать анимацию, ведь есть repeatDur, где тоже можно фиксировать длительность анимации. И пусть в end тоже можно указывать время напрямую, его отличительными особенностями являются привязка к событиям и возможность указать список значений.

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

<animate id="idle" end="action.begin" begin="0s" repeatCount="indefinite" .../>
<animate id="action" begin="click" .../>

Анимация покоя запущена по умолчанию. При клике на элемент запустится анимация активности и прервет анимацию покоя

Комбинирование атрибутов end и begin

Как уже известно, и begin, и end могут принимать список значений, но все еще не понятно, как будет вести себя анимация, если указать список в обоих атрибутах. А получатся, своего рода, повторения с настраиваемой длительностью и интервалами между ними… не понятно? Сейчас все объясню.

Первое, что нужно знать – количество значений в списках должно совпадать. Каждая пара значений begin-end определяет одно «повторение». А время между концом одного «повторения» и началом следующего определяет задержку. Я неспроста называю их «повторениями», анимация не приостанавливается и продолжается, а прерывается и начинается с начала. Выходит, мы можем отдельно регулировать длительность каждого повторения и устанавливать разные задержки после каждого повторения

<animate
    dur="3s"
    begin="1s; 5s; 9s"
    end = "2s; 8s; 11s"
    .../>

В примере анимация имеет 3 «повторения». Первый начнется через секунду после загрузки документа и продлится лишь одну секунду из трех. Затем задержка в 3 секунды, и после нее полная анимация в 3 секунды. Опять задержка, но уже в 1 секунду. Последнее повторение прервется после двух секунд анимации

А можно еще как-нибудь прервать анимацию?
Еще парочка бесполезных атрибутов в копилку
Конееееечно, есть еще целых два атрибута – min и max. Как понятно из названия, min определяет минимальную, а max максимальную длительность. Сначала длительность анимации рассчитывается по значениям dur, repeatCount, repeatDur, end. А после полученная длительность подгоняется под рамки, задаваемые min и max. На бумаге все красиво, посмотрим, как это работает на практике.

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

<animate dur="10s" repeatDur="7s" end="5s" max="4s" .../>

Прервется на 4 секунде

А вот min повезло меньше. Если вычисленная длительность анимации больше чем min, то он игнорируется, что логично. Однако если вычисленная длительность меньше чем min, то он… иногда игнорируется, а иногда нет.
Чего, почему?! Вот в этом моменте очень легко запутаться, так что читай внимательно.

У нас есть два варианта, когда вычисленная длительность меньше min:

  1. Потому что сама анимация закончилась, т.е. dur * repeatCount < min
  2. <animate dur="2s" repeatCount="2" min="5s" .../>В этом варианте атрибут min просто игнорируется, анимация остановится на четвертой секунде
  3. Потому что repeatDur или end ограничил длительность. Тут происходит еще одно ветвление
    • У repeatDur наивысший приоритет, поэтому если он задан, и он меньше чем min, то min игнорируется
    • <animate dur="1s" repeatCount ="indefinite" repeatDur="3s" end="5s" min="4s" .../>Хотя значение repeatDur меньше, чем min, анимация все равно прервется через 3 секунды
    • Если repeatDur не задан, а значение end меньше min, то атрибут end будет игнорироваться, а анимация прервется на значении min
    • <animate dur="1s" repeatCount ="indefinite" end="2s" min="4s" .../>В результате анимация прервется на 4 секунде, т.е. min выступает в роли нового end

Из-за обилия атрибутов, прерывающих анимацию, возникает сильная путаница. По итогу большого смысла в max и min нет, ведь грамотно написанная анимация исключает необходимость в них.

Как управлять ключевыми кадрам и где указывать функцию времени?

Для этого нужно знать атрибуты keyTimes, keySplines, calcMode. Указав в values список, мы объявляем ключевые кадры, но они распределены равномерно. Благодаря атрибуту keyTimes мы можем ускорять или замедлять переход из одного состояния в другое. В нем, так же в виде списка, указываются значения для каждого кадра. Значения представляют положение ключевого кадра на временной оси в процентном соотношении, относительно длительности всей анимации (0 – 0%; 0,5 – 50%; 1 – 100%).

Есть несколько правил: каждое значение представляет число с плавающей точкой от 0 до 1, кол-во значений в списках должно совпадать, первое значение обязательно 0, а последнее 1, каждое следующее значение должно быть больше предыдущего. Думаю, ты понимаешь, что нет смысла использовать keyTimes без values. А теперь пример

<animate values="15; 10; 45; 55; 50" keyTimes="0; 0.1; 0.6; 0.9; 1" .../>

По умолчанию все преобразования происходят линейно, чтобы это изменить, нужно указать другой режим в calcMode. И вариантов тут не много:

  • linear – стандартное значение, в объяснении не нуждается
  • paced – временные промежутки рассчитываются так, чтобы скорость между ключевыми кадрами была постоянной
  • discrete – анимация переключается между ключевыми кадрами скачками, без интерполяции
  • spline – можно сказать, что это ручной режим управления (о нем попозже)

К сожалению, это все встроенные функции, тут ты не найдешь ease in\out как в CSS. Так что эти нужды придется удовлетворять режиму, который я обозвал «ручным».

Самый трудный для понимания — это paced, так что его объясню поподробней. Для начала посмотри, как работает анимация в стандартном режиме. Длительность анимации составляет 2 секунды и у нас есть 3 ключевых кадра – начальный, промежуточный, конечный

<animate dur="2s" values="100; 200; 150" .../>

Если понаблюдать за анимацией, то станет очевидно, что перемещение между ключевыми кадрами происходит за равные промежутки времени. Расстояние между первым и вторым составляет 100, а между вторым и третьим – 50, т.е. половину от первого пути. Путем не хитрых вычислений становится ясно, что элемент будет проходить второй отрезок в два раза медленнее, чем первый.Теперь добавим calcMode="paced" и посмотрим, что изменилось.

<animate dur="2s" values="100; 200; 150" calcMode="paced" .../>

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

Теперь посмотрим на режим spline и атрибут keySplines. Они имеют некоторое сходство …хмм…

Если spline определяет ручной режим, то атрибут keySplines определяет значения для этого режима. Очевидно, что одно без другого не работает. Значения в keySplines задаются списком, где указываются координаты двух точек для кубической Безье.

Подробнее о кубической функции Безье

Количество значений в keySplines должно быть на 1 меньше чем values. Связано это с тем, что мы указываем значения не для ключевых кадров, а для промежутков меду ними.

<animate
    values="100; 200; 150"
    keySplines=".25 .1 .25 1;
                0   0  .58 1"
    calcMode="spline"
    .../>

Координаты точек функции Безье разделяются пробелами или запятыми, а значения списка через точку с запятой

Первый и самый неприятный минус – нельзя задать общую функцию времени для всех ключевых кадров, придется дублировать функцию для каждого кадра.
Второй – если хочется задать функцию времени для атрибутов from-to или by, то нужен костыль: придется задать keyTimes со значениями "0; 1"

<animate
    from="10"
    to="50"
    keyTimes="0; 1"
    keySplines=".25 .1 .25 1;"
    calcMode="spline"
    .../>

Если вместо from-to использовать values с двумя значениями, то такой проблемы не будет

Как реализовать накопительные анимации?

Сначала немного теории – следующее повторение у накопительной анимации продолжится там, где закончилось предыдущее. Как бы круто, но не очень… Огорчает тот факт, что накопительные анимации работают только в пределах повторений.

Теперь о том, как сделать анимацию накопительной: нужно установить атрибут accumulate (который по умолчанию none) в значение sum

<animate by="100" repeatCount="3" accumulate="sum".../>

Стоит иметь в виду, что если использовать values или from-to, то все повторения, кроме первого, будут вести они себя как при additive="sum". А еще accumulate игнорируется если задан только один to.

Постигаем морфинг контура

Теперь, когда я объяснил основы, пора переходить к по настоящему крутым и сложным вещам. Уверен, что кто-то открыл эту статью исключительно ради этого раздела.

Морфинг контуров – это анимация атрибута d у тега path, что позволяет создать эффект плавного изменения формы фигуры. На данный момент встроенными средствами такое можно сделать только с помощью SMIL. В values указывается список значений для атрибута d, через которые пройдет элемент. Также можно использовать from-to. В общем виде морфинг контура выглядит так

<animate attributeName="d" values="состояние 1; состояние 2; ..." .../>

А теперь перейдем к тонкостям сего процесса:

Для тех кто в танке – атрибут d содержит в себе набор точек, которые впоследствии поочередно соединяются и получается фигура. При более детальном рассмотрении можно заметить, что список значений похож на набор команд для ЧПУ станка (или «робота» на уроках информатики). Команд достаточно много, одни отвечают за «перемещение курсора», другие за «рисование», какие-то за то, насколько кривая будет линия и т.д. (все команды тут).

Чтобы морфинг сработал, количество команд должно совпадать, и они должны быть одного типа. Если проигнорировать это условие, то интерполяция будет отсутствовать – анимация будет скакать от одного состояния к другому, как при calcMode="discrete". На первый взгляд ничего сложного и это так, если ты будешь анимировать фигуры без кривых. Если же нет, то тут начинаются сложности.

При создании сложной графики все используют векторные редакторы, а у них есть привычка максимально оптимизировать «код». Обычно это плюс, но не в нашем случае. На выходе у нас возможно будет список одной длинны, но с командами разного типа, а это нарушение одного из правил. Я использовал Adobe Illustrator и не нашел опции, которая могла поправить положение дел. Иногда, по воле богов дизигна, эта проблема отсутствует. А если серьезно, то вероятность возникновения проблемы прямо пропорциональна сложности фигуры и морфинга.

На данный момент единственным решением проблемы я вижу преобразование «кривого кода» в веб-приложении Shape Shifter. Это тот вариант, которым пользуюсь я. Помимо починки битого кода Shape Shifter позволяет посмотреть результат, при желании добавить анимации другого типа и экспортировать результат в удобном формате.

Далее будет пошаговый туториал, где я расскажу, как сделать такую красивую анимацию

Тег set является укороченной версией animate, за исключением того, что он не может в интерполяцию. Он используется для мгновенного изменения атрибута на определенный отрезок времени, т.е. работает по принципу переключателя. Вследствие этого игнорирует атрибуты, связанные с интерполяцией и не поддерживает накопительные или относительные анимации. Значение задается исключительно с помощью атрибута to, атрибуты values, from, by игнорируются

<set attributeName="cx" to="200" begin="click" dur="5s" .../>

Элемент изменяет положение по клику, по истечении 5 секунд возвращается на исходное место

Если не указывать атрибут dur, то элемент останется в этом состоянии до перезагрузки документа. В остальном он аналогичен animate.

<animateTransform>

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

Как трансформировать?

В качестве анимируемого атрибута выступает transform. Режим трансформации указывается в атрибуте type и принимает 4 типа значений – перемещение, поворот, масштабирование, сдвиг по осям.

translate – перемещение элемента относительно его текущего положения. В качестве значений принимает смещение в формате [x, y], где y является необязательным параметром

<animateTransform attributeName="transform" type="translate" from="0, -10" to="0, 10" .../>

Перемещает элемент по оси Y

rotate – поворачивает элемент относительно центра вращения. В качестве значений принимает угол поворота и координаты центра вращения [deg, x, y], координаты центра указывать не обязательно. По умолчанию центр вращения находится в верхнем левом углу SVG документа

<animateTransform attributeName="transform" type="rotate" from="0, 150, 150" to="45, 150, 150" .../>

Поворот на 45 градусов вокруг точки с координатами 150, 150

Также центр вращения можно изменить с помощью CSS свойства transform-origin, где помимо координат можно указать проценты. По умолчанию процентные значения рассчитываются по размерам всего документа, чтобы проценты считались относительно элемента, нужно задать CSS свойство transform-box со значением fill-box.

scale – масштабирует элемент. В качестве значений принимает числа с плавающей точкой в формате [scale] для обеих осей, или отдельно для каждой оси [scaleX, scaleY] (1 соответствует нормальному размеру элемента). Если не менять transform-box, о котором я говорил выше, то элемент масштабируется относительно всего документа. Пустое пространство вокруг элемента тоже изменяется вместе с ним, поэтому визуально кажется, что элемент смещается в сторону

<animateTransform attributeName="transform" type="scale" from="1, 1" to="2, 1" .../>

Растягивает элемент по оси Х

skewX или skewY – сдвигает элемент относительно оси. В качестве значения принимает угол наклона [deg]. По дефолту центр сдвига – верхний левый угол, так что тут работает тот же прикол с transform-box и transform-origin, что и в других трансформациях

<animateTransform attributeName="transform" type="skewX" from="0" to="45" .../>
<animateTransform attributeName="transform" type="skewY" from="90" to="0" .../>

Один сдвигает по X, другой по Y

Суммирование и переопределение трансформаций

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

<rect transform="skewY(115)" ...>
    <animateTransform type="translate" from="-10" to="10" additive="replace" .../>
    <animateTransform type="rotate" from="0" to="90" additive="sum" .../>
</rect>

В данном примере сдвиг прямоугольника будет переопределен на перемещение и поворот

<animateMotion>

Нужен, чтобы анимировать движение элемента вдоль траектории. animateMotion поддерживает атрибуты animate и имеет 3 собственных – path, rotate, keyPoints.

Варианты определения траектории

Определить траекторию движения можно несколькими способами – использовать знакомые нам атрибуты from, to, by или values, новый атрибут path или дочерний тег <mpath>. Я перечислил способы по возрастанию приоритета и объяснять я их буду в том же порядке.

В атрибуты from, to, by указываются координаты точек, values то же, но уже в виде списка

<animateMotion from="0,0" to="50,100" .../>
<animateMotion values="0,0; 0,100; 100,100; 0,0" .../>

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

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

<animateMotion path="M 0 0 c 3.4 -6.8 27.8 -54.2 56 -37.7 C 73.3 -27.5 89.6 -5.1 81.9 5.9 c -5.8 8.3 -24.7 8.7 -45.4 -0.4" .../>

Данный путь описывает вот такую кривую

Последний способ – использовать в качестве траектории сторонний элемент <path>. Для этого в теге <mpath> нужно указать ссылку на этот элемент, а сам тег нужно поместить внутрь <animateMotion>. Этот вариант имеет ту же особенность с относительными координатами. По своей сути этот способ как бы «копирует» из элемента значение атрибута d в атрибут <path>

<path id="movement" .../>
...
<animateMotion ...>
    <mpath href="#movement"/>
</animateMotion>

Элемент, который определяет траекторию, может даже не отображаться в документе. Его можно просто определить в <defs>

Поворот элемента относительно траектории

Есть возможность заставить элемент поворачиваться по направлению движения, используя атрибут rotate. Он принимает 3 типа значений: auto, auto-reverse и число, обозначающее поворот в градусах

<animateMotion rotate="auto" .../>

По умолчанию rotate имеет значение 0. Любое численное значение фиксирует угол на протяжении всей анимации. Автоматические режимы auto и auto-reverse изменяют угол поворота элемента соответственно касательной к траектории. И отличаются направлением этой касательной. У auto она направлена вперед, а у auto-reverse назад

Как управлять перемещением по траектории?

Траектория представляет собой кривую, у которой есть начало и есть конец, эти точки обозначаются числами 0 и 1 соответственно. Любое положение на кривой можно определить числом в этом диапазоне. Перечисляя точки в атрибуте keyPoints, можно определить любой вид движения по траектории. Но этого недостаточно, чтобы управлять перемещением, для этого нужна целая система из атрибутов.

Для начала нужно установить calcMode в положение linear или spline. В отличие от других тегов, animateMotion по умолчанию имеет значение paced (почему-то анимация не хочет работать в этом режиме). Также необходимо указать атрибут keyTimes. Только выполнив эти действия, анимация заработает как надо

<animateMotion keyPoints="0.5; 1; 0; 0.5" keyTimes="0; 0.25; 0.75; 1" calcMode="linear" .../>

В примере анимация стартует в середине траектории, движется до конца, затем в начало, и заканчивает движение опять в середине