Повышение эффективности расширения и усиления; свернуть анимацию

Стивен МакГрюэр
Stephen McGruer

ТЛ;ДР

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

Ранее мы публиковали обновления о том, как создавать эффективные эффекты параллакса и бесконечные скроллеры . В этом посте мы рассмотрим, что нужно, если вам нужна эффективная анимация клипов. Если вы хотите увидеть демо-версию , посетите репозиторий Sample UI Elements на GitHub .

Возьмем, к примеру, расширяющееся меню:

Некоторые варианты создания этого более производительны, чем другие.

Плохо: анимация ширины и высоты элемента контейнера.

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

.menu {
  overflow: hidden;
  width: 350px;
  height: 600px;
  transition: width 600ms ease-out, height 600ms ease-out;
}

.menu--collapsed {
  width: 200px;
  height: 60px;
}

Непосредственная проблема этого подхода заключается в том, что он требует анимации width и height . Эти свойства требуют расчета макета и рисования результатов для каждого кадра анимации, что может быть очень затратным и обычно приводит к потере 60 кадров в секунду. Если для вас это новость, прочтите наши руководства по производительности рендеринга , где вы можете получить дополнительную информацию о том, как работает процесс рендеринга.

Плохо: используйте свойства CSS clip или clip-path.

Альтернативой анимации width и height может быть использование свойства clip (сейчас устарело) для анимации эффекта расширения и свертывания. Или, если хотите, вместо этого вы можете использовать clip-path . Однако использование clip-path поддерживается хуже, чем clip . Но clip устарел. Верно. Но не отчаивайтесь, это все равно не то решение, которое вам нужно!

.menu {
  position: absolute;
  clip: rect(0px 112px 175px 0px);
  transition: clip 600ms ease-out;
}

.menu--collapsed {
  clip: rect(0px 70px 34px 0px);
}

Хотя это лучше, чем анимация width и height элемента меню, недостатком этого подхода является то, что он все равно вызывает отрисовку. Кроме того, свойство clip , если вы пойдете по этому пути, требует, чтобы элемент, с которым оно работает, находился либо в абсолютном, либо в фиксированном положении, что может потребовать некоторых дополнительных раздумий.

Хорошо: анимация весов

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

Недостатком этого подхода, как и большинства вещей, связанных с производительностью рендеринга, является то, что он требует некоторой настройки. Однако оно того стоит!

Шаг 1: Рассчитайте начальное и конечное состояния

При подходе, использующем масштабируемую анимацию, первым шагом является считывание элементов, которые сообщают вам, каким должен быть размер меню как в свернутом, так и в развернутом виде. Возможно, в некоторых ситуациях вы не можете получить оба этих бита информации за один раз, и вам нужно, скажем, переключить некоторые классы, чтобы иметь возможность читать различные состояния компонента. Однако если вам нужно это сделать, будьте осторожны: getBoundingClientRect() (или offsetWidth и offsetHeight ) заставляет браузер запускать стили и проходы макета, если стили изменились с момента их последнего запуска.

function calculateCollapsedScale () {
    // The menu title can act as the marker for the collapsed state.
    const collapsed = menuTitle.getBoundingClientRect();

    // Whereas the menu as a whole (title plus items) can act as
    // a proxy for the expanded state.
    const expanded = menu.getBoundingClientRect();
    return {
    x: collapsed.width / expanded.width,
    y: collapsed.height / expanded.height
    };
}

В случае чего-то вроде меню мы можем сделать разумное предположение, что вначале оно будет иметь свой естественный масштаб (1, 1). Этот естественный масштаб представляет собой развернутое состояние, а это означает, что вам нужно будет анимировать уменьшенную версию (которая была рассчитана выше) обратно до этого естественного масштаба.

Но ждать! Наверняка это также приведет к масштабированию содержимого меню, не так ли? Ну, как вы можете видеть ниже, да.

Так что же вы можете с этим поделать? Ну, вы можете применить встречное преобразование к содержимому, например, если контейнер уменьшен до 1/5 от его нормального размера, вы можете масштабировать содержимое в 5 раз, чтобы предотвратить сжатие содержимого. Есть две вещи, на которые следует обратить внимание:

  1. Противотрансформация также является операцией масштабирования . Это хорошо , потому что его тоже можно ускорить, как и анимацию на контейнере. Возможно, вам потребуется убедиться, что анимируемые элементы получают собственный слой компоновщика (чтобы помочь графическому процессору), и для этого вы можете добавить will-change: transform к элементу или, если вам нужна поддержка старых браузеров, backface-visiblity: hidden .

  2. Противопреобразование должно рассчитываться для каждого кадра. Здесь все может стать немного сложнее, потому что если предположить, что анимация выполнена в CSS и использует функцию замедления, то самому замедлению необходимо противодействовать при анимации встречного преобразования. Однако вычисление обратной кривой, скажем, для cubic-bezier(0, 0, 0.3, 1) не так уж и очевидно.

Тогда может возникнуть соблазн рассмотреть возможность анимации эффекта с помощью JavaScript. В конце концов, вы затем можете использовать уравнение замедления для расчета значений масштаба и противомасштаба для каждого кадра. Обратной стороной любой анимации на основе JavaScript является то, что происходит, когда основной поток (в котором выполняется ваш JavaScript) занят какой-либо другой задачей. Короткий ответ: ваша анимация может заикаться или вообще останавливаться, что не очень хорошо для UX.

