Применение правил тригонометрии для создания качественной анимации

Автор оригинала: Nash Vail
  • Перевод
Автор материала, перевод которого мы сегодня публикуем, Нэш Вэйл, говорит, что недавно он занимался исследованием лендинг-страниц. В ходе работы он наткнулся на один сайт. Это был отличный, полезный ресурс. Однако, в ходе работы с ним, Нэш заметил, нечто неприятное.

Неестественная анимация

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

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


Плавная анимация

1. Позиционирование круга


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

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


Этот код позволяет нарисовать кружок в центре элемента <svg>. На следующем рисунке показан такой кружок, даже скорее шарик. Надо отметить, что его внешний вид не соответствует вышеприведённому коду, но нас сейчас интересуют не внешние эффекты, а факт наличия круга в центре некоей области, предназначенной для вывода графических элементов.


Кружок в центре области для вывода графических элементов

Разберёмся с тем, что тут происходит, сопоставив код и его графическое представление.

Итак, свойства width и height, заданные в коде, представляют, соответственно, ширину и высоту прямоугольного элемента SVG.


Ширина и высота SVG-прямоугольника

По умолчанию SVG-элементы используют традиционную систему координат, начало которой находится в левом верхнем углу. В такой системе координат значения по осям X и Y возрастают при перемещении по ней вправо и вниз. Кроме того, каждая точка в этой системе координат соответствует одному пикселю. В результате, например, четыре угла прямоугольника имеют координаты, зависящие от заданной ему ширины и высоты.


Координаты углов SVG-прямоугольника

Следующий шаг, который заключается в размещении круга в центре поля, подразумевает использование математических принципов, которые изучают в начальной школе. А именно, координаты центра фигуры являются результатом деления ширины и высоты поля на 2 (width/2, height/2), что даёт нам (150, 75). Мы присваиваем эти значения свойствам cx и cy, что позволяет поместить круг по центру поля.


Нахождение центра фигуры

2. Перемещение круга


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


Схема перемещений фигуры

▍2.1. Математические принципы периодического движения


Периодичность — это некое явление, происходящее через регулярные промежутки времени. Самый простой пример периодичности — это ежедневный восход и заход солнца. Периодичность свойственна и применяемой нами системе отсчёта времени. Скажем, если сейчас 6:30 вечера, то через 24 часа снова будет 6:30 вечера, а ещё через 24 часа — опять 6:30 вечера. В данном случае перед нами — нечто регулярное, происходящее с интервалом ровно в 24 часа.

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


Цикл восхода и захода солнца

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

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

Для того чтобы построить двумерную кривую, нам нужны две координаты — x и y. В нашем случае это координата time, представляющая время суток, и координата positionOfTheSun, соответствующая позиции солнца.


Цикл восхода и захода солнца, представленный в виде графика

Вертикальная ось, или ось Y — это вертикальная позиция солнца на небе, а горизонтальная ось, или ось X — это время. С течением времени позиция солнца изменяется, при этом данный процесс воспроизводится каждые 24 часа.

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

verticalPositionInTheSky = sunsVerticalPositionAt( [time] )

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


Выяснение позиции солнца с использованием графика функции

Тут мы выбираем момент времени (t1), в который нам нужно узнать позицию солнца, после чего рисуем в нашей системе координат вертикальную прямую, а в той точке, где она пересекает график, рисуем горизонтальную прямую, и находим координату её пересечения с осью y. Эта координата и даёт нам позицию солнца в небе в заданный момент времени.

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


Периодическая кривая

Этот график является универсальным представлением некоего периодического процесса. Значение (то, что было вертикальной позицией солнца в нашем примере) повторяется по мере изменения другого значения (в нашем примере эти вторым значением было время).

В математике существует немало периодических функций, мы остановимся на самой простой, широко известной функции, которую и будем использовать для создания безупречной анимации. Это — синусоидальная функция, описываемая формулой y = sin(x). Вот её график.


Синусоидальная кривая

Вам это ничего не напоминает? Например — тот график, который мы строили, основываясь на анализе поведения солнца?

Мы можем подставлять в формулу y = sin(x) значения x и получать значения y. Собственно говоря — выглядит это так же, как выяснение позиции солнца по соответствующему графику.

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

В уравнении y = sin(x), стоит обратить внимание на y и x, на то, как меняется значение y при изменении значения x (несложно заметить, что поведение y похоже на то, что мы уже видели в примере с солнцем).

Кроме того, стоит обратить внимание на то, что максимальное значение, которого достигает y, является 1, а минимальное представлено значением -1.

Это — всего лишь особенность синусоидальной функции. Значения, которые выдаёт функция y = sin(x), находятся в диапазоне от -1 до +1.

Этот диапазон, кстати, несложно изменить. Именно этим мы скоро и займёмся. Однако сначала давайте соберём всё, с чем мы уже разобрались, и сделаем так, чтобы круг в элементе SVG начал двигаться.

▍2.2. Переход от математики к программированию


Итак, внутри элемента ... имеется круг с идентификатором c. Вот как получить доступ к этому кругу из JavaScript. После этого мы сможем перемещать его.

let c = document.getElementbyId('c');
animate();
function animate() {
  requestAnimationFrame(animate);
}

В этом коде мы получаем ссылку на круг и сохраняем её в переменной c. Так же тут подготовлен механизм для выполнения анимации. А именно, здесь мы используем функцию requestAnimationFrame, которой передаём функцию animate. Эта функция рекурсивно вызывает сама себя, используя requestAnimationFrame, что позволяет выполнять любой код внутри функции animate с периодичностью до 60 раз в секунду. Это даёт возможность выполнять анимацию с частотой до 60 FPS (кадров в секунду). Подробности о requestAnimationFrame можно почитать здесь.

Самое важное, что надо знать о функции animate, заключается в том, что код внутри неё, при каждом её вызове, описывает один кадр анимации. При следующем рекурсивном вызове этой функции выполняется небольшое изменение, отражающееся в следующем кадре анимации. Это происходит снова и снова, на очень высокой скорости (до 60 FPS), а в результате получается то, что выглядит как анимация.

Рассмотрим эти положения на практике, проанализировав следующий фрагмент кода.

let c = document.getElementById('c');
let currentAnimationTime = 0;
const centreY = 75;
animate();
function animate() {
  c.setAttribute('cy', centreY + (Math.sin(currentAnimationTime)));
  
  currentAnimationTime += 0.15;
  requestAnimationFrame(animate);
}

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


Перемещения круга

Вот что тут происходит. После того, как у нас имеются координаты центра круга, cx и cy, можно сказать, что мы наполовину готовы к тому, чтобы его анимировать. Теперь, во-первых, не будем трогать cx, так как нам не нужно, чтобы круг менял горизонтальную позицию.

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


Изменение координаты y центра круга

Константа centreY хранит координату y центра круга (75), что позволяет, для изменения вертикальной позиции фигуры, прибавлять некие значения к этой константе или вычитать их из неё.

Переменная currentAnimationTime, которой в самом начале присвоено значение 0, используется для управления скоростью анимации. Чем сильнее мы её увеличим при каждом вызове, тем быстрее круг будет перемещаться. Здесь, экспериментальным путём, выбрано значение 0.15. Как оказалось, оно даёт вполне приемлемо выглядящие перемещения фигуры.

Показатель currentAnimationTime — это x в уравнении y = sin(x). Выполняя расчёты, необходимые для подготовки следующего кадра анимации, мы передаём функции Math.sin (это — стандартная функция JavaScript) значение currentAnimationTime, увеличенное на предыдущем шаге, и прибавляем число, которое выдаёт эта функция, к константе centreY.


Затем то, что получилось, прибавляем к cy, пользуясь методом setAttribute.


Как мы уже знаем, функция y = sin(x), для любого значения x, возвращает значения, находящиеся в диапазоне от -1 до 1. В результате, значения, которые мы назначаем cy, будут находиться в диапазоне от centreY - 1 до centreY + 1. Это приводит к вертикальному перемещению круга в пределах 1 пикселя.


Вертикальное перемещение фигуры, заданное в коде

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

Напомним, что в конце раздела 2.1. речь шла об особенностях синусоидальной функции. Для того чтобы расширить диапазон перемещений фигуры, нужно умножить то, что выдаёт функция Math.sin, на некое число, которое и станет представлением верхней и нижней границы перемещений.

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


Масштабирование графика

Теперь, после того, как мы это выяснили, внесём изменения в код.

let c = document.getElementById('c');
let currentAnimationTime = 0;
const centreY = 75;
animate();
function animate() {
  c.setAttribute('cy', 
  centreY + (20 *(Math.sin(currentAnimationTime))));
  
  currentAnimationTime += 0.15;
  requestAnimationFrame(animate);
}

А вот как теперь будет вести себя круг.


Перемещения круга

В результате можно видеть, как круг плавно перемещается вверх и вниз. Хорошо получилось, правда?

Только что мы увеличили амплитуду синусоидальной функции, умножив её на число. Теперь мы хотим добавить к нашей анимации ещё два круга, один — до того, который уже есть, второй — после него, и сделать так, чтобы они двигались так же, как и исходный круг. В коде описание новых фигур выглядит так:

