Анимируй это: WAAPI

Еще один способ работы с анимацией в вебе.
4 минуты4756

Здравствуйте!

Анимация на веб-страницах — важный инструмент в деле улучшения пользовательского взаимодействия. Хорошая и уместная анимация в интерфейсе помогает пользователю решать свои задачи с помощью вашего продукта. Так что уметь готовить анимацию важно и нужно. Поэтому вот вам перевод статьи Лизи Лайнхарт «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.

Ресурсы

javascriptfrontend_developerанимацияwebcss
Нашли ошибку в тексте? Напишите нам.
Спасибо,
что читаете наш блог!