CSS
JavaScript

JavaScript で CSS の cubic bezier

JavaScript でアニメーションを書くときに CSS の timing-function で使う cubic-bezier っぽい関数がほしいので作ってみます。

こういう雰囲気のやつです。

See the Pen cubic bezier 1 by hoo (@hoo-chan) on CodePen.


ベジエ曲線の式

Wikipedia のページ から引用します。


bezier.png



3 次のベジエ曲線の式

今考えている曲線の制御点は 4 つなので、定義の式に $N=4$ を代入して展開します。

\def\v#1{{\mathbf #1}}

\begin{align}
\v{P}(t)
&=\sum_{i=0}^{3}\v{B}_{i}J_{3,i}\tag{1}\\
&=\v{B}_{0}J_{3,0}+\v{B}_{1}J_{3,1}+\v{B}_{2}J_{3,2}+\v{B}_{3}J_{3,3}\tag{2}\\
&=\v{B}_{0}\binom{3}{0}t^{0}(1-t)^{3}+\v{B}_{1}\binom{3}{1}t^{1}(1-t)^{2}+\v{B}_{2}\binom{3}{2}t^{2}(1-t)^{1}+\v{B}_{3}\binom{3}{3}t^{3}(1-t)^{0}\tag{3}\\
&=\v{B}_{0}(1-t)^{3}+\v{B}_{1}3t(1-t)^{2}+\v{B}_{2}3t^{2}(1-t)+\v{B}_{3}t^{3}\tag{4}\\
&=\v{B}_{0}(1-3t+3t^{2}-t^{3})+\v{B}_{1}(3t-6t^{2}+3t^{3})+\v{B}_{2}(3t^{2}-3t^{3})+\v{B}_{3}t^{3}\tag{5}\\
&=(-\v{B}_{0}+3\v{B}_{1}-3\v{B}_{2}+\v{B}_{3})t^{3}+(3\v{B}_{0}-6\v{B}_{1}+3\v{B}_{2})t^{2}+(-3\v{B}_{0}+3\v{B}_{1})t-\v{B}_{0}\tag{6}
\end{align}

$t$ の式が得られました。今回の目的はアニメーションの制御なので点は 2 つの要素で書けます。

\begin{align}

\v{P}(t)=\begin{bmatrix}X(t)\\Y(t)\end{bmatrix},\:
\v{B}_{i}=\begin{bmatrix}X_{i}\\Y_{i}\end{bmatrix}\:(i=0,1,2,3)\tag{7}
\end{align}

$(7)$ を使って $(6)$ を書き換えると $(8)$ になります。

\begin{align}

\begin{bmatrix}X(t)\\Y(t)\end{bmatrix}=
\begin{bmatrix}
(-X_{0}+3X_{1}-3X_{2}+X_{3})t^{3}+(3X_{0}-6X_{1}+3X_{2})t^{2}+(-3X_{0}+3X_{1})t-X_{0}\\
(-Y_{0}+3Y_{1}-3Y_{2}+Y_{3})t^{3}+(3Y_{0}-6Y_{1}+3Y_{2})t^{2}+(-3Y_{0}+3Y_{1})t-Y_{0}
\end{bmatrix}\tag{8}
\end{align}

CSS の cubic bezier を思い出すと、最初と最後の制御点は固定です。ここで値を代入します。

\begin{align}

\begin{bmatrix}X_{0}\\Y_{0}\end{bmatrix}=\begin{bmatrix}0\\0\end{bmatrix},\:
\begin{bmatrix}X_{3}\\Y_{3}\end{bmatrix}=\begin{bmatrix}1\\1\end{bmatrix}\tag{9}
\end{align}

$(9)$ によって $(8)$ はこうなります。

\begin{align}

\begin{bmatrix}X(t)\\Y(t)\end{bmatrix}=
\begin{bmatrix}
(3X_{1}-3X_{2}+1)t^{3}+(-6X_{1}+3X_{2})t^{2}+3X_{1}t\\
(3Y_{1}-3Y_{2}+1)t^{3}+(-6Y_{1}+3Y_{2})t^{2}+3Y_{1}t
\end{bmatrix}\tag{10}
\end{align}

これを描いてみたのが冒頭のやつです。


x の関数にする

$(10)$ で $X(t)$ と $Y(t)$ が得られましたが、目的は CSS の cubic-bezier なので、ほしいのは $x$ を渡すと 1 つの $y$ が返ってくる関数($y=f(x)$)です。


3 次方程式を解く

欲しい関数は $f(x)=Y(X^{-1}(x))$ ですから $X^{-1}(x)$ が求まれば OK です。そのためには $t$ の 3 次方程式を解きます。

\begin{align}

x&=(3X_{1}-3X_{2}+1)t^{3}+(-6X_{1}+3X_{2})t^{2}+3X_{1}t\tag{11}\\
\end{align}

$(11)$ を整理すると一般的な 3 次方程式になります(なってしまいます)。

\begin{align}

t^{3}+At^{2}+Bt+C&=0\tag{12}
\end{align}

時間がないのでここから先はやめました。


テーブルをつくる

解くのは諦めてテーブルを作ってそこから求めるようにします。getCoordinate が $(10)$ の実装です。

function generateCubicBezier(x1, y1, x2, y2, step) {

const table = generateTable(x1, x2, step);
const tableSize = table.length;
cubicBezier.getT = getT;
cubicBezier.table = table;
return cubicBezier;
function cubicBezier(x) {
if (x <= 0) {
return 0;
}
if (1 <= x) {
return 1;
}
return getCoordinate(y1, y2, getT(x));
}
function getT(x) {
let xt1, xt0;
if (x < 0.5) {
for (let i = 1; i < tableSize; i++) {
xt1 = table[i];
if (x <= xt1[0]) {
xt0 = table[i - 1];
break;
}
}
} else {
for (let i = tableSize - 1; i--;) {
xt1 = table[i];
if (xt1[0] <= x) {
xt0 = table[i + 1];
break;
}
}
}
return xt0[1] + (x - xt0[0]) * (xt1[1] - xt0[1]) / (xt1[0] - xt0[0]);
}
function getCoordinate(z1, z2, t) {
return (3 * z1 - 3 * z2 + 1) * t * t * t + (-6 * z1 + 3 * z2) * t * t + 3 * z1 * t;
}
function generateTable(x1, x2, step) {
step = step || 1 / 30;
const table = [[0, 0]];
for (let t = step, previousX = 0; t < 1; t += step) {
const x = getCoordinate(x1, x2, t);
if (previousX < x) {
table.push([x, t]);
previousX = x;
}
}
table.push([1, 1]);
return table;
}
}

動作確認はこちら。

See the Pen cubic bezier 2 by hoo (@hoo-chan) on CodePen.

お行儀の悪いパラメータにするとこうなっちゃいますが、そういうパラメータにしなければ大丈夫です。

negative.png

例えば ease-in-out がほしいときは (0.42, 0.00, 0.58, 1.00) を渡します。

const easeInOut = generateCubicBezier(0.42, 0, 0.58, 1);

実際に使ってみた例はこちら:https://qiita.com/hoo-chan/items/398cfc8514c0f1cd984d