二维空间下的向量旋转
# 二维空间下的向量旋转

向量运算是计算机图形学的数学基础，而向量的旋转是向量的一种常见操作，本文将详细讲解向量在二维空间下的旋转原理。

> 在前端项目中，旋转一个元素我们会使用CSS的 `rotate` 函数，本文会让你对 `rotate` 有一个全新的认识。

## 1. 向量

二维中间中的向量其实就是一个包含了两个数值的数组，一个是 x 坐标值，一个是 y 坐标值。

<div style="text-align:center;"><img src="https://gw.alicdn.com/imgextra/i1/O1CN01zwz2J81cAWlFrG73d_!!6000000003560-2-tps-500-500.png" width="300"></div>

向量既可以表示一个 **“点”**（x, y），也可以表示一个从原点（0,0）到坐标点（x, y）的 **“线”** 称为 “向量”。

## 2. 向量的旋转

向量的旋转指的是将一个已知向量旋转给定的弧度后得到一个旋转后的新向量，也就是说我们使用 “向量” 和 “弧度” 求出旋转后的“新向量”。我们通过一段伪代码来描述：

```javascript
/**
 * v1: 原始向量
 * v2: 旋转后的新向量
 * rad: 旋转弧度
 */
const v2 = v1.rotate(rad);
```

若想实现一个 `rotate` 函数来旋转向量，**“矩阵的线性变换”** 可以帮助我们做到这一点。

### 2.1 矩阵的线性变换

当一个矩阵和一个向量做乘法时，将产生一个新的向量（可以理解为：向量和矩阵做乘法描述了一个运动），矩阵描述了一个二维空间如何变换（旋转、拉伸等），向量相当于一个入参。

这与函数类似，当一个矩阵描述了二维空间逆时针旋转90°，那么任意一个向量与该矩阵做乘法时，都会得到一个逆时针旋转了90°的新向量。

$$
\begin{pmatrix}
1 & 2 \\
3 & 4 \\
\end{pmatrix}
\times
\begin{pmatrix}
x \\
y \\
\end{pmatrix}
=x\
\begin{pmatrix}
1 \\
3 \\
\end{pmatrix}
+\
y\
\begin{pmatrix}
2 \\
4 \\
\end{pmatrix}
=\
\begin{pmatrix}
1x + 2y \\
3x + 4y \\
\end{pmatrix}
$$


上面公式中，左侧的 `2x2` 的矩阵描述了二维空间如何变换，右侧的 `(x, y)` 是一个向量，整体计算过程给出了矩阵和向量通过乘法得到一个新的向量。

#### 2.1.1 基向量

为什么 `2x2` 的矩阵可以描述二维空间如何变换？这个问题很关键，因此这里单独用一个小节展开介绍一下。

空间中的任意线性变换都可以由“基向量”的变换来表示，而基向量的数量与维度保持一致，比如二维空间中，基向量就是两个被称为：`i` 帽和 `j` 帽的向量。这两个基向量都有各自的`x`坐标（1, 0）和`y`坐标（0, 1），因此加起来就是一个 `2x2` 的矩阵：

$$
\begin{pmatrix}
1 & 0 \\
0 & 1 \\
\end{pmatrix}
$$

左列表示 `i` 帽，右列表示 `j` 帽，上面的数字为 `x` 坐标，下面的数字为 `y` 坐标，空间中的表示如下图所示：

<div style="text-align:center;"><img src="https://gw.alicdn.com/imgextra/i2/O1CN0136RGnH1zN6o2N2Rdd_!!6000000006701-2-tps-500-500.png" width="300"></div>

如上图所示，向量 `v` 是由基向量 `i` 帽和 `j` 帽相加得到的，因此当 `i` 帽和 `j` 帽的坐标发生变化时，向量 `v` 就会发生变化，比如逆时针旋转90°，那么矩阵值为：

此时 `2x2` 矩阵值为：

$$
\begin{pmatrix}
0 & -1 \\
1 & 0 \\
\end{pmatrix}
$$

空间中的表示如下图所示：

<div style="text-align:center;"><img src="https://gw.alicdn.com/imgextra/i1/O1CN01WCr4By1zeVr1wSddS_!!6000000006739-2-tps-500-500.png" width="300"></div>

由于整个空间是线性变化的，因此基向量的变化等同于该空间下任意向量、坐标、图形的变化。

