在前端项目中,旋转一个元素我们会使用CSS的rotate
函数,本文会让你对rotate
有一个全新的认识。
向量
二维中间中的向量其实就是一个包含了两个数值的数组,一个是 x 坐标值,一个是 y 坐标值。
向量既可以表示一个 “点”(x, y),也可以表示一个从原点(0,0)到坐标点(x, y)的 “线” 称为 “向量”。
向量的旋转
向量的旋转指的是将一个已知向量旋转给定的弧度后得到一个旋转后的新向量,也就是说我们使用 “向量” 和 “弧度” 求出旋转后的“新向量”。我们通过一段伪代码来描述:
/** * v1: 原始向量 * v2: 旋转后的新向量 * rad: 旋转弧度 */ const v2 = v1.rotate(rad);
若想实现一个 rotate
函数来旋转向量,“矩阵的线性变换” 可以帮助我们做到这一点。
▐ 矩阵的线性变换
当一个矩阵和一个向量做乘法时,将产生一个新的向量(可以理解为:向量和矩阵做乘法描述了一个运动),矩阵描述了一个二维空间如何变换(旋转、拉伸等),向量相当于一个入参。
这与函数类似,当一个矩阵描述了二维空间逆时针旋转90°,那么任意一个向量与该矩阵做乘法时,都会得到一个逆时针旋转了90°的新向量。
上面公式中,左侧的 2x2
的矩阵描述了二维空间如何变换,右侧的 (x, y)
是一个向量,整体计算过程给出了矩阵和向量通过乘法得到一个新的向量。
- 基向量
为什么 2x2
的矩阵可以描述二维空间如何变换?这个问题很关键,因此这里单独用一个小节展开介绍一下。
空间中的任意线性变换都可以由“基向量”的变换来表示,而基向量的数量与维度保持一致,比如二维空间中,基向量就是两个被称为:i
帽和 j
帽的向量。这两个基向量都有各自的x
坐标(1, 0)和y
坐标(0, 1),因此加起来就是一个 2x2
的矩阵:
左列表示 i
帽,右列表示 j
帽,上面的数字为 x
坐标,下面的数字为 y
坐标,空间中的表示如下图所示:
如上图所示,向量 v
是由基向量 i
帽和 j
帽相加得到的,因此当 i
帽和 j
帽的坐标发生变化时,向量 v
就会发生变化,比如逆时针旋转90°,那么矩阵值为:
此时 2x2
矩阵值为:
由于整个空间是线性变化的,因此基向量的变化等同于该空间下任意向量、坐标、图形的变化。
举个例子:空间中任意一个向量 v→(3, 2)
,与上面的矩阵相乘,将得到一个全新的逆时针旋转90°后的新向量:
- 小结
回到我们的主题,就是说,当我们拥有矩阵和向量时,我们就可以得到变换后的新向量。向量是我们已知的,矩阵我们暂时还没有,但我们有一个弧度值,所以我们要做一件事,就是将弧度转化为矩阵。然后再用这个矩阵和向量做乘法得出变换后的新向量。
▐ 将弧度值转化为矩阵
本节介绍一种通过弧度值来计算旋转后基向量 i
帽与 j
帽坐标的方法。
- 基向量
i
帽旋转后的坐标
旋转前,基向量 i
帽的坐标为:(1, 0),旋转后 i
帽的坐标是:(cosθ, sinθ)。
向量 a
在向量 b
上的投影等于两个向量夹角的余弦值乘以向量 a
的长度,反过来 b
在 a
上的投影就是余弦值乘以向量 b
的长度。
在我们的场景里就是旋转后 i
帽和旋转前 i
帽夹角的余弦值乘以 i
帽的长度,而由于 i
帽的长度为 1,因此余弦值就是我们要的 i
帽旋转后在 x
轴的坐标。
至此,我们得到了旋转后 i
帽的 x
坐标:cosθ
。
旋转后的 i
帽在 y
轴的位置,我们可以用夹角的正弦值来计算。用夹角与边长来求出三角形的第三条边,而这第三条边的长度就是旋转后 i
帽在 y
轴的位置,如下图所示:
如上图所示,边 d
等于旋转后的 i
帽在 y
轴的位置等于 sinθ
,我们用一个简单的推导步骤来证明:
sinθ = 对边 / 斜边 已知斜边边长为 1(i帽长度为1,旋转后长度不变) sinθ = d / 1 d / 1 = sinθ d = sinθ * 1 d = sinθ
至此,我们得出旋转后 i
帽的 y
坐标:sinθ
,同时我们也得到了旋转后 i
帽的完整坐标:
向量旋转的代码实现
通过上一节详细的介绍,我们产出了一个公式:
最左侧的 2x2
矩阵是使用弧度计算出来的,与右边的向量做乘法,得出最后的一个新的向量,代码中直接使用最后一步生产出来的那个向量即可,代码如下:
/** * @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 ]; }