4、2D坐标变换:平移、旋转、缩放,用矩阵说话。

说到2D坐标变换,我脑子里第一个蹦出来的词就是「矩阵」。

你想想看,一个点从位置A跑到位置B,背后其实是一堆数学运算。平移、旋转、缩放,这三个操作单独看都不难。但把它们组合起来,用手算?那可就头大了。

矩阵就是来解决这个问题的。它把复杂的变换,变成了一套统一的乘法规则。

4.1 为什么非要用矩阵?

我刚开始做UI框架的时候,也想过不用矩阵。直接加减乘除不就行了?

平移就是 x+dx, y+dy。缩放就是 x*sx, y*sy。旋转稍微麻烦点,但也能用三角函数硬算。

问题出在「组合」上。

比如我要先旋转45度,再平移(10,20),再缩放2倍。如果不用矩阵,你得写三个函数,调三次,中间还得传临时变量。代码又长又容易出错。

用矩阵就不一样了。三个变换各自对应一个矩阵,乘在一起,得到一个「复合矩阵」。然后所有点乘以这个矩阵,一步到位。

核心思想:矩阵让「变换的组合」变成了「矩阵的乘法」。这是线性代数给图形学最大的礼物。

4.2 齐次坐标:给矩阵加点料

这里有个坑。平移是加法,旋转和缩放是乘法。它们不是同一类运算。

如果只用2x2矩阵,你只能表示旋转和缩放。平移?没门。

解决方案是引入「齐次坐标」。说白了,就是把二维的点 (x, y) 变成三维的 (x, y, 1)。

多出来的那个1,就是给平移留的位置。

我的习惯:只要涉及坐标变换,我永远用齐次坐标。哪怕当前只有旋转,我也用3x3矩阵。因为后续加平移时,不用改代码结构。

4.3 三种基本变换的矩阵形式

直接上干货。假设我们有一个点 P = (x, y),齐次坐标下就是 (x, y, 1)。

4.3.1 平移矩阵

把点移动 (tx, ty):

| 1  0  tx |   | x |   | x + tx |
| 0  1  ty | * | y | = | y + ty |
| 0  0  1  |   | 1 |   |   1    |

嗯,很简单。左上角是单位矩阵,右下角是1,平移量放在第三列。

4.3.2 旋转矩阵

绕原点逆时针旋转 θ 角度:

| cosθ  -sinθ  0 |   | x |   | x*cosθ - y*sinθ |
| sinθ   cosθ  0 | * | y | = | x*sinθ + y*cosθ |
|   0      0    1 |   | 1 |   |        1        |

这里要注意,顺时针旋转的话,把 sinθ 的符号反过来就行。

我曾经踩过的坑:在 Canvas 和 SVG 里,Y轴是向下的。所以「逆时针」在屏幕上的表现,和数学课本里是反的。做UI的时候,一定要确认坐标系方向。

4.3.3 缩放矩阵

沿X轴缩放 sx 倍,沿Y轴缩放 sy 倍:

| sx  0   0 |   | x |   | x * sx |
| 0   sy  0 | * | y | = | y * sy |
| 0   0   1 |   | 1 |   |   1    |

如果 sx = sy,就是等比缩放。否则就是拉伸。

4.4 矩阵的组合:从左到右,还是从右到左?

这是初学者最容易搞混的地方。

假设我想先平移,再旋转。对应的矩阵是 T 和 R。那么复合矩阵 M 是 R * T 还是 T * R?

答案是:M = R * T。

为什么?因为矩阵乘法不满足交换律。你先做的变换,矩阵要放在右边。

我个人的记忆方法是:从右往左读。最右边的矩阵最先作用于点。

// 先平移,再旋转
M = R * T

// 应用时:P' = M * P = R * (T * P)
// 先算 T*P(平移),再算 R*(结果)(旋转)

实战经验:我在做手势识别时,需要把触摸点的坐标从屏幕空间变换到视图空间。顺序是:先缩放(适配分辨率),再平移(适配偏移),最后旋转(适配方向)。矩阵组合时,我总在纸上先写一遍顺序,再敲代码。省得 debug 到怀疑人生。

