Pull to refresh

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

Reading time41 min
Views161K
В данной статье я хочу осветить тонкости работы с 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 с нашим изображением. Однако отличия начинаются еще на этапе параметров экспорта


Детально объяснять все параметры я не вижу смысла, с этим прекрасно справляется сам Illustrator в секции «Описание».

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

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

Экспортировать

<svg id="Слой_1" data-name="Слой 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 51 51">
  <defs>
    <style>
      .cls-1 {
        fill: none;
        stroke: #4ec931;
        stroke-miterlimit: 10;
      }

      .cls-2 {
        fill: #4ec931;
      }

      .cls-3 {
        fill: #fff;
      }
    </style>
  </defs>
  <title>my_icon_E</title>
  <circle class="cls-1" cx="25.5" cy="25.5" r="20"/>
  <circle class="cls-1" cx="25.5" cy="25.5" r="25"/>
  <g id="Слой_2" data-name="Слой 2">
    <circle class="cls-2" cx="25.5" cy="25.5" r="15"/>
    <polygon class="cls-3" points="25.5 34.8 34 20.3 17 20.3 25.5 34.8"/>
  </g>
</svg>

Сохранить

<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 23.0.2, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
<svg version="1.1" id="Слой_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
     viewBox="0 0 100 100" style="enable-background:new 0 0 100 100;" xml:space="preserve">
