Как сделать анимацию вдоль окружности

Оцените материал
(0 голосов)

Проблема

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

Другой, очень забавный пример можно найти на популярном российском веб-сайте  habrahabr.ru. В соответствии с лучшими практиками оформления страниц для ошибки 404 эта страница содержит навигационное меню, позволяющее перейти к некоторым основным разделам веб-сайта. Каждый элемент меню представлен в виде планеты, вращающейся по окружности, а текст наверху гласит: «Слетайте на другие наши планеты». Разумеется, в данном случае логично перемещать планеты по окружности и не вращать их дополнительно вокруг своей оси, иначе текст станет невозможно прочитать.

 


Это лишь пара из множества подобных примеров. Но как реализовать такой эффект с помощью анимации CSS?
Мы будем работать над очень простым примером аватара, движущегося по окружности, — что-то вроде упрощенной версии упомянутого выше эффекта из Google+. Разметка выглядит так:

HTML
<div class="path">
<img src="/lea.jpg" class="avatar" />
</div>

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


Если вы сомневаетесь в своем умении создавать круглые фигуры с помощью CSS, то обратитесь к секрету Гибкие эллисы.

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

@keyframes spin {
to { transform: rotate(1turn); }
}
.avatar {
animation: spin 3s infinite linear;
transform-origin: 50% 150px; /* 150px = path radius */
}

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



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

 

 

 

Решение с двумя элементами

Поварив проблему в подсознании несколько месяцев, я все же придумал решение. Основная идея та же, что и в секрете «Параллелограммы» или в секрете «Изображения в форме ромба»: вложенные трансформации, отменяющие друг друга. Однако на этот раз это будет происходить не статически, а в каждом кадре анимации. Хитрость в том, что, как и в вышеупомянутых секретах, в данном решении нам потребуются два элемента. Следовательно, нам необходимо изменить наш исходный чистый HTML-код, добавив дополнительную обертку в форме блока  div:

HTML
<div class="path">
<div class="avatar">
<img src="/lea.jpg" />
</div>
</div>

Давайте применим нашу исходную анимацию, которую мы уже тестировали выше, к обертке . avatar. Как видно на рисунке, решение пока не работает, поскольку сам элемент также вращается. Но что, если применить к аватару другое вращение и поворачивать его вокруг своей оси на тот же угол, но в противоположном направлении? Тогда два вращения будут отменять друг друга, и мы будем видеть только движение по окружности, создаваемое разницей между центрами трансформаций!

Но мы пока не решили еще одну проблему: у нас нет статического вращения, которое мы могли бы отменить, только анимация, проходящая через целый диапазон углов. Например, если бы угол был равен  60deg , то мы бы отменили его с помощью  -60deg (или  300deg ), если бы это было  70deg, то мы бы для отмены использовали  -70deg (или  290deg ). Но если угол может быть любым в диапазоне от  0deg до  360deg (или от  0turn до  1turn , что эквивалентно), то что нам делать? Ответ намного проще, чем может казаться. Мы всего лишь определим анимацию на противоположном  диапазоне (от  360deg до  0deg ), вот так:

@keyframes spin {
to { transform: rotate(1turn); }
}
@keyframes spin-reverse {
from { transform: rotate(1turn); }
}
.avatar {
animation: spin 3s infinite linear;
transform-origin: 50% 150px; /* 150px = радиус пути */
}
.avatar > img {
animation: spin-reverse 3s infinite linear;
}

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

Код, однако, не мешало бы слегка улучшить. Мы повторяем все параметры анимации как минимум дважды. Если бы нам потребовалось изменить продолжительность анимации, нам пришлось бы отредактировать два значения, что идет вразрез с принципами DRY.

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

@keyframes spin {
to { transform: rotate(1turn); }
}
@keyframes spin-reverse {
from { transform: rotate(1turn); }
}
.avatar {
animation: spin 3s infinite linear;
transform-origin: 50% 150px; /* 150px = радиус пути */
}
.avatar > img {
animation: inherit;
animation-name: spin-reverse;
}

Но почему мы используем целую новую анимацию только для того, чтобы отменить первоначальную? Помните свойство  animation-direction из секрета «Как сделать мерцающую анимацию»? В том секрете мы узнали, в каких ситуациях оказывается полезным значение  alternate. Здесь же мы будем использовать значение  reverse, для того чтобы получить перевернутую копию нашей исходной анимации, что вообще избавит нас от необходимости создавать вторую анимацию:

@keyframes spin {
to { transform: rotate(1turn); }
}
.avatar {
animation: spin 3s infinite linear;
transform-origin: 50% 150px; /* 150px = радиус пути */
}
.avatar > img {
animation: inherit;
animation-direction: reverse;
}

