Создание анимации орбиты с помощью 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 переменных.
Вы, возможно, зададитесь вопросом, зачем все эти хлопоты, чтобы в итоге получить ту же самую анимацию. Справедливо! Это был просто базовый пример, демонстрирующий концепцию и синтаксис.
С этого момента рассмотрим серию всё более интересных анимаций орбиты и увидим, как CSS переменные помогают их создавать.
Простая анимация орбиты
Вот демонстрация луны (.moon
), вращающейся вокруг планеты (.planet
) и остающейся при этом в вертикальном положении. Посмотрите, а потом разберёмся с CSS.
В основе этой анимации лежит 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
.
Эллиптическая анимация орбиты
Давайте изменим анимацию так, чтобы она следовала по эллиптической траектории, и добавим немного слоистости.
Мы сохраним прежнюю 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
не принимал значение. Почему? Я не знаю.
Анимация множественных эллиптических орбит
Зачем останавливаться только на луне?
Это продолжает развивать то, что уже было выяснено. Суть в том, что спутник (.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
, чтобы выполнить последовательность из трёх преобразований.
rotate
, чтобы изменить ориентацию эллиптической траектории на--rotation
градусов.translate
, чтобы разместить изображение как раньше, но теперь в пределах повёрнутой ориентации.rotate
снова в противоположную сторону, чтобы отменить поворот изображения (сравните эту картофелину с картофелиной в самой первой демонстрации — её ориентация совпадает).
Луна, спутник и картофель имеют собственный --rotation
. Чтобы они не врезались друг в друга, у них также есть ступенчатые отрицательные значения animation-delay
.
.moon {
--rotation: 0deg;
animation-delay: 0s;
}
.satellite {
--rotation: 60deg;
animation-delay: -2s;
}
.potato {
--rotation: 120deg;
animation-delay: -4s;
}
Анимация множественных колеблющихся эллиптических орбит
Ещё один, просто для развлечения. Давайте сделаем его колеблющимся.
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 переменных. Взгляните на эту демонстрацию.
Если присмотреться, можно увидеть, что слово chunky
прыгает пиксель за пикселем. У него нет субпиксельной анимации. Это легче увидеть на не retina дисплее.
Однако я обнаружил, что использование will-change
может немного сгладить ситуацию (по крайней мере, в Chrome и Safari, к сожалению, не в Firefox). Вот что я использую, чтобы сделать слово smooth
менее нервным.
.smooth {
will-change: translate;
}
Анимация по-прежнему выполняется в основном потоке, но, по крайней мере, мы получаем субпиксельную анимацию, что делает её более плавной.
До следующей встречи
Использование CSS переменных для анимации невероятно удобно, потому что позволяет создавать анимацию разумным образом. Можно пропустить анимированные значения через математические вычисления, чтобы создать анимацию, не прибегая к утомительному кадрированию или вложению нескольких анимированных элементов.
В любом случае надеюсь, вам было весело готовить орбитальный картофель вместе со мной.