Природна анімація в інтерфейсах

\begin{tikzpicture}
\def\t{0}
\def\r{3.1415}
\begin{axis}[width=12cm,height=7cm,
ticks=none,
xmin=-0.5, xmax=3.8,
axis y line=left,axis x line=bottom,
xlabel=$t$,ylabel=$x$, 
every axis x label/.style={at={(current axis.south east)},anchor=south},
every y axis label/.style={at={(current axis.north west)},anchor=west},
enlargelimits=true,mark size=1
]
\addplot[smooth,blue,domain=\t\r,samples=80] {1-cos(deg(x*3))};
\addplot[mark=*] coordinates {(\t,0)};
\addplot[mark=*] coordinates {(\r,2)};
\end{axis}
\end{tikzpicture}
Рис. 0. КДПВ

Анімація в інтерфейсі робить наочним зміна його стану. Наприклад, при невдалій відправки довга форма прокручується неправильно заповненого поля. Чи збільшується по натисненню фотографія розсовує навколишні елементи.

Без анімації складніше сприймати різкі і раптові зміни. Разом з тим анімація повинна бути короткою і ненав'язливою, щоб не заважати користувачеві.

Анімація здається природною, коли повторює звичний рух предметів навколишнього світу. Під катом я розповім, як робив анімацію на основі фізичних законів. Дивіться готовий результат на демо-сторінці (там один блок слід за іншим при русі миші).


Згадуємо фізику
Переміщення об'єктів описується зміною координат x з плином часу t. Якщо ви спробуєте підібрати функцію x(t) «на око», ви витратите багато часу, домагаючись плавного та природного руху. Що вибрати? Гіперболу? Параболу? Куди її перемістити? Як повернути?

За прикладами руху краще всього звернутися до предметів навколишнього світу. Математичний закон їх руху диктується фізикою. Толкнем брусок, що лежить на столі. Він проходить певну відстань, зупиняючись під дією сили тертя. В хорошому наближенні сила сухого тертя ковзання постійна, і залежність x(t) виявляється параболою. Таке уповільнення можна використовувати, якщо в початковий момент об'єкт анімації вже рухався.

\begin{tikzpicture}
\def\t{0}
\def\r{3.4}
\begin{axis}[width=10cm,height=7cm,
ticks=none,
xmin=-0, xmax=3.8,
axis y line=left,axis x line=bottom,
xlabel=$t$,ylabel=$x$, 
every axis x label/.style={at={(current axis.south east)},anchor=south},
every y axis label/.style={at={(current axis.north west)},anchor=west},
enlargelimits=true,mark size=1
]
\addplot[smooth,blue,domain=\t\r,samples=80]{-(x-\r)^2} node[pos=0.75,black,anchor=south east,inner sep=2pt]{$x=A+Bt+Ct^2$};
\addplot[dashed,domain=\r-0.7:\r,samples=2]{0};
\addplot[mark=*] coordinates {(\t-\r*\r)};
\addplot[mark=*,green!50!black] coordinates {(\r,0)} node[pin=-90:{\scriptsize{\text{плавна зупинка :)}}}]{};
\end{axis}
\end{tikzpicture}
Рис. 1. Гальмування сухим тертям по параболі

Сила в'язкого тертя пропорційна швидкості руху тіла. У такому разі тіло рухатиметься до точки зупинки по експоненті за нескінченно великий час. Якщо експоненту спотворити, щоб обмежити час руху, така анімація буде здаватися неприродною. З-за труднощів з зупинкою в розумний час не слід використовувати модель в'язкого тертя, тільки якщо симуляція самого в'язкого тертя не є метою.

