Создание анимации орбиты с помощью CSS переменных

Источник: «Making Orbit Animations with CSS Custom Properties»
Давайте анимируем CSS переменные! Это мощный способ применения CSS анимации, которая в противном случае была бы утомительной или непрактичной.

Анимируем, что дальше

Сейчас объясню. Возьмём, к примеру, эту простую CSS анимацию.

@keyframes slide-top {
from { translate: -100px; }
to { translate: 100px; }
}

.top-potato {
animation: slide-top 3s infinite alternate;
}

Она перемещает что-то слева направо, затем обратно, снова и снова. Она отлично работает, но её можно переписать, чтобы использовались CSS переменные.

@property --x {
syntax: '<length>';
inherits: true;
initial-value: 0px;
}

@keyframes slide-bottom {
from { --x: -100px; }
to { --x: 100px; }
}

.bottom-potato {
translate: var(--x);
animation: slide-bottom 3s infinite alternate;
}

Первое, что делаем — регистрируем CSS переменную --x с помощью @property. Это важно, так как позволяет анимировать значение --x. Затем, вместо того чтобы анимировать translate напрямую, анимируем --x и установим translate в var(--x).

И вот результаты: сверху — классический подход, снизу — подход с использованием CSS переменных.

See the Pen

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

С этого момента рассмотрим серию всё более интересных анимаций орбиты и увидим, как CSS переменные помогают их создавать.

Простая анимация орбиты

Вот демонстрация луны (.moon), вращающейся вокруг планеты (.planet) и остающейся при этом в вертикальном положении. Посмотрите, а потом разберёмся с CSS.

See the Pen

В основе этой анимации лежит CSS переменная --angle, проходящая полный круг от 0deg до 360deg. Поскольку это значения угла, необходимо зарегистрировать CSS переменную как <angle>.

@property --angle {
syntax: '<angle>';
inherits: true;
initial-value: 0deg;
}

@keyframes revolve {
from { --angle: 0deg; }
to { --angle: 360deg; }
}

С помощью элементарной тригонометрии можно превратить значение анимированного угла в координаты (x, y), следующие по идеально круговой траектории. Не волнуйтесь, если вы не тригонометрист (реальное слово, хотите, верьте, хотите нет)! Формула для этого довольно проста.

x = cos(angle) * amplitude
y = sin(angle) * amplitude

Вместе cos() и sin() создают точки вдоль окружности с радиусом 1. Это довольно мало (и не имеет единицы измерения), поэтому используется amplitude — с её помощью можно увеличить окружность на некоторое пиксельное значение.

Теперь, когда есть формула, давайте переведём её в CSS и добавим несколько завершающих штрихов.

.moon {
--amplitude: 150px;
--x: calc(cos(var(--angle)) * var(--amplitude));
--y: calc(sin(var(--angle)) * var(--amplitude));
translate: var(--x) var(--y);
animation: revolve 12s linear infinite;
}

Это та же математика, о которой говорилось выше, с --x и --y, передаваемыми в свойство translate для позиционирования орбитальной луны. Объявление animation приводит в движение CSS переменную --angle, приводя всё в движение.

Обратите внимание, что --angle был зарегистрирован через @property, а --x и --y — нет. Их не нужно было регистрировать, так как их значения не анимировались напрямую, а только косвенно через --angle.

Эллиптическая анимация орбиты

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

See the Pen

Мы сохраним прежнюю CSS переменную --angle, но добавим анимированную переменную --z, используя которую будем управлять z-index луны, чтобы она проходила перед планетой или позади неё.

Можно было бы просто анимировать z-index напрямую, но я склоняюсь к подходу анимированных CSS переменных, чтобы показать, как это работает, и поделиться одной странностью, с которой столкнулся.

@property --angle {
syntax: '<angle>';
inherits: true;
initial-value: 0deg;
}

@property --z {
syntax: '<integer>';
inherits: true;
initial-value: 0;
}

@keyframes revolve {
from {
--angle: 0deg;
--z: -1;
}
to {
--angle: 360deg;
--z: 0;
}
}

--z регистрируется как <integer> и анимируется от -1 (позади планеты) до 0 (перед планетой). Целые числа в CSS анимируются с шагом в целое число, что означает отсутствие промежуточных десятичных значений. Значение перевернётся в средней точке анимации, что идеально, так как в это время луна будет находиться слева от планеты.

Эллиптическая траектория достигается за счёт уменьшения амплитуды по оси y при сохранении той же амплитуды по оси x. Таким образом, вместо одной амплитуды --amplitude у нас будет амплитуда --x-amplitude и амплитуда --y-amplitude с разными значениями.

