文章首发自本人博客 hcysun.me。javascript
前不久开源了一个插件化移动端运动效果库 finger-mover,说到运动效果,不得不提到CSS3的 transform
,也就是变换。这篇文章归纳了在实现 finger-mover 时对 transform
的理解与总结。css
注:文中的图片多数截取自视频:线性代数的本质,也强烈建议你们系统的观看这套视频。另外若是文中有误请不吝指教。html
文章结构以下:前端
* 矩阵
* 概述
* 向量
* 什么是向量
* 基向量
* 线性变换
* 如何用数值描述线性变换?
* 回到 CSS 的 transform复制代码
我不知道你们所理解的矩阵是怎样的,但我所理解的矩阵是:该阵法免疫法术攻击且100%反伤对方随机一个单位(回合制游戏)。java
以上描述是在小学时代的理解,如今可能有所不一样,慢慢说......git
矩阵,是线性代数中涉及的内容,线性代数在科学领域有不少应用的场景,以下:github
大部分同窗在大学时期应该都学过一本叫作线性代数的书,若是没猜错的话,大家的老师在教学的时候大多都是概念性的灌输,好比矩阵乘法如何运算,加法如何运算,你们只要记住就ok了,可是大部分同窗都不理解,为何矩阵的乘法要这样算?矩阵乘法的意义是什么?,特别是咱们搞计算机的,若是有作过 2D/3D 变换的同窗必定据说过矩阵,好比在前端的CSS中,使用 transform
作 2D/3D 的变换,其中就应用到了矩阵的知识,这篇文章并非一篇数学性质的文章,因此你们不要看了感受一阵眩晕,这篇文章的目的在于:从矩阵与空间之间的关系讲述:为何矩阵能够应用在空间操做(变换)。或者用一句大白话:这玩意儿怎么就能让div
翻过来,转过去,扭的他爹都不认识他的。web
先看一段 css 代码:编程
/* 2D */
transform: matrix(1, 0, 0, 1, 0, 0);
/* 3D */
transform: matrix(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);复制代码
上面两行 css 代码其实什么变换都不会作,由于那是变换的默认状态,即没有变换。可是其中使用到了 matrix
,翻译成中文叫作:矩阵。若是有深刻研究过 css 的同窗对这两行代码也许不陌生,可是大多数人在使用 transform
变换时不多直接使用 matrix
矩阵,除非你不想让人看懂你在作些什么鸟变换...,因此更多的时候,咱们会使用相似以下语法:ide
transform: translateX(100px) rotateZ(30deg);复制代码
如上代码所示,一目了然,要作什么变换你们一看就知道了。但其实,这只是一个语法糖,其底层依然使用的是 matrix
。
若是想要理解矩阵为什么能够应用到 2D/3D 变换,那么只从数值水平的角度理解是不够的,你须要从几何的角度去理解矩阵,这存在着根本性的差别。而这,也就是本篇文章的真正意义。
不过,这须要咱们先了解一些必要的基本概念,这些概念相当重要,首先就是向量
既然矩阵是线性代数的一部分,那么就不得不提到 向量,由于向量是线性代数最基础、最根源的组成部分,因此咱们要先搞清楚,向量是什么?我说过,这篇文章不会很“数学”,因此你们不要被吓到。用一句话描述向量是什么:
向量:空间中的箭头
这个在你们的印象里应该很好理解,这个箭头由两个因素决定:方向
和 长度
,咱们先把目光局限在二维空间下,如图:
上图中,在一个坐标系中画了四个不一样长度和方向的箭头,每一个箭头从原点发出,他们表明了二维空间中的四个向量,在线性代数中,向量一般以原点做为起点。
若是你已经理解了“向量是空间中的箭头”这种观点,下面咱们再进一步,咱们从新用一句话来描述向量:
向量:是有序的数字列表
假设你们对坐标系的概念都有所了解,咱们仍是把目光局限在二维空间,在坐标系中任意选取单位长度,这样咱们就可以使用一个一个的刻度来标刻这个坐标系,选取特定的方向为x/y轴的正方向,那么不难看出,每个向量,均可以用惟一的一个坐标来表示,一样的,坐标系中的每个坐标都对应着一个惟一的箭头(向量),以下图:
在坐标系中,因为坐标一般用来标示一个点,如 P(2, 8)
表示点 P 的坐标为 (2, 8)
,为了区分点和向量,在表示向量时,咱们一般把坐标竖着写,而后用一对儿中括号来描述,如上图中的:
在三维空间也是同样的道理,以下图,我就不作重复的解释,惟一不一样的是,每个向量由 x/y/z 三个数字组成的坐标来表示:
对于向量,你只须要知道它是“空间中的箭头”或者“有序的数字列表”这就足够了,怎么样?不难理解吧,咱们继续往下看,在坐标系中存在一种特殊的向量,咱们称之为 基向量。
基向量,也叫单位向量,是单位长度为1的向量,以下图中:i帽
和 j帽
就是这个二维坐标系的基向量:
对于向量,咱们就先介绍到这里,这已经足够了。除了向量,还有一个概念须要你们了解,即线性变换。
“变换”本质上是“函数”的一种花哨的叫法,玩编程的都知道函数,与在数学中的概念相似,函数接收输入的内容,并输出对应的结果,如图:
变换也是一样的道理,只不过接收向量做为输入,并输出变换后的向量:
既然 “变换” 与 “函数” 本质相同,那么为何叫变换而不叫函数呢?这实际上就暗示了咱们,你能够把这个输入输出的过程,看作一个向量从初始状态到最终状态的一个变化过程,以下图:
如今,咱们把状况宏观一下,目前只讨论一个向量的变换,咱们知道,二维空间的一整个平面,能够看作是由无数个向量组成(或者无数个点组成,由于每个点惟一标识一个向量,因此这里说平面由无数个向量组成),假如这无数个向量同时作相同的变换,那其实就能够看作是平面的变换,以下图:
变换前:
变换后:
不过,并不是全部变换都叫作线性变换,线性变换必需要知足下面两个条件:
以下变换,就不是一个线性变换,由于直线变成了曲线:
在上一小节中咱们知道,空间的变换也能够说是向量的变换,而向量在空间中,能够用一组有序的数字列表来表示(即向量的坐标),因此向量变换先后,必然会引发“有序数字列表的变换”,那么咱们是否能够用数字去描述变换呢?
以前在向量一节中,咱们了解过基向量,单位长度为1,其实空间中的任意一个向量咱们均可以看作是:基向量变换后的和向量,以下图:
向量 v 的坐标是
3
和
-2
看作两个标量,也就是纯数字,那么向量
v 能够看作是基向量被标量缩放后相加获得的和向量:
v = 3
i + (-2
j)
了解了这些,咱们如今就经过一个例子,来认识一个相当重要的事实,假如咱们有向量 v = -1i + 2j,以下图:
此时,基向量 i 的坐标是 (1, 0)
【注意:为了方便,这里就用圆括号表明向量的坐标,下同】,基向量 j 的坐标是 (0, 1)
,假设通过了某些变换以后,基向量 i 的坐标变为 (1, -2)
,基向量 j 的坐标变为 (3, 0)
,以下图:
那么变换后的向量 v 依然知足 v = -1i + 2j,以下:
以上例子所描述的事实,其实是线性变换的性质的推论,该性质能够从几何角度表述为:线性变换后的网格平行且等距。
既然线性变换先后都知足该线性关系:v = -1i + 2j
那么很容易根据变换后 i帽
和 j帽
的坐标推算出变换后 v 的坐标:
也就是 (5, 2)
,即:
那么咱们是否能够认为,给定任意一个向量,其坐标 (x, y)
,咱们能够经过变换后的基向量的坐标推断出该向量变换后的坐标呢?答案是确定的,假如基向量变换后的坐标 i帽
和 j帽
以下图:
那么任意向量 (x, y)
在通过变换后的坐标计算以下:
这告诉咱们另一个事实,二维空间的线性变换仅由四个数字彻底肯定,这四个数字就是基向量 i 变换后 i帽 的坐标,以及基向量 j 变换后 j帽 的坐标,以下图:
是否是很酷?只须要四个数字,咱们就肯定了二维空间的一个变换。一般,咱们把这四个数字放到一个 2 x 2
的格子中,咱们称之为 2 x 2
矩阵:
如今,当你再看到 2 x 2
矩阵的时候,你的第一几何直观反映应该是:它描述了一个二维空间的变换。
咱们把状况通常化,以下图:
咱们有一个 2 x 2
的矩阵 [a, c] [b, d]
,其中 [a, c]
是基向量 i 变换后的坐标,[b, d]
是基向量 j 变换后的坐标,那么根据这个变换,以及线性变换的性质,咱们能够推断出任意向量 [x, y]
变换后的坐标:
实际上,这就是数学家之因此这样定义 矩阵的向量乘法 的缘由。
到了这里,让咱们整理一下思路,首先,对于一个 2 x 2
的矩阵,你的直观几何感觉应该是,第一列的两个数是对基向量 i 的变换,第二列的两个数是对基向量 j 的变换,这四个数字组成的 2 x 2
的矩阵,描述了一个对空间的线性变换,咱们能够根据这个变换推断出任意一点(或者任意向量)变换后的坐标。
其实我么你还能够换一个角度考虑,咱们就单纯的把 2 x 2
矩阵叫作变换,那么向量与矩阵的乘积,就要能够看作是该向量应用了这个变换。其实,这就是矩阵向量乘法的几何意义。
说了一大堆,是时候回到 CSS
的 transform
,咱们来看一下2D变换下 transform
属性的 matrix
写法:
transform: matrix(a, b, c, d, e, f);复制代码
在文章开始,咱们知道各个参数默认值以下:
transform: matrix(1, 0, 0, 1, 0, 0);复制代码
有的同窗可能会问:说好的 2 x 2
矩阵也就是四个数字就能肯定一个二维空间变换,你这里明明有6个数啊,其实,transform
2D变换是一个 3 * 3
的矩阵,为何是这样?由于:位移(translate),前面咱们说过,线性变换要知足其中一个特色:原点不能移动,可是位移却使原点发生了移动,因此 2 x 2
矩阵知足不了需求,只能再加一列,也就是 3 x 3
的矩阵。
把 matrix
中的 a b c d e f
放到一个 3 x 3
的矩阵中应该是这样的:
其实,在没有位移(translate)
的状况下,[a, b] [c, d]
四个数字组成的 2 x 2
矩阵是彻底能够描述2D变换的,如今咱们只看由 [a, b] [c, d]
组成的 2 x 2
矩阵:
咱们把 a b c d
四个数字使用默认值替换一下,即:a = 1
,b = 0
,c = 0
,d = 1
,以下:
经过以前的介绍,咱们在看到这个矩阵的时候,应该知道,第一列的坐标 (1, 0)
应该是基向量 i 变换后的坐标,可是基向量 i 在变换前的坐标就是 (1, 0)
,也就是说没有任何变换,同理,基向量 j 也没有任何变换,因此说,这就是 a b c d
默认值设定为下面代码所示的值的缘由:
transform: matrix(a, b, c, d, e, f);
// a b c d 默认值为 1 0 0 1
transform: matrix(1, 0, 0, 1, e, f);复制代码
那么你们想一想一下,咱们把 a
的值从 1
变为 2
会发生什么?若是把 a
的值从 1
变为 2
那么矩阵以下:
也就是说,基向量 i 的坐标从 (1, 0)
变成了 (2, 0)
,这是在干什么?是否是基向量 i 被放大为了原来的二倍?举一个通俗的例子:本来单位长度1表明20px,被放大后单位长度1则表明40px。一样的,当咱们把 a
的值从 1
变为 0.5
则意味着把基向量 i 缩小为原来的一半。事实上:在 transform: matrix()
中,修改 a
的值,就是在改变 x
轴方向的缩放比例:
transform: matrix(2, 0, 0, 1, 0, 0);
/* 等价于 */
transform: scaleX(2);复制代码
相信你们已经知道了,修改 d
的值,就是改变 y
轴的缩放比例:
transform: matrix(1, 0, 0, 4, 0, 0);
/* 等价于 */
transform: scaleY(4);复制代码
那么旋转要如何修改 matrix
中的值呢?其实,想要知道如何修改 a b c d
的值,只须要知道,旋转后基向量 i 和 j 的坐标就能够了,将旋转后的坐标对号填入就能够获得变换矩阵,下面,咱们就来看看如何肯定旋转后基向量 i 和 j 的坐标。
咱们知道,在 web
开发中的坐标系和数学中的坐标系在正方向的选取上不太一致,在你们所熟悉的坐标系中,正方向的选取以下:
而在 web
开发中,坐标系的正方向选取是这样的:
假设咱们将其顺时针旋转 45 度,以下图:
假设,上图中咱们旋转的是单位向量,那么旋转后单位向量 i 的坐标应该是 (cosθ, sinθ)
,单位向量 j 的坐标应该是 (-sinθ, cosθ)
,因此若是用矩阵表示的话,应该是这样的:
若是写到 matrix
里,天然就是下面这个样子:
transform: matrix(cosθ, sinθ, -sinθ, cosθ, 0, 0)复制代码
因此,若是咱们要顺时针旋转 45 度,下面两种写法是等价的:
/* * Math.cos(Math.PI / 180 * 45) = 0.707106 * Math.sin(Math.PI / 180 * 45) = 0.707106 */
transform: matrix(0.707106, 0.707106, -0.707106, 0.707106, 0, 0)
/* 等价于 */
transform: rotate(45deg);复制代码
经过上面缩放和旋转的例子,咱们已经知道了,2 x 2
的矩阵确实可以描述二维空间的变换,这也就是矩阵可以操做空间的缘由。在 transform
中,除了缩放(scale
)、旋转(rotate
) 还有倾斜(skew
),对于倾斜,相似于咱们寻找旋转后基向量的坐标同样,你只须要根据倾斜所定义的变换规则,找到基向量变换后的坐标就能够了,实际上倾斜对应以下规则:
transform: matrix(1, tan(θy), tan(θx), 1, 0, 0);复制代码
你们本身拿只笔在纸上画一画应该就能搞清楚倾斜在作什么样子的变换。
不管 缩放(scale
)、旋转(rotate
) 仍是倾斜(skew
),他们都不会是原点发生改变,因此使用 a b c d
四个数字组成的矩阵彻底能够描述,可是不要忘了,咱们还有一个 位移(translate
),这时,就不得不提到 e f
了,我想我不说你们也都知道了,e f
分别表明了 x y
方向的位移,事实也如你们所想:
transform: matrix(1, 0, 0, 1, 100, 200)
/* 等价于 */
transform: translateX(100px) translateY(200px);复制代码
至此,transform
使用 3 x 3
矩阵:
除了2D变换,还有3D变换,在 transform
中,使用 4 x 4
的矩阵描述3D变换,但实际上,三维空间的线性变换只须要一个 3 x 3
的矩阵就能够描述了,那么为何搞了一个 4 x 4
矩阵呢?实际上这和咱们在将二维空间的变换使用 3 x 3
矩阵的道理是同样的,那就是位移。
咱们来看一下3D变换的 matrix
默认值:
transform: matrix(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p);
transform: matrix(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);复制代码
这十六个数字就是 4 x 4
矩阵的 16 个数值:
若是换成对应数字,是这样的:
相似于咱们讲解 2D 变换同样,其中由
3 x 3
矩阵用来描述空间的 3D 线性变换,如:
rotateX
rotateY
scaleZ
等等,注意:
rotateZ
是 2D 变换哦。
而 m
n
o
则分别用来描述位移:translateX
translateY
translateZ
。