Вот и всё! Возможно, решение не идеальное, так как содержит в себе требование дополнительного элемента, но мы реализовали довольно сложную анимацию с помощью менее десятка строк CSS-кода!

ПОПРОБУЙТЕ САМИ!
http://play.csssecrets.io/circular-2elements

Решение с одним элементом

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

Это позволило бы воплощать решения, аналогичные предыдущему, с использованием только одного элемента, да и в целом казалось весьма здравой идеей. Как выяснилось, любое значение  transform-origin можно имитировать с помощью двух трансформаций  translate() . Например, следующие два фрагмента кода эквивалентны:

transform: rotate(30deg);
transform-origin: 200px 300px;
transform: translate(200px, 300px)
rotate(30deg)
translate(-200px, -300px);
transform-origin: 0 0;



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

Следовательно, благодаря этой идее мы можем использовать одно и то же значение  transform-origin для обеих наших предыдущих анимаций (нам снова придется создать отдельные анимационные эффекты, так как ключевые кадры у них теперь не совпадают):

@keyframes spin {
from {
transform: translate(50%, 150px)
rotate(0turn)
translate(-50%, -150px);
}
to {
transform: translate(50%, 150px)
rotate(1turn)
translate(-50%, -150px);
}
}
@keyframes spin-reverse {
from {
transform: translate(50%,50%)
rotate(1turn)
translate(-50%,-50%);
}
to {
transform: translate(50%,50%)
rotate(0turn)
translate(-50%, -50%);
}
}
.avatar {
animation: spin 3s infinite linear;
}
.avatar > img {
animation: inherit;
animation-name: spin-reverse;
}

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

@keyframes spin {
from {
transform: translate(50%, 150px)
rotate(0turn)
translate(-50%, -150px)
translate(50%,50%)
rotate(1turn)
translate(-50%,-50%)
}
to {
transform: translate(50%, 150px)
rotate(1turn)
translate(-50%, -150px)
translate(50%,50%)
rotate(0turn)
translate(-50%, -50%);
}
}
.avatar { animation: spin 3s infinite linear; }

Определенно, код улучшается, но он все еще длинный и непонятный. Можно ли сделать его более емким? Здесь возможно несколько потенциальных усовершенствований. Решение, лежащее на поверхности, заключается в том, чтобы объединить соседние трансформации translate() , в частности  translate(-50%, -150px) и  translate(50%, 50%) . К сожалению, процентные и абсолютные значения невозможно комбинировать (если только не прибегать к помощи функции  calc() , но это также не улучшит читабельность кода). Однако горизонтальные трансформации отменяют друг друга, то есть, по сути, у нас здесь две трансляции по оси Y ( translateY(-150px) translateY(50%) ). Кроме того, поскольку вращения отменяют друг друга, мы можем также убрать горизонтальные трансляции до и после них и объединить вертикальные. Теперь наши ключевые кадры выглядят так:

@keyframes spin {
from {
transform: translateY(150px) translateY(-50%)
rotate(0turn)
translateY(-150px) translateY(50%)
rotate(1turn);
}
to {
transform: translateY(150px) translateY(-50%)
rotate(1turn)
translateY(-150px) translateY(50%)
rotate(0turn);
}
}
.avatar { animation: spin 3s infinite linear; }
Обратите внимание, что нам больше не требуются два элемента HTML: теперь мы можем просто применить класс avatar к самому изображению, так как мы больше не определяем для них стили по отдельности. Это уже короче, и повторений меньше, но все же не идеально. Можно ли сделать код еще лучше? Если в качестве начального положения аватара установить центр круга, то можно избавиться от первых двух трансляций, которые, по сути, помещают его в центр. Тогда анимация становится такой:

@keyframes spin {
from {
transform: rotate(0turn)
translateY(-150px) translateY(50%)
rotate(1turn);
}
to {
transform: rotate(1turn)
translateY(-150px) translateY(50%)
rotate(0turn);
}
}
.avatar { animation: spin 3s infinite linear; }

Кажется, это лучшее, чего мы можем достичь на сегодняшний день. Этот код нельзя назвать идеальным с точки зрения принципов DRY, но он довольно короткий. Здесь минимум повторений и нет лишних элементов HTML. Для того чтобы сделать его еще более емким и избежать повторения радиуса пути, можно воспользоваться помощью препроцессора, но я оставлю это в качестве упражнения для читателя.

ПОПРОБУЙТЕ САМИ!

http://play.csssecrets.io/circular

На десерт сегодня видео о том, как собирают Порше 911. Никакой музыки или закадрового голоса, что сейчас редкость. Настоящее видео для релаксации

Прочитано 372 раз Последнее изменение Суббота, 01 июля 2017 13:07
Понравилась запись? Подпишитесь на обновления по почте:

Нетология

TemplateMonster

geekbrains.ru/

Поиск по сайту

Google+