1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

[JavaScript] arcTo の終点の座標を計算する

Last updated at Posted at 2025-09-10

CanvasRenderingContext2DPath2D はパスの終点の座標を取得する機能がないため、別途計算する必要があります。

参考「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,
);
1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?