3次ベジエ曲線とアニメーション・タイミング関数

SVG (SMIL):
開始 終了 ease linear ease-in ease-out ease-in-out
SVG (SMIL):
circle ellipse rect. round rect.
2021/08/16 山田 泰司

はじめに

CSS アニメーションなしに Javascript でアニメーションを実現する際、CSS アニメーションで使われるタイミング関数である「3次ベジエ曲線」の逆関数が必要になる。すなわち、後述の2次元の3次方程式 \(B_C(\tau)\) において、アニメーションの時間軸に対応する \(x\) 軸における解 \(\tau = {B_C}_x^{-1}(x)\) を求め、そのパラメータ \(\tau\) から \(y\) 軸に対応するタイミング関数値 \(y = {B_C}_y(\tau)\) を挙動の効果に使うことになる。

文献「Javascript で cubic-bezier を計算する」のように、ニュートン法で3次方程式の解を求めても実用上十分なのであるが、面倒とは言えせっかく解の公式があるので、ここでは「3次方程式の解の導出」に倣い、ニュートン法なしに CSS アニメーション相当のタイミング関数を用意することを主眼とする。ついでに、ベジエ曲線だけでなく「ステップ関数」もタイミング関数に必要になるので、実際に動作しているそれらの数式についてもまとめておく。

✔︎ 3次ベジエ曲線・ステップ関数による CSS/Javascript アニメーション

それぞれ CSS アニメーション・タイミング関数(上のバー)と、互換の拙作 Javascript コードによるアニメーション・タイミング関数(下のバー)の挙動を比較できる。

SVG (SMIL):
ease linear ease-in ease-out ease-in-out cubic-bezier(.5 -.5 .5 1.5)
ease linear ease-in ease-out ease-in-out B(.5, -.5, .5, 1.5)

✔︎ cubic-bezier animation-timing-function

ease (CSS, Javascript: cubic_bezier(.25, .1, .25, 1))

linear (CSS, Javascript: cubic_bezier(0, 0, 1, 1))

steps(2000, jump-end) (CSS, Javascript: steps_jumps_end(2000))

ease-in (CSS, Javascript: cubic_bezier(.42, 0, 1, 1))

ease-out (CSS, Javascript: cubic_bezier(0, 0, .58, 1))

ease-in-out (CSS, Javascript: cubic_bezier(.42, 0, .58, 1))

cubic-bezier(0.2,-2,0.8,2) (CSS, Javascript)

cubic-bezier(0.1, -0.6, 0.2, 0) (CSS, Javascript)

cubic-bezier(0.5, -0.5, 0.5, 1.5) (CSS, Javascript)

✔︎ steps animation-timing-function

steps(5, start) (CSS, Javascript: steps_jump_start(5))

steps(5, jump-start) (CSS, Javascript: steps_jump_start(5))

steps(5, end) (CSS, Javascript: steps_jump_end(5))

steps(5, jump-end) (CSS, Javascript: steps_jump_end(5))

steps(5, jump-end) (CSS, Javascript: cubic_bezier(0, 0, 1, 1))

steps(5, jump-none) (CSS, Javascript: steps_jump_none(5))

steps(5, jump-both) (CSS, Javascript: steps_jump_both(5))

step-start (CSS, Javascript: steps_jump_start(1))

step-end (CSS, Javascript: steps_jump_end(1))

✔︎ cubic bezier ~ steps animation-timing-function

cubic-bezier(1, 0, 0, 1) ~ cubic-bezier(1, 0, 0, 1) (CSS, Javascript: cubic_bezier(1, 0, 0, 1), cubic_bezier(1, 0, 0, 1))

cubic-bezier(0, 1, 1, 0) ~ cubic-bezier(0, 1, 1, 0) (CSS, Javascript: cubic_bezier(0, 1, 1, 0), cubic_bezier(0, 1, 1, 0))

cubic-bezier(1, 0, 0, 1) ~ steps(5, jump-end) (CSS, Javascript: cubic_bezier(1, 0, 0, 1), n_steps_jump_end(5))

steps(5, jump-start) ~ cubic-bezier(0, 1, 1, 0) (CSS, Javascript: n_steps_jump_start(5), cubic_bezier(1, 0, 0, 1))

cubic-bezier(1, 0, 0, 1) ~ steps(5, jump-both) (CSS, Javascript: cubic_bezier(1, 0, 0, 1), n_steps_jump_both(5))

steps(5, jump-none) ~ cubic-bezier(0, 1, 1, 0) (CSS, Javascript: n_steps_jump_none(5), cubic_bezier(1, 0, 0, 1))

