Анимируй это: WAAPI
Здравствуйте!
Анимация на веб-страницах — важный инструмент в деле улучшения пользовательского взаимодействия. Хорошая и уместная анимация в интерфейсе помогает пользователю решать свои задачи с помощью вашего продукта. Так что уметь готовить анимацию важно и нужно. Поэтому вот вам перевод статьи Лизи Лайнхарт «Getting Started With The Web Animation Api». Приятного чтения!
Есть много вариантов анимации в вебе — от анимаций на чистом CSS до библиотек вроде GSAP. Web Animations Api (WAAPI) — попытка скомбинировать мощь CSS с гибкостью JavaScript, чтобы обеспечить возможность создания сложных цепочек анимаций. Между WAAPI и библиотеками типа GSAP есть большие различия, но главное различие в том, что WAAPI нативно поддерживается браузерами без необходимости подгрузки внешних библиотек.
Поскольку это мой первый опыт с этим API, сперва я поигралась с этим примером: codepen.io/lisilinhart/full/dzNYKb. Эта статья в первую очередь содержит в себе общие важные принципы WAAPI, полезные ресурсы и опыт, который я приобрела. Я не планирую углубляться в каждую функцию.
С чего я начала
Самым полезным ресурсом для меня оказалась статья «Let’s talk about the WebAnimations API» Дэна Уилсона. Кроме того, я нашла полезным видео «Alice in Web Animations API Land» Рэйчел Нэйборс.
Статья «CSS Animations vs Web Animations API» помогла мне понять отличия между анимациями CSS и доступными возможностями. После того, как я разобралась с основами, по большей части я пользовалась официальной документацией, чтобы понять тонкости API.
Основные принципы
Я нарисовала небольшую иллюстрацию компонентов, которые совместно работают в WAAPI. Каждый раз, когда мы что-то анимируем через element.animate() или new Animation() или document.timeline.play(), мы получаем объект анимации с различными методами типа play() или pause().
KeyframeEffect внутри SequenceEffect или GroupEffect
KeyframeEffects — основные компоненты создания анимации. Важно понимать, что сами по себе они ничего не делают, если мы не добавим их в Animation() или проиграем их через document.timeline.play(). Когда мы используем element.animate(), по сути это то же самое, как если бы мы создали KeyframeEffect и обернули его в Animation(myEffect).
KeyframeEffect принимает три аргумента: анимируемый элемент, массив кадров анимации и объект, содержащий настройки времени анимации. Effect можно сохранить в переменной для использования в SequenceEffect или GroupEffect.
SequenceEffect проигрывает KeyframeEffects друг за другом, а GroupEffect проигрывает их одновременно.
const effectOne = new KeyframeEffect(target, frames, options); const effectTwo = new KeyframeEffect(anotherTarget, frames, options); const sequence = new SequenceEffect([ effectOne, effectTwo, ]); const animation = document.timeline.play(sequence) animation.pause();
После создания анимации мы можем использовать ее методы типа play(), pause() или cancel() и менять свойства, например playbackRate.
Как структурировать кадры
Для анимации планет я сделала цикл из элементов частиц, радиуса и фона каждой планеты, и создала SequenceEffect на основе одного KeyframeEffect. Чтобы сохранить обзор, я создала объект, включающий в себя все массивы кадров, которые мне нужны.
const effects = { fadeInLeft: [ { transform: "translate(-100%, 0%) scale(0.6)", opacity: 0 }, { transform: "translate(0, 0) scale(1)", opacity: 1 } ], fadeInRight: [ { transform: "translate(100%, 0%) scale(0.6)", opacity: 0 }, { transform: "translate(0, 0) scale(1)", opacity: 1 } ], }
Таким образом, я просто ссылаюсь на них, используя effects.fadeInLeft, что кажется мне более адекватным, чем создание кучи переменных для каждого эффекта. Аналогичный подход я применила для объекта, содержащего настройки.
Свойство fill
fill работает ровно так же, как animation-fill-mode — определяет, сохраняются ли изменения, произведенные анимацией после ее завершения, или откатываются к исходному состоянию. Для анимации планет это важное свойство, поскольку анимация fadeIn отличается от fadeOut. Когда fadeIn анимирует все дочерние элементы div.planet, fadeOut трансформирует и меняет прозрачность только родительского div.planet. Если я захочу снова анимировать дочерние элементы, родитель все равно будет скрыт, и вы не увидите анимацию. Установка fill в none для родителя и использование animation.cancel() на fadeIn анимации по сути обнуляет все изменения и позволяет провести весь цикл анимации снова.
const timings = { background: { fill: "forwards", duration: 800, direction: "normal", easing: "cubic-bezier(0.2, 0, .3, 1.5)" }, planet: { fill: "none", duration: 800, direction: "normal", easing: "cubic-bezier(.5, 0, .3, 1)" }, ... };
cubic-bezier ваш друг
Использование ease-in-out хорошо подходит для большинства анимаций, но вам стоит разобраться с синтаксисом cubic-bezier, потому что он позволяет создавать более интересные анимации. Установка последнего аргумента больше 1 дает планете небольшой отскок при fadeIn. Я рекомендую поиграться с сервисом cubic-bezier.com, чтобы лучше понять синтаксис.
easing: "cubic-bezier(0.2, 0, .3, 1.5)"
Обработка направления при обновлении слайдера
Поскольку я хотела, чтобы планеты могли меняться в обоих направлениях, в зависимости от движения слайдера, мне нужно было создать четыре разных эффекта: fadeInLeft, fadeInRight, fadeOutLeft и fadeOutRight. Указатель слайдера тоже анимирован и позиционирован отлично от основной картинки, поэтому для него будут отдельные кадры и настройки с соответствующими эффектами.
Для отслеживания события обновления я добавила обработчик, который завернула в функцию debounce из lodash библиотеки, чтобы избавиться от паразитных срабатываний.
range.addEventListener( "input", _.debounce(e => updateSlider(e.target.value), 300) );
Чтобы определить направление движения, я сохраняю предыдущее состояние слайдера в переменную, и сравниваю ее с новым состоянием при каждом обновлении. Если значение больше, направление ставится равным 1, и проигрывается document.timeline.play(fadeInLeft) для соответствующей планеты, после того, как будет закончено document.timeline.play(fadeOutRight) для предыдущей. В противном случае все происходит в другом направлении.
Анимируйте только тогда, когда завершилась предыдущая анимация
Поскольку я не хотела, чтобы анимация прерывалась или ломалась, я завела переменную isAnimating со значением false по умолчанию. Когда анимация запускается, я ставлю переменную в true, и обратно в false, когда анимация заканчивается. Таким образом, если прокрутить слайдер в момент анимации, вызывается setTimeout и обновление дожидается конца активной анимации.
animation.onfinish = () => { isAnimating = false; };
Бонус: переменные CSS
Так как я люблю переменные CSS, я решила использовать их в этом примере, потому что они делают все проще. Для планет я просто обновляю --planet-color и --particle-color в классах, вместо переназначения всех свойств для дочерних элементов.
.planet { --planet-color: #ffd260; } .planet--1 { --planet-color: #3FB4C0; --particle-color: #FEDB82; } .background { background: var(--planet-color); } .particle { background: var(--particle-color); }
Обновление цвета указателя слайдера с переменными CSS
Я хотела, чтобы указатель слайдера имел цвет, соответствующий планете. Для этого я завела переменную --planet-color для указателя, и обновляю ее через JavaScript при прокрутке слайдера.
range.style.setProperty(`--planet-color`, planetColors[index]);
У меня уже было объявлено свойство --planet-color на верхнем уровне, но поскольку я уже проставила цвета планет в CSS и просто хотела обновлять цвет указателя, я добавила свойство напрямую для input, и это оказалось быстрее.
Если хотите побольше узнать о производительности переменных CSS, взгляните на мою статью «Performance of CSS variables».
Заключение
Мне нравится использовать GSAP для создания последовательных анимаций, просто потому что это просто, поддерживается всеми браузерами, отлично работает с SVG и позволяет делать много крутых штук. После погружения в WAAPI у меня есть четкое понимание: WAAPI — не замена GSAP, просто потому что GSAP нацелен на более широкие возможности. Чтобы получить преимущество от WAAPI, вам все еще нужно хорошо разбираться в том, как работают CSS анимации, в противном случае у вас будут сложности.
Тем не менее мне кажется, что это отлично — иметь больше нативных возможностей для анимации без необходимости загрузки целой библиотеки типа GSAP.