Рис. 0. КДПВ
Анимация в интерфейсе делает наглядным изменение его состояния. Например, при неудачной отправке длинная форма прокручивается к неправильно заполненному полю. Или увеличивающаяся по нажатию фотография раздвигает окружающие элементы.
Без анимации сложнее воспринимать резкие и внезапные изменения. Вместе с тем анимация должна быть короткой и ненавязчивой, чтобы не мешать пользователю.
Анимация кажется естественной, когда повторяет привычное движение предметов окружающего мира. Под катом я расскажу, как делал анимацию на основе физических законов. Смотрите готовый результат на демо-странице (там один блок следует за другим при движении мыши).
Вспоминаем физику
Перемещение объектов описывается изменением координат x с течением времени t. Если вы попытаетесь подобрать функцию x(t) «на глазок», вы потратите много времени, добиваясь плавного и естественного движения. Что выбрать? Гиперболу? Параболу? Куда ее переместить? Как повернуть?
За примерами движения лучше всего обратиться к предметам окружающего мира. Математический закон их движения диктуется физикой. Толкнем брусок, лежащий на столе. Он проходит определенное расстояние, замедляясь под действием силы трения. В хорошем приближении сила сухого трения скольжения постоянна, и зависимость x(t) оказывается параболой. Такое замедление можно использовать, если в начальный момент объект анимации уже двигался.
Рис. 1. Торможение сухим трением по параболе
Сила вязкого трения пропорциональна скорости движения тела. В таком случае тело будет двигаться к точке остановки по экспоненте за бесконечно большое время. Если экспоненту исказить, чтобы ограничить время движения, такая анимация будет казаться неестественной. Из-за трудностей с остановкой в разумное время не следует использовать модель вязкого трения, только если симуляция самого вязкого трения не является целью.
Рис. 2. Торможение по экспоненте в вязкой среде
Отклоненный от положения равновесия маятник (или грузик на пружине) плавно набирает скорость, проходит положение равновесия и плавно тормозит. Затем движение повторяется в обратную сторону, и так до бесконечности (если трения нет). График такого движения — синусоида. Периодический повтор нам не особо интересен, а вот движение маятника между крайними точками получается плавным и естественным.
Рис. 3. Движение маятника по синусоиде между крайними точками
В JS-библиотеках и CSS есть заготовки easing-функций для создания специальных эффектов. Почти все заготовки следует использовать в специальных случаях, с осторожностью. Только синусоида более-менее универсальна.
Во-первых, синусоидальная траектория переводит тело из одного покоящегося положения в другое. Во-вторых, продолжительность такого движения равна половине периода. Движение ограничено во времени. Продолжительность не зависит от внешних обстоятельств и начальных условий. Она зависит только от свойств самой системы и определяется соотношением жесткости и инерционности.
Обычно я выбираю длительность анимации по синусоиде в 200 миллисекунд. Такая длительность в несколько раз больше времени реакции человека. Анимация хорошо заметна, но не успевает раздражать.
Давайте научимся проводить синусоидальную траекторию по начальным условиям, времени движения и точке остановки.
Как провести синусоиду через две точки
Пусть тело покоится в начальный и конечный момент времени. Тогда касательные к графику в точках t1 и t2 горизонтальны, а сам график — это полупериод синусоиды.
Рис. 4. График движения между двумя положениями покоя
Уравнение, описывающее полупериод синусоиды, легко подобрать:
После окончания одной анимации мы можем начинать другую опять по этой формуле. Но что делать, если новая анимация должна начаться, пока еще не закончилась старая? Чтобы обеспечить плавность движения, мы останавливаем текущую анимацию (синяя линия) и начинаем новую анимацию (красная линия) с ненулевой начальной скоростью:
Рис. 5. График движения с ненулевой начальной скоростью
Без математических вычислений не получится написать формулу, соответствующую красной линии. Давайте проделаем эти вычисления.
Семейство всех возможных синусоид описывается уравнением
с четырьмя неизвестными параметрами A, B, C и . Я сдвинул начало отчета времени в точку t2, чтобы сразу избавиться от второго слагаемого. Действительно, производная должна быть нулевой, потому что касательная в точке t2 горизонтальна. Это возможно, когда B=0.
Так как , то подставляя в (1), получаем . Отсюда исключаем C:
Продифференцируем, чтобы найти скорость
Нам известно положение x1 и скорость v в начальный момент времени:
Из этой системы уравнений нужно найти A и . Пора вводить новую переменную вместо . Ее смысл — разность фаз синусоиды в начальной и конечной точке. Например, для графика на рис. 4 , потому что на промежутке укладывается полупериод синусоиды. На рис. 5 , потому что меньше половины периода.
После подстановки и небольших преобразований приходим к системе
Разделим почленно первое уравнение на второе:
Параметр в правой части известен заранее. Он определяет требуемый характер движения. Если , то начальная скорость мала, тело сначала должно ускориться. Если , начальная скорость велика, тело должно замедляться.
Тригонометрические функции в левой части сводятся к тангенсу половинного угла. В итоге у нас нелинейное уравнение относительно k:
Проанализировать его решения можно на графике. Нарисуем график левой и правой части при некоторых значениях параметра :
Рис. 6. Графическое решение уравнения (2)
Обсудим получившиеся решения.
- Рассмотрим точку A. Это решение существует при и соответствует изображенному на рисунке 5: . Как ожидалось, . В пределе нулевой скорости , красная прямая совпадет с осью ординат, точка A уйдет по тангенсоиде в бесконечность. В этом пределе . Пока всё идет правильно.
- Точка C отвечает значению . Такое случается, когда тело в первый момент времени движется вперед, а надо двигаться назад. Теперь . Движение описывается фрагментом синусоиды, большим чем полупериод, но меньшим, чем период:
. Тело тормозит, останавливается, движется назад и останавливается в требуемом месте. - Из графика видно, что при точка B попадает в диапазон . Тело пройдет по синусоиде больше, чем полный период колебаний: . Причина такого странного решения в том, что точка остановки находится слишком близко по сравнению с характерным расстоянием v (t2 − t1). Поэтому провести синусоиду без дополнительной остановки и возврата не получится.
Приближенное решение
Мы решили математическую задачу проведения синусоиды, но жизнь от этого проще не стала. Во-первых, чтобы определить параметры синусоиды, надо решить нелинейное уравнение (2). Привет, итерационные методы! Во-вторых, уравнение имеет бесконечное количество решений, а требуемое решение не всегда существует.
Эти трудности возникли от того, что мы зафиксировали продолжительность анимации ровно в 200 миллисекунд. Однако ничего страшного не случится, если анимация продлится, скажем, 180 миллисекунд. Или даже 250 миллисекунд. Нам важнее остановка в заданном месте, а точной продолжительностью анимации мы пожертвуем для упрощения расчетов.
Ослабив требования на продолжительность анимации, мы проделаем такой трюк. Предположим, что у нас есть приближенное решение нелинейного уравнения (2). Оно является точным решением уравнения с другим параметром
Ему соответствует другое время окончания анимации:
Теперь неизвестные параметры траектории A и элементарно выражаются через и .
Я подобрал подходящее для наших целей приближение к уравнению (2):
Синяя сплошная линия соответствует точному уравнению (2), а красная пунктирная — его приближению:
Рис. 7. Сравнение точного соотношения (2) и его приближения
А еще в случае предлагаю взять чуть больше, чем 1/2, и сократить время анимации, чтобы избежать отскока и возврата.
Применение
Код и сферический пример использования есть на демо-странице. Поводите мышью и посмотрите, как черный блок следует за оранжевым.
Описанная схема применяется и в готовом продукте. Я разработал ее для синхронной прокрутки исходника и предпросмотра в markdown- и latex- редакторе математических текстов.
Идею и первоначальную реализацию нашел на демо-странице js-парсера markdown-it. В их варианте анимация получилась рваной и подтормаживающей. Тому есть несколько причин:
- Для анимации применяется линейная функция:
$(...).stop(true).animate({scrollTop: ...}, 100, 'linear')
. Вместо гладкого графика получается ломаная. - Анимация через
jQuery().stop().animate()
тормозит по сравнению сrequestAnimationFrame()
. - Чтобы избежать падения производительности, «проглатываются» события
onscroll
, следующие чаще чем 50 миллисекунд. В моем варианте такой проблемы нет. Последовательные событияonscroll
корректируют положение точки остановки и не замедляют анимацию.
Чтобы добиться важной для продукта качественной анимации, я проработал метод вычисления на основе физических уравнений, и реализовал его через специальный браузерный метод requestAnimationFrame()
. Метод хорошо работает при любой прокрутке: клавишами PageUp/PageDown, через перемещение полос прокрутки, колесико мыши, тачпад, тачскрин.
Автор: parpalak