Вот обновлённые стили для луны.

.moon {
--x-amplitude: 150px;
--y-amplitude: 40px;
--x: calc(cos(var(--angle)) * var(--x-amplitude));
--y: calc(sin(var(--angle)) * var(--y-amplitude));
translate: var(--x) var(--y);
z-index: calc(var(--z)); /* calc() для Safari */
animation: revolve 6s linear infinite;
}

Обратите внимание, что var(--z) обёрнута в calc(). В этом нет необходимости, но это исправляет проблему в Safari, когда z-index не принимал значение. Почему? Я не знаю.

Анимация множественных эллиптических орбит

Зачем останавливаться только на луне?

See the Pen

Это продолжает развивать то, что уже было выяснено. Суть в том, что спутник (.satellite) и картофель (.potato) движутся по наклонной эллиптической орбите. Вот CSS, позволяющий добиться этого.

.moon, .satellite, .potato {
--x-amplitude: 150px;
--y-amplitude: 40px;
--x: calc(cos(var(--angle)) * var(--x-amplitude));
--y: calc(sin(var(--angle)) * var(--y-amplitude));
transform:
rotate(var(--rotation))
translate(var(--x), var(--y))
rotate(calc(var(--rotation) * -1));
z-index: calc(var(--z)); /* calc() для Safari */
animation: revolve 6s linear infinite;
}

В основном здесь всё знакомо, но разница в том, что вместо translate используется transform, чтобы выполнить последовательность из трёх преобразований.

  1. rotate, чтобы изменить ориентацию эллиптической траектории на --rotation градусов.
  2. translate, чтобы разместить изображение как раньше, но теперь в пределах повёрнутой ориентации.
  3. rotate снова в противоположную сторону, чтобы отменить поворот изображения (сравните эту картофелину с картофелиной в самой первой демонстрации — её ориентация совпадает).

Луна, спутник и картофель имеют собственный --rotation. Чтобы они не врезались друг в друга, у них также есть ступенчатые отрицательные значения animation-delay.

.moon {
--rotation: 0deg;
animation-delay: 0s;
}

.satellite {
--rotation: 60deg;
animation-delay: -2s;
}

.potato {
--rotation: 120deg;
animation-delay: -4s;
}

Анимация множественных колеблющихся эллиптических орбит

Ещё один, просто для развлечения. Давайте сделаем его колеблющимся.

See the Pen

CSS такой же, как и раньше, только с обновлённой формулой для --x и --y.

.moon, .satellite, .potato {
--x-amplitude: 150px;
--y-amplitude: 40px;
--wobble-multiplier: 20;
--wobble-amplitude: 5px;
--x: calc(cos(var(--angle)) * var(--x-amplitude) + cos(var(--wobble-multiplier) * var(--angle)) * var(--wobble-amplitude));
--y: calc(sin(var(--angle)) * var(--y-amplitude) + sin(var(--wobble-multiplier) * var(--angle)) * var(--wobble-amplitude));

/* и всё остальное, что было раньше. */
}

Не буду углубляться в математику, но по сути это та же формула орбиты, что и раньше, но к ней добавлена ещё одна орбита, меньшая и более быстрая (буквально, через +).

Знаете, что самое замечательное в этой демонстрации? Благодаря анимированному подходу к CSS переменным, всё, что нужно было сделать, это обновить математику для одной анимации. Никаких изменений HTML для добавления дополнительных элементов, никакой анимации ключевых кадров.

Производительность

В настоящее время анимация CSS переменных всегда обрабатывается главным потоком, даже если они используются со свойствами, которые могут быть ускорены на GPU (например, translate и transform). Другими словами, эти анимации не будут такими плавными. Для получения более подробной информации у Bramus есть хорошее объяснение этой проблемы.

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

See the Pen

Если присмотреться, можно увидеть, что слово chunky прыгает пиксель за пикселем. У него нет субпиксельной анимации. Это легче увидеть на не retina дисплее.

Однако я обнаружил, что использование will-change может немного сгладить ситуацию (по крайней мере, в Chrome и Safari, к сожалению, не в Firefox). Вот что я использую, чтобы сделать слово smooth менее нервным.

.smooth {
will-change: translate;
}

Анимация по-прежнему выполняется в основном потоке, но, по крайней мере, мы получаем субпиксельную анимацию, что делает её более плавной.

До следующей встречи

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

В любом случае надеюсь, вам было весело готовить орбитальный картофель вместе со мной.

Комментарии


Дополнительные материалы

Предыдущая Статья

Не проспите AbortController

Следующая Статья

Практические советы по доступности, которые можно применить сегодня