<svg width="300" height="150">
  <circle id="cLeft" cx="120" cy="75" r="10" />
  <circle id="cCentre" cx="150" cy="75" r="10" />
  <circle id="cRight" cx="180" cy="75" r="10" />
</svg>

Мы внесли в код некоторые изменения и по-новому его организовали. Для начала, обратите внимание на две строки, выделенные полужирным шрифтом. Они описывают два новых круга, один размещён на 30 пикселей (150 - 30 = 120) левее исходного, второй — на 30 пикселей правее (150 + 30 = 180).

Ранее мы назначили единственному кругу идентификатор c. Круг был один, и работать с ним это не мешало. Но теперь, так как у нас имеется три круга, им полезно будет назначить более понятные идентификаторы. Именно это здесь и сделано. Сейчас, если рассматривать круги слева направо, они имеют идентификаторы cLeft, cCentre и cRight. Идентификатор исходного круга, c, изменён на cCentre.

Если теперь запустить наш код, в результате можно увидеть следующее.


Три круга

Пока всё нормально, но новые круги неподвижны. Исправим это.

let cLeft= document.getElementById('cLeft'),
  cCenter = document.getElementById('cCenter'),
  cRight = document.getElementById('cRight');
let currentAnimationTime = 0;
const centreY = 75;
const amplitude = 20;
animate();
function animate() {
  cLeft.setAttribute('cy', 
  centreY + (amplitude *(Math.sin(currentAnimationTime))));
  cCenter.setAttribute('cy', 
  centreY + (amplitude * (Math.sin(currentAnimationTime))));
  cRight.setAttribute('cy', 
  centreY + (amplitude * (Math.sin(currentAnimationTime))));
  currentAnimationTime += 0.15;
  requestAnimationFrame(animate);
}

Тут добавлено несколько строк кода, где мы, во-первых, получаем ссылки на новые объекты, а во-вторых — применяем к ним те же правила анимации, что и к исходному. Вот что всё это нам дало.


Анимированные круги

Теперь двигаются и новые круги. Однако, всё это пока ещё не похоже на ту анимацию, которую мы стремимся создать.

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


Анимированные круги

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

Теперь, для того, чтобы достичь нужного эффекта, нам нужно внести очень небольшие изменения в код. Однако, понимание того, что это за изменения, и как они влияют на результат, очень важно. Поэтому давайте с ними разберёмся.

Если представить перемещения кругов в виде графиков зависимости их координат cy от currentAnimationTime, у нас получится следующее.


Графики зависимости координат фигур от времени

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

Сдвиг — это параллельный перенос графика, который не меняет формы или размера графика функции. Он лишь меняет его местоположение. Сдвиг может быть горизонтальным или вертикальным. Нас в данном случае интересуют горизонтальные сдвиги графиков. Обратите внимание на то, как изменение значения a на следующей иллюстрации приводит к горизонтальному перемещению графика функции y=sin(x).


Перенос графика функции (Desmos)

Для того чтобы понять, как это работает, вернёмся к примеру с солнцем. Итак, в том примере мы пользовались функцией sunsVerticalPositionAt(t). Этой функции можно передать время и узнать вертикальную позицию солнца в заданное время. То есть, например, для того, чтобы узнать высоту солнца над горизонтом в 9 утра, можно воспользоваться конструкцией sunsVerticalPositionAt(9).

Теперь рассмотрим функцию вида sunsVerticalPositionAt(t-3).

Присмотритесь к ней повнимательнее. Если передать ей какую-то отметку времени t, она вычтет из этого значения 3, и вернёт положение солнца на 3 часа раньше заданного времени.


Сравнение функций, принимающих t и t-3

В результате, при t=9 новая функция вернёт то же самое, что старая, при t=6, при t=12 — то же самое, что и старая при t=9, и так далее. Мы изменили функцию так, чтобы она возвращала значения, предшествующие тем значениям, которые мы ей передаём.

Другими словами, мы сдвинули график функции вправо по оси x.

Взгляните на следующий рисунок. Старая функция при t=6 даёт нам значение B. После сдвига графика то же самое значение получается при t=9.


Старый график и сдвинутый график

Аналогично, если мы прибавим, а не вычтем 3, получив функцию sunsVerticalPosition(t + 3), график сдвинется влево, другими словами, функция будет давать нам значения на 3 часа позже переданного ей времени.

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


Сдвиг графиков функций, задающих анимацию кругов

Для того чтобы выразить это в коде, в него нужно будет внести следующее небольшое изменение.

let cLeft= document.getElementById('cLeft'),
  cCenter = document.getElementById('cCenter'),
  cRight = document.getElementById('cRight');