\begin{tikzpicture}
\def\t{0}
\def\r{3.8}
\begin{axis}[width=10cm,height=7cm,
ticks=none,
xmin=-0, xmax=3.8,
axis y line=left,axis x line=bottom,
xlabel=$t$,ylabel=$x$, 
every axis x label/.style={at={(current axis.south east)},anchor=south},
every y axis label/.style={at={(current axis.north west)},anchor=west},
enlargelimits=true,mark size=1
]
\addplot[smooth,blue,domain=\t\r,samples=80] {1-exp(-x*1.0)} node[pos=0.45,black,anchor=south east,inner sep=2pt]{$x=A-Be^{-\alpha t}$};
\addplot[dashed,domain=\t\r,samples=2]{1};
\addplot[mark=*] coordinates {(\t,0)};
\addplot[red!80!black] coordinates {(3.4,1)} node[pin=-90:{\scriptsize{\text{не зупиняється :(}}}]{} ;
\end{axis}
\end{tikzpicture}
Рис. 2. Гальмування по експоненті в в'язкому середовищі

Відхилений від положення рівноваги маятник (або грузик на пружині) плавно набирає швидкість, проходить положення рівноваги і плавно гальмує. Потім рух повторюється у зворотний бік, і так до нескінченності (якщо тертя немає). Графік такого руху — синусоїда. Періодичний повтор нам не особливо цікавий, а от рух маятника між крайніми точками виходить плавним і природним.

\begin{tikzpicture}
\def\t{0}
\def\r{3.1415}
\begin{axis}[width=10cm,height=7cm,
ticks=none,mark size=1,
xmin=-0.5, xmax=3.6,
axis y line=left,axis x line=bottom,
xlabel=$t$,ylabel=$x$, 
every axis x label/.style={at={(current axis.south east)},anchor=south},
every y axis label/.style={at={(current axis.north west)},anchor=west},
enlargelimits=true
]
\addplot[smooth,blue,domain=\t\r,samples=80] {1-cos(deg(x))} node[pos=0.52,black,anchor=south east,inner sep=2pt]{$x=A-B\cos\omega t$};
\addplot[dashed,domain=\t\t+0.6,samples=2] {1-cos(deg(\t))};
\addplot[dashed,domain=\r-0.6:\r,samples=2] {1-cos(deg(\r))};
\addplot[mark=*,green!50!black] coordinates {(\t,0)} node[pin=90:{\scriptsize{\text{плавний запуск :)}}}]{};
\addplot[mark=*,green!50!black] coordinates {(\r,2)} node[pin=-90:{\scriptsize{\text{\quad плавна зупинка :)}}}]{};
\end{axis}
\end{tikzpicture}
Рис. 3. Рух маятника по синусоїді між крайніми точками

В JS-бібліотеках і CSS є заготовки easing-функцій для створення спеціальних ефектів. Майже всі заготовки слід використовувати в спеціальних випадках, з обережністю. Тільки синусоїда більш-менш універсальні.

По-перше, синусоїдальна траєкторія переводить тіло з одного спочиваючого положення в інше. По-друге, тривалість такого руху дорівнює половині періоду. Рух обмежено у часі. Тривалість не залежить від зовнішніх обставин і початкових умов. Вона залежить тільки від властивостей самої системи і визначається співвідношенням жорсткості та інерційності.

Зазвичай я вибираю тривалість анімації по синусоїді в 200 мілісекунд. Така тривалість в декілька разів більше часу реакції людини. Анімація добре помітна, але не встигає дратувати.

Давайте навчимося проводити синусоїдальну траєкторію за початкових умов, часу руху і точки зупинки.

Як провести синусоїду через дві точки
Нехай тіло покоїться в початковий і кінцевий момент часу. Тоді дотичні до графіку в точках t1 і t2 горизонтальні, а сам графік — це напівперіод синусоїди.

\begin{tikzpicture}
\def\t{0}
\def\r{3.1415}
\begin{axis}[
ticks=none,
xmin=-1, xmax=4.5,
axis y line=left,axis x line=bottom,
xlabel=$t$,ylabel=$x$, 
every axis x label/.style={at={(current axis.south east)},anchor=south},
every y axis label/.style={at={(current axis.north west)},anchor=west},
enlargelimits=true
]
\addplot[smooth,blue,domain=\t\r,samples=80] 
{1-cos(deg(x))};
\addplot[dashed,domain=\t\t+1,samples=2] 
{1-cos(deg(\t))};
\addplot[dashed,domain=\r-1:\r,samples=2] 
{1-cos(deg(\r))};
\addplot[mark=*,mark size=1] coordinates {(\t,0)} node[pin=95:{$(t_1,x_1)$}]{} ;
\addplot[mark=*,mark size=1] coordinates {(\r,2)} node[pin=-85:{$(t_2,x_2)$}]{} ;
\end{axis}
\end{tikzpicture}
Рис. 4. Графік руху між двома положеннями спокою

Рівняння, що описує напівперіод синусоїди, легко підібрати:

x(t)=x_1+{x_2-x_1\over 2}\left[1 - \cos\left(\pi{t-t_1\over t_2-t_1}\right)\right].
Після закінчення однієї анімації ми можемо починати іншу знову за цією формулою. Але що робити, якщо нова анімація повинна початися, поки ще не закінчилася стара? Щоб забезпечити плавність руху, ми зупиняємо поточну анімацію (синя лінія) і починаємо нову анімацію (червона лінія) з ненульовою початковою швидкістю:

