CSS: transfrom & transform-origin

1,046 阅读2分钟

理解transform

css中,使用transform来指定一个(或多个)“函数”,表示要进行的变换;本质上,这里其实是设置了一个变换矩阵。 我们举一个简单的例子: 假设在有一个元素的四个顶点位置的齐次坐标分别为: A=[111]A=\begin{bmatrix} 1 \\ 1 \\1 \end{bmatrix} B=[121]B=\begin{bmatrix} 1 \\ 2 \\1 \end{bmatrix} C=[321]C=\begin{bmatrix} 3 \\ 2 \\1 \end{bmatrix} D=[311]D=\begin{bmatrix} 3 \\ 1 \\1 \end{bmatrix} 。如下图所示:(y轴向下为正方向)

1.png

当我们给这个元素指定一个变换

transform: scale(2, 2);

其实就是构建了一个用来缩放的变换矩阵:

[200020001]\begin{bmatrix} 2 & 0 & 0 \\ 0 & 2 & 0 \\ 0 & 0 & 1 \end{bmatrix}

使用这个矩阵对元素的四个顶点进行变换:

A=[200020001][111]=[221]A'= \begin{bmatrix} 2 & 0 & 0 \\ 0 & 2 & 0 \\ 0 & 0 & 1 \end{bmatrix} \begin{bmatrix} 1 \\ 1 \\ 1 \end{bmatrix} = \begin{bmatrix} 2 \\ 2 \\ 1 \end{bmatrix}
B=[200020001][121]=[241]B'= \begin{bmatrix} 2 & 0 & 0 \\ 0 & 2 & 0 \\ 0 & 0 & 1 \end{bmatrix} \begin{bmatrix} 1 \\ 2 \\1 \end{bmatrix} = \begin{bmatrix} 2 \\ 4 \\ 1 \end{bmatrix}
C=[200020001][321]=[641]C'= \begin{bmatrix} 2 & 0 & 0 \\ 0 & 2 & 0 \\ 0 & 0 & 1 \end{bmatrix} \begin{bmatrix} 3 \\ 2 \\ 1 \end{bmatrix} = \begin{bmatrix} 6 \\ 4 \\ 1 \end{bmatrix}
D=[200020001][311]=[621]D'= \begin{bmatrix} 2 & 0 & 0 \\ 0 & 2 & 0 \\ 0 & 0 & 1 \end{bmatrix} \begin{bmatrix} 3 \\ 1 \\1 \end{bmatrix} = \begin{bmatrix} 6 \\ 2 \\ 1 \end{bmatrix}

变换后的结果如图所示:(y轴向下为正方向)

2.png

好像有点问题,这个元素确实按照我们的设置,面积变为了原来的四倍,但是,它的中心点向右下角移动了,这是为什么呢?

其实,在上面我们给出的变换矩阵其实隐含了一个信息:基于画布所在的平面空间进行变换。如果我们想要让上面的元素的中心点不发生变化,那我们就需要指定要基于哪个平面空间(坐标系)进行变换,这其实就是transform-origin属性的作用。

使用transform-origin指定变换基于哪个坐标系

还拿上面的例子举例,我们需要重新指定变换的坐标系:(y轴向下为正方向)

3.png

我们把画布的坐标系记作CC;把这次变换所用的坐标系记为CtransformC_{transform}。则,CtransformC_{transform}的原点的齐次坐标为Otransform=[21.51]O_{transform}=\begin{bmatrix} 2 \\ 1.5 \\ 1 \end{bmatrix}

现在,我们用坐标系CtransformC_{transform}来重新定义A,B,C,DA,B,C,D的坐标,记为Atransfrom,Btransfrom,Ctransfrom,DtransfromA_{transfrom}, B_{transfrom}, C_{transfrom}, D_{transfrom}

A=[10.51]A=\begin{bmatrix} -1 \\ -0.5 \\1 \end{bmatrix} B=[10.51]B=\begin{bmatrix} -1 \\ 0.5 \\1 \end{bmatrix} C=[10.51]C=\begin{bmatrix} 1 \\ 0.5 \\1 \end{bmatrix} D=[10.51]D=\begin{bmatrix} 1 \\ -0.5 \\1 \end{bmatrix}

变换仍然是上面的,应用该变换矩阵:

Atransfrom=[200020001][10.51]=[211]A_{transfrom}'= \begin{bmatrix} 2 & 0 & 0 \\ 0 & 2 & 0 \\ 0 & 0 & 1 \end{bmatrix} \begin{bmatrix} -1 \\ -0.5 \\1 \end{bmatrix} = \begin{bmatrix} -2 \\ -1 \\ 1 \end{bmatrix}
Btransfrom=[200020001][10.51]=[211]B_{transfrom}'= \begin{bmatrix} 2 & 0 & 0 \\ 0 & 2 & 0 \\ 0 & 0 & 1 \end{bmatrix} \begin{bmatrix} -1 \\ 0.5 \\1 \end{bmatrix} = \begin{bmatrix} -2 \\ 1 \\ 1 \end{bmatrix}
Ctransfrom=[200020001][10.51]=[211]C_{transfrom}'= \begin{bmatrix} 2 & 0 & 0 \\ 0 & 2 & 0 \\ 0 & 0 & 1 \end{bmatrix} \begin{bmatrix} 1 \\ 0.5 \\1 \end{bmatrix} = \begin{bmatrix} 2 \\ 1 \\ 1 \end{bmatrix}
Dtransfrom=[200020001][10.51]=[211]D_{transfrom}'= \begin{bmatrix} 2 & 0 & 0 \\ 0 & 2 & 0 \\ 0 & 0 & 1 \end{bmatrix} \begin{bmatrix} 1 \\ -0.5 \\1 \end{bmatrix} = \begin{bmatrix} 2 \\ -1 \\ 1 \end{bmatrix}

