卷一卷CSS的transform实现原理,头发又掉了10根!!!

1,452 阅读7分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第15天,点击查看活动详情

今天内卷的知识点是css属性transformtransform的英文翻译是变换,它可以对元素进行平移、旋转、缩放、倾斜等操作,这些操作都是通过矩阵来实现的,所以我们可以通过矩阵来理解transform的工作原理。

1. 矩阵

矩阵是不是都忘记了?没关系,我们先来复习一下矩阵的知识。

1.1 矩阵的定义

矩阵是一个二维数组,它的元素是实数,矩阵的元素可以用A[i][j]来表示,其中i表示行,j表示列,矩阵的行数和列数分别用mn 表示,所以矩阵的元素个数为m * n

在数学中,矩阵的是按照行优先的顺序来排列的,也就是说,矩阵的第一个元素是A[0][0],第二个元素是A[0][1] ,第三个元素是A[0][2],以此类推,第n个元素是A[0][n - 1],第n + 1个元素是A[1][0],第n + 2个元素是A[1][1],以此类推。

上面这些都是数学上的定义,那么在计算机中,矩阵是怎么表示的呢?我们可以用二维数组来表示矩阵,比如下面这个矩阵:

$$
\begin{bmatrix}
1 & 2 & 3 \
4 & 5 & 6 \
7 & 8 & 9
\end{bmatrix}
$$

我们可以用下面的二维数组来表示:

const matrix = [
    [1, 2, 3],
    [4, 5, 6],
    [7, 8, 9],
];

也就是对于我们不搞数学的人来说,矩阵就是一个二维数组,所以不理解没关系,知道它是一个二维数组就行了。

1.2 矩阵的运算

矩阵的运算有加法、减法、乘法、除法等,这里我们只讲乘法,因为我们只需要知道矩阵乘法就可以理解transform的工作原理了。

矩阵的乘法有两种,一种是矩阵乘以一个数,另一种是矩阵乘以另一个矩阵,这里我们只讲矩阵乘以另一个矩阵。

先来从简单的来,就是一个矩阵乘以一个数,比如下面这个矩阵,后面都直接用二维数组来表达矩阵了:

const matrix = [
    [1, 2, 3],
    [4, 5, 6],
    [7, 8, 9],
];

我们可以把这个矩阵乘以一个数,比如2,那么结果就是:

const result = [
    [1 * 2, 2 * 2, 3 * 2],
    [4 * 2, 5 * 2, 6 * 2],
    [7 * 2, 8 * 2, 9 * 2],
];

// 运算结果
// [
//   [2, 4, 6],
//   [8, 10, 12],
//   [14, 16, 18],
// ]

我们可以看到,矩阵乘以一个数,就是把矩阵中的每一个元素都乘以这个数,这个很好理解。

那么矩阵乘以另一个矩阵呢?我们先来看一个例子:

const matrix1 = [
    [1, 2, 3],
    [4, 5, 6],
    [7, 8, 9],
];

const matrix2 = [
    [1, 2, 3],
    [4, 5, 6],
    [7, 8, 9],
];

我们把这两个矩阵相乘,结果是:

const result = [
    [1 * 1 + 2 * 4 + 3 * 7, 1 * 2 + 2 * 5 + 3 * 8, 1 * 3 + 2 * 6 + 3 * 9],
    [4 * 1 + 5 * 4 + 6 * 7, 4 * 2 + 5 * 5 + 6 * 8, 4 * 3 + 5 * 6 + 6 * 9],
    [7 * 1 + 8 * 4 + 9 * 7, 7 * 2 + 8 * 5 + 9 * 8, 7 * 3 + 8 * 6 + 9 * 9],
];

// 运算结果
// [
//   [30, 36, 42],
//   [66, 81, 96],
//   [102, 126, 150],
// ]

这个就有点难理解了,我们先来看一下矩阵乘以另一个矩阵的定义:

矩阵乘法是指两个矩阵的乘积,其定义为:若矩阵A的列数等于矩阵B的行数,则称矩阵A与矩阵B可相乘,记作AB,其乘积C是一个矩阵,其行数等于矩阵A的行数,列数等于矩阵B的列数,且C的第i行第j列的元素是矩阵A的第i行与矩阵B的第j列的对应元素的乘积之和。

