VBAで楕円の円弧を描画

ExcelのVBAで作図する際に、**楕円の円弧のパラメーター**をどう設定すればいいのかよく忘れてしまうので、ここにまとめておく。

正しく設定できれば、3Dっぽい図も自信を持って描ける。

![sphere.png](https://qiita-image-store.s3.amazonaws.com/0/127130/ffa4517c-6b7e-842f-2ce4-5cda93acf1b7.png)

## ポイント

* Excelの座標は**左手系**
	* x軸は右向きが正、y軸は下向きが正
	* その結果、角度は時計回りが正
* オブジェクト作成時に指定する座標値は、楕円弧の右上1/4についての値
	* 作成後は弧の範囲によって変化
* 楕円弧の両端を表す角度は、**楕円中心から見た角度**
	* 初期値（右上1/4）は、-90° から 0° までの時計回り
	* 図形を伸縮したときに、異なる変形をしたような違和感

## 基本的な描き方
[`Shapes.AddShape`](https://msdn.microsoft.com/ja-jp/library/office/ff821384.aspx) メソッドを使い、種類には `msoShapeArc` を指定すると、**楕円弧の右上1/4**を描ける。座標値には開始点（左上）の位置と幅・高さを指定する。
描き終わったら [`Shape.Adjustments`](https://msdn.microsoft.com/ja-jp/library/office/ff197295.aspx) プロパティの値を書き換え、弧の開始と終了の角度を指定する。これはGUIで黄色いハンドルを操作するのと同じ。 

```vb
Sub DrawEllipticArc(left, top, width, height)
	Set shp = ActiveSheet.Shapes.AddShape(msoShapeArc, left, top, width, height)

	' 以下は楕円弧の描画範囲を変更する。デフォルトは-90°～0°
	With shp.Adjustments
		.Item(1) = -90
		.Item(2) = 0
	End With
End Sub
```

![ellipse_1.png](https://qiita-image-store.s3.amazonaws.com/0/127130/2293baf7-d3cc-ed67-d54b-2dd0df218848.png)

Excelの座標はディスプレイと同様で、左上が(0,0)で、x軸は右向きが正、**y軸は下向きが正**になっている。角度はx軸正方向を基準にしてy軸正方向に回ると増えていく。そのため弧の右上1/4というのは-90°～0°ということになる。

`AddShape` に指定した座標値はあくまで弧の右上1/4を描く段階のもので、作成した図形の座標値は弧の描画範囲によって変化する。試した感じだと、GUIが囲む領域（楕円全体）とは一致せず、弧と中心をちょうど含む矩形領域としての値に見える。

### 指定値を変更して描画
個人的には開始点の位置よりも楕円の中心を指定できると嬉しい。また、幅や高さにマイナスを指定するとエラーとなるが、図形を反転させてくれれば都合がいいこともある。これらに対応させると以下のコードになる。

```vb
' (x0,y0)を中心とした半径rx,ryの楕円弧を描く
Sub DrawEllipticArc(x0, y0, rx, ry)
	ra = Abs(rx)
	rb = Abs(ry)
	Set shp = ActiveSheet.Shapes.AddShape(msoShapeArc, x0, y0 - rb, ra, rb)
	If rx < 0 Then shp.Flip msoFlipHorizontal
	If ry < 0 Then shp.Flip msoFlipVertical

	' 以下は楕円弧の描画範囲を変更する。デフォルトは-90°～0°
	With shp.Adjustments
		.Item(1) = -90
		.Item(2) = 0
	End With
End Sub
```

![ellipse_2.png](https://qiita-image-store.s3.amazonaws.com/0/127130/5f2107c1-1ee7-a20a-c863-bfb9f0792313.png)

### （参考）楕円/弦/パイなどの場合
楕円形を描く一般的な図形では、`AddShape` に指定する座標値が楕円全体に対する値である。そのため幅や高さは楕円の半径の2倍となる。

```vb
' (x0,y0)を中心とした半径rx,ryのパイを描く
Sub DrawPie(x0, y0, rx, ry)
	ra = Abs(rx)
	rb = Abs(ry)
	Set shp = ActiveSheet.Shapes.AddShape(msoShapePie, x0 - ra, y0 - rb, ra * 2, rb * 2)
	If rx < 0 Then shp.Flip msoFlipHorizontal
	If ry < 0 Then shp.Flip msoFlipVertical

	' 以下はパイの描画範囲を変更する。デフォルトは0°～270°（=-90°）
	With shp.Adjustments
		.Item(1) = 0
		.Item(2) = 270
	End With
End Sub
```

![pie_1.png](https://qiita-image-store.s3.amazonaws.com/0/127130/115988c7-d653-bbae-5b98-0d61377bd831.png)

これらの図形は楕円弧と異なり、描画範囲の角度をどう変更したとしても楕円全体でみた座標値が使われ続けるらしい。

## 楕円弧を伸縮
図形は幅や高さを変更することにより伸縮できる。しかし楕円弧などに対しては、この操作は恐らく直感に合わない変形を起こしてしまう。

例えば、真円の-60°～+30°部分を描いた図形を縦に潰してみる。下の絵で左側の変形が起きることを期待すると思うが、実際は右側のようになる。

![scaling_arc.png](https://qiita-image-store.s3.amazonaws.com/0/127130/9d35413e-cdb5-b5c7-7653-9bcf474e9cc7.png)

Excelの場合、楕円弧上での角度は**楕円の中心から見た角度**を表す。右側（現実）の伸縮の様子をよく見てみると、楕円の中心から見た角度は変わっていないことが分かる（弧を「塗り潰し」しておくと分かりやすい）。逆に左側（理想）の伸縮をするにはこの角度を計算し直さなければならない。

```vb
' 楕円弧arcを横にsx倍・縦にsy倍の倍率で伸縮する
Sub ScaleArc(arc, sx, sy)
	arc.ScaleWidth  sx, msoFalse, msoScaleFromMiddle
	arc.ScaleHeight sy, msoFalse, msoScaleFromMiddle
	With arc.Adjustments
		.Item(1) = ConvertArcAngle(.Item(1), sx, sy)
		.Item(2) = ConvertArcAngle(.Item(2), sx, sy)
	End With
End Sub

Function ConvertArcAngle(psi, sx, sy)
	psi1 = WorksheetFunction.Radians(psi)
	psi2 = WorksheetFunction.Atan2(sx * Cos(psi1), sy * Sin(psi1))
	ConvertArcAngle = WorksheetFunction.Degrees(psi2)
End Function
```

<!--
2: 楕円弧を線形変換
当初は剪断変形を扱おうとしたが、導出の手間が同じだったので汎用的なこちらにした。数式で表すと以下の通り。（行列式 $\Delta = ad - bc$ はゼロでないとする。）

```math
\left(
\begin{matrix}
x' \\ y'
\end{matrix}
\right)
=
\left(
\begin{matrix}
a & b \\ c & d
\end{matrix}
\right)
\left(
\begin{matrix}
x \\ y
\end{matrix}
\right)
=
\left(
\begin{matrix}
ax+by \\ cx+dy
\end{matrix}
\right)
```

線形変換は、長方形を伸縮・回転・反転などして平行四辺形に変形できる。**楕円の場合は変換後も楕円になる**ので、パラメーターさえ計算できればExcelでも変換を再現できる。

```vb
Sub LinearMapArc(arc, a, b, c, d)
	rx = arc.Width
	ry = arc.Height

	val1 = Sqr((a * rx + d * ry) ^ 2 + (b * ry - c * rx) ^ 2)
	val2 = Sqr((a * rx - d * ry) ^ 2 + (b * ry + c * rx) ^ 2)
	val3 = a * c * rx ^ 2 + b * d * ry ^ 2
	sx = (val1 + val2) / 2 / rx
	sy = (val1 - val2) / 2 / ry
	theta = WorksheetFunction.Degrees(Asin(2 * val3 / val1 / val2) / 2)

	arc.ScaleWidth  sx, msoFalse, msoScaleFromMiddle
	arc.ScaleHeight sy, msoFalse, msoScaleFromMiddle
	With arc.Adjustments
		.Item(1) = ConvertArcAngle(.Item(1), a, b, c, d, theta)
		.Item(2) = ConvertArcAngle(.Item(2), a, b, c, d, theta)
	End With
	arc.IncrementRotation theta
End Sub

Function ConvertArcAngle(psi, a, b, c, d, theta)
	psi1 = WorksheetFunction.Radians(psi)
	x = Cos(psi1)
	y = Sin(psi1)
	psi2 = Atn2(a * x + b * y, c * x + d * y)
	ConvertArcAngle = WorksheetFunction.Degrees(psi2) - theta
End Function
```
-->