cubic-bezier(1, 0, 0, 1) ~ step-end (CSS, Javascript: cubic_bezier(1, 0, 0, 1), n_steps_jump_end(1))

step-start ~ cubic-bezier(0, 1, 1, 0) (CSS, Javascript: n_steps_jump_start(1), cubic_bezier(1, 0, 0, 1))

執筆時点で SeaMonkey では steps アニメーション・タイミング関数は未サポートのようである。

CSS アニメーションと Javascript によるアニメーションの両者は、若干ずれるのは致し方ないとして、ほぼ似たような挙動を呈していることがわかる。さて、これらのタイミング関数がどのように定義されているか、数式で明確に示していこう。

✔︎ 3次ベジエ曲線・ステップ関数と時刻

The animation timing function \(T(t)\) with the time \(t\)

\[ T(t): t \mapsto y,\quad t = [0, 1], y \simeq [0, 1] \in \R \] where \[ [l, u] := \{x \in \R \mid l \le x \le u\} \]

The control points \(V_i\) and the coefficients \(A_i\)

\[ V_i, A_i \in \R^2,\quad i \in \N_0 \] \[ V_i = ({V_i}_x, {V_i}_y),\quad A_i = ({A_i}_x, {A_i}_y),\quad {V_i}_x, {V_i}_y, {A_i}_x, {A_i}_y \in \R \] \[ A_1 = 3V_1,\quad A_2 = 3(V_2 - V_1) - A_1,\quad A_3 = 1 - A_1 - A_2 \]

The cubic Bézier curve \(B_C(\tau)\) with the parameter \(\tau\)

\[ \begin{align*} P = B_C(\tau) &= A_3\tau^3 + A_2\tau^2 + A_1\tau \\ &= \begin{cases} {A_3}_x\tau^3 + {A_2}_x\tau^2 + {A_1}_x\tau = {B_C}_x(\tau) = P_x \\ {A_3}_y\tau^3 + {A_2}_y\tau^2 + {A_1}_y\tau = {B_C}_y(\tau) = P_y \end{cases} \end{align*} \] \[ B_C(\tau): \tau \mapsto P,\qquad P \in \R^2,\quad \tau \in \R \]

The inverse function of \(B_C(\tau)\) for \(P_x\) corresponds the time \(t\)

\[ t = P_x = {{B_C}_x}(\tau) = {A_3}_x\tau^3 + {A_2}_x\tau^2 + {A_1}_x\tau \] \[ \tau = {B_C}_x^{-1}(t) \]

The animation timing function cubic-bezier(p1, p2, p3, p4) \(= T_{B_C}(t)\)

\[ V_1 = ({V_1}_x, {V_1}_x) = (\text{p1}, \text{p2}),\qquad V_2 = ({V_2}_x, {V_2}_x) = (\text{p3}, \text{p4}) \] \[ T_{B_C}(t) = {B_C}_y({B_C}_x^{-1}(t)) \] where \[ \text{p1}, \text{p3}(= {V_1}_x, {V_2}_x) \in [0, 1] \]

The animation timing function steps(\(n\), jump-start) \(= T_{S_{\text{jump-start}}}(t)\)

\[ T_{S_{\text{jump-start}}}(n, t) = \min\left(t + \frac1n, 1\right) \]

The animation timing function steps(\(n\), jump-end) \(= T_{S_{\text{jump-end}}}(t)\)

\[ T_{S_{\text{jump-start}}}(n, t) = t \]

The animation timing function steps(\(n\), jump-none) \(= T_{S_{\text{jump-none}}}(t)\)

\[ T_{S_{\text{jump-none}}}(n, t) = \min\left(t + \frac{t}{n - 1}, 1\right) \]

The animation timing function steps(\(n\), jump-both) \(= T_{S_{\text{jump-both}}}(t)\)

\[ T_{S_{\text{jump-both}}}(n, t) = t - \frac{t - 1}{n + 1} \] where \[ \min(a, b) = \begin{cases} a,&\quad \text{if \(a \lt b\)} \\ b,&\quad \text{otherwise} \end{cases} \]

✔︎ ベジエ曲線

The \(n+1\) Bernstein basis polynomials of degree \(n\)

\[ b_{n,\nu}(x) = \dbinom{n}{\nu}x^{\nu}(1-x)^{n-\nu},\qquad x\in \R,\quad \nu = [0, n], n\in \N_0 \] \[ \begin{array}{llll} b_{0,0}(x) = 1 \\ b_{1,0}(x) = (1-x), & b_{1,1}(x) = x \\ b_{2,0}(x) = (1-x)^2, & b_{2,1}(x) = 2x(1-x), & b_{2,2}(x) = x^2 \\ b_{3,0}(x) = (1-x)^3, & b_{3,1}(x) = 3x(1-x)^2, & b_{3,2}(x) = 3x^2(1-x), & b_{3,3}(x) = x^3 \\ &\vdots \end{array} \]