这个定义有点绕,我们来看一下例子:

const matrix1 = [
    [1, 2, 3],
    [4, 5, 6],
];

const matrix2 = [
    [1, 2],
    [3, 4],
    [5, 6],
];

我们把这两个矩阵相乘,结果是:

const result = [
    [1 * 1 + 2 * 3 + 3 * 5, 1 * 2 + 2 * 4 + 3 * 6],
    [4 * 1 + 5 * 3 + 6 * 5, 4 * 2 + 5 * 4 + 6 * 6],
];

// 运算结果
// [
//   [22, 28],
//   [49, 64],
// ]

我们可以看到,矩阵乘以另一个矩阵,就是把矩阵中的每一行的每一个元素乘以另一个矩阵中的每一列的每一个元素,然后把结果相加,这个结果就是矩阵乘以另一个矩阵的结果。

还是有点绕,我们来解析一下运算过程:

result[0][0] = matrix1[0][0] * matrix2[0][0] + matrix1[0][1] * matrix2[1][0] + matrix1[0][2] * matrix2[2][0];
result[0][1] = matrix1[0][0] * matrix2[0][1] + matrix1[0][1] * matrix2[1][1] + matrix1[0][2] * matrix2[2][1];
result[1][0] = matrix1[1][0] * matrix2[0][0] + matrix1[1][1] * matrix2[1][0] + matrix1[1][2] * matrix2[2][0];
result[1][1] = matrix1[1][0] * matrix2[0][1] + matrix1[1][1] * matrix2[1][1] + matrix1[1][2] * matrix2[2][1];

单独拿出来看,就很好理解了,看result[0][0]这个结果,用图标识一下:

image.png

上面图解析了result[0][0]的运算过程,我们可以看到,result[0][0]的值是由第一个矩阵的第一行的每一个元素乘以第二个矩阵的第一列的每一个元素,然后把结果相加得到的。

其他的result[0][1]result[1][0]result[1][1]的运算过程也是一样的,拿result[0][1] 来看,就是用第一个矩阵的第一行的每一个元素乘以第二个矩阵的第二列的每一个元素,然后把结果相加得到的。

用代码来实现一下:

function matrixMultiply(matrix1, matrix2) {
    const result = [];
    for (let i = 0; i < matrix1.length; i++) {
        result[i] = [];
        for (let j = 0; j < matrix2[0].length; j++) {
            let sum = 0;
            for (let k = 0; k < matrix1[0].length; k++) {
                sum += matrix1[i][k] * matrix2[k][j];
            }
            result[i][j] = sum;
        }
    }
    return result;
}

感兴趣的阅读一些代码实现,运行一下代码效果,矩阵就讲到这里了。

2. 矩阵在 transform 中的应用

我们知道,transformCSS中的一个属性,用来对元素进行变换,比如平移、旋转、缩放等,我们可以通过transform 属性来实现一些动画效果,比如旋转木马、3D翻转等。

transform属性可以接受多个变换函数,比如:

div {
    width: 100px;
    height: 100px;
    background-color: red;
    transform: rotate(30deg) translate(100px, 100px);
}

这个属性接受了两个变换函数,分别是旋转、平移,那么矩阵在这里起到了什么作用呢?

先来看看效果: image.png

如果我们将这两个变换函数换个顺序,比如:

div {
    width: 100px;
    height: 100px;
    background-color: red;
    transform: translate(100px, 100px) rotate(30deg);
}

再来看看效果: image.png

对比一下,我们可以发现,两个效果是不一样的,这就是矩阵在transform中起到的作用。

3. 获取 transform 的矩阵

上面我们说了,transform 属性可以接受多个变换函数,但是我们也不知道上面为什么会有这种效果,就换了个顺序,效果就不一样了,我们需要知道transform 属性的矩阵是怎么计算的,才能知道为什么会有这种效果。

所以我们需要获取transform属性的矩阵,然后再去计算一下,看看是不是我们想要的结果。

我们可以通过getComputedStyle来获取transform属性的矩阵,比如:

const div = document.querySelector('div');
const matrix = getComputedStyle(div).transform;