需要注意的是,这里变换后的坐标,仍然是基于坐标系CtransformC_{transform}的,我们把它画出来:(y轴向下为正方向)

4.png

变换过程

总结一下,对一个元素进行变换时,transform用来设置变换使用的矩阵,transform-origin则用来指定变换基于哪个坐标系。整体的变换流程为:

  1. 将基于坐标系CC的顶点坐标转换到基于坐标系CtransformC_{transform}的坐标
  2. 应用变换矩阵,变换后的顶点基于CtransformC_{transform}
  3. 将变换后的顶点从坐标系CtransformC_{transform}转换到坐标系CC

我们上面已经解决了上面的第2步,对于1、3,需要涉及到坐标系的转换。

坐标系的转换

对于第1步的坐标转换,其实就是一个如下图所示的平移操作:(y轴向下为正方向,由于没有合适的作图工具,只能每次都提醒一下。。。)

image.png

对于上面的例子,我们已经知道了坐标系CtransformC_{transform}的坐标原点Otransform=[21.5]O_{transform}=\begin{bmatrix} 2 \\ 1.5 \end{bmatrix}。其实只需要将元素的四个顶点都减去OtransformO_{transform}实现平移。但在实现上,平移也是一种变换,我们仍然使用变换矩阵来实现这个变换操作。我们把这个变换记为:

T=[102011.5001]T = \begin{bmatrix} 1 & 0 & -2 \\ 0 & 1 & -1.5 \\ 0 & 0 & 1 \\ \end{bmatrix}

对于第3步来说,其实就是和第1步反着来;变换矩阵为TT的逆:

T1=[102011.5001]T^{-1} = \begin{bmatrix} 1 & 0 & 2 \\ 0 & 1 & 1.5 \\ 0 & 0 & 1 \\ \end{bmatrix}

因此,整体的变换矩阵为:T1MTT^{-1}MT

用canvas模拟transform

我们先创建一个canvas,并且设置它的尺寸:

const canvas = canvasRef.current;
const dpr = window.devicePixelRatio;
const width = canvas.clientWidth * dpr;
const height = canvas.clientHeight * dpr;
canvas.width = width;
canvas.height = height;

然后,获取context进行绘制:

我们使用4个二维向量描述一个元素的顶点,代码中使用Three.js提供的工具来进行坐标处理

const ctx = canvas.getContext('2d');
draw(ctx, points, matrix, transformOrigin);

/**
 * ctx: canvas 2d context
 * points: 四个二维向量
 * transformMatrix: transform指定的变换矩阵
 * transformOrigin: transform-origin指定的原点
 */
function draw(ctx, points, transformMatrix, transformOrigin) {

  const toWorld = new THREE.Matrix3(); // 就是我们上面说的T的逆
  toWorld.elements = [1, 0, 0, 0, 1, 0, transformOrigin.x, transformOrigin.y, 1];
  const toLocal = toWorld.clone().invert(); // 就是我们上面说的T

  // 把三个变换组合起来
  const effect = new THREE.Matrix3().multiplyMatrices(transformMatrix, toLocal).premultiply(toWorld);

  // 对四个顶点都进行变换
  const pointsTransfromed = points.map(p => p.clone().applyMatrix3(effect));
  
  // 保存context的当前状态 后续进行恢复
  ctx.save();
  // 先用半透明的红色绘制变换前的元素
  ctx.fillStyle = 'rgba(255, 0, 0, 0.5)';
  baseDraw(points);

  // 再用半透明的蓝色绘制变换后的元素
  ctx.fillStyle = 'rgba(0, 0, 255, 0.5)';
  baseDraw(pointsTransfromed);
  ctx.restore();

  // 绘制元素的函数
  function baseDraw(points) {
    ctx.beginPath();
    
    points.forEach(({ x, y }, i) => {
      if(i === 0) {
        ctx.moveTo(x, y);
        return
      }

      ctx.lineTo(x, y);
    })
    ctx.closePath();

    ctx.fill();
  }
}

上面的代码中的transformOriginCSS设置还略有不同,我们直接给出了这个transformOigin的坐标,而CSS是通过一些相对值确定的。

transform的组合

在上面,我们仅举了一个很简单的变换scale(2, 2),实际上,transform可以指定多个变换:

transform: translate(1, 1) scale(2, 2) rotate(30deg, 45deg);

就像我们之前说的,这三个变换其实每一个都是一个变换矩阵,由于矩阵的乘法满足结合律,所以,直接把它们的变换矩阵从左到右按顺序乘起来即可MtranslateMscaleMrotateM_{translate}M_{scale}M_{rotate};由于坐标变换时,齐次坐标在矩阵的右边MtranslateMscaleMrotatePM_{translate}M_{scale}M_{rotate}P,效果上其实是从右到左逐个应用变换的。MDN文档对此也有相应描述:

One or more of the CSS transform functions to be applied. The transform functions are multiplied in order from left to right, meaning that composite transforms are effectively applied in order from right to left.

文档中说说的“函数”从左到右按顺序相乘,其实是它们对应的变换矩阵按顺序相乘。