CanvasRenderingContext2D
や Path2D
はパスの終点の座標を取得する機能がないため、別途計算する必要があります。
参考「CanvasRenderingContext2D - Web APIs | MDN」
参考「Path2D - Web APIs | MDN」
参考「CanvasRenderingContext2D: arcTo() method - Web APIs | MDN」
1. ベクトル
制御点を
\begin{align}
P_0&(x_0, y_0) \\
P_1&(x_1, y_1) \\
P_2&(x_2, y_2) \\
\end{align}
とし、終点を
L(x, y)
とします。
ベクトル
\begin{align}
\vec{\mathstrut u} &= \overrightarrow{\mathstrut P_1 P_0} \\
&= (x_0 - x_1, y_0 - y_1) \\
\vec{\mathstrut v} &= \overrightarrow{\mathstrut P_1 P_2} \\
&= (x_2 - x_1, y_2 - y_1) \\
\vec{\mathstrut t} &= \overrightarrow{\mathstrut P_1 L} \\
&= (x - x_1, y - y_1) \\
\end{align}
を定義します。
ベクトルの長さは
\begin{align}
\left| \vec{\mathstrut u} \right| &= \sqrt{(x_0 - x_1)^2 + (y_0 - y_1)^2} \\
\left| \vec{\mathstrut v} \right| &= \sqrt{(x_2 - x_1)^2 + (y_2 - y_1)^2} \\
\end{align}
になります。
2. ベクトルのなす角
ベクトルの内積
\begin{align}
\vec{\mathstrut u} \cdot \vec{\mathstrut v} &= \left| \vec{\mathstrut u} \right| \left| \vec{\mathstrut v} \right| \cos\theta \\
&= (x_0 - x_1) (x_2 - x_1) + (y_0 - y_1) (y_2 - y_1) \\
\end{align}
より
\cos\theta = \dfrac {\vec{\mathstrut u} \cdot \vec{\mathstrut v}} {\left| \vec{\mathstrut u} \right| \left| \vec{\mathstrut v} \right|}
からベクトルのなす角を求められます。
3. 半角
3.1. 平方根を用いて計算する場合
$\tan \dfrac \theta 2 > 0$ のとき、三角関数の半角の公式
\tan^2 \dfrac \theta 2 = \dfrac{1 - \cos\theta}{1 + \cos\theta}
より
\tan \dfrac \theta 2 = \sqrt{ \dfrac{1 - \cos\theta}{1 + \cos\theta} }
になります。
3.2. 逆三角関数を用いて計算する場合
逆三角関数を用いると
\tan \dfrac \theta 2 = \tan \dfrac {\arccos(\cos\theta)} 2
になります。
4. 終点
$\cos\theta \ne \pm 1$ のとき、2 本の直線に接する円の半径を $r$ とすると、ベクトル $\vec{\mathstrut t}$ の長さは
\begin{align}
\left| \vec{\mathstrut t} \right| &= \dfrac r {\tan \dfrac \theta 2} \\
&= r \sqrt{ \dfrac{1 + \cos\theta}{1 - \cos\theta} } \\
&= \dfrac r {\tan \dfrac {\arccos(\cos\theta)} 2} \\
\end{align}
となり、
\vec{\mathstrut t} = \left| \vec{\mathstrut t} \right| \dfrac {\vec{\mathstrut v}} {\left| \vec{\mathstrut v} \right|}
になります。
$\cos\theta = \pm 1$ のとき、制御点 $P_0, P_1, P_2$ は直線状に並び、2 本の直線に接する円は存在しないため、終点 $L$ を制御点 $P_1$ とします。
終点 $L$ の座標は
(x, y) =
\begin{cases}
(x_1, y_1), &\text{if $\cos\theta = \pm 1$} \\
(x_1, y_1) + \vec{\mathstrut t}, &\text{otherwise} \\
\end{cases}
になります。
5. コード
5.1. 半角の計算方法の選択
実際に JavaScript で計算すると逆三角関数を用いた方が誤差が小さそうなため、逆三角関数を選択します。
const f = theta => {
const c = Math.cos(2 * theta);
console.log(
Math.tan(theta),
Math.tan(Math.acos(c) / 2),
Math.sqrt((1 - c) / (1 + c)),
c,
);
};
f(0.01 * Math.PI);
f(0.49 * Math.PI);
f(0.25 * Math.PI);
0.03142626604335115 0.03142626604335117 0.03142626604335117 0.9980267284282716
31.820515953773853 31.820515953773853 31.820515953773935 -0.9980267284282716
0.9999999999999999 0.9999999999999999 0.9999999999999999 6.123233995736766e-17
5.2. 終点の計算
//
const Renderer = class {
#canvas;
context;
get canvas() {
return this.#canvas;
}
init({ width = 300, height = 150 } = {}) {
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
const context = canvas.getContext('2d');
//
this.#canvas = canvas;
this.context = context;
}
};
//
const magnitude = v => Math.sqrt(v.x * v.x + v.y * v.y);
const dot = (u, v) => u.x * v.x + u.y * v.y;
const getLastPoint = (points, radius) => {
const u = { x: points[0].x - points[1].x, y: points[0].y - points[1].y };
const v = { x: points[2].x - points[1].x, y: points[2].y - points[1].y };
const mu = magnitude(u);
const mv = magnitude(v);
const c = dot(u, v) / (mu * mv);
if ( 1 + c < Number.EPSILON || 1 - c < Number.EPSILON ) {
return points[1];
} else {
const mt = radius / Math.tan(Math.acos(c) / 2);
const t = { x: mt * v.x / mv, y: mt * v.y / mv };
return { x: points[1].x + t.x, y: points[1].y + t.y };
}
};
//
const f = (renderer, points, radius) => {
//
const path = new Path2D();
path.moveTo(points[0].x, points[0].y);
path.arcTo(
points[1].x, points[1].y,
points[2].x, points[2].y,
radius,
);
renderer.context.stroke(path);
//
const { x, y } = getLastPoint(points, radius);
const pathLastPoint = new Path2D();
pathLastPoint.arc(x, y, 4, 0, 2 * Math.PI);
renderer.context.fill(pathLastPoint);
};
//
const renderer = new Renderer();
renderer.init({ width: 360, height: 360 });
renderer.context.lineWidth = 3;
document.body.appendChild(renderer.canvas);
//
f(
renderer,
[
{ x: 60, y: 60 },
{ x: 300, y: 180 },
{ x: 60, y: 300 },
],
100,
);