В этом уроке вы научитесь создавать анимацию значка загрузки с помощью SVG и GSAP - анимационная платформа от GreenSock. Эффект кольца загрузки опирается на очень полезный плагин - DrawSVGPlugin. Этот платный плагин способен помочь с созданием множества SVG анимаций и эффектов.
Далее я во всех подробностях расскажу о создании колеса загрузки. Для этого я воспользуюсь TimelineMax от GSAP и DrawSVGPlugin. Последний нужен для того, чтобы постепенно открывать (или прятать) фрагменты векторной графики, что может сильно помочь в случае с эффектом загрузки.
Для начала советую ознакомиться со следующими инструментами:
GSAP (GreenSock Animation Platform);
TimelineMax;
DrawSVGPlugin.
Для некоторых читателей эти понятия уже не в новинку. На всякий случай для тех, кто все же с ними не знаком: GSAP - это набор JavaScript-библиотек, который может анимировать практически все.
Перейдем к делу. Мы попробуем создать значок загрузки. Это будет забавная анимация: полоска будет как бы выпрыгивать из какой-то жидкости и затем нырять туда снова. Почему бы и нет?
Для начала пропишем SVG в HTML:
<div id="container">
<svg id="loader" width="200px" height="200px" viewBox="0 0 200 200">
<path id="jump" fill="none" stroke="#2d2d2d" stroke-width="10" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" d="M47.5,94.3c0-23.5,19.9-42.5,44.5-42.5s44.5,19,44.5,42.5" />
<g stroke="#2d2d2d" stroke-width="1">
<ellipse id="circleL" fill="none" stroke-miterlimit="10" cx="47.2" cy="95.6" rx="10.7" ry="2.7" />
<ellipse id="circleR" fill="none" stroke-miterlimit="10" cx="136.2" cy="95.6" rx="10.7" ry="2.7" />
</g>
</svg>
</div>
Векторная графика была создана в Adobe Illustrator. То же самое можно сделать и в других приложениях: например, в Sketch (только для Mac) и Inkscape. Я нарисовал искривленную полоску и два круга на жидкости в Illustrator, как показано на картинке ниже. Вы можете заметить, что вокруг рисунка расположен большой красный прямоугольник. На это есть своя причина: когда копируешь графику из Illustrator на HTML-страницу, атрибуты viewBox для нее берутся из этого выделенного прямоугольника. То есть, если бы я выделил только полоску и рябь, в viewBox для графики были бы только суммарные широта и высота, а вокруг анимации бы не было никакого места. Не забывайте, что рябь будет "расплываться" за пределы холста, поэтому понадобится немного дополнительного пространства.
Чтобы получить более крупный холст (и чтобы рисунок находился в его центре), вместе с графикой нужно копировать и прямоугольник. Если мне понадобится SVG-холст размером 200х150, то нужно просто сделать прямоугольник 200х150, выделить его и всю остальную графику (в целом она занимает меньше пространства) и скопировать в HTML-файл. Затем можно просто удалить прямоугольник. Это просто небольшая хитрость, которую я использую, чтобы не получалось так, что элементы анимации обрезаются краями холста. В таком случае также не нужно скрупулезно менять все атрибуты так, чтобы все эти фрагменты оказались внутри холста.
Итак, у нас есть несколько элементов: выпрыгивающая полоска и 2 окружности, олицетворяющие рябь. Заметьте, что у всех элементов есть свой ID. Их можно создать в Illustrator, поменяв название слоя. Однако чаще всего получается так, что к ID добавляются какие-то другие символы, что немного сбивает с толку. Некоторые атрибуты, которые Illustrator добавляет в SVG-элементы, можно просто убрать, оставив только viewBox и другие параметры.
На этом HTML с SVG закончились.
Структура и макет
Создадим ссылки на все, что нам понадобится. Я всегда ввожу переменные, которые ссылаются на документы. В таком случае у вас в кэше уже будет необходимая ссылка, и коду не нужно будет обращаться к DOM каждый раз, когда вам захочется заставить элемент что-либо сделать. Однако будьте осторожны: это может занять много ценной памяти на телефонах, поэтому нужно учитывать требования ваших целевых устройств.
var container = document.getElementById('container');
var loader = document.getElementById('loader');
var circleL = document.getElementById('circleL');
var circleR = document.getElementById('circleR');
var jump = document.getElementById('jump');
Можно заметить, что у выпрыгивающей линии есть отражение, которое мы не стали рисовать. Просто продублируем линию и добавим к «loader», то есть сделаем ссылку на этот рисунок. Совсем скоро мы перевернем эту копию и уменьшим ее непрозрачность.
var jumpRef = jump.cloneNode();
loader.appendChild(jumpRef);
Далее нужно разместить рисунок по центру при помощи GSAP. Выровняем контейнер и графику внутри него при помощи TweenMax, потому что это нужно сделать лишь однажды и это не будет частью анимации. В CSS атрибуты xPercent и yPercent - то же самое, что trasform: translate(-50%;-50%)
, но с префиксами.
TweenMax.set([container, loader], {
position: 'absolute',
top: '50%',
left: '50%',
xPercent: -50,
yPercent: -50
})
Наконец, воспользуемся TweenMax, чтобы перевернуть отражение полоски. Поменяем transformOrigin на 110% по оси Y и поставим отрицательное значение scaleY. В transformOrigin можно было бы поставить и 100%, но тогда отражение находится слишком близко к оригиналу.
TweenMax.set(jumpRef, {
transformOrigin: '50% 110%',
scaleY: -1,
alpha: 0.05
})
Время анимации!
Теперь мы готовы начать анимировать с помощью TimelineMax. Нам очень поможет его возможность связывать анимированные элементы. Об этом мы еще поговорим позже.
Сначала сделаем ссылку на новую копию TimelineMax. Это тоже объект, в котором можно устанавливать различные свойства и обработчики событий, такие как delay, yoyo, onComplete и так далее. В данном случае мы сделаем так, чтобы TimelineMax повторялся вечно, присвоив значение -1.
var myTimeline = new TimelineMax({
repeat: -1
})
Теперь начнем добавлять твины на временную шкалу. Как упоминалось ранее, твины TimelineMax/Lite могут связываться между собой, то есть не нужно будет делать копию переменной каждый раз, когда вы добавляете твин. Это нужно сделать только единожды в самом начале. Только не помещайте между твинами точку с запятой.
Сначала используем DrawSVGPlugin, чтобы и выныривающая полоска, и ее отражение исчезли. Здесь разделенное пробелами значение определяет сегмент. «0% 0%» означает, что и начало, и конец фрагмента принимают значение 0%. Теперь можно начать отрисовывать их с левой стороны. Обратите внимание, что можно анимировать обе линии одновременно, передав их TimelineMax в виде массива.
Также сделаем так, чтобы радиус кругов-ряби по осям X и Y был 0 (то есть пренебрежимо мал) при помощи конструкции «attr», которая встроена в TweenMax и сделана для анимирования. Важно: после первого упоминания set не используется точка с запятой, и поэтому следующие упоминания связываются с первым. В этом случае не нужно вновь делать копию myTimeline.
myTimeline.set([jump, jumpRef], {
drawSVG: '0% 0%'
}).set([circleL, circleR], {
attr: {
rx: 0,
ry: 0,
}
})
Сейчас мы только присваиваем значения, а сам процесс анимации еще не начался. Временная шкала будет повторяться вечно, поэтому каждый раз, когда она начинает проигрываться сначала, они будут использоваться для инициализации состояний элементов. Пока что длина анимации - 0.
Теперь мы будем вызывать to - а это уже настоящая анимация. Ура!
Обе линии начинают появляться на 30% своей длины. Значение drawSVG «0% 30%», что фрагмент начнется на 0%, а закончится на 30% от всей длины. На изображении ниже показано, как это выглядит. Мы также используем linear.ease, чтобы анимация была гладкой и как бы рисовалась.
.to([jump, jumpRef], 0.4, {
drawSVG: '0% 30%',
ease: Linear.easeNone
})
Теперь сделаем так, чтобы левый кружок, олицетворяющий рябь, стал увеличиваться. Для этого анимируем его rx и ry атрибуты. Если бы у них обоих было бы одинаковое значение «+=30», тогда был бы ровный круг. Однако окружности под действием перспективы выглядят иначе. Именно поэтому значение rx должно быть больше ry. Тогда у нас получится овал. Этот фрагмент автоматически добавляется в конец временной шкалы (на 0,4 с, потому что именно столько сейчас длится анимация).
В конце был добавлен еще один ни с чем не связанный параметр («-=0.1»). Это значит, что этот кусок анимации будет воспроизводиться на 0,3 с (0,4-0,1). Благодаря анимации как бы смешаются между собой и не будут выглядеть слишком искусственно.
.to(circleL, 2, {
attr: {
rx: '+=30',
ry: '+=10'
},
alpha: 0,
ease: Power1.easeOut
}, '-=0.1')
Продолжим анимировать полоски. Теперь они движутся вперед, и за ними поспевают их концы. Мы вновь добавляем этот фрагмент в конец анимации, затем двигаем его немного назад по временной шкале (на 1,9 с). Теперь значение drawSVG – «50% 80%», то есть 30% полоски все еще остаются невидимы (начало - на 50% длины, конец - на 80%).
.to([jump, jumpRef], 1, {
drawSVG: '50% 80%',
ease: Linear.easeNone
}, '-=1.9')
Последнее действие для полосок. Теперь мы анимируем их до конца. Значение drawSVG стало «100% 100%», то есть начало и конец фрагмента совпадают. Мы также заставили этот момент наступить раньше - на 0,7 с.
.to([jump, jumpRef], 0.7, {
drawSVG: '100% 100%',
ease: Linear.easeNone
}, '-=0.9')
Не забываем про кружок-рябь, который находится справа. Он сначала увеличивается до 30 по оси X и 10 по оси Y, как и его левая копия, затем исчезает.
.to(circleR, 2, {
attr: {
rx: '+=30',
ry: '+=10'
},
alpha: 0,
ease: Power1.easeOut
}, '-=.5');
myTimeline.timeScale(3);
При запуске этой анимации она будет воспроизводиться вечно. Когда я сделал эту работу, я долгое время подгонял не связанные ни с чем параметры, чтобы моменты, когда полоска выпрыгивает и когда начинается рябь, выглядели натурально. Когда же я наконец закончил, то понял, что анимация двигается слишком медленно (хорошо хоть движение было равномерным). Чтобы ускорить движение, я добавил эту строчку:
myTimeline.timescale(3);
Полный код
Ниже представлен получившийся код:
var container = document.getElementById('container');
var loader = document.getElementById('loader');
var circleL = document.getElementById('circleL');
var circleR = document.getElementById('circleR');
var jump = document.getElementById('jump');
var jumpRef = jump.cloneNode();
loader.appendChild(jumpRef);
TweenMax.set([container, loader], {
position: 'absolute',
top: '50%',
left: '50%',
xPercent: -50,
yPercent: -50
})
TweenMax.set(jumpRef, {
transformOrigin: '50% 110%',
scaleY: -1,
alpha: 0.05
})
var tl = new TimelineMax({
repeat: -1,
yoyo: false
});
tl.timeScale(3);
tl.set([jump, jumpRef], {
drawSVG: '0% 0%'
})
.set([circleL, circleR], {
attr: {
rx: 0,
ry: 0,
}
})
.to([jump, jumpRef], 0.4, {
drawSVG: '0% 30%',
ease: Linear.easeNone
})
.to(circleL, 2, {
attr: {
rx: '+=30',
ry: '+=10'
},
alpha: 0,
ease: Power1.easeOut
}, '-=0.1')
.to([jump, jumpRef], 1, {
drawSVG: '50% 80%',
ease: Linear.easeNone
}, '-=1.9')
.to([jump, jumpRef], 0.7, {
drawSVG: '100% 100%',
ease: Linear.easeNone
}, '-=0.9')
.to(circleR, 2, {
attr: {
rx: '+=30',
ry: '+=10'
},
alpha: 0,
ease: Power1.easeOut
}, '-=.5')
Вот и все. Сначала мы отрисовали необходимую графику в Illustrator (и дали слоям названия, которые соответствуют их ID). Далее мы выбрали все элементы и перенесли их на HTML-страницу, где немного подчистили ID и ненужные атрибуты, которые прописывает Illustrator. Затем мы дублировали выпрыгивающую линию, чтобы получить отражение, а с помощью TweenMax, TimelineMax и DrawSVGPlugin сделали копию TimelineMax и разбили на промежуточные кадры появление линий и расширение кружков-ряби. После этого пришлось очень долго подбирать время, пока работа не станет выглядеть естественно. Уже потом мы поняли, что действие развивается слишком медленно, и ускорили его. Так получилась наша финальная анимация.
Анимированные линии - популярный эффект в видеографике и веб-разработках. Теперь вы можете легко и просто создать его самостоятельно, используя GSAP.