Анимационный проект Гудини

Улучшите анимацию вашего веб-приложения

Вкратце: Animation Worklet позволяет создавать императивные анимации, которые выполняются с собственной частотой кадров устройства, обеспечивая дополнительную плавность без рывков™, делая анимацию более устойчивой к рывкам основного потока и позволяя прокручивать её, а не по времени. Animation Worklet доступен в Chrome Canary (под флагом «Экспериментальные функции веб-платформы»), и мы планируем пробную версию Origin для Chrome 71. Вы можете начать использовать его в качестве прогрессивного улучшения уже сегодня .

Еще один API анимации?

На самом деле нет, это расширение того, что у нас уже есть, и не без оснований! Давайте начнём с самого начала. Если вы хотите анимировать любой элемент DOM в вебе сегодня, у вас есть два с половиной варианта: CSS Transitions для простых переходов из точки A в точку B, CSS Animations для потенциально цикличных, более сложных анимаций с временными интервалами и Web Animations API (WAAPI) для практически произвольной сложности анимации. Поддержка WAAPI выглядит довольно мрачно, но она развивается. А пока есть полифилл .

Все эти методы объединяет то, что они не имеют состояния и управляются временем. Однако некоторые из эффектов, которые пытаются реализовать разработчики, не зависят ни от времени, ни от состояния. Например, печально известный параллакс-скроллер, как следует из названия, управляется прокруткой. Реализовать производительный параллакс-скроллер в вебе сегодня на удивление сложно.

А как насчёт отсутствия состояния? Вспомните, например, адресную строку Chrome на Android. Если прокрутить вниз, она исчезает из поля зрения. Но как только вы прокручиваете вверх, она возвращается, даже если вы уже на середине страницы. Анимация зависит не только от позиции прокрутки, но и от предыдущего направления прокрутки. Она сохраняет состояние .

Ещё одна проблема — стилизация полос прокрутки. Они, как известно, не стилизуются — или, по крайней мере, недостаточно стилизованы. Что, если я хочу, чтобы на полосе прокрутки был нянь-кот ? Какой бы метод вы ни выбрали, создание собственной полосы прокрутки не будет ни производительным, ни простым .

Суть в том, что все эти вещи неудобны и практически нереализуемы для эффективной реализации. Большинство из них основаны на событиях и/или requestAnimationFrame , что может удерживать частоту 60 кадров в секунду, даже если ваш экран способен работать с частотой 90, 120 кадров в секунду или выше, используя лишь малую часть драгоценного бюджета кадров основного потока.

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

Учебник по анимации и временным линиям

WAAPI и Animation Worklet широко используют временные шкалы, позволяя вам управлять анимацией и эффектами так, как вам нужно. Этот раздел — краткое введение в временные шкалы и их работу с анимацией.

У каждого документа есть document.timeline . Она начинается с 0 с момента создания документа и отсчитывает миллисекунды с момента его создания. Все анимации документа работают относительно этой временной шкалы.

Чтобы сделать вещи более конкретными, давайте взглянем на этот фрагмент WAAPI.

const animation = new Animation(
  new KeyframeEffect(
    document.querySelector('#a'),
    [
      {
        transform: 'translateX(0)',
      },
      {
        transform: 'translateX(500px)',
      },
      {
        transform: 'translateY(500px)',
      },
    ],
    {
      delay: 3000,
      duration: 2000,
      iterations: 3,
    }
  ),
  document.timeline
);

animation.play();

При вызове animation.play() анимация использует currentTime времени в качестве своего начального времени. Задержка анимации составляет 3000 мс, что означает, что анимация начнётся (или станет «активной»), когда шкала времени достигнет значения `startTime

  • 3000. . After that time, the animation engine will animate the given element from the first keyframe ( translateX(0) ), through all intermediate keyframes ( translateX(500px) ) all the way to the last keyframe ( translateY(500px) ) in exactly 2000ms, as prescribed by the длительности options. Since we have a duration of 2000ms, we will reach the middle keyframe when the timeline's is startTime + 3000 + 1000 and the last keyframe at startTime + 3000 + 2000. Дело в том, что временная шкала контролирует, где мы находимся в нашей анимации!

Как только анимация достигнет последнего ключевого кадра, она вернётся к первому и начнёт следующую итерацию анимации. Этот процесс повторяется в общей сложности 3 раза, поскольку мы задали iterations: 3 . Если бы мы хотели, чтобы анимация никогда не останавливалась, мы бы записали iterations: Number.POSITIVE_INFINITY . Вот результат приведённого выше кода.

WAAPI невероятно мощный, и в нём есть множество дополнительных функций, таких как плавность, начальные смещения, весовые коэффициенты ключевых кадров и поведение заливки, которые превзошли бы возможности этой статьи. Если хотите узнать больше, рекомендую прочитать статью о CSS-анимации на сайте CSS Tricks.

Написание анимационного ворклета

Теперь, когда мы разобрались с концепцией временных шкал, мы можем перейти к рассмотрению Animation Worklet и того, как он позволяет работать с ними! API Animation Worklet не только основан на WAAPI, но и — в контексте расширяемой сети — представляет собой низкоуровневый примитив, объясняющий работу WAAPI. С точки зрения синтаксиса они невероятно похожи:

Анимационный ворклет ВААПИ
new WorkletAnimation(
  'passthrough',
  new KeyframeEffect(
    document.querySelector('#a'),
    [
      {
        transform: 'translateX(0)'
      },
      {
        transform: 'translateX(500px)'
      }
    ],
    {
      duration: 2000,
      iterations: Number.POSITIVE_INFINITY
    }
  ),
  document.timeline
).play();
      
        new Animation(

        new KeyframeEffect(
        document.querySelector('#a'),
        [
        {
        transform: 'translateX(0)'
        },
        {
        transform: 'translateX(500px)'
        }
        ],
        {
        duration: 2000,
        iterations: Number.POSITIVE_INFINITY
        }
        ),
        document.timeline
        ).play();
        

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

Обнаружение особенностей

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

if ('animationWorklet' in CSS) {
  // AnimationWorklet is supported!
}

Загрузка ворклета

Ворклеты — это новая концепция, представленная рабочей группой Houdini для упрощения разработки и масштабирования многих новых API. Мы подробнее рассмотрим ворклеты позже, но пока для простоты можно рассматривать их как недорогие и легковесные потоки (вроде рабочих).

Перед объявлением анимации нам нужно убедиться, что мы загрузили ворклет с именем «passthrough»:

// index.html
await CSS.animationWorklet.addModule('passthrough-aw.js');
// ... WorkletAnimation initialization from above ...

// passthrough-aw.js
registerAnimator(
  'passthrough',
  class {
    animate(currentTime, effect) {
      effect.localTime = currentTime;
    }
  }
);

Что здесь происходит? Мы регистрируем класс как аниматор с помощью вызова registerAnimator() ворклета AnimationWorklet, присваивая ему имя «passthrough». Это то же имя, которое мы использовали в конструкторе WorkletAnimation() выше. После завершения регистрации промис, возвращаемый методом addModule() будет разрешён, и мы сможем начать создавать анимации с помощью этого ворклета.

Метод animate() нашего экземпляра будет вызываться для каждого кадра, который браузер хочет отрисовать, передавая currentTime временной шкалы анимации, а также эффект, который в данный момент обрабатывается. У нас есть только один эффект, KeyframeEffect , и мы используем currentTime для установки его localTime , поэтому этот аниматор называется «сквозным». С этим кодом для ворклета WAAPI и AnimationWorklet, представленный выше, ведут себя абсолютно одинаково, как видно в демо .

Время

Параметр currentTime нашего метода animate() — это currentTime временной шкалы, которое мы передали конструктору WorkletAnimation() . В предыдущем примере мы просто передали это время эффекту. Но поскольку это код JavaScript, мы можем искажать время 💫

function remap(minIn, maxIn, minOut, maxOut, v) {
  return ((v - minIn) / (maxIn - minIn)) * (maxOut - minOut) + minOut;
}
registerAnimator(
  'sin',
  class {
    animate(currentTime, effect) {
      effect.localTime = remap(
        -1,
        1,
        0,
        2000,
        Math.sin((currentTime * 2 * Math.PI) / 2000)
      );
    }
  }
);

Мы берём Math.sin() для currentTime и преобразуем это значение в диапазон [0; 2000], который соответствует временному диапазону, для которого определён наш эффект. Теперь анимация выглядит совершенно иначе , без изменения ключевых кадров или параметров анимации. Код ворклета может быть сколь угодно сложным и позволяет программно определять, какие эффекты воспроизводятся, в каком порядке и с какой интенсивностью.

Варианты вместо вариантов

Возможно, вам захочется повторно использовать рабочий лет, изменив его номера. Для этого конструктор WorkletAnimation позволяет передать объект параметров рабочему лету:

registerAnimator(
  'factor',
  class {
    constructor(options = {}) {
      this.factor = options.factor || 1;
    }
    animate(currentTime, effect) {
      effect.localTime = currentTime * this.factor;
    }
  }
);

new WorkletAnimation(
  'factor',
  new KeyframeEffect(
    document.querySelector('#b'),
    [
      /* ... same keyframes as before ... */
    ],
    {
      duration: 2000,
      iterations: Number.POSITIVE_INFINITY,
    }
  ),
  document.timeline,
  {factor: 0.5}
).play();

В этом примере обе анимации управляются одним и тем же кодом, но с разными параметрами.

Дайте мне ваш местный штат!

Как я уже упоминал, одна из ключевых проблем, которую призван решить анимационный ворклет, — это анимация с сохранением состояния. Анимационные ворклеты могут сохранять состояние. Однако одна из основных особенностей ворклетов заключается в том, что их можно переносить в другой поток или даже уничтожать для экономии ресурсов, что также уничтожит их состояние. Чтобы предотвратить потерю состояния, анимационный ворклет предлагает хук, вызываемый перед уничтожением ворклета. Его можно использовать для возврата объекта состояния. Этот объект будет передан конструктору при повторном создании ворклета. При первоначальном создании этот параметр будет иметь undefined .

registerAnimator(
  'randomspin',
  class {
    constructor(options = {}, state = {}) {
      this.direction = state.direction || (Math.random() > 0.5 ? 1 : -1);
    }
    animate(currentTime, effect) {
      // Some math to make sure that `localTime` is always > 0.
      effect.localTime = 2000 + this.direction * (currentTime % 2000);
    }
    destroy() {
      return {
        direction: this.direction,
      };
    }
  }
);

При каждом обновлении этой демонстрации вероятность вращения квадрата составляет 50/50. Если браузер удалит ворклет и перенесёт его в другой поток, при создании возникнет ещё один вызов Math.random() , что может привести к внезапному изменению направления. Чтобы этого не произошло, мы возвращаем случайно выбранное направление анимации как состояние и используем его в конструкторе, если оно предусмотрено.

Вхождение в пространственно-временной континуум: ScrollTimeline

Как было показано в предыдущем разделе, AnimationWorklet позволяет нам программно определять, как перемещение по временной шкале влияет на эффекты анимации. Но до сих пор нашей временной шкалой всегда был document.timeline , который отслеживает время.

ScrollTimeline открывает новые возможности и позволяет управлять анимацией с помощью прокрутки вместо времени. Для этой демонстрации мы повторно используем наш самый первый рабочий сквозной метод:

new WorkletAnimation(
  'passthrough',
  new KeyframeEffect(
    document.querySelector('#a'),
    [
      {
        transform: 'translateX(0)',
      },
      {
        transform: 'translateX(500px)',
      },
    ],
    {
      duration: 2000,
      fill: 'both',
    }
  ),
  new ScrollTimeline({
    scrollSource: document.querySelector('main'),
    orientation: 'vertical', // "horizontal" or "vertical".
    timeRange: 2000,
  })
).play();

Вместо передачи document.timeline мы создаём новый ScrollTimeline . Вы, возможно, догадались, что ScrollTimeline использует не время, а позицию прокрутки scrollSource для установки currentTime в ворклете. Прокрутка до самого верха (или влево) означает, что currentTime = 0 , а прокрутка до самого низа (или вправо) устанавливает currentTime в timeRange . Прокручивая поле в этой демонстрации , вы можете управлять положением красного поля.

Если вы создаёте ScrollTimeline с элементом, который не прокручивается, currentTime шкалы времени будет NaN . Поэтому, особенно учитывая адаптивный дизайн, всегда следует быть готовым к тому, что значение currentTime будет равно NaN . Часто разумно установить значение по умолчанию 0.

Связывание анимации с позицией прокрутки — это давно идущий поиск, но так и не достигший такого уровня точности (если не считать хитрых обходных путей с CSS3D). Animation Worklet позволяет реализовать эти эффекты простым и высокопроизводительным способом. Например, эффект параллакс-прокрутки, как в этом демо, показывает, что теперь для определения анимации, управляемой прокруткой, требуется всего пара строк.

Под капотом

Ворклеты

Ворклеты — это контексты JavaScript с изолированной областью действия и очень небольшой поверхностью API. Небольшая поверхность API позволяет проводить более агрессивную оптимизацию из браузера, особенно на недорогих устройствах. Кроме того, ворклеты не привязаны к конкретному циклу событий, но могут перемещаться между потоками по мере необходимости. Это особенно важно для AnimationWorklet.

Композитор NSync

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

В Chrome (как и во многих других браузерах) есть процесс, называемый компоновщиком. Его задача — и я сильно упрощаю — состоит в том, чтобы упорядочивать слои и текстуры, а затем использовать графический процессор для обновления экрана с максимально возможной частотой, в идеале с той же частотой, с которой может обновляться экран (обычно 60 Гц). В зависимости от того, какие CSS-свойства анимируются, браузеру может потребоваться только компоновщик, в то время как другие свойства должны выполнять макет, что может выполнять только основной поток. В зависимости от того, какие свойства вы планируете анимировать, ваш рабочий процесс анимации будет либо привязан к основному потоку, либо запущен в отдельном потоке, синхронизированном с компоновщиком.

Пощечина по запястью

Обычно существует только один процесс компоновщика, который потенциально используется несколькими вкладками, поскольку графический процессор — ресурс, подверженный высокой конкуренции. Если компоновщик каким-либо образом блокируется, весь браузер останавливается и перестает реагировать на действия пользователя. Этого следует избегать любой ценой. Что же произойдёт, если ваш ворклет не сможет предоставить компоновщику необходимые данные вовремя для отрисовки кадра?

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

Заключение

AnimationWorklet обладает множеством преимуществ и возможностей для веба. Очевидные преимущества — расширенный контроль над анимацией и новые способы управления ею, обеспечивающие новый уровень визуальной точности в вебе. Кроме того, архитектура API позволяет сделать приложение более устойчивым к сбоям, одновременно получая доступ ко всем новым возможностям.

Animation Worklet доступен на Canary, и мы планируем запустить Origin Trial с Chrome 71. Мы с нетерпением ждём ваших новых впечатлений от веб-приложений и предложений по улучшению. Существует также полифил , предоставляющий тот же API, но не обеспечивающий изоляцию производительности.

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