在涉及到前端图形学的时候,几乎避免不了 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 =\begin{pmatrix}a & c & e \\b & d & f \\0 & 0 & 1 \\\end{pmatrix}$$

怎么用呢?
答案是:矩阵乘法

假设页面上有一个点 point_old 的坐标为  (oldX, oldY),转换后新的点 point_new 坐标为  (newX, newY)

在运算过程中,点的矩阵描述方式如下:

$$point_{old} =\begin{pmatrix}oldX \\oldY \\1\end{pmatrix}\\point_{new} =\begin{pmatrix}newX \\newY \\1\end{pmatrix}$$

计算方式为:

$$point_{new} = M * point_{old}$$

$$\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}$$

所以:

$$point_{new}\begin{cases}newX = a*oldX + c*oldY + e \\newY = b*oldX + d*oldY + f \\\end{cases}$$

在这六个参数中,ef 主要负责偏移量,其余 abcd 则代表不同的放大倍数。

现在我们知道,可以通过对这六个参数的控制,实现不同的效果了。

比如默认状态下,matrix(1, 0, 0, 1, 0, 0) 代表了什么也不动,因为套用上述计算公式,

$$\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}$$

结果可以发现,点坐标没有任何变换。

到这里,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 为平移函数,当只有一个参数时,表示图形水平移动了多少的距离。
即:

$$newX = x + oldX$$

那么很简单的,构造矩阵 matrix(1, 0, 0, 1, x, 0) 即可实现 translate(x) 的效果:

$$\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}$$

<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 轴方向的平移运动。

即:

$$\begin{cases}newX = x + oldX \\newY = y + oldY\end{cases}$$

同理,可构造矩阵 matrix(1, 0, 0, 1, x, y) 实现 translate(x, y) 的效果:

$$\begin{pmatrix}newX \\newY \\1 \\\end{pmatrix} =\begin{pmatrix}1 & 0 & x \\0 & 1 & y \\0 & 0 & 1 \\\end{pmatrix}\begin{pmatrix}oldX \\oldY \\1 \\\end{pmatrix} =\begin{pmatrix}1*oldX+0*oldY+x \\0*oldX+1*oldY+y \\1 \\\end{pmatrix} =\begin{pmatrix}x + oldX \\y + oldY \\1 \\\end{pmatrix}$$

<div>
    <h1>transform: translate(x, y)</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,50)"></rect>
    </svg>

    <h1>transform: matrix(1, 0, 0, 1, x, y)</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,50)"></rect>
    </svg>
</div>

④ transform: scale(s)

scale 为缩放函数,当只有一个参数时,表示图形在水平和纵向两个轴上,实现等比例的放大缩小。

即:

$$\begin{cases}newX = s*oldX \\newY = s*oldY\end{cases}$$

由于这里是成比例放大,所以可得变换矩阵 matrix(s, 0, 0, s, 0, 0)

$$\begin{pmatrix}newX \\newY \\1 \\\end{pmatrix}=\begin{pmatrix}s & 0 & 0 \\0 & s & 0 \\0 & 0 & 1 \\\end{pmatrix}\begin{pmatrix}oldX \\oldY \\1 \\\end{pmatrix}=\begin{pmatrix}s*oldX+0*oldY+0 \\0*oldX+s*oldY+0 \\1 \\\end{pmatrix} =\begin{pmatrix}s*oldX \\s*oldY \\1 \\\end{pmatrix}$$

