在涉及到前端图形学的时候,几乎避免不了 transform 属性的应用。
而 transform 一共内置了五种不同大类的函数(矩阵变形、平移、缩放、旋转、倾斜,具体细节有九个),开发者经常容易被不同函数的组合变换,搞到晕头转向。
当面对需要精准定位的需求时,如果对 transform 的计算原理理解不透彻,就会导致代码冗长、复杂度增加,易读性也会迅速下降。
事实上,前端里的 transform 有很多种,比如 CSS 和 SVG 中的 transform 属性就有些许不同。不过万变不离其宗,它们底层的数学原理大体是一致的。
所以为了方便描述,本篇这里以 SVG transform 为主。
一来,可以免去 CSS 中大量关于单位不同的换算,排开很多跟原理无关的细节;
二来,作为矢量格式的 SVG 足够精简,用来描述数学计算方式,矢量化参数拥有天生的优势;
① transform: matrix(a, b, c, d, e, f)
说到图形学,那必然会涉及到矩阵运算。
matrix 函数可以说是最本源的存在,如果将前端页面想象成一块画布,matrix 就是这块画布的改造者。只需要设定不同的参数,就可以用 matrix 将图形随意变换。
同时,matrix 函数还是其他四类功能函数的核心,这四类分别是平移、缩放、旋转、倾斜,他们的实现方式都可以用 matrix 等价替换。
matrix 函数的参数是一个 3x3 的方阵矩阵,只不过这个矩阵中只有六个变量,所以函数声明里显式的参数列表长度为 6。
矩阵形式如下(假设为 M):
M = ( a c e b d f 0 0 1 ) M = \begin{pmatrix} a & c & e \\ b & d & f \\ 0 & 0 & 1 \\ \end{pmatrix} M=⎝⎛ab0cd0ef1⎠⎞
怎么用呢?
答案是:矩阵乘法
假设页面上有一个点 point_old 的坐标为 ***(oldX, oldY)***,转换后新的点 point_new 坐标为 ***(newX, newY)***。
在运算过程中,点的矩阵描述方式如下:
p o i n t o l d = ( o l d X o l d Y 1 ) p o i n t n e w = ( n e w X n e w Y 1 ) point_{old} = \begin{pmatrix} oldX \\ oldY \\ 1 \end{pmatrix} \\ point_{new} = \begin{pmatrix} newX \\ newY \\ 1 \end{pmatrix} pointold=⎝⎛oldXoldY1⎠⎞pointnew=⎝⎛newXnewY1⎠⎞
计算方式为:
p o i n t n e w = M ∗ p o i n t o l d point_{new} = M * point_{old} pointnew=M∗pointold
( n e w X n e w Y 1 ) = ( a c e b d f 0 0 1 ) ( o l d X o l d Y 1 ) = ( a ∗ o l d X + c ∗ o l d Y + e b ∗ o l d X + d ∗ o l d Y + f 1 ) \begin{pmatrix} newX \\ newY \\ 1 \\ \end{pmatrix} = \begin{pmatrix} a & c & e \\ b & d & f \\ 0 & 0 & 1 \\ \end{pmatrix} \begin{pmatrix} oldX \\ oldY \\ 1 \\ \end{pmatrix} = \begin{pmatrix} a*oldX+c*oldY+e \\ b*oldX+d*oldY+f \\ 1 \\ \end{pmatrix} ⎝⎛newXnewY1⎠⎞=⎝⎛ab0cd0ef1⎠⎞⎝⎛oldXoldY1⎠⎞=⎝⎛a∗oldX+c∗oldY+eb∗oldX+d∗oldY+f1⎠⎞
所以:
p o i n t n e w { n e w X = a ∗ o l d X + c ∗ o l d Y + e n e w Y = b ∗ o l d X + d ∗ o l d Y + f point_{new} \begin{cases} newX = a*oldX + c*oldY + e \\ newY = b*oldX + d*oldY + f \\ \end{cases} pointnew{ newX=a∗oldX+c∗oldY+enewY=b∗oldX+d∗oldY+f
在这六个参数中,e、f 主要负责偏移量,其余 a、b、c、d 则代表不同的放大倍数。
现在我们知道,可以通过对这六个参数的控制,实现不同的效果了。
比如默认状态下,matrix(1, 0, 0, 1, 0, 0) 代表了什么也不动,因为套用上述计算公式,
( n e w X n e w Y 1 ) = ( 1 0 0 0 1 0 0 0 1 ) ( o l d X o l d Y 1 ) = ( 1 ∗ o l d X + 0 ∗ o l d Y + 0 0 ∗ o l d X + 1 ∗ o l d Y + 0 1 ) = ( o l d X o l d Y 1 ) \begin{pmatrix} newX \\ newY \\ 1 \\ \end{pmatrix} = \begin{pmatrix} 1 & 0 & 0 \\ 0 & 1 & 0 \\ 0 & 0 & 1 \\ \end{pmatrix} \begin{pmatrix} oldX \\ oldY \\ 1 \\ \end{pmatrix} = \begin{pmatrix} 1*oldX+0*oldY+0 \\ 0*oldX+1*oldY+0 \\ 1 \\ \end{pmatrix} = \begin{pmatrix} oldX \\ oldY \\ 1 \\ \end{pmatrix} ⎝⎛newXnewY1⎠⎞=⎝⎛100010001⎠⎞⎝⎛oldXoldY1⎠⎞=⎝⎛1∗oldX+0∗oldY+00∗oldX+1∗oldY+01⎠⎞=⎝⎛oldXoldY1⎠⎞
结果可以发现,点坐标没有任何变换。
到这里,transform 的核心函数 matrix() 是如何计算的,应该已经清楚了。
那么接下来看看剩下其他所有的函数是如何实现和 matrix 转换的。
<h1>default</h1>
<svg x="0px" y="0px" width="600px" height="300px">
<line label="axisX" fill="none" stroke="black" stroke-width="10" x1="0" y1="0" x2="600" y2="0" />
<line label="axisY" fill="none" stroke="black" stroke-width="10" x1="0" y1="0" x2="0" y2="300" />
<rect x="0" y="0" width="200" height="100" fill="red" opacity="0.9"></rect>
</svg>
② transform: translate(x)
translate 为平移函数,当只有一个参数时,表示图形水平移动了多少的距离。
即:
n e w X = x + o l d X newX = x + oldX newX=x+oldX
那么很简单的,构造矩阵 matrix(1, 0, 0, 1, x, 0) 即可实现 translate(x) 的效果:
( n e w X n e w Y 1 ) = ( 1 0 x 0 1 0 0 0 1 ) ( o l d X o l d Y 1 ) = ( 1 ∗ o l d X + 0 ∗ o l d Y + x 0 ∗ o l d X + 1 ∗ o l d Y + 0 1 ) = ( x + o l d X o l d Y 1 ) \begin{pmatrix} newX \\ newY \\ 1 \\ \end{pmatrix} = \begin{pmatrix} 1 & 0 & x \\ 0 & 1 & 0 \\ 0 & 0 & 1 \\ \end{pmatrix} \begin{pmatrix} oldX \\ oldY \\ 1 \\ \end{pmatrix} = \begin{pmatrix} 1*oldX+0*oldY+x \\ 0*oldX+1*oldY+0 \\ 1 \\ \end{pmatrix} = \begin{pmatrix} x + oldX\\ oldY \\ 1 \\ \end{pmatrix} ⎝⎛newXnewY1⎠⎞=⎝⎛100010x01⎠⎞⎝⎛oldXoldY1⎠⎞=⎝⎛1∗oldX+0∗oldY+x0∗oldX+1∗oldY+01⎠⎞=⎝⎛x+oldXoldY1⎠⎞
<div>
<h1>transform: translate(x)</h1>
<svg x="0px" y="0px" width="600px" height="300px">
<line label="axisX" fill="none" stroke="black" stroke-width="10" x1="0" y1="0" x2="600" y2="0" />
<line label="axisY" fill="none" stroke="black" stroke-width="10" x1="0" y1="0" x2="0" y2="300" />
<rect x="0" y="0" width="200" height="100" fill="red" opacity="0.9" transform="translate(100)"></rect>
</svg>
<h1>transform: matrix(1, 0, 0, 1, x, 0)</h1>
<svg x="0px" y="0px" width="600px" height="300px">
<line label="axisX" fill="none" stroke="black" stroke-width="10" x1="0" y1="0" x2="600" y2="0" />
<line label="axisY" fill="none" stroke="black" stroke-width="10" x1="0" y1="0" x2="0" y2="300" />
<rect x="0" y="0" width="200" height="100" fill="red" opacity="0.9" transform="matrix(1,0,0,1,100,0)"></rect>
</svg>
</div>
③ transform: translate(x, y)
这里可以看做单一参数的 translate 函数的重载函数,第二个参数 y 值,代表在笛卡尔坐标系下的二维平面中,y 轴方向的平移运动。
即:
{ n e w X = x + o l d X n e w Y = y + o l d Y \begin{cases} newX = x + oldX \\ newY = y + oldY \end{cases} {
n