ベジエ曲線の描画
別の記事でアニメーションをつけるのにベジエ曲線が必要だったので、つくりがてら記事にすることにしました。
ベジエ曲線はいくつかの制御点から得られる曲線で、制御点の数によって3つなら2次ベジエ曲線、4つなら3次ベジエ曲線などと呼ばれます。私がほしかったのは3次なのでこれ以降は3次限定です。
## 3次ベジエ曲線
3次ベジエ曲線は4つの制御点からなります。それぞれ位置を B0,B1,B2,B3 とすると曲線上の点 P(t) は次式で表されます。
ただし 0≤t≤1 です。P(0) は B0 で P(1) は B3 になります。
(t: number) => [number, number]
を返す getCubicBezierFunction
は次のように書けます。
1type Point = [number, number];2const getCubicBezierFunction = (3 [x0, y0]: Point,4 [x1, y1]: Point,5 [x2, y2]: Point,6 [x3, y3]: Point,7) => (t1: number): Point => {8 const t2 = t1 ** 2, t3 = t1 ** 3;9 const u1 = 1 - t1, u2 = u1 ** 2, u3 = u1 ** 3;10 return [11 u3 * x0 + 3 * u2 * t1 * x1 + 3 * u1 * t2 * x2 + t3 * x3,12 u3 * y0 + 3 * u2 * t1 * y1 + 3 * u1 * t2 * y2 + t3 * y3,13 ];14};
次のサンプルは上記の関数を使っており、 t に対応する (x,y) を確認できます。
これで (B0,B1,B2,B3,t) から (x,y) が求められるようになりましたが、私がほしかったのは
CSS の cubic-bezier(0.42,0,0.58,1)
のように使えるもので、以下のようにアニメーションの進み方を指定するものです。
x が時間で、曲線との交点の y 座標がアニメーションの進捗になっています。つまり、ほしいのは (B0,B1,B2,B3,x) から y を求める関数です。
B0=(0,0) と B3=(1,1) は固定なので、(x(t),y(t)) は次のようになります。
これをy について解こうとすると x が一意に定まらないケースがあって困ります。仕方がないので N 個の t で (x,y) を求めておいて、内分点で近似することにしました。
1type Point = [number, number];2const getTimingFunction = (p1: Point, p2: Point, N = 20) => {3 const samples: Array<Point> = [4 [0, 0], // p05 ...(function* () {6 const bezier = getCubicBezierFunction([0, 0], p1, p2, [1, 1]);7 const step = 1 / N;8 for (let t = step; t < 1; t += step) {9 yield bezier(t);10 }11 })(),12 [1, 1], // p313 ];14 return (x: number): number => {15 if (x <= 0) {16 return 0; // p0[0]17 }18 if (1 <= x) {19 return 1; // p3[0]20 }21 let index = samples.findIndex((point) => x < point[0]);22 const [x1, y1] = samples[index - 1];23 const [x2, y2] = samples[index];24 const r = (x - x1) / (x2 - x1);25 return y1 * (1 - r) + y2 * r;26 };27};
ちょうどいい N はいくつなのかという問題ですが、N=20で良さそうでした。N=10 だと両端の動きにぎこちなさがありますが、アニメーションの周期 T が小さければ気にならないですね。以下では N と T を変更できるようにしているので試してみてください。
<easing-function>
の ease, ease-in, ease-out, ease-in-out は次のように書けます。
1const ease = getTimingFunction([0.25, 0.1], [0.25, 1.0]);2const easeIn = getTimingFunction([0.42, 0.0], [1.00, 1.0]);3const easeOut = getTimingFunction([0.00, 0.0], [0.58, 1.0]);4const easeInOut = getTimingFunction([0.42, 0.0], [0.58, 1.0]);
以上です。