<div>
    <h1>transform: scale(s)</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="scale(2)"></rect>
    </svg>

    <h1>transform: matrix(s, 0, 0, s, 0, 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(2,0,0,2,0,0)">
        </rect>
    </svg>
</div>

⑤ transform: scale(sx, sy)

这里同样的,也是拥有两个参数的重载函数,由此可以分开控制不同轴向的缩放倍率。

即:

$$\begin{cases}newX = sx*oldX \\newY = sy*oldY\end{cases}$$

同理可得变换矩阵 matrix(sx, 0, 0, sy, 0, 0)

$$\begin{pmatrix}newX \\newY \\1 \\\end{pmatrix}=\begin{pmatrix}sx & 0 & 0 \\0 & sy & 0 \\0 & 0 & 1 \\\end{pmatrix}\begin{pmatrix}oldX \\oldY \\1 \\\end{pmatrix} =\begin{pmatrix}sx*oldX+0*oldY+0 \\0*oldX+sy*oldY+0 \\1 \\\end{pmatrix} =\begin{pmatrix}sx*oldX \\sy*oldY \\1 \\\end{pmatrix}$$

<div>
    <h1>transform: scale(sx, sy)</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="scale(0.5,2)"></rect>
    </svg>

    <h1>transform: matrix(sx, 0, 0, sy, 0, 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(0.5,0,0,2,0,0)"></rect>
    </svg>
</div>

⑥ transform: rotate(a)

rotate 为旋转函数,当参数个数为 1 时,表示以当前元素坐标系原点为旋转点,旋转角度为 a 度。

需要提前注意的是,这里的单位为 deg,角度制。

而在接下来换算成 matrix 的过程中,需要用到三角函数。

所以在数值上,需要将角度制,转换成弧度制:

$$a'=\frac{\pi}{180}*a$$

此外,由于在二维平面旋转运动下,任意点到旋转圆心的距离不变。所以为了方便计算,我们在这里使用极坐标系,推导笛卡尔坐标系下物体运动的方式。

根据极坐标系,我们用有序数对  (ρ, θ)  表示任意点 P 的坐标,ρ 代表极径,θ** 代表极角(弧度制)。

记为 P(ρ, θ)

那么,任意点旋转 a 角度(a'  弧度)后的坐标即为:P(ρ, θ + a')**

利用坐标系间的映射关系:

$$\begin{cases}X = \rho*cos(\theta) \\Y = \rho*sin(\theta) \\\end{cases}$$

可得:

$$newP = oldP(\rho,\theta + a')$$

$$\begin{cases}newX = \rho*cos(\theta+a') \\newY = \rho*sin(\theta+a') \\\end{cases}$$

进一步展开可得:

$$\begin{aligned}newX &= \rho*cos(\theta+a') \\ &= \rho*cos(\theta)*cos(a')-\rho*sin(\theta)*sin(a') \\ &= oldX*cos(a')-oldY*sin(a') \\ &= cos(a')*oldX + (-1)*sin(a')*oldY \\\end{aligned}$$

$$\begin{aligned}newY & = \rho*sin(\theta+a') \\ & = \rho*sin(\theta)*cos(a')+\rho*cos(\theta)*sin(a') \\ & = oldY * cos(a') + oldX * sin(a') \\ & = sin(a') * oldX + cos(a') * oldY\end{aligned}$$

根据上式,可以推出变换矩阵为 matrix(cos(a'), sin(a'), -sin(a'), cos(a'), 0, 0)

$$\begin{aligned}\begin{pmatrix}newX \\newY \\1 \\\end{pmatrix}& =\begin{pmatrix}cos(a') & -sin(a') & 0 \\sin(a') & cos(a') & 0 \\0 & 0 & 1 \\\end{pmatrix}\begin{pmatrix}oldX \\oldY \\1 \\\end{pmatrix} \\ & =\begin{pmatrix}cos(a')*oldX-sin(a')*oldY+0 \\sin(a')*oldX+cos(a')*oldY+0 \\1 \\\end{pmatrix} \\& =\begin{pmatrix}\rho*cos(a')*cos(\theta)-\rho*sin(a')*sin(\theta) \\\rho*sin(a')*cos(\theta)+\rho*cos(a')*sin(\theta) \\1 \\\end{pmatrix} \\& =\begin{pmatrix}\rho*cos(\theta + a') \\\rho*sin(\theta + a') \\1 \\\end{pmatrix}\end{aligned}$$

<div>  
    <h1>transform: rotate(a)</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="rotate(30)"></rect>  
    </svg>

    <h1>transform: matrix(cos(a'), sin(a'), -sin(a'), cos(a'), 0, 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(0.866025,0.5,-0.5,0.866025,0,0)"></rect>
    </svg>
</div>

⑦ transform: rotate(a, x, y)

当 rotate 函数被指定旋转点后,情况稍微复杂了一点。

由于函数本质上控制的是画布本身,也可以理解为坐标系本身。

所以,如果想要坐标系上的某一个图形围绕具体一个点旋转,则需要以下三个步骤:

第一、将旋转点从坐标系原点,移动至指定点;
第二、该指定点默认为坐标系原点,开始旋转;
第三、为了保持旋转时其他图案的不变,将坐标系原点从指定点移动回初始点位。

所以,通常指定点的旋转,会采用  <translate(x, y)><rotate(a)><translate(-x, -y)> 的方式。

translate 中的参数 xy 即为 rotate(a, x, y) 中的指定点坐标。

那么这种情况,应当如何用 matrix 描述呢?

我们假设上述三个变换矩阵分别为:

$$\begin{cases}translate(x,y)= T_1 =\begin{pmatrix}1 & 0 & x \\0 & 1 & y \\0 & 0 & 1 \\\end{pmatrix} \\rotate(a) = R =\begin{pmatrix}cos(a') & -sin(a') & 0 \\sin(a') & cos(a') & 0 \\0 & 0 & 1 \\\end{pmatrix} \\translate(-x,-y)=T_2=\begin{pmatrix}1 & 0 & -x \\0 & 1 & -y \\0 & 0 & 1 \\\end{pmatrix}\end{cases}$$

则,根据函数执行方式可得矩阵计算方式为:

$$\begin{pmatrix}newX \\newY \\1 \\\end{pmatrix} =T_1 * R * T_2 *\begin{pmatrix}oldX \\oldY \\1 \\\end{pmatrix}$$

即:

$$\begin{aligned}M & = T_1 * R * T_2 \\& =\begin{pmatrix}1 & 0 & x \\0 & 1 & y \\0 & 0 & 1 \\\end{pmatrix}\begin{pmatrix}cos(a') & -sin(a') & 0 \\sin(a') & cos(a') & 0 \\0 & 0 & 1 \\\end{pmatrix}\begin{pmatrix}1 & 0 & -x \\0 & 1 & -y \\0 & 0 & 1 \\\end{pmatrix} \\&=\begin{pmatrix}cos(a') & -sin(a') & x \\sin(a') & cos(a') & y \\0 & 0 & 1 \\\end{pmatrix}\begin{pmatrix}1 & 0 & -x \\0 & 1 & -y \\0 & 0 & 1 \\\end{pmatrix} \\&=\begin{pmatrix}cos(a') & -sin(a') & -x*cos(a')+y*sin(a')+x \\sin(a') & cos(a') & -x*sin(a')-y*cos(a')+y \\0 & 0 & 1\end{pmatrix}\end{aligned}$$

也就是说,变换矩阵为 :
matrix(cos(a'), sin(a'), -sin(a'), cos(a'), -xcos(a')+ysin(a')+x, -xsin(a')-ycos(a')+y)

$$\begin{pmatrix}newX \\newY \\1\end{pmatrix} =\begin{pmatrix}cos(a') & -sin(a') & -x*cos(a')+y*sin(a')+x \\sin(a') & cos(a') & -x*sin(a')-y*cos(a')+y \\0 & 0 & 1\end{pmatrix}\begin{pmatrix}oldX \\oldY \\1 \\\end{pmatrix}$$

<div>
    <h1>transform: rotate(a, x, y)</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="rotate(30,0,100)"></rect>
    </svg>

    <h1>transform: matrix(cos(a'), sin(a'), -sin(a'), cos(a'), -x*cos(a')+y*sin(a')+x, -x*sin(a')-y*cos(a')+y)</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(0.866025, 0.5, -0.5, 0.866025, 50.0, 13.39746)"></rect>
    </svg>
</div>

⑧ transform: skewX(a)

skewX 表示的是 x 轴方向上的倾斜,同样这里将使用三角函数,也同样的,存在弧度制下的:

$$a'=\frac{\pi}{180}*a$$

由于倾斜只发生在 x 轴方向,由此可得:

$$\begin{cases}newX = \Delta x + oldX = tan(a')*oldY + oldX\\newY = oldY\end{cases}$$

故,变换函数为 matrix(1, 0, tan(a'), 1, 0, 0)

$$\begin{aligned}\begin{pmatrix}newX \\newY \\1 \\\end{pmatrix}& =\begin{pmatrix}1 & tan(a') & 0 \\0 & 1 & 0 \\0 & 0 & 1 \\\end{pmatrix}\begin{pmatrix}oldX \\oldY \\1 \\\end{pmatrix} \\& =\begin{pmatrix}1*oldX+tan(a')*oldY+0 \\0*oldX+1*oldY+0 \\1 \\\end{pmatrix}\\&=\begin{pmatrix}tan(a')*oldY + oldX\\oldY \\1 \\\end{pmatrix}\end{aligned}$$

<div>
    <h1>transform: skewX(a)</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="skewX(30)"></rect>
    </svg>

    <h1>transform: matrix(1, 0, tan(a'), 1, 0, 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.577350,1,0,0)"></rect>
    </svg>
</div>

⑨ transform: skewY(a)

skewY 表示的是 y 轴方向的倾斜,原理同上:

$$\begin{cases}newX = oldX \\newY = \Delta y + oldY = tan(a')*oldX + oldY \\\end{cases}$$

可得变换函数 matrix(1, tan(a'), 0, 1, 0, 0)

$$\begin{aligned}\begin{pmatrix}newX \\newY \\1 \\\end{pmatrix}& =\begin{pmatrix}1 & 0 & 0 \\tan(a') & 1 & 0 \\0 & 0 & 1 \\\end{pmatrix}\begin{pmatrix}oldX \\oldY \\1 \\\end{pmatrix} \\& =\begin{pmatrix}1*oldX + 0*oldY + 0 \\ tan(a')*oldX+1*oldY + 0 \\1 \\\end{pmatrix}\\&=\begin{pmatrix}oldX\\tan(a')*oldX+oldY \\1 \\\end{pmatrix}\end{aligned}$$

<div>
    <h1>transform: skewY(a)</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="skewY(30)"></rect>
    </svg>

    <h1>transform: matrix(1, tan(a'), 0, 1, 0, 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.577350,0,1,0,0)"></rect>
    </svg>
</div>

综上,就是 transform 全部函数的计算方式了。

或者也可以认为是它的矩阵运算描述。

当然,代码实现的时候可能会为了减少不必要的矩阵运算,从而做了最优化处理。但是理解它的运算原理,清楚底层的计算逻辑,却是十分有益的。

【此文原创,欢迎转发,禁止搬运】

03-05 22:37