let currentAnimationTime = 0;
const centreY = 75;
const amplitude = 20;
animate();
function animate() {
cLeft.setAttribute('cy', 
  centreY + (amplitude *(Math.sin(currentAnimationTime))));
cCenter.setAttribute('cy', 
  centreY + (amplitude * (Math.sin(currentAnimationTime - 1))));
cRight.setAttribute('cy', 
  centreY + (amplitude * (Math.sin(currentAnimationTime - 2))));
currentAnimationTime += 0.15;
  requestAnimationFrame(animate);
}

Вот и всё! Мы сдвинули графики, ответственные за анимацию кругов cCenter и cRight. Теперь круги ведут себя так, как нам нужно.


Готовая анимация

Итоги


Как видите, теперь анимация выглядит вполне достойно, движения объектов рассчитаны с абсолютной математической точностью. Конечно, этот простой пример вполне поддаётся улучшению, но он даёт базу, на которой можно построить качественную анимацию. Для того чтобы лучше понять, как отдельные параметры анимации влияют на итоговый результат — поэкспериментируйте со значением currentAnimationFrame, которое ответственно за скорость анимации, и со значением amplitude, задающим амплитуду движения, да и с другими значениями тоже. На самом деле, теперь вы можете сделать подобную анимацию именно такой, как вам нужно.

Уважаемые читатели! Как вы занимаетесь разработкой анимаций для своих проектов?

RUVDS.com
1125,00
RUVDS – хостинг VDS/VPS серверов
Поделиться публикацией

Похожие публикации

Комментарии 23

    +15
    Ту часть, где объяснение периодов для дошкольников, вообще не надо было переводить. Остальное укладывается в одну строчку: «Шарики должны двигаться под синусоиде».
      +18

      Шарики должны двигаться под синусоиде, каждый со своим сдвигом по фазе!

        0
        «Палочки должны быть попендикулярны»! ©
        +2
        Возможно у меня напрочь отсутствует тригонометрический вкус, но анимация на первой гифке мне кажется более приятной (естественной), чем на последней.
          +1
          По моему — все одинаково плохо, фпс настолько низкий, что все неприятно дёргается. И я не понимаю, почему его нельзя было сделать больше.
            +2
            Чтобы GIF-ка на КДПВ с 15 мегабайт до 50 разрослась?
              0
              За такие гифки на КДПВ надо вообще в рид онли переводить…
              • НЛО прилетело и опубликовало эту надпись здесь
                –1
                Она же вроде-как в HTML, надо было просто ссылку на демо дать.
                +1
                ну вот розовый на первой гифке вообще в магазин ушел по своим делам, а черные вполне вместе.
                +2
                Абсолютно согласен.
                Графики-графики и вот… вы разницы не заметите, но она есть! (сурок в тему)
                  0

                  Мне кажется, что если рассматривать такую категорию как "приятность", то уже стОит говорить о такой штуке как easing. Ну и fps побольше, как же без него....

                  +10
                  Вау! Математика математика за 8 класс на Хабре!
                    +2
                    Было бы интересно посмотреть на наложенные друг на друга анимации.
                      +10
                      Из пушки по воробьям. Для таких анимаций есть CSS c transition-timing-function.
                        +4
                        Нельзя вот так взять и на 10 страницах с рисунками и анимацией описать вот это все. Это завуалированное оскорбление фронтендщиков, не иначе. </шутка>
                          0
                          Я с java-скриптами знаком очень поверхностно. В них вот такая бесконечная рекурсия — нормальное явление? Стек вызовов не разрастется до неприличия если вкладка с такой анимацией останется открытой весь день?
                            0
                            AFAIK коллстек очищается при вызове setTimeout. Если точнее, любые асинхронные вызовы создают свой коллстек.
                              0

                              Нет там никакой рекурсии. requestAnimationFrame() просто ставит функцию в очередь на выполнение на следующем кадре анимации. Здесь функция animate() в самом конце просто ставит в очередь саму себя и таким образом постоянно запускается снова. Стек вызовов не растёт, в нём всегда один вызов (функции animate()).

                                0
                                Спасибо, после прочтения описания этой функции все стало понятно :)
                              +1
                              Главный фейл этих анимаций – дискретность каждого кадра, отсутствие размытия движения (motion blur). Ну и первый принцип анимации от Диснея squash & stretch не помешал бы!
                                +1
                                Дистиллированный перфекционизм. Тратить такие усилия на столь незначительные моменты можно только когда всё остальное — идеально.
                                  0
                                  Небольшая опечатка в коде, в Svg id=«cCentre»
                                  а в скрипте cCenter = document.getElementById('cCenter')

                                  Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                  Самое читаемое