概述
矩陣,是線性代數中涉及的内容,線性代數在科學領域有很多應用的場景,如下:
大部分同學在大學時期應該都學過一本叫做線性代數的書,如果沒猜錯的話,你們的老師在教學的時候大多都是概念性的灌輸,比如矩陣乘法如何運算,加法如何運算,大家隻要記住就ok了,但是大部分同學都不了解,為什麼矩陣的乘法要這樣算?矩陣乘法的意義是什麼?,特别是我們搞計算機的,如果有做過 2D/3D 變換的同學一定聽說過矩陣,比如在前端的CSS中,使用
transform
做 2D/3D 的變換,其中就應用到了矩陣的知識,這篇文章并不是一篇數學性質的文章,是以大家不要看了感覺一陣眩暈,這篇文章的目的在于:從矩陣與空間之間的關系講述:為什麼矩陣可以應用在空間操作(變換)。或者用一句大白話:這玩意兒怎麼就能讓
div
翻過來,轉過去,扭的他爹都不認識他的。
先看一段 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
矩陣,除非你不想讓人看懂你在做些什麼鳥變換...,是以更多的時候,我們會使用類似如下文法:
transform: translateX(100px) rotateZ(30deg);
如上代碼所示,一目了然,要做什麼變換大家一看就知道了。但其實,這隻是一個文法糖,其底層依然使用的是
matrix
。
如果想要了解矩陣為何可以應用到 2D/3D 變換,那麼隻從數值水準的角度了解是不夠的,你需要從幾何的角度去了解矩陣,這存在着根本性的差異。而這,也就是本篇文章的真正意義。
不過,這需要我們先了解一些必要的基本概念,這些概念至關重要,首先就是向量
向量
什麼是向量
既然矩陣是線性代數的一部分,那麼就不得不提到 向量,因為向量是線性代數最基礎、最根源的組成部分,是以我們要先搞清楚,向量是什麼?我說過,這篇文章不會很“數學”,是以大家不要被吓到。用一句話描述向量是什麼:
向量:空間中的箭頭
這個在大家的印象裡應該很好了解,這個箭頭由兩個因素決定:
方向
和
長度
,我們先把目光局限在二維空間下,如圖:
上圖中,在一個坐标系中畫了四個不同長度和方向的箭頭,每個箭頭從原點發出,他們代表了二維空間中的四個向量,線上性代數中,向量通常以原點作為起點。
如果你已經了解了“向量是空間中的箭頭”這種觀點,下面我們再進一步,我們重新用一句話來描述向量:
向量:是有序的數字清單
假設大家對坐标系的概念都有所了解,我們還是把目光局限在二維空間,在坐标系中任意選取機關長度,這樣我們就能夠使用一個一個的刻度來标刻這個坐标系,選取特定的方向為x/y軸的正方向,那麼不難看出,每一個向量,都可以用唯一的一個坐标來表示,同樣的,坐标系中的每一個坐标都對應着一個唯一的箭頭(向量),如下圖:
在坐标系中,由于坐标通常用來标示一個點,如
P(2, 8)
表示點 P 的坐标為
(2, 8)
,為了區分點和向量,在表示向量時,我們通常把坐标豎着寫,然後用一對兒中括号來描述,如上圖中的:
在三維空間也是一樣的道理,如下圖,我就不做重複的解釋,唯一不同的是,每一個向量由 x/y/z 三個數字組成的坐标來表示:
對于向量,你隻需要知道它是“空間中的箭頭”或者“有序的數字清單”這就足夠了,怎麼樣?不難了解吧,我們繼續往下看,在坐标系中存在一種特殊的向量,我們稱之為 基向量。
基向量
基向量,也叫機關向量,是機關長度為1的向量,如下圖中:
i帽
j帽
就是這個二維坐标系的基向量:
對于向量,我們就先介紹到這裡,這已經足夠了。除了向量,還有一個概念需要大家了解,即線性變換。
線性變換
“變換”本質上是“函數”的一種花哨的叫法,玩程式設計的都知道函數,與在數學中的概念類似,函數接收輸入的内容,并輸出對應的結果,如圖:
變換也是同樣的道理,隻不過接收向量作為輸入,并輸出變換後的向量:
既然 “變換” 與 “函數” 本質相同,那麼為什麼叫變換而不叫函數呢?這實際上就暗示了我們,你可以把這個輸入輸出的過程,看做一個向量從初始狀态到最終狀态的一個變化過程,如下圖:
現在,我們把情況宏觀一下,目前隻讨論一個向量的變換,我們知道,二維空間的一整個平面,可以看做是由無數個向量組成(或者無數個點組成,因為每一個點唯一辨別一個向量,是以這裡說平面由無數個向量組成),假如這無數個向量同時做相同的變換,那其實就可以看做是平面的變換,如下圖:
變換前:
變換後:
不過,并非所有變換都叫做線性變換,線性變換必須要滿足下面兩個條件:
- 1、直線在變換後仍然為直線,不能有所彎曲
- 2、原點不能移動
如下變換,就不是一個線性變換,因為直線變成了曲線:
如何用數值描述線性變換?
在上一小節中我們知道,空間的變換也可以說是向量的變換,而向量在空間中,可以用一組有序的數字清單來表示(即向量的坐标),是以向量變換前後,必然會引起“有序數字清單的變換”,那麼我們是否可以用數字去描述變換呢?
之前在向量一節中,我們了解過基向量,機關長度為1,其實空間中的任意一個向量我們都可以看做是:基向量變換後的和向量,如下圖:
向量 v 的坐标是 ,如果我們把
3
和
-2
看做兩個标量,也就是純數字,那麼向量 v 可以看做是基向量被标量縮放後相加得到的和向量: v = 3i + (-2j)
了解了這些,我們現在就通過一個例子,來認識一個至關重要的事實,假如我們有向量 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
說了一大堆,是時候回到
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 個數值:
如果換成對應數字,是這樣的: