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

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

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

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

На самом деле нет, это расширение того, что у нас уже есть, и на то есть веские причины! Начнем с самого начала. Если вы сегодня хотите анимировать любой элемент DOM в Интернете, у вас есть два с половиной варианта: CSS-переходы для простых переходов от A к B, CSS-анимации для потенциально циклических, более сложных анимаций, основанных на времени, и 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 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 невероятно мощный инструмент, и в этом API имеется множество других функций, таких как замедление, начальное смещение, взвешивание ключевых кадров и поведение заполнения, которые выходят за рамки этой статьи. Если вы хотите узнать больше, я рекомендую прочитать эту статью «Анимация CSS и хитрости CSS».

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

Теперь, когда у нас есть концепция временных шкал, мы можем начать изучать 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 . Поэтому, особенно учитывая адаптивный дизайн, вы всегда должны быть готовы к использованию NaN в качестве currentTime . Часто разумно установить значение по умолчанию, равное 0.

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

Под капотом

Рабочие листы

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

Композитор NSync

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

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

Похлопать по запястью

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

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

Заключение

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

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

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