The Bézier curve \(B_{n-1}(\tau)\) of degree \(n-1\) with the parameter \(\tau\)

\[ B(\tau) = \sum_{i=0}^{n-1}V_ib_{n-1,i}(\tau) \] \[ \begin{align*} B_0(\tau) &= V_0b_{0,0}(\tau),\quad &n=1 \\ B_1(\tau) &= V_0b_{1,0}(\tau) + V_1b_{1,1}(\tau),\quad &n=2 \\ B_2(\tau) &= V_0b_{2,0}(\tau) + V_1b_{2,1}(\tau) + V_2b_{2,2}(\tau),\quad &n=3 \\ B_3(\tau) &= V_0b_{3,0}(\tau) + V_1b_{3,1}(\tau) + V_2b_{3,2}(\tau) + V_3b_{3,3}(\tau),\quad &n=4 \\ &\vdots \end{align*} \] \[ \begin{array}{lllll} B_0(\tau) &= V_0 \\ B_1(\tau) &= V_0(1-\tau) &+ V_1\tau \\ B_2(\tau) &= V_0(1-\tau)^2 &+ V_1(2\tau(1-\tau)) &+ V_2\tau^2 \\ B_3(\tau) &= V_0(1-\tau)^3 &+ V_1(3\tau(1-\tau)^2) &+ V_2(3\tau^2(1-\tau)) &+ V_3\tau^3 \\ &\vdots \end{array} \]

The cubic Bézier curve \(B_3(\tau)\) with the control points \(V_0 = (0, 0)\) and \(V_3 = (1, 1)\)

\[ \begin{align*} B_3(\tau) &= V_1(3\tau(1-\tau)^2) + V_2(3\tau^2(1-\tau)) + \tau^3 \\ &= V_1(3\tau-6\tau^2+3\tau^3) + V_2(3\tau^2-3\tau^3) + \tau^3 \\ &= 3V_1\tau - 6V_1\tau^2 + 3V_1\tau^3 + 3V_2\tau^2 - 3V_2\tau^3 + \tau^3 \\ &= (3V_1 - 3V_2 + 1)\tau^3 - 3(2V_1 - V_2)\tau^2 + 3V_1\tau \\ &= A_3\tau^3 + A_2\tau^2 + A_1\tau = B_C(\tau) = P \end{align*} \] \[ \begin{array}{lll} A_1 &= 3V_1 \\ A_2 &= -3(2V_1 - V_2) &= 3(V_2 - V_1) - A_1 \\ A_3 &= 3V_1 - 3V_2 + 1 &= 1 - A_1 - A_2 \end{array} \]

✔︎ 3次方程式の解の導出

\(a = {A_3}_x\), \(b = {A_2}_x\), \(c = {A_1}_x\), \(d = -P_x\)

\[ ax^3 + bx^2 + cx + d = 0 \]

\(x = \chi + \varepsilon\)

\[ a(\chi + \varepsilon)^3 + b(\chi + \varepsilon)^2 + c(\chi + \varepsilon) + d = 0 \] \[ a(\chi^3 + 3\chi^2\varepsilon + 3\chi\varepsilon^2 + \varepsilon^3) + b(\chi^2 + 2\chi\varepsilon + \varepsilon^2) + c(\chi + \varepsilon) + d = 0 \] \[ a\chi^3 + 3a\chi^2\varepsilon + 3a\chi\varepsilon^2 + a\varepsilon^3 + b\chi^2 + 2b\chi\varepsilon + b\varepsilon^2 + c\chi + c\varepsilon + d = 0 \] \[ a\chi^3 + 3a\chi^2\varepsilon + b\chi^2 + 3a\chi\varepsilon^2 + 2b\chi\varepsilon + c\chi + a\varepsilon^3 + b\varepsilon^2 + c\varepsilon + d = 0 \] \[ a\chi^3 + \left(3a\varepsilon + b\right)\chi^2 + \left(3a\varepsilon^2 + 2b\varepsilon + c\right)\chi + a\varepsilon^3 + b\varepsilon^2 + c\varepsilon + d = 0 \]

ここで \(\varepsilon = -\frac{b}{3a}\) であれば \(\chi\) の2次の項が消せることがわかる。それを代入する。

