Gojabako ZoneKei Ito

ベジエ曲線の描画

に公開に更新)履歴 (4)

別の記事でアニメーションをつけるのにベジエ曲線が必要だったので、つくりがてら記事にすることにしました。

ベジエ曲線はいくつかの制御点から得られる曲線で、制御点の数によって3つなら2次ベジエ曲線、4つなら3次ベジエ曲線などと呼ばれます。私がほしかったのは3次なのでこれ以降は3次限定です。

## 3次ベジエ曲線

3次ベジエ曲線は4つの制御点からなります。それぞれ位置を B0,B1,B2,B3 とすると曲線上の点 P(t) は次式で表されます。

#math1
P(t)=(1t)3B0+3(1t)2tB1+3(1t)t2B2+t3B3(1)

ただし 0t1 です。P(0)B0P(1)B3 になります。

(t: number) => [number, number] を返す getCubicBezierFunction は次のように書けます。

#code1
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) を確認できます。

制御点はマウスまたは矢印キーで移動できます。t は下のスライダーで変更できます。#app1
cubicBezierの動作確認アプリケーション
t = 0.40

これで (B0,B1,B2,B3,t) から (x,y) が求められるようになりましたが、私がほしかったのは CSS の cubic-bezier(0.42,0,0.58,1) のように使えるもので、以下のようにアニメーションの進み方を指定するものです。

cubic-bezier(0.42, 0, 0.58, 1) は最初と最後がゆっくり動きます。#app2
timingFunctionの動作確認アプリケーション

x が時間で、曲線との交点の y 座標がアニメーションの進捗になっています。つまり、ほしいのは (B0,B1,B2,B3,x) から y を求める関数です。

B0=(0,0)B3=(1,1) は固定なので、(x(t),y(t)) は次のようになります。

#math2
x(t)y(t)=3(1t)2tx1+3(1t)t2x2+t3=3(1t)2ty1+3(1t)t2y2+t3(2)(3)

これをy について解こうとすると x が一意に定まらないケースがあって困ります。仕方がないので N 個の t(x,y) を求めておいて、内分点で近似することにしました。

#code2
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 が小さければ気にならないですね。以下では NT を変更できるようにしているので試してみてください。

制御点はマウスまたは矢印キーで移動できます。#app3
timingFunctionの動作確認アプリケーション
T = 2000 ms
N = 15

<easing-function> の ease, ease-in, ease-out, ease-in-out は次のように書けます。

#code3
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]);

以上です。