注意:如果transform属性没有设置,那么getComputedStyle获取到的transform属性的值是none

这样我们就可以获取到transform属性的矩阵了,但是这个矩阵是一个字符串,我们需要把它转换成一个矩阵,然后再去计算一下,看看是不是我们想要的结果。

先将矩阵转换成一个数组,然后再转换成一个二维数组:

const matrix = getComputedStyle(div).transform;
const matrixArr = /matrix((.*))/.exec(matrix)[1].split(',').map(item => Number(item.trim()));
const matrix2d = [
    [matrixArr[0], matrixArr[1], matrixArr[4]],
    [matrixArr[2], matrixArr[3], matrixArr[5]],
    [0, 0, 1]
];

现在我们拿到的矩阵是一个二维数组,如果我们不加任何变换,那么这个矩阵就是dom的初始矩阵,也就是:

const matrix2d = [
    [1, 0, 0],
    [0, 1, 0],
    [0, 0, 1]
];

那么我们可以通过这个矩阵,来计算transform属性的矩阵,比如:

const matrix2d = [
    [1, 0, 0],
    [0, 1, 0],
    [0, 0, 1]
];

const rotateMatrix = [
    [Math.cos(30 * Math.PI / 180), Math.sin(30 * Math.PI / 180), 0],
    [-Math.sin(30 * Math.PI / 180), Math.cos(30 * Math.PI / 180), 0],
    [0, 0, 1]
];

const translateMatrix = [
    [1, 0, 100],
    [0, 1, 100],
    [0, 0, 1]
];

let result = matrixMultiply(rotateMatrix, matrix2d);
result = matrixMultiply(translateMatrix, result);

console.log(result.toString());

// 0.8660254037844386,0.5,100, -0.5,0.8660254037844386,100, 0,0,1

这样我们就可以计算出transform属性的矩阵了,然后再去看看是不是我们想要的结果。

注意:上面计算出来的矩阵需要如果使用matrix函数,根据matrix函数的参数顺序,需要把值的位置交换一下,下面列出了顺序列表,建议大家自行尝试,不要喂到嘴里。

matrix( scaleX(), skewY(), skewX(), scaleY(), translateX(), translateY() )

4. transform 属性的矩阵计算

上面其实已经写了矩阵的计算了,这里主要讲的是transform属性的矩阵计算的原理。

我们写一个transform属性的时候如果需要加上多个变换,那么我们需要把这些变换的矩阵相乘,比如:

div {
    transform: translate(100px, 100px) rotate(30deg);
}

这个时候浏览器加载是从右往左的,也就是先计算rotate的矩阵,然后再计算translate的矩阵,最后再把这两个矩阵相乘,得到最终的矩阵。

按照这个规则,我们用上面写的矩阵计算函数,来计算一下:

const matrix2d = [
    [1, 0, 0],
    [0, 1, 0],
    [0, 0, 1]
];

const rotateMatrix = [
    [Math.cos(30 * Math.PI / 180), Math.sin(30 * Math.PI / 180), 0],
    [-Math.sin(30 * Math.PI / 180), Math.cos(30 * Math.PI / 180), 0],
    [0, 0, 1]
];

const translateMatrix = [
    [1, 0, 100],
    [0, 1, 100],
    [0, 0, 1]
];

let result = matrixMultiply(rotateMatrix, matrix2d);
result = matrixMultiply(translateMatrix, result);
// 0.8660254037844386,0.5,100, -0.5,0.8660254037844386,100, 0,0,1

result = matrixMultiply(translateMatrix, matrix2d);
result = matrixMultiply(rotateMatrix, result);
// 0.8660254037844386,-0.5,0, 0.5,0.8660254037844386,0, 0,0,1

可以看到,两个矩阵的计算结果是不一样的,这就是transform属性的矩阵计算的原理。

也就解释我最开始的问题了,为什么transform属性的translaterotate的顺序不一样,结果是不一样的。

5. 总结

本文主要讲了transform属性的矩阵计算的原理,以及如何计算transform属性的矩阵,希望对大家有所帮助。

6. 参考

[1] CSS3 transform 属性

[2] CSS3 matrix

[2] CSS3 matrix 英文