4.5 逆变换:怎么变回去?

有变换就有逆变换。平移的逆是反向平移,旋转的逆是反向旋转,缩放的逆是倒数缩放。

在矩阵里,逆变换就是求逆矩阵。

// 平移矩阵的逆
| 1  0  -tx |
| 0  1  -ty |
| 0  0   1  |

// 旋转矩阵的逆(其实就是转置,因为旋转矩阵是正交矩阵)
| cosθ   sinθ  0 |
| -sinθ  cosθ  0 |
|   0      0   1 |

// 缩放矩阵的逆
| 1/sx  0    0 |
| 0     1/sy 0 |
| 0     0    1 |

为什么要学逆变换?因为你在做「取消操作」或者「坐标反算」时,一定会用到。

4.6 用 SVG 画一张变换流程图

下面这张图,展示了从原始坐标到最终坐标的完整变换链路。

2D坐标变换流程 原始坐标 (x, y) 平移 T 旋转 R 缩放 S P' 复合矩阵 M = S * R * T 最终坐标 P' = M * P = S * (R * (T * P)) 从右往左读:先平移 → 再旋转 → 最后缩放

4.7 代码示例:用矩阵实现变换组合

下面是一个简单的 JavaScript 实现。我用数组表示3x3矩阵,用函数生成每种变换的矩阵,然后演示如何组合。

// 矩阵乘法:3x3 矩阵相乘
function multiplyMatrices(a, b) {
  let result = [
    [0, 0, 0],
    [0, 0, 0],
    [0, 0, 0]
  ];
  for (let i = 0; i < 3; i++) {
    for (let j = 0; j < 3; j++) {
      result[i][j] = a[i][0] * b[0][j] + a[i][1] * b[1][j] + a[i][2] * b[2][j];
    }
  }
  return result;
}

// 平移矩阵
function translationMatrix(tx, ty) {
  return [
    [1, 0, tx],
    [0, 1, ty],
    [0, 0, 1]
  ];
}

// 旋转矩阵(角度制)
function rotationMatrix(angleDeg) {
  let rad = angleDeg * Math.PI / 180;
  let cos = Math.cos(rad);
  let sin = Math.sin(rad);
  return [
    [cos, -sin, 0],
    [sin,  cos, 0],
    [0,    0,   1]
  ];
}

// 缩放矩阵
function scaleMatrix(sx, sy) {
  return [
    [sx, 0,  0],
    [0,  sy, 0],
    [0,  0,  1]
  ];
}

// 组合:先平移(10,20),再旋转45度,最后缩放2倍
let T = translationMatrix(10, 20);
let R = rotationMatrix(45);
let S = scaleMatrix(2, 2);

// 注意顺序:从右往左
let M = multiplyMatrices(S, multiplyMatrices(R, T));

// 应用变换到点 (5, 5)
let point = [5, 5, 1];
let result = [
  M[0][0] * point[0] + M[0][1] * point[1] + M[0][2] * point[2],
  M[1][0] * point[0] + M[1][1] * point[1] + M[1][2] * point[2],
  1
];
console.log(`变换后的坐标: (${result[0].toFixed(2)}, ${result[1].toFixed(2)})`);
// 输出: 变换后的坐标: (-7.07, 24.14)

一个小技巧:在实际项目中,我不会每次都手动算矩阵乘法。我会用 gl-matrix 或者 DOMMatrix 这样的库。但理解原理很重要——至少 debug 时你知道问题出在哪。

4.8 总结一下

2D坐标变换用矩阵,说白了就是三个字:统一、组合、高效。

  • 统一:平移、旋转、缩放都用3x3矩阵表示
  • 组合:多个变换通过矩阵乘法合并成一个
  • 高效:所有点乘以同一个矩阵,一次计算到位

嗯,这就是矩阵的魅力。它把一堆零散的数学运算,变成了一套优雅的代数系统。

下次你写 Canvas 或者 SVG 的变换代码时,不妨想想背后的矩阵。你会发现,那些 API 的调用顺序,其实就是在组装矩阵。

一句话记住:变换的顺序 = 矩阵乘法的顺序,从右往左读。

可可精品资料库