举个例子🌰：空间中任意一个向量 `v→(3, 2)`，与上面的矩阵相乘，将得到一个全新的逆时针旋转90°后的新向量：

$$
\begin{pmatrix}
0 & -1 \\
1 & 0 \\
\end{pmatrix}
\times
\begin{pmatrix}
3 \\
2 \\
\end{pmatrix}
=3\
\begin{pmatrix}
0 \\
1 \\
\end{pmatrix}
+\
2\
\begin{pmatrix}
-1 \\
0 \\
\end{pmatrix}
=\
\begin{pmatrix}
3 * 0 + 2 * -1 \\
3 * 1 + 2 * 0 \\
\end{pmatrix}
=\
\begin{pmatrix}
-2 \\
3 \\
\end{pmatrix}
$$

空间中的表示如下图所示：

<div style="text-align:center;"><img src="https://gw.alicdn.com/imgextra/i2/O1CN01cyPpH01eiXP8r8YgE_!!6000000003905-2-tps-500-500.png" width="300"></div>

#### 2.1.2 小结

回到我们的主题，就是说，当我们拥有矩阵和向量时，我们就可以得到变换后的新向量。向量是我们已知的，矩阵我们暂时还没有，但我们有一个弧度值，所以我们要做一件事，就是将弧度转化为矩阵。然后再用这个矩阵和向量做乘法得出变换后的新向量。

### 2.2 将弧度值转化为矩阵

本节介绍一种通过弧度值来计算旋转后基向量 `i` 帽与 `j` 帽坐标的方法。

#### 2.2.1 基向量 `i` 帽旋转后的坐标

旋转前，基向量 `i` 帽的坐标为：（1, 0），旋转后 `i` 帽的坐标是：（cosθ, sinθ）。

$$
旋转前 i 帽的坐标：\
\begin{pmatrix}
1 \\
0 \\
\end{pmatrix}
\ \ \ \ \ \ \ \ \ \ 
旋转后 i 帽的坐标：\
\begin{pmatrix}
cosθ \\
sinθ \\
\end{pmatrix}
$$

计算 `i` 帽旋转后的坐标，本质上是计算旋转后的 `i` 帽在 `x` 轴的位置以及在 `y` 轴的位置。

旋转后的 `i` 帽在 `x` 轴的位置等同于旋转后的 `i` 帽在旋转前 `i` 帽上的投影，如下图所示：

<div style="text-align:center;"><img src="https://gw.alicdn.com/imgextra/i2/O1CN013Wasax1ywXpcVkDwa_!!6000000006643-2-tps-500-500.png" width="300"></div>

向量 `a` 在向量 `b` 上的投影等于两个向量夹角的余弦值乘以向量 `a` 的长度，反过来 `b` 在 `a` 上的投影就是余弦值乘以向量 `b` 的长度。 

在我们的场景里就是旋转后 `i` 帽和旋转前 `i` 帽夹角的余弦值乘以 `i` 帽的长度，而由于 `i` 帽的长度为 1，因此余弦值就是我们要的 `i` 帽旋转后在 `x` 轴的坐标。

至此，我们得到了旋转后 `i` 帽的 `x` 坐标：`cosθ`。

旋转后的 `i` 帽在 `y` 轴的位置，我们可以用夹角的正弦值来计算。用夹角与边长来求出三角形的第三条边，而这第三条边的长度就是旋转后 `i` 帽在 `y` 轴的位置，如下图所示：

<div style="text-align:center;"><img src="https://gw.alicdn.com/imgextra/i3/O1CN01lnn1571koO9wP1VNV_!!6000000004730-2-tps-500-500.png" width="300"></div>

如上图所示，边 `d` 等于旋转后的 `i` 帽在 `y` 轴的位置等于 `sinθ`，我们用一个简单的推导步骤来证明：

```
sinθ = 对边 / 斜边
已知斜边边长为 1（i帽长度为1，旋转后长度不变）
sinθ = d / 1
d / 1 = sinθ
d = sinθ * 1
d = sinθ
```

至此，我们得出旋转后 `i` 帽的 `y` 坐标：`sinθ`，同时我们也得到了旋转后 `i` 帽的完整坐标：

$$
\begin{pmatrix}
cosθ \\
sinθ \\
\end{pmatrix}
$$

#### 2.2.2 基向量 `j` 帽旋转后的坐标