<style type="text/css">
    .st0{fill:none;stroke:#4EC931;stroke-miterlimit:10;}
    .st1{fill:#4EC931;}
    .st2{fill:#FFFFFF;}
</style>
<circle class="st0" cx="50" cy="50" r="20"/>
<circle class="st0" cx="50" cy="50" r="25"/>
<g id="Слой_2">
    <circle class="st1" cx="50" cy="50" r="15"/>
    <polygon class="st2" points="50,59.3 58.5,44.8 41.5,44.8    "/>
</g>
</svg>

Помимо отличий в именовании CSS классов и оформления в целом, которые кто-то может посчитать вкусовщиной, есть и другие проблемы. При «экспортировании» все изображение уменьшилось в 2 раза. Судить об этом можно по размерам фигур и атрибуту viewBox. Так как это векторная графика, хуже от этого не стало, но все равно неприятно. «Сохранение» же оставило размеры, которые я указал в Illustrator.

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



Объем файла достаточно большой, так что я приведу только проблемную часть

<g id="кнопка-21" data-name="кнопка">
    <path class="cls-9" d="M477.94,456.75a1.83,1.83,0,0,1-.9,1.36l-4.91,3.1a7.29,7.29,0,0,1-7.5,0l-16.29-9.72a1.85,1.85,0,0,1-.92-1.56v-3.68a1.85,1.85,0,0,0,.92,1.56l.38.23,15.91,9.49a7.29,7.29,0,0,0,7.5,0l4.53-2.86.38-.23a1.87,1.87,0,0,0,.9-1.36Z" transform="translate(-5.5 -5.5)"/>
    <path class="cls-10" d="M477,451.19l-16.38-9.5a7.28,7.28,0,0,0-7.32,0l-5,2.9a1.88,1.88,0,0,0-.94,1.51v.17a1.85,1.85,0,0,0,.92,1.56l.38.23,15.91,9.49a7.29,7.29,0,0,0,7.5,0l4.53-2.86.38-.23a1.87,1.87,0,0,0,.9-1.36v-.51A1.88,1.88,0,0,0,477,451.19Z" transform="translate(-5.5 -5.5)"/>
</g>
<g id="кнопка-22" data-name="кнопка">
    <path class="cls-9" d="M525.37,557.86a1.85,1.85,0,0,1-.9,1.36l-33.22,19.64a7.29,7.29,0,0,1-7.5,0l-16.29-9.72a1.85,1.85,0,0,1-.92-1.56v-3.68a1.85,1.85,0,0,0,.92,1.56l.38.23,15.91,9.49a7.29,7.29,0,0,0,7.5,0l32.84-19.41.38-.23a1.83,1.83,0,0,0,.9-1.36Z" transform="translate(-5.5 -5.5)"/>
    <path class="cls-10" d="M524.45,552.3l-16.38-9.51a7.31,7.31,0,0,0-7.32,0l-33.27,19.44a1.89,1.89,0,0,0-.94,1.51v.17a1.85,1.85,0,0,0,.92,1.56l.38.23,15.91,9.49a7.29,7.29,0,0,0,7.5,0l32.84-19.41.38-.23a1.83,1.83,0,0,0,.9-1.36v-.5A1.86,1.86,0,0,0,524.45,552.3Z" transform="translate(-5.5 -5.5)"/>
</g>

Заметил что-нибудь необычное? Если ты косо смотришь на атрибут transform, то ты прав. Именно он портит всю малину. При «экспортировании» изображения Illustrator приписывает его ко ВСЕМ элементам <path>. При этом такой проблемы не наблюдается при «сохранении».

Если ты до сих пор не понимаешь моего негодования, то я объясню: если захочешь анимировать перемещение такого элемента, то он будет смещаться в сторону. В данном случае на 5.5 по обеим осям. Связанно это с тем, что анимация перемещения изменяет атрибут transform, сбрасывая все прошлые значения. Конечно, это можно обойти, но разве не лучше избежать проблемы, чем потом исправлять ее последствия…

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

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

    <circle ...>
        <animate .../>
    </circle>
    
    В данном случае анимация будет применена к элементу circle
  2. Передать ссылку на элемент. Пригодится если хочется, чтобы все анимации были собраны в одно месте

    <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, я тебя предупредил)

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

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

Краткий гайд как разочароваться в жизни
  1. Открываем спецификацию по SVG анимации за 14 марта 2019
  2. В разделе про attributeName видим, что он наследует стандарт SMIL Animation за (о ужас) 2001 год
  3. Читаем определение attributeName
  4. Profit!

Перевод определения гласит примерно следующее:
«Определяет имя целевого атрибута. Префикс XMLNS может использоваться для указания пространства имен XML для атрибута. Префикс будет интерпретироваться в области действия элемента анимации.»
Хмм, легче не стало. Что придет в голову человеку, который не знает XML, после прочтения такого определения? Правильно. То же самое что и мне.

Я понял его буквально и подумал, что это должно выглядеть так

<animate attributeName="xmlns:*имя атрибута*"/>

Подумал и благополучно забыл про этот способ до написания этой статьи. Проблемы начались, когда я решил проверить его на практике. Думаю, я никого не удивлю, если скажу, что это не работает. Спустя несколько часов безуспешных поисков я загуглил «xmlns prefix» и к моему удивлению увидел, что xmlns это не сам префикс, а (сконцентрируйся, сейчас будет сложно) конструкция определения пространства имен с префиксами.

Выглядит она следующем образом:

<*тег* xmlns:*префикс*="*полный url адрес*" ...>
Тут я понял, что ничего не понял…в самом начале…и сейчас в принципе тоже

Спустя еще пару часов я наконец нашел, что искал, в Namespaces in XML. Вот оригинальный пример:

<x xmlns:n1="http://www.w3.org" xmlns="http://www.w3.org" >
    <good a="1" n1:a="2" />
</x>

Но знаешь, что самое смешное? Это все равно не работает. Хотя сделано все по книжке

<svg version="1.1"
    xmlns="http://www.w3.org/2000/svg"
    xmlns:n1="http://www.w3.org/2000/svg">
    <circle id="www" n1:r="10" .../>
    <animate href="#www" attributeName="n1:r" .../>
</svg>

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

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

Чтобы явно указать, к чему принадлежит анимируемый атрибут, используется 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

    <animate dur="2s" repeatCount="2" min="5s" .../>
    В этом варианте атрибут min просто игнорируется, анимация остановится на четвертой секунде
  2. Потому что 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 задаются списком, где указываются координаты двух точек для кубической Безье.

Подробнее о кубической функции Безье
Это функция, которая определяет интенсивность движения. Она состоит из 4 чисел с плавающей точкой от 0 до 1 и в общем виде выглядит так: cubic-bezier(x1, y1, x2, y2). Пары чисел являются координатами и образуют две точки, которые определяют кривую.

Еще подробнее о cubic-bezier можешь прочитать в сети. Для построения кубической Безье я советую пользоваться онлайн сервисами, также там можно взглянуть на примеры и поиграть со значениями.

Количество значений в 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 позволяет посмотреть результат, при желании добавить анимации другого типа и экспортировать результат в удобном формате.

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

Teach me!
Плант такой: я создам SVG каркас в Adobe Illustrator, напишу анимацию и не одну, починю косяки в Shape Shifter и сделаю морфинг. Я буду использовать как CSS, так и SMIL анимацию.

Будем делать анимированную иконку вопроса. Для начала нужно создать новый проект в Illustrator. Так как в итоге мы получим векторное изображение, большое разрешение тут не нужно. Я считаю комфортным для работы размер монтажной области 200x200 пикселей.

Далее нужен, собственно, сам знак вопроса. Вместо того, чтобы рисовать его вручную, я воспользуюсь инструментом «Текст». Поиграв с параметрами и шрифтами, получаем такой симпатичный вопросик

Но в таком виде анимировать его не получится. Поэтому нужно преобразовать текст в кривые. Щелкаем правой кнопкой мыши по тексту и выбираем «Преобразовать в кривые»

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

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

На этом каркас готов и пора переходить к анимации. Но перед этим нужно сохранить сам проект, потому что он нам еще пригодится. Только после этого сохраняем проект как SVG файл. (как правильно экспортировать документ из Illustrator я писал в самом начале статьи)

Когда я заглянул внутрь полученного документа, я очень удивился, потому что Illustrator сохранил мой круг как элемент <path>, а не <circle>. Так я плавно подвожу к тому, что настало время прибраться. Нужно убрать все лишнее, оптимизировать некоторые моменты и просто сделать так, чтобы было приятно работать. Кто-то делает это с помощью сторонних программ (например SVGO), но я предпочитаю делать все ручками. В процессе создания анимации еще придется не раз перелопатить структуру файла.

В примерах стер содержимое атрибута d, которое занимает много места)

Вот как было
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 23.0.2, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
     viewBox="0 0 200 200" style="enable-background:new 0 0 200 200;" xml:space="preserve">
<style type="text/css">
    .st0{fill:#D38911;}
    .st1{fill:#87872B;}
    .st2{fill:#CEB629;}
    .st3{fill:none;stroke:#DD913E;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:12,200;}
</style>
<g id="Pulse">
    <g>
        <path class="st0" d="M100,87c44.1,0,80,35.9,80,80s-35.9,80-80,80s-80-35.9-80-80S55.9,87,100,87 M100,82c-46.9,0-85,38.1-85,85
            s38.1,85,85,85s85-38.1,85-85S146.9,82,100,82L100,82z"/>
    </g>
</g>
<g id="_x3F__1_">
    <path id="side" class="st1" d=" Много букв "/>
    <path id="front" class="st2" d=" Много букв "/>
</g>
<g id="Particles">
    <line class="st3" x1="80" y1="162.9" x2="42" y2="59.1"/>
    <line class="st3" x1="90.1" y1="148.8" x2="59.8" y2="28.8"/>
    <line class="st3" x1="107.9" y1="155.6" x2="124.9" y2="15.9"/>
    <line class="st3" x1="94.4" y1="160.4" x2="154.3" y2="7.2"/>
    <line class="st3" x1="119.3" y1="157" x2="159.2" y2="75.5"/>
    <line class="st3" x1="98" y1="169" x2="87.7" y2="10.7"/>
    <line class="st3" x1="80.4" y1="147.6" x2="63.2" y2="14.1"/>
</g>
</svg>

И как стало
<?xml version="1.0" encoding="utf-8"?>
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200">
<style type="text/css">
    #Pulse{fill: none; stroke: #D38911; stroke-width: 5;}
    #side{fill:#87872B;}
    #front{fill:#CEB629;}
    .particles{fill:none;stroke:#DD913E;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:12,200;}
</style>
<circle id="Pulse" cx="100" cy="167" r="85"/>
<g id="Sign">
    <path id="side" d=" Много букв "/>
    <path id="front" d=" Много букв "/>
</g>
<g id="Particles">
    <line class="particles" x1="80" y1="162.9" x2="42" y2="59.1"/>
    <line class="particles" x1="90.1" y1="148.8" x2="59.8" y2="28.8"/>
    <line class="particles" x1="107.9" y1="155.6" x2="124.9" y2="15.9"/>
    <line class="particles" x1="94.4" y1="160.4" x2="154.3" y2="7.2"/>
    <line class="particles" x1="119.3" y1="157" x2="159.2" y2="75.5"/>
    <line class="particles" x1="98" y1="169" x2="87.7" y2="10.7"/>
    <line class="particles" x1="80.4" y1="147.6" x2="63.2" y2="14.1"/>
</g>
</svg>
Моей целью была не максимальная оптимизация файла для экономии места, а удобство работы. Тут еще есть что удалить, но это мне не мешает, так что и так сойдет

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

#Pulse{fill: none; stroke: #D38911; stroke-width: 5; transform: rotateX(80deg);}
._transformer{transform-box: fill-box; transform-origin: center;}

Чтобы круг поворачивался корректно и никуда не ехал, нужен класс _transformer, который я присвою кругу и другим элементам, которые будут трансформироваться. Об этом «лайфхаке» я расскажу подробнее в части про <animateTransform>.

Кто-то спросит: почему ты сразу не сплюснул круг, когда рисовал его в Illustrator? Тогда бы не пришлось возиться с ним сейчас…

Все просто – это делалось для того, чтобы была разница в толщине линии. Если просто сплюснуть круг, такого эффекта не добиться. Конечно, это можно было бы сделать с помощью <path>, но тогда и размер файла был бы больше и анимировать такой элемент ГОРАЗДО сложнее.

Собстна, анимация! Происходить будет следующее – при нажатии на вопрос радиус и обводка круга будут увеличиваться, а его прозрачность уменьшаться. Первые две анимации будут происходить линейно, а исчезновение – под самый конец. Сделаем значение радиуса у circle равным нулю, чтобы его не было видно. Так как animate может анимировать только один атрибут за раз, придется сделать 3 синхронные анимации

<circle id="Pulse" class="_transformer" cx="100" cy="167" r="0">
    <animate id="doPulse" attributeName="r" values="0;85;" dur=".8s" begin="Sign.click" calcMode="spline" keySplines="0,0,.58,1"/>
    <animate attributeName="stroke-width" values="5;12;" dur=".8s" begin="doPulse.begin"/>
    <animate attributeName="opacity" values="0.5;1;1;0" keyTimes="0;0.2;0.5;1" dur=".8s" begin="doPulse.begin"/>
</circle>
Если бы не было необходимости привязывать анимацию к клику, разумнее было бы реализовать ее на CSS


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


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

<linearGradient id="light-gradient">
    <stop offset="0%" stop-color="#ffffff00"/>
    <stop offset="10%" stop-color="#FFF"/>
    <stop offset="90%" stop-color="#FFF"/>
    <stop offset="100%" stop-color="#ffffff00"/>
</linearGradient>
<mask id="light-mask"> <rect y="0" x="90" class="_transformer" width="20" height="220" fill="url(#light-gradient)" /> </mask>

Отлично, маска готова. Но для того, чтобы реализовать задуманное, нужно изменить структуру файла. Так как у нас будут два совершенно одинаковых знака вопроса, я заменю их на конструкцию <use>, а элемент <path> перемещу в <defs>

<defs>
    <path id="question" d=" Тут рисуется знак вопроса "/>
</defs>
...
<use id="front" href="#question"/>
<use id="light" href="#question" mask="url(#light-mask)"/>

Вот что получается

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

#light-mask rect{ animation: highlight 4s infinite; }
@keyframes highlight {
    0% { transform: translate(-100px,0) rotate(-50deg); }
    30%  { transform: translate(100px,0) rotate(-50deg); }
    100%  { transform: translate(100px,0) rotate(-50deg); }
}

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

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

<g id="Sign" class="_transformer">
    <path id="side" d=" Очередной длинный набор команд "/>
    <use id="front" href="#question"/>
    <use id="light" href="#question" mask="url(#light-mask)"/>
    <animateTransform 
        id="idle"
        attributeName="transform"
        type="translate"
        values="0,0;0,-5;0,0"
        dur="6s"
        begin="0s; jump.end"
        end="click"
        repeatCount="indefinite"
    />
    <animateTransform
        id="jump"
        attributeName="transform"
        type="translate"
        calMode="spline"
        values="0,0;0,10;0,-35;0,5;0,0"
        keyTimes="0;0.1;0.35;0.6;1"
        keySpline="0,0,.58,1;0,0,.58,1;.42,0,1,1;0,0,.58,1"
        dur="1s"
        begin="idle.end"
    />
</g>
Все анимации применяются к группе, чтобы элементы внутри двигались вместе

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

<animateTransform
    attributeName="transform"
    type="scale"
    additive="sum"
    values="1,1;1.1,0.8;0.9,1.2;1.1,0.8;1,1"
    keyTimes="0;0.1;0.35;0.7;1"
    dur="1s"
    begin="idle.end"
/>

Еще я изменил время для анимации круга, чтобы она запускалась, когда знак приземляется. Результат:

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

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

<g id="Particles"> ... </g>
<g id="Sign" class="_transformer"> ... </g>

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

.particles{ opacity:.7; stroke-width:0; ... }

Дальше анимация, которая сама по себе не несет ничего особенного. Просто уменьшаем длину и толщину штриха, попутно перемещая его по прямой

@keyframes sparks {
    0% { stroke-dasharray: 20,200; stroke-width: 5px; }
    100% { stroke-dasharray: 4,200; stroke-width: 0px; stroke-dashoffset: -180; }
}

Интерес вызывает то, как мы будем применять эту анимацию. Я хочу, чтобы частицы разлетались, когда знак «приземляется». Но тут же появляются сложности: анимация написана на CSS и, если просто применить ее к нашим частицам, она будет воспроизводиться сразу при открытии документа. А если делать ту же анимацию на SMIL, то их будет 3 на каждый атрибут, и они будут дублироваться для каждой линии, а их целых 7. Вариант, мягко говоря, не очень…

Решение проблемы элегантно – вместо того, чтобы выбирать что-то одно, будем использовать их одновременно. Анимация будет написана на CSS, а интерактивную часть будет обрабатывать SMIL.

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

.Particles_active .particles{ animation: sparks .7s; }

И добавим элемент <set>, который будет присваивать этот класс группе, когда настанет нужный момент (подробнее про set расскажу в следующем разделе)

<g id="Particles">
    <line class="particles" x1="80" y1="162.9" x2="42" y2="59.1"/>
    ...
    <line class="particles" x1="80.4" y1="147.6" x2="63.2" y2="14.1"/>
    <set attributeName="class" to="Particles_active" dur=".7s" begin="jump.begin + .5s"/>   
</g>

Что мы получили в результате такого манёвра:

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


Морфинг контуров я оставил на сладкое. У нас уже есть иллюзия объема, но мы ее усилим, сделав небольшое вращение. Так как знак не 3D объект, просто крутить его не получится. Поэтому будем изменять контуры так, чтобы казалось, что знак вращается.

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

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


Я предпочитаю открывать в браузере, потому что там удобнее искать. Копируем сгенерированные значения и идем с ними на сайт Shape Shifter.

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

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


Все, что остается, это просто вставить скопированное значение в toValue. Вставленное значение сразу же изменяется и его можно оттуда копировать. На этом «починку» можно считать оконченной

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

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

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

<animate
    attributeName="d"
    calMode="spline"
    values=" Состояние 1; Состояние 2; Состояние 1"
    dur="5s"
    keySpline=".42,0,.58,1;.42,0,.58,1"
    repeatCount="indefinite"
/>

Состояние 1 – это нормальный вид вопроса, а состояние 2 – повернутое


На этом создание анимированной иконки вопроса заканчивается. Напоследок предлагаю взглянуть на финальный результат на Codepen.

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

<set>


Тег 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" .../>
В примере анимация стартует в середине траектории, движется до конца, затем в начало, и заканчивает движение опять в середине

P.S.

Разбираясь с animateMotion, я наткнулся на информацию, что то же самое можно, вроде как, сделать на CSS. Но под конец написания статьи у меня не было ни сил, ни желания с этим разбираться. Для энтузиастов я просто оставлю ссылку на документацию.


Особая благодарность


Bhudh за огромную работу по коррекции статьи
Tags:
Hubs:
+39
Comments21

Articles