December 5, 2023

Анимированный SVG лоадер

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

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

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

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

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

Приступим к реализации.

Этап 1. Подготовка векторной основы

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

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

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

Итак, у нас есть два векторных элемента, квадрат:

<path
  d="M700,700.5 L700,еь300 L300,300 L300,700.5 L700,700.5"
  stroke="#A94005"
  stroke-width="40"
  stroke-linecap="square"
  fill="none"
>

и круг:

<circle fill="#F9C141" cx="670" cy="670" r="50">

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

Path траектории выглядит таким образом

<path
  d="M700,700.5 L700,300 L300,300 L300,700.5 L700,700.5 C926.01092,451.207538 961.074865,338.373425 805.191837,361.99766 C675.217992,381.695332 220.510369,567.838892 124.783651,640.760981 C-62.8581216,783.701533 61.7032261,922.899504 451.788931,852.02978 C553.891878,833.479991 694.785354,782.970064 874.469359,700.5"
>

Здесь видно, первая часть траектории полностью совпадает с Path квадрата. Т.е. часть пути наша точка будет двигаться по контуру квадрата.

Этап 2. Подготовка SVG

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

<svg width="200px" height="200px" class="loader" viewBox="0 0 1000 1000" version="1.1" xmlns="http://www.w3.org/2000/svg">
  <path
    d="M700,700.5 L700,300 L300,300 L300,700.5 L700,700.5"
    stroke="#A94005"
    stroke-width="40"
    stroke-linecap="square"
    fill="none"
  ></path>
  
  <circle fill="#F9C141" cx="670" cy="670" r="50"></circle>
</svg>

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

Этап 3. Анимация

Теперь добавить, собственно, саму анимацию. Начнем с квадрата. Здесь будем использовать классический подход с применением stroke-dasharray / stroke-dashoffset.

Немного теории

stroke-dasharray представляет stroke элемента в виде пунктира. Для того, чтобы рассчитать этот параметр нужным нам образом, необходимо знать длину контура нашего элемента, именно это значения является ключевым для этого атрибута. Определить длину контура можно как эмпирически (подобрать вручную), так и по средствам математики, например

В нашем случае, длина контура квадрата будет 4 * 400 = 1600

Т.к. контур нашей фигуры должен быть отрисован одной сплошной линией, то и длина штрих у нас будет равна длине контура, т.е.

<path ... stroke-dasharray="1600">

Вторым атрибутом stroke-dashoffset добавим смещение первого штриха. Т.к. изначально квадрат не должен быть еще нарисован, сделаем смещение на всю длину контура

<path ... stroke-dashoffset="1600">

Далее, будет анимировать атрибут stroke-dashoffset от 1600 до 0 имитируя отрисовку контура. Делать это будет с помощь тэга <animante>

<path
    d="M700,700.5 L700,300 L300,300 L300,700.5 L700,700.5"
    stroke="#A94005"
    stroke-width="40"
    stroke-linecap="square"
    fill="none"
    stroke-dasharray="1600"
    stroke-dashoffset="1600"
  >
    <animate
       fill="freeze"
       dur="4s"
       repeatCount="indefinite"
       attributeName="stroke-dashoffset"
       values="1600; 1200; 1200; 800; 800; 400; 400; 0; 0; 0"
       keyTimes="0; 0.115; 0.125; 0.24; 0.25; 0.365; 0.375; 0.49; 0.5; 1"
    ></animate>
  </path>

Обращу, так же, внимание, что здесь присутствуют два ключевых атрибута values и keyTimes. Они работают в паре и задают нелинейность самой анимации. Первым атрибутом мы задаем значения, которых должна достигнуть анимация к определенному моменту времени, вторым - сами моменты времени, где 0 - начало анимации, 1 - её конец

В нашем варианте мы будем делать небольшие остановки в каждом углу квадрата, а в конце, половину времени анимации не будет (stroke-dasharray к этому времени уже достигнет 0), т.к. после отрисовки контура квадрата будет дополнительное анимированное движение точки.

Теперь перейдем к самой точке. Её движение, условно, можно разделить на две части. Первая часть - повторение контура квадрата в том же тайминге, вторая часть - эллиптический оборот вокруг квадрата. Делать перемещение будем с помощью <animateMotion>

<circle fill="#F9C141" cx="0" cy="0" r="50">
    <animateMotion
       path="M700,700.5 L700,300 L300,300 L300,700.5 L700,700.5 C926.01092,451.207538 961.074865,338.373425 805.191837,361.99766 C675.217992,381.695332 220.510369,567.838892 124.783651,640.760981 C-62.8581216,783.701533 61.7032261,922.899504 451.788931,852.02978 C553.891878,833.479991 694.785354,782.970064 874.469359,700.5"
       dur="4s"
       calcMode="linear"
       repeatCount="indefinite"
       keyPoints="0; 0.103; 0.103; 0.206; 0.206; 0.309; 0.309; 0.412; 0.412; 1; 1"
       keyTimes="0; 0.115; 0.125; 0.24; 0.25; 0.365; 0.375; 0.49; 0.6; 0.7; 1"
    ></animateMotion>
  </circle>

<animateMotion>, в качестве атрибута, принимает path - траекторию движение (ту самую, которую мы рисовали в редакторе), в остальном он очень похож на <animate>. Еще одно особенностью здесь является использование keyPoints. Он, так же как и values, работает в паре с keyTimes, разница только в том, что значениями является расстояние, которое пройдет элемент к данному моменту времени, где 0 - начальная точка пути, 1 - конечная точка пути. В нашем случае, длина контура квадрата составлеят 0.412 от всего маршрута, эту часть пути точка должна пройти за половину времени (точнее за 0.49 времени, т.е. к тому моменту, когда квадрат будет полностью отрисован).

Так как анимацию у нас не аддитивная, я выставил исходные координаты точки в 0

cx="0"
cy="0"

Так же, рекомендую всегда явно указывать calcMode="linear", так как некоторые браузеры, такие как Safari и Mozilla, могут иметь произвольные значения по умолчанию, что иногда приводит к нерабочей анимации в них.

Осталось собрать все вместе и наслаждаться результатом

Мои телеграмм-каналы:

EN - https://t.me/frontend_almanac
RU - https://t.me/frontend_almanac_ru

English version: https://blog.frontend-almanac.com/FXCAKMCTfXy