获取 `j` 帽旋转后的坐标方法与获取 `i` 帽一样，仍然是通过夹角的正弦值与余弦值来获取坐标。旋转前，基向量 `j` 帽的坐标为：（0, 1），旋转后 `j` 帽的坐标是：（-sinθ, cosθ）：

$$
旋转前 j 帽的坐标：\
\begin{pmatrix}
0 \\
1 \\
\end{pmatrix}
\ \ \ \ \ \ \ \ \ \ 
旋转后 j 帽的坐标：\
\begin{pmatrix}
-sinθ \\
cosθ \\
\end{pmatrix}
$$

由于旋转后的 `j` 帽在旋转前 `j` 帽上的投影是投在了 `y` 轴，因此 cosθ 得到的是旋转后 `j` 帽在 `y` 轴的位置。同理，sinθ 则是旋转后 `j` 帽在 `x` 轴的位置，由于在 `x` 轴小于0的位置，因此需要给位置（sinθ）加一个负号：`-sinθ`，如下图所示：

<div style="text-align:center;"><img src="https://gw.alicdn.com/imgextra/i1/O1CN01rCVEwG1enZlHLvMY9_!!6000000003916-2-tps-500-500.png" width="300"></div>

至此，我们得到了旋转后 `j` 帽的完整坐标：

$$
\begin{pmatrix}
-sinθ \\
cosθ \\
\end{pmatrix}
$$

#### 2.2.3 小结

通过本小节的详细介绍，我们得知了一种通过弧度值来计算旋转矩阵（旋转后基向量 `i` 帽与 `j` 帽坐标）的方法，旋转矩阵如下：

$$
\begin{pmatrix}
cosθ & -sinθ \\
sinθ & cosθ \\
\end{pmatrix}
$$
### 2.3 小结

向量的旋转指的是将一个已知向量旋转给定的弧度或角度后得到一个旋转后的新向量。矩阵描述了一个二维空间如何变换，当一个矩阵和一个向量做乘法时，将产生一个新的向量。而这个描述了空间如何变换的矩阵可以通过弧度计算得出。

至此，我们已经掌握了向量旋转的理论知识，一个完整的向量旋转矩阵计算如下所示：

$$
\begin{pmatrix}
cosθ & -sinθ \\
sinθ & cosθ \\
\end{pmatrix}
\times
\begin{pmatrix}
x \\
y \\
\end{pmatrix}
=x\
\begin{pmatrix}
cosθ \\
sinθ \\
\end{pmatrix}
+\
y\
\begin{pmatrix}
-sinθ \\
cosθ \\
\end{pmatrix}
=\
\begin{pmatrix}
x * cosθ + y * -sinθ \\
x * sinθ + y * cosθ \\
\end{pmatrix}
$$

## 3. 向量旋转的代码实现

通过上一节详细的介绍，我们产出了一个公式：

$$
\begin{pmatrix}
cosθ & -sinθ \\
sinθ & cosθ \\
\end{pmatrix}
\times
\begin{pmatrix}
x \\
y \\
\end{pmatrix}
=x\
\begin{pmatrix}
cosθ \\
sinθ \\
\end{pmatrix}
+\
y\
\begin{pmatrix}
-sinθ \\
cosθ \\
\end{pmatrix}
=\
\begin{pmatrix}
x * cosθ + y * -sinθ \\
x * sinθ + y * cosθ \\
\end{pmatrix}
$$

最左侧的 `2x2` 矩阵是使用弧度计算出来的，与右边的向量做乘法，得出最后的一个新的向量，代码中直接使用最后一步生产出来的那个向量即可，代码如下：

```javascript
/**
 * @param {[x, y]} v 原始向量
 * @param {number} rad 旋转弧度
 * @return {[x, y]} 旋转后的新向量
 */
function rotate(v, rad) {
  const c = Math.cos(rad);
  const s = Math.sin(rad);
  const [x, y] = v;
  
  return [
  	x * c + y * -s,
  	x * s + y * c
  ];
}
```

## 4. Demo展示：基于向量旋转实现的一棵树

[![image](https://user-images.githubusercontent.com/3739368/179392140-f942d441-3ca9-45c7-abc4-f41bc6264af7.png)](https://codepen.io/berwin-the-typescripter/pen/qBoRpEE)

> Demo designed by 月影