Шаг 2. Создавайте CSS-анимацию на лету.

Решение, которое на первый взгляд может показаться странным, состоит в том, чтобы динамически создать анимацию по ключевым кадрам с нашей собственной функцией замедления и внедрить ее на страницу для использования в меню. (Большое спасибо инженеру Chrome Роберту Флэку за указание на это!) Основное преимущество этого подхода заключается в том, что анимацию по ключевым кадрам, которая изменяет преобразования, можно запускать в композиторе, а это означает, что на нее не влияют задачи в основном потоке.

Чтобы создать анимацию по ключевым кадрам, мы делаем шаг от 0 до 100 и вычисляем, какие значения масштаба потребуются для элемента и его содержимого. Затем их можно свести к строке, которую можно внедрить на страницу как элемент стиля. Внедрение стилей вызовет на странице проход «Пересчитать стили», что является дополнительной работой, которую должен выполнить браузер, но он сделает это только один раз, когда компонент загружается.

function createKeyframeAnimation () {
    // Figure out the size of the element when collapsed.
    let {x, y} = calculateCollapsedScale();
    let animation = '';
    let inverseAnimation = '';

    for (let step = 0; step <= 100; step++) {
    // Remap the step value to an eased one.
    let easedStep = ease(step / 100);

    // Calculate the scale of the element.
    const xScale = x + (1 - x) * easedStep;
    const yScale = y + (1 - y) * easedStep;

    animation += `${step}% {
        transform: scale(${xScale}, ${yScale});
    }`;

    // And now the inverse for the contents.
    const invXScale = 1 / xScale;
    const invYScale = 1 / yScale;
    inverseAnimation += `${step}% {
        transform: scale(${invXScale}, ${invYScale});
    }`;

    }

    return `
    @keyframes menuAnimation {
    ${animation}
    }

    @keyframes menuContentsAnimation {
    ${inverseAnimation}
    }`;
}

Бесконечно любопытные могут задаться вопросом о функции ease() внутри цикла for. Вы можете использовать что-то подобное, чтобы сопоставить значения от 0 до 1 с упрощенным эквивалентом.

function ease (v, pow=4) {
  return 1 - Math.pow(1 - v, pow);
}

Вы также можете использовать поиск Google, чтобы нарисовать, как это выглядит . Удобный! Если вам нужны другие уравнения замедления, посмотрите Tween.js от Soledad Penadés , который содержит их целую кучу.

Шаг 3. Включите анимацию CSS

Когда эти анимации созданы и запечены на странице с помощью JavaScript, последним шагом является переключение классов, позволяющих использовать анимацию.

.menu--expanded {
  animation-name: menuAnimation;
  animation-duration: 0.2s;
  animation-timing-function: linear;
}

.menu__contents--expanded {
  animation-name: menuContentsAnimation;
  animation-duration: 0.2s;
  animation-timing-function: linear;
}

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

Когда дело доходит до свертывания элемента обратно, есть два варианта: обновить CSS-анимацию, чтобы она запускалась в обратном направлении, а не вперед. Это будет работать нормально, но «ощущение» анимации будет обратным, поэтому, если вы использовали кривую замедления, обратный ход будет казаться облегченным , что сделает его медленным. Более подходящее решение — создать вторую пару анимаций для сворачивания элемента. Их можно создать точно так же, как анимацию расширенных ключевых кадров, но с поменянными местами начальным и конечным значениями.

const xScale = 1 + (x - 1) * easedStep;
const yScale = 1 + (y - 1) * easedStep;

Более продвинутая версия: круговые раскрытия.

Эту технику также можно использовать для создания круговой анимации расширения и свертывания.

Принципы во многом такие же, как и в предыдущей версии, где вы масштабируете элемент и противомасштабируете его непосредственных дочерних элементов. В этом случае масштабируемый элемент имеет border-radius 50 %, что делает его круглым, и обернут другим элементом с overflow: hidden , что означает, что вы не видите, как круг расширяется за пределы границ элемента.

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

Код эффекта кругового расширения можно найти в репозитории GitHub .

Выводы

Итак, у вас есть способ создания качественной клиповой анимации с использованием масштабных преобразований. В идеальном мире было бы здорово увидеть ускорение анимации клипов (есть ошибка в Chromium, созданная Джейком Арчибальдом), но пока мы этого не дошли, вам следует быть осторожными при анимации clip или clip-path и определенно избегать анимации. width или height .

Также было бы удобно использовать веб-анимацию для подобных эффектов, поскольку у них есть API JavaScript, но они могут работать в потоке композитора, если вы анимируете только transform и opacity . К сожалению, поддержка веб-анимаций не очень хороша , хотя вы можете использовать прогрессивное улучшение, чтобы использовать их, если они доступны.

if ('animate' in HTMLElement.prototype) {
    // Animate with Web Animations.
} else {
    // Fall back to generated CSS Animations or JS.
}

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

Если вы хотите просмотреть код этого эффекта, загляните в репозиторий образцов элементов пользовательского интерфейса GitHub и, как всегда, сообщите нам, как у вас дела, в комментариях ниже.