CGAffineTransformを知る

#はじめに

SwiftにはCGAffineTransformという構造体が存在します。2Dグラフィックの描画に使われる、アフィン変換を表します。

アフィン変換を知らなくても、変更したい値を渡せばスケーリング、回転、移動を行うCGAffineTransformを作成することができて、デフォルトにリセットできる機能があります。

CGAffineTransformはUIViewに存在するtransformというPropertyを用いて、Viewに対して変更を加えます。

```swift:
var transform: CGAffineTransform { get set }
```



これはアフィン変換を知らないけど、CGAffineTransformを使っていた自分が、CGAffineTransformの仕組みを理解するための記事になります。



#アフィン変換とは

> 幾何学の分野で、ある図形を回転させたり引き延ばしたりする変換をアフィン変換と呼ぶ。
もう少しきちんと説明すると、「アフィン変換とは平行移動と線形変換を組み合わせた変換」のこと。

具体的には、線形変換（拡大縮小、剪断、回転）、平行移動があり、これらの組み合わせで表現されます。

```math
\begin{bmatrix}
a & b & 0 \\
c & d & 0 \\
t_{x} & t_{y} & 1 \\
\end{bmatrix}
```


CGAffineTransformで使用するアフィン変換は、上記の3行3列の行列式で表されます。

3列目は常に(0,0,1)ですので、CGAffineTransformのデータ構造には最初の2列のみの値が含まれます。

ここからはframeが`CGRect(x: 0, y: 0, width: 50, height: 50)`のUIViewに対して、どうやって計算しているかを見ていきます。

<img width="300" alt="スクリーンショット 2019-12-21 16.33.36.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/300780/8cb5a971-404c-b7a4-e611-9ba3ac2eb433.png">

・本来はSwiftにはanchorPointというPropertyで回転など幾何学的変更における中心点が決まっていて、デフォルトでは中央なのですが、今回は左上を設定しています。


##rotation

CGAffineTransformでは

```swift:
init(rotationAngle angle: CGFloat)
```

となっています、angleをラジアンで指定すると

iOSでは、正の値は反時計回りの回転を指定し、負の値は時計回りの回転を指定します。
macOSでは、正の値は時計回りの回転を指定し、負の値は反時計回りの回転を指定します。

angle = 40を指定した場合

<img width="300" alt="スクリーンショット 2019-12-21 23.56.00.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/300780/a558d93d-1f60-d496-7f52-5175beed8047.png">


このようにcenterを軸に40度反時計回りに回転します

回転するためのアフィン変換の行列はこのようになっており、受け取ったangleを元にsin,cosを割り出し、各値が決定します。

```math
\begin{bmatrix}
\cos a & \sin a & 0 \\
- \sin a & \cos a & 0 \\
0 & 0 & 1 \\
\end{bmatrix}
```

計算としては、angleを受け取ったことによって決定した上記の値を回転の計算式に当てはめます。

変換後のx座標 = x座標 x cosθ - y座標 x sinθ + tx
変換後のy座標 = x座標 x sinθ + y座標 x cosθ + ty

を行なっています。

上記の式を元に各座標を求めることができます。

##scale

CGAffineTransformでは

```swift:
init(scaleX sx: CGFloat, y sy: CGFloat)
```

となっています、scaleX, Yを指定するとViewはそれぞれx,y方向に指定したScaleの倍率分拡大されます。

sx: 2, sy: 1を指定した場合

<img width="300" alt="スクリーンショット 2019-12-22 2.55.03.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/300780/eb68ac82-c114-8809-dcc9-10e7aeb9a970.png">


このように指定した方向に値の倍率分拡大します。

スケーリングするためのアフィン変換の行列はこのようになっており、受け取ったscaleX, Yを元に各値が決定します。

```math
\begin{bmatrix}
s_{x} & 0 & 0 \\
0 & s_{y} & 0 \\
0 & 0 & 1 \\
\end{bmatrix}
```


計算としては、受け取ったscale値をそのまま座標に乗算します

変換後のx座標 = x座標 x ScaleX
変換後のy座標 = y座標 x ScaleY

を行なっています。

上記の式を元に各座標を求めることができます。


##translation

CGAffineTransformでは

```swift:
init(translationX tx: CGFloat, y ty: CGFloat)
```

となっています、translationX,Yを指定するとViewはそれぞれx,y方向に指定したtranslationの分移動します。
tx: 50, ty: 30を指定した場合

<img width="300" alt="スクリーンショット 2019-12-22 3.14.50.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/300780/b1f0cb75-0b5a-1d3a-470b-949d8b2290a2.png">

このように指定した方向に値の分移動します。

スケーリングするためのアフィン変換の行列はこのようになっており、受け取ったtranslationX,Yを元に各値が決定します。

```math
\begin{bmatrix}
1 & 0 & 0 \\
0 & 1 & 0 \\
t_{x} & s_{y} & 1 \\
\end{bmatrix}
```


計算としては、受け取ったtranslation値をそのまま座標に加算します

変換後のx座標 = x座標 + translationX
変換後のy座標 = y座標 + translationY

上記の式を元に各座標を求めることができます。

#おまけ

CGAffineTransformには

```swift:
init(a: CGFloat, b: CGFloat, c: CGFloat, d: CGFloat, tx: CGFloat, ty: CGFloat)
```
というinitが存在して、今までのいずれかのアフィン変換の行列を指定してあげれば、これからでも操作を行うことが可能です。

試しに40度の回転をしてみます。回転の行列はこうなので、

```math
\begin{bmatrix}
\cos a & \sin a & 0 \\
- \sin a & \cos a & 0 \\
0 & 0 & 1 \\
\end{bmatrix}
```


```swift:
import UIKit

class ViewController: UIViewController {
	
	@IBOutlet weak var affineView: UIView! {
		didSet {
			affineView.layer.anchorPoint = .init(x: 0, y: 0)
			affineView.frame = .init(x: 0, y: 0, width: 50, height: 50)
			
			let angle: CGFloat = 40.0
			
			let radian = (angle * (CGFloat.pi / 180))
			
			let affineSin = sin(radian)
			let affineCos = cos(radian)
			
			let affine = CGAffineTransform(a: affineCos,
										   b: affineSin,
										   c: -affineSin,
										   d: affineCos,
										   tx: 0,
										   ty: 0)

			affineView.transform = affine
		}
	}

	
	override func viewDidLoad() {
		super.viewDidLoad()
	}
}


```
このように同じ行列に指定してあげると、この方法でも`init(rotationAngle angle: CGFloat)`と結果を得ることができます。



#最後に

本来はデフォルトである、中心を軸にした説明を行っていないので、今後加筆していきたいと思います。

ありがとうございました。

#参考リンク

[CGAffineTransform](https://developer.apple.com/documentation/coregraphics/cgaffinetransform)

[アフィン変換とは](http://zellij.hatenablog.com/entry/20120523/p1)