Главная » Хабрахабр » [Перевод] Применение правил тригонометрии для создания качественной анимации

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Для того чтобы это сделать, дадим имя нашему графику, например — 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, используется для управления скоростью анимации. 15. Здесь, экспериментальным путём, выбрано значение 0. Как оказалось, оно даёт вполне приемлемо выглядящие перемещения фигуры.

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

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

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

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

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

1. Напомним, что в конце раздела 2. Для того чтобы расширить диапазон перемещений фигуры, нужно умножить то, что выдаёт функция 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. Именно это здесь и сделано. Но теперь, так как у нас имеется три круга, им полезно будет назначить более понятные идентификаторы. Идентификатор исходного круга, c, изменён на cCentre. Сейчас, если рассматривать круги слева направо, они имеют идентификаторы cLeft, cCentre и cRight.

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

Три круга

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

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, задающим амплитуду движения, да и с другими значениями тоже. На самом деле, теперь вы можете сделать подобную анимацию именно такой, как вам нужно.

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


Оставить комментарий

Ваш email нигде не будет показан
Обязательные для заполнения поля помечены *

*

x

Ещё Hi-Tech Интересное!

Наша книжная полка С#-программиста. А что у вас?

Привет! Обычно мы рекомендуем несколько источников, сопровождая их своими комментариями, почему именно они будут полезны. Будущие студенты Veeam Academy часто спрашивают нас о книгах, которые были бы полезны при подготовке к поступлению на наш курс по программированию на С#. Поэтому ...

[Из песочницы] Компрессия больших массивов простых чисел

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