\[ \begin{gather*} a\chi^3 + \left\{3a\left(-\frac{b}{3a}\right) + b\right\}\chi^2 + \left\{3a\left(-\frac{b}{3a}\right)^2 + 2b\left(-\frac{b}{3a}\right) + c\right\}\chi \\+ a\left(-\frac{b}{3a}\right)^3 + b\left(-\frac{b}{3a}\right)^2 + c\left(-\frac{b}{3a}\right) + d = 0 \end{gather*} \] \[ a\chi^3 + \left(\frac{b^2}{3a} - \frac{2b^2}{3a} + c\right)\chi - \frac{ab^3}{(3a)^3} + \frac{b^3}{(3a)^2} - \frac{bc}{3a} + d = 0 \] \[ a\chi^3 + \left(-\frac{b^2}{3a} + c\right)\chi + \frac{2b^3}{27a^2} - \frac{bc}{3a} + d = 0 \]

途中は省略するが、数値的には以下の手順で求めることができる。

\[ \begin{align*} p &= -\frac{b^2}{3a^2} + \frac{c}{a} \\ q &= \frac{2b^3}{27a^3} - \frac{bc}{3a^2} + \frac{d}{a} \\ D &= 81q^2 + 12p^3 \end{align*} \] \[ \begin{align*} u_0 &= \begin{cases} \dfrac{-9q +i\sqrt{-D}}{18}, &\text{if \(D \lt 0\)} \\ \dfrac{-9q + \sqrt{D} +i\sqrt{D}}{18}, &\text{otherwise} \end{cases} \\ v_0 &= \begin{cases} \dfrac{-9q -i\sqrt{-D}}{18}, &\text{if \(D \lt 0\)} \\ \dfrac{-9q - \sqrt{D} -i\sqrt{D}}{18}, &\text{otherwise} \end{cases} \end{align*} \]

if \(D \lt 0\) then

\[ z_u = \sqrt[6]{\re(u_0)^2 + \im(u_0)^2},\qquad z_v = \sqrt[6]{\re(v_0)^2 + \im(v_0)^2} \] \[ u = z_u\cos\frac{\angle{u_0}}{3} + iz_u\sin\frac{\angle{u_0}}{3},\qquad v = z_v\cos\frac{\angle{v_0}}{3} + iz_v\sin\frac{\angle{v_0}}{3} \]

otherwise

\[ u = \begin{cases} -\sqrt[3]{-\re(u_0)}, &\text{if \(u_0 \lt 0\)} \\ \sqrt[3]{ \re(u_0)}, &\text{otherwise} \end{cases},\qquad v = \begin{cases} -\sqrt[3]{-\re(v_0)}, &\text{if \(v_0 \lt 0\)} \\ \sqrt[3]{ \re(v_0)}, &\text{otherwise} \end{cases} \] \[ \omega_0 = \frac{-1 + i\sqrt{3}}{2},\qquad \omega_1 = \frac{-1 - i\sqrt{3}}{2} \] \[ \begin{align*} \chi_0 &= \re(u) + \re(v) + i(\im(u) + \im(v)) \\ \chi_1 &= \re(\omega_0)\re(u) - \im(\omega_0)\im(u) + \re(\omega_1)\re(v) - \im(\omega_1)\im(v)) \\ & + i(\im(\omega_0)\re(u) + \re(\omega_0)\im(u) + \im(\omega_1)\re(v) + \re(\omega_0)\im(v)) \\ \chi_2 &= \re(\omega_1)\re(u) - \im(\omega_1)\im(u) + \re(\omega_0)\re(v) - \im(\omega_0)\im(v) \\ & + i(\im(\omega_1)\re(u) + \re(\omega_1)\im(u) + \im(\omega_0)\re(v) + \re(\omega_0)\im(v)) \end{align*} \] \[ x_i = \chi_i - \frac{b}{3a} \]

✔︎ 3次方程式の判別式

実数解としてどの添字 \(i\) の解を選ぶべきかは判別式 \(D_0\), \(D_1\) からわかる。

\[ D_0 = b^2c^2 - 4ac^3 - 4b^3d - 27a^2d^2 + 18abcd,\quad D_1 = -2b^3 + 9abc - 27a^2d \] \[ i = \begin{cases} 0, &\text{\(D_0 \lt 0\) or \(D_1 = 0\) when \(D_0 = 0\)} \\ 1, &\text{\(D_1 \lt 0\) when \(D_0 = 0\) or \(D_1 \nless 0\) when \(D_0 = 0\)} \\ 2, &\text{otherwise \(D_0 \nless 0\)} \end{cases} \]

結論

以上の導出結果を用いて以下の Javascript による CSS 互換のアニメーション・タイミング関数を実装している。

参考文献は animation-timing-functions.js に記してある。

Written by Taiji Yamada at 2021/08/16