可视化学习:CSS transform与仿射变换
引言
在幾年前,我就在一些博客中看到關(guān)于CSS中transform的分析,講到它與線性代數(shù)中矩陣的關(guān)系,但當(dāng)時(shí)由于使用transform比較少,再加上我畢竟是個(gè)數(shù)學(xué)學(xué)渣,對(duì)數(shù)學(xué)有點(diǎn)畏難心理,就有點(diǎn)看不下去,所以只是隨便掃了兩眼,就沒有再繼續(xù)了解了。現(xiàn)在在學(xué)習(xí)可視化,又遇到了這個(gè)點(diǎn),又說到這是可視化的基礎(chǔ)知識(shí),既然這樣,那看來還是逃不過去,那就再多了解一點(diǎn)吧。
transform的作用
使用過transform的前端小伙伴一定不陌生,通過對(duì)CSS中transform屬性的設(shè)置,我們可以對(duì)DOM元素進(jìn)行縮放、旋轉(zhuǎn)、平移,以及扭曲,從而改變?cè)氐奈恢谩⑿螤睢⒋笮『徒嵌取?/p>
仿射變換
CSS中的transform對(duì)應(yīng)到圖形學(xué)中的概念就是仿射變換。
仿射變換簡單來說就是“線性變換 + 平移”。
在CSS中對(duì)某個(gè)DOM元素應(yīng)用仿射變換,可以簡單理解成是把這個(gè)元素原本的整個(gè)坐標(biāo)系進(jìn)行了變換,并且這個(gè)坐標(biāo)系的原點(diǎn)在最初始時(shí)位于DOM元素的中心,X軸朝右、Y軸朝上、Z軸朝外,也就是朝向屏幕。
所以就是說,對(duì)某個(gè)DOM元素進(jìn)行仿射變換,就相當(dāng)于對(duì)它所對(duì)應(yīng)的幾何圖形的每個(gè)頂點(diǎn)向量進(jìn)行仿射變換。
關(guān)于圖形的仿射變換,有兩個(gè)性質(zhì):
第一,仿射變換不改變直線段的形狀,也就是說,應(yīng)用仿射變換后,直線段依舊是直線段;
第二,應(yīng)用相同的仿射變換后,兩條直線段的長度比例保持不變。
平移
接下來我們先說平移,平移變換是最簡單的仿射變換。
假設(shè)存在一個(gè)向量P(x0, y0),我們想要把它沿著另一個(gè)向量Q(x1, y1)的方向移動(dòng)對(duì)應(yīng)距離,那只要將兩個(gè)向量相加,我們就可以得到這個(gè)新的向量它的坐標(biāo)。
x = x0 + x1
y = y0 + y1
這就是平移變換的公式。
線性變換
根據(jù)公式可以看出,應(yīng)用平移變換后,原始坐標(biāo)系的原點(diǎn)會(huì)發(fā)生變化。
但是應(yīng)用線性變換后,原點(diǎn)卻并不會(huì)變化;下面來講解兩個(gè)常用的線性變換:旋轉(zhuǎn)和縮放。
-
旋轉(zhuǎn)
首先我們先來看旋轉(zhuǎn)變換。
假設(shè)存在一個(gè)向量
P(x0, y0),長度為r,與X軸夾角為θ,現(xiàn)在將它逆時(shí)針旋轉(zhuǎn)α角,那么此時(shí)新的向量P'的坐標(biāo)x和y分別是多少呢?首先我們根據(jù)圓的參數(shù)方程,可以得到如下公式:
x0 = r * cosθ y0 = r * sinθ x = r * cos(α+θ) y = r * sin(α+θ)但這樣并看不出新舊坐標(biāo)之間的關(guān)聯(lián),所以需要進(jìn)行推導(dǎo)。
在上圖中,我們假設(shè)旋轉(zhuǎn)θ角后得到了一個(gè)新的坐標(biāo)系(藍(lán)色),此時(shí)我們可以求得向量P'在新坐標(biāo)系的坐標(biāo),此時(shí)P'在新坐標(biāo)系的坐標(biāo)可以表示為:
x' = r * cosα -> AF y' = r * sinα -> AI分別相當(dāng)于是線段AF和AI的長度。
此時(shí)依舊看不出新舊坐標(biāo)之間的關(guān)聯(lián),我們還需要繼續(xù)推導(dǎo),求出向量P'在原坐標(biāo)系的值,在上圖中相當(dāng)于我們要求出線段AJ和AK的長度。
-
先來求AJ的長度
首先我們從圖中可以看出
AJ = AG - JG, 并且AG = AF * cosθ;同時(shí)JG 和LF的長度相同,DF與AI的長度相同,且角FDJ的度數(shù)也是θ,所以可以得到
JG = AI * sinθ。最終我們可以得到如下公式:
AJ = AF * cosθ - AI * sinθ = r * cosα * cosθ - r * sinα * sinθ又因?yàn)椋?/p>
x0 = r * cosθ y0 = r * sinθ就可以得到AJ的長度,也就是新向量的x坐標(biāo)
x = x0 * cosα - y0 * sinα -
接著來求AK的長度
從圖中我們也可以看出
AK = AM + MK,并且AM = AI * cosθMK又可以分為MN和NK兩段,相當(dāng)于
MK = AF * sinθ。最終我們可以得到:
AK = AI * cosθ + AF * sinθ = r * sinα * cosθ + r * cosα * sinθ再加上原坐標(biāo)和角度及半徑的關(guān)系,就可以得到AK的長度,也就是新向量的y坐標(biāo):
y = x0 * sinα + y0 * cosα
至此我們就得到了新坐標(biāo)和原坐標(biāo)以及旋轉(zhuǎn)角度之間的關(guān)系,也就是旋轉(zhuǎn)變換的公式:
x = x0 * cosα - y0 * sinα y = x0 * sinα + y0 * cosα根據(jù)線性代數(shù)的知識(shí),我們可以使用矩陣的形式來表示以上公式:
[x] [cosα -sinα] [x0] | | = | | x | | [y] [sinα cosα] [y0] -
-
縮放
接著我們繼續(xù)看縮放變換。縮放變換相當(dāng)于是讓向量與標(biāo)量相乘。
比如我們使X軸縮放比例為sx,使Y軸縮放比例為sy,就可以得到新向量的坐標(biāo)為:
x = sx * x0 y = sy * y0縮放比旋轉(zhuǎn)簡單一些,可以直接寫出矩陣形式的公式:
[x] [sx 0] [x0] | | = | | x | | [y] [0 sy] [y0]
至此,我們就基本了解了仿射變換的公式,并且可以看出線性變換的公式可以用矩陣相乘的形式進(jìn)行表示。
除了不改變?cè)c(diǎn),線性變換還有另外一個(gè)性質(zhì),就是可以進(jìn)行疊加;多個(gè)線性變換的疊加結(jié)果就是將線性變換的矩陣依次相乘,最后再與原始向量相乘。
根據(jù)以上內(nèi)容,我們可以得到仿射變換的一般表達(dá)式:
P = M x P0 + P1
M為多個(gè)線性變換的疊加結(jié)果,也就是變換矩陣的相乘結(jié)果,P0為原始向量坐標(biāo),P1為平移。
公式優(yōu)化
為了便于計(jì)算,我們還可以對(duì)以上的仿射變換表達(dá)式進(jìn)行優(yōu)化,通過增加維度來使用矩陣進(jìn)行表示:
[P] [M P1] [P0]
| | = | | x | |
[1] [0 1] [1 ]
這實(shí)際上就是給線性空間增加了一個(gè)維度,用高維度的線性變換表示了低維度的仿射變換。
這種n+1維坐標(biāo)被稱為齊次坐標(biāo),對(duì)應(yīng)的矩陣被稱為齊次矩陣。
我們需要注意,由于平移變換會(huì)改變坐標(biāo)原點(diǎn),不同的變換順序很可能會(huì)導(dǎo)致不同的變換結(jié)果,所以要注意矩陣相乘的順序。
公式應(yīng)用
接下來我們就來應(yīng)用一下線性變換的公式。
假設(shè)現(xiàn)在在頁面上有一個(gè)div。
<div class="block separate">我使用分開寫</div>
.block {
width: 100px;
height: 100px;
color: #fff;
background: orange;
&.separate {
transform: rotate(30deg) translate(100px, 50px) scale(1.5);
}
}
通過簡單的旋轉(zhuǎn)和平移,我們改變了元素的角度、位置和大小。
此時(shí)我們對(duì)于transform的變換是分開寫的,但在CSS的transform中,可以使用一個(gè)matrix函數(shù),讓我們對(duì)這些變換進(jìn)行合并編寫。
首先我們引入一個(gè)ogl庫,使用其中定義的矩陣類Mat3(也可以借助其他數(shù)學(xué)庫,比如mathjs):
import { Mat3 } from 'ogl';
然后針對(duì)上面的3個(gè)變換,分別定義三個(gè)變換矩陣,分別是旋轉(zhuǎn)矩陣、平移矩陣和縮放矩陣:
const rad = Math.PI / 6;
let a = new Mat3(
// 旋轉(zhuǎn)矩陣
Math.cos(rad), -Math.sin(rad), 0,
Math.sin(rad), Math.cos(rad), 0,
0, 0, 1
);
let b = new Mat3(
// 平移矩陣
1, 0, 100,
0, 1, 50,
0, 0, 1
);
let c = new Mat3(
// 縮放矩陣
1.5, 0, 0,
0, 1.5, 0,
0, 0, 1
);
// -------------
// 使用math.js
const a = math.matrix(
[
[Math.cos(rad), -Math.sin(rad), 0],
[Math.sin(rad), Math.cos(rad), 0],
[0, 0, 1]
]
);
const b = math.matrix(
[
[1, 0, 100],
[0, 1, 50],
[0, 0, 1]
]
);
const c = math.matrix(
[
[1.5, 0, 0],
[0, 1.5, 0],
[0, 0, 1]
]
);
接著對(duì)三個(gè)矩陣進(jìn)行相乘,得到axbxc的結(jié)果:
const res = [a, b, c].reduce((prev, current) => {
return current.multiply(prev); // prev x current 結(jié)果保存到current
});
// -------------
// 使用math.js
let res = math.multiply(a, b);
res = math.multiply(res, c);
最后我們利用CSS變量將JS的計(jì)算結(jié)果應(yīng)用到樣式上:
.block {
// ...
&.combine {
--trans: none;
transform: var(--trans);
}
}
由于CSS的matrix是一個(gè)簡寫的齊次矩陣,它省略了三階齊次矩陣第三行的0,0,1,所以只有6個(gè)值。
const combine = document.querySelector('.combine');
const s = res.slice(0, 6);
matrix貌似是列主序,所以在設(shè)置的時(shí)候,需要按如下順序賦值:
const combine = document.querySelector('.combined');
combine.style.setProperty('--trans', `matrix(
${s[0]},${s[3]},
${s[1]},${s[4]},
${s[2]},${s[5]},
)`);
// -------------
// 使用math.js
const s = Array.from(res).map(item => item.value);
combine.style.setProperty('--trans', `matrix(
${s[0]},${s[3]},
${s[1]},${s[4]},
${s[2]},${s[5]}
)`);
可以明顯看出,這樣使用的效果,和rotate、translate和scale分開寫的效果是一樣的。
總結(jié)
利用仿射變換,我們可以快速繪制出形態(tài)、位置、大小各異的眾多幾何圖形,比如實(shí)現(xiàn)粒子動(dòng)畫。
也許在普通的前端開發(fā)中,用不到太多,也并不太需要說去利用matrix去減少CSS的代碼體積,但如果要去做可視化方面的開發(fā),仿射變換還是可以多去了解一下。
總結(jié)
以上是生活随笔為你收集整理的可视化学习:CSS transform与仿射变换的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 婚纱礼服馆介绍文字文案28句
- 下一篇: CSS画几何图形系列