\begin{tikzpicture}
\def\tgnt{0.7}
\def\t{0}
\def\r{3.1415}
\def\tb{1}
\def\rb{\r+\tb}
\def\dx{1.27}
\begin{axis}[
ticks=none,
xmin=-1, xmax=4.9,
axis y line=left,axis x line=bottom,
xlabel=$t$,ylabel=$x$, 
every axis x label/.style={at={(current axis.south east)},anchor=south},
every y axis label/.style={at={(current axis.north west)},anchor=west},
enlargelimits=true,mark size=1
]
\addplot[smooth,blue,domain=\t\tb,samples=80] 
{-cos(deg(x))+1};
\addplot[smooth,dotted,blue,domain=\tb:\r,samples=10] 
{-cos(deg(x))+1};
\addplot[blue,dashed,
domain=\t\t+\tgnt,samples=2] 
{-cos(deg(\t))+1};
\addplot[smooth,thick,red,domain=\tb:\rb,samples=80] 
{-1.5*cos(deg(1+0.69*(x-\tb)))+\dx};
\addplot[dashed,red,domain=\tb-\tgnt:\tb+\tgnt,samples=2] 
{-1.5*cos(deg(\tb))+\dx+sin(deg(\tb))*(x-\tb)};
\addplot[dashed,red,domain=\rb-\tgnt:\rb,samples=2] 
{1.5+\dx};
\addplot[mark=*] coordinates {(\t,0)};
\addplot[mark=*] coordinates {(\tb,1-cos(deg(\tb)))} node[pin=-85:{$(t_1,x_1)$}]{} ;
\addplot[mark=*] coordinates {(\r,2)};
\addplot[mark=*] coordinates {(\rb,1.5+\dx)} node[pin=-85:{$(t_2,x_2)$}]{} ;
\end{axis}
\end{tikzpicture}
Рис. 5. Графік руху з ненульовою початковою швидкістю

Без математичних обчислень не вийде написати формулу, що відповідає червоній лінії. Давайте зробимо ці обчислення.

Сімейство всіх можливих синусоїд описується рівнянням

f(t)=A\cos\omega (t-t_2)+B\sin\omega (t-t_2)+C
з чотирма невідомими параметрами A, B, C і \omega>0. Я зрушив початок звіту часу в точку t2, щоб відразу позбутися від другого доданку. Дійсно, похідна f'(t_2)=B\omegaповинна бути нульовою, тому що дотична в точці t2 горизонтальна. Це можливо, коли B=0.

Так як f(t_2)=x_2, то підставляючи t=t_2в (1), отримуємо f(t_2)=A+C. Звідси виключаємо C:

f(t)=x_2 + A\left[\cos\omega (t-t_2)-1\right].
Продиференціюємо, щоб знайти швидкість

f'(t)=-A\,\omega\sin\omega (t-t_2).
Нам відомо положення x1 швидкість v у початковий момент часу:

\begin{cases}
x_1\!\!\!\!\!&=x_2+A\left[\cos\omega(t_1-t_2)-1\right],\\
v\!\!\!\!\!&=-A\,\omega\sin\omega(t_1-t_2).
\end{cases}
З цієї системи рівнянь потрібно знайти A і \omega. Пора вводити нову змінну k=\omega(t_2-t_1)замість \omega. Її сенс — різниця фаз синусоїди в початковій і кінцевій точці. Наприклад, для графіка на рис. 4 k=\pi, тому що на проміжку (t_1,t_2)укладається напівперіод синусоїди. На рис. 5 k<\pi, тому що t_2-t_1менше половини періоду.

Після підстановки і невеликих перетворень приходимо до системи

\begin{cases}
x_2-x_1&=A\left(1-\cos k\right),\\
v(t_2-t_1)\!\!\!\!&=A\k\sin k.
\end{cases}
Розділимо почленно перше рівняння на друге:

{x_2-x_1\over v(t_2-t_1)}={1-\cos k\over k\sin k}\quad
\Rightarrow\quad{1-\cos k\over\sin k}=\alpha k,\quad\text{де}
\ \alpha={x_2-x_1\over v(t_2-t_1)}.
Параметр \alphaу правій частині відомий заздалегідь. Він визначає необхідний характер руху. Якщо \alpha\gg1, то початкова швидкість мала, тіло спочатку має прискоритися. Якщо \alpha\ll1початкова швидкість велика, тіло повинно сповільнюватися.

Тригонометричні функції в лівій частині зводяться до тангенсу половинного кута. В результаті у нас нелінійне рівняння відносно k:

\text{tg}\,{k\over2}=\alpha k.
Проаналізувати його рішення можна на графіку. Намалюємо графік лівої і правої частини при деяких значеннях параметра \alpha:

\begin{tikzpicture}\small
\def\aa{1.5}
\def\ab{0.3}
\def\ac{-0.5}
\begin{axis}[legend pos=south east,mark size=1,samples=2,
restrict y to domain=-8:8,
width=12cm, height=250pt,
xmin=-10.5, xmax=10.5,
ytick={-6,-3,...,6},
xtick={-9.4247,-3.1416,...,10},
xticklabels={$-{3\pi}$,$-{\pi}$,${\pi}$,${3\pi}$},
axis x line=center,
axis y line=center,
xlabel=$k$]
\addplot[blue!70!black,domain=-9.4247:9.4247,semithick,samples=802]{tan(deg(x/2))};
\addplot[red]{\aa*x};
\addplot[green!70!black,domain=-9.4247:9.4247]{\ab*x};
\addplot[olive,domain=-9.4247:9.4247]{\ac*x};
\addplot[mark=*] coordinates {(2.65,3.97)} node[anchor=west]{$A$};
\addplot[mark=*] coordinates {(8.69,2.61)} node[anchor=west]{$B$};
\addplot[mark=*] coordinates {(4.06,-2.03)} node[anchor=west]{$C$};
\legend{$\text{tg}\,k/2$,$\aa\,k$,$\ab\,k$,$\ac\,k$}
\end{axis}
\end{tikzpicture}
Рис. 6. Графічне рішення рівняння (2)

Обговоримо отримані рішення.
  1. Розглянемо точку A. Це рішення існує при \alpha>1/2і відповідає зображеному на малюнку 5: \begin{tikzpicture}
\def\t{1}
\def\r{3.1415}
\begin{axis}[width=1.9 cm,height=2cm,hide axis,ticks=none,
xmin=\t,xmax=\r,mark size=0.3]
\addplot[smooth,blue,domain=\t\r,samples=80] {-cos(deg(x))};
\addplot[mark=*] coordinates {(\t-cos(deg(x)))};
\addplot[mark=*] coordinates {(\r,-cos(deg(x)))};
\end{axis}
\end{tikzpicture}. Як очікувалося, k<\pi. В межі нульової швидкості \alpha\to\infty, червона пряма збігається з віссю ординат, точка A піде по тангенсоиде в нескінченність. У цьому межі k\to\pi. Поки все йде правильно.
  2. Точка C відповідає значенню \alpha<0. Таке трапляється, коли тіло в перший момент часу рухається вперед, а треба рухатися назад. Тепер \pi<k<2\pi. Рух описується фрагментом синусоїди, більшим ніж напівперіод, але меншим, ніж період:
    \begin{tikzpicture}
\def\t{-1.7}
\def\r{3.1415}
\begin{axis}[width=2.2 cm,height=2cm,hide axis,ticks=none,
xmin=\t,xmax=\r,mark size=0.3]
\addplot[smooth,blue,domain=\t\r,samples=80] {cos(deg(x))};
\addplot[mark=*] coordinates {(\t,cos(deg(x)))};
\addplot[mark=*] coordinates {(\r cos(deg(x)))};
\end{axis}
\end{tikzpicture}. Тіло гальмує, зупиняється, рухається назад і зупиняється в необхідному місці.
  3. З графіка видно, що при 0<\alpha<1/2точка B потрапляє в діапазон 2\pi<k<3\pi. Тіло пройде по синусоїді більше, ніж повний період коливань: \begin{tikzpicture}
\def\t{-1.6}
\def\r{2*3.1415}
\begin{axis}[width=2.5 cm,height=2cm,hide axis,ticks=none,
xmin=\t,xmax=\r,mark size=0.3]
\addplot[smooth,blue,domain=\t\r,samples=80] {cos(deg(x))};
\addplot[mark=*] coordinates {(\t,cos(deg(x)))};
\addplot[mark=*] coordinates {(\r cos(deg(x)))};
\end{axis}
\end{tikzpicture}. Причина такого дивного рішення в тому, що точка зупинки знаходиться занадто близько порівняно з характерним відстанню v (t2t1). Тому провести синусоїду без додаткової зупинки і повернення не вийде.
Про квантову механікуСхожі на (2) рівняння виникають при вирішенні задач квантової механіки про рівні енергії частинок в прямокутних потенціальних ямах. Там доводиться зшивати, наприклад, синусоїду і експоненту. Умова відсутності зламів дає подібні рівняння з нескінченною кількістю коренів.

Наближене рішення
Ми вирішили задачку проведення синусоїди, але життя від цього легше не стало. По-перше, щоб визначити параметри синусоїди, треба розв'язати нелінійне рівняння (2). Привіт, ітераційні методи! По-друге, рівняння має нескінченну кількість розв'язків, а потрібне рішення не завжди існує.

Ці труднощі виникли від того, що ми зафіксували тривалість анімації рівно 200 мілісекунд. Проте нічого страшного не трапиться, якщо анімація триватиме, скажімо, 180 мілісекунд. Або навіть 250 мілісекунд. Нам важливіше зупинка на заданому місці, а точної тривалістю анімації ми пожертвуємо для спрощення розрахунків.

Послабивши вимоги на тривалість анімації, ми проробимо такий трюк. Припустимо, що у нас є наближене рішення нелінійного рівняння (2). Воно є точним розв'язком рівняння з іншим параметром

\alpha'={1\over k'}\,\text{tg}{k'\over 2},
Йому відповідає інший час закінчення анімації:

t_2'=t_1+{x_2-x_1\over v\alpha'}.
Тепер невідомі параметри траєкторії A і \omegaелементарно виражаються через <img src=«tex.s2cms.ru/svg/%5Calpha' alt=»\alpha'"/>.

Я підібрав підходить для наших цілей наближення до рівняння (2):

{1\over 2\alpha}\approx1-\left({k\over\pi}\right)^2.
Синя суцільна лінія відповідає точному рівнянню (2), а червона пунктирна — його наближення:

\begin{tikzpicture}\small
\begin{axis}[legend pos=south east,
restrict y to domain=-8:8,
width=12cm,
xmin=-7.3, xmax=7.3,
ytick={-6,-3,...,6},
xtick={-6.2832,-3.1416,...,10},
xticklabels={$-{2\pi}$,$-{\pi}$,$0$,${\pi}$,${2\pi}$},
axis x line=center,y axis line=center,
xlabel=$k$,ylabel=$\alpha$]
\addplot[smooth,samples=580,blue!70!black,domain=-7:7]{tan(deg(x/2))/x};
\addplot[smooth,samples=580,red,dashed,domain=-7:7]{0.5/(1 - (x/pi)^2)};
\legend{$(1/k)\,\text{tg}\,k/2$,$0.5/\!\left[1 - ({k/\pi})^2\right]$}
\end{axis}
\end{tikzpicture}
Рис. 7. Порівняння точного співвідношення (2) і його наближення

А ще в випадку 0&amp;lt;\alpha&amp;lt;1/2пропоную взяти <img src=«tex.s2cms.ru/svg/%5Calpha' alt=»\alpha'"/> трохи більше, ніж 1/2, і скоротити час анімації, щоб уникнути відскоку і повернення.

Застосування
Код та сферичний приклад використання є на демо-сторінці. Поводите мишею і подивіться, як чорний блок слід за помаранчевим.

Описана схема застосовується і в готовому продукті. Я розробив її для синхронної прокрутки джерела та попереднього перегляду markdown — і latex — редактор математичних текстів.

Ідею і первинну реалізацію знайшов на демо-сторінці js-парсера markdown-it. В їх варіанті анімація вийшла рваною і подтормаживающей. Тому є кілька причин:
  1. Для анімації застосовується лінійна функція:
    $(...).stop(true).animate({scrollTop: ...}, 100, 'linear')
    . Замість гладкого графіка виходить ламана.
  2. Анімація через
    jQuery().stop().animate()
    гальмує порівняно з
    requestAnimationFrame()
    .
  3. Щоб уникнути падіння продуктивності, «проковтуються» події
    onscroll
    , наступні частіше ніж 50 мілісекунд. В моєму варіанті такої проблеми немає. Послідовні події
    onscroll
    коригують положення точки зупинки і не уповільнюють анімацію.
Щоб домогтися важливою для продукту якісної анімації, я пропрацював метод обчислення на основі фізичних рівнянь, і реалізував його через спеціальний браузерний метод
requestAnimationFrame()
. Метод добре працює при будь прокрутці: клавішами page up/page down, через переміщення смуг прокручування, коліщатком миші, тачпад, тачскрін.

Оригінал поста і картинокЦей пост я набирав у згаданому редакторі. Викладаю исходникможе кому-небудь знадобиться tex-код графіків.
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.