Canvas 变换矩阵:translate/rotate/scale 的心智模型

在 Canvas 里,几乎所有「移动」「旋转」「缩放」相关的问题,最后都会落到三个函数上:translaterotatescale
这一篇不打算推矩阵公式,而是从工程视角讲清楚几件事:变换是怎么叠加的、为什么顺序很重要,以及如何用这套机制围绕任意点旋转和实现画布缩放/平移。

在没有任何变换时:

  • 原点在画布左上角;
  • x 轴向右,y 轴向下。

调用 translate / rotate / scale 时:

  • 本质上是在「移动/旋转/拉伸」当前坐标系;
  • 后续所有绘图命令(包括 fillRectlineTo 等)都会在新的坐标系下执行。

可以用一个直觉模型来理解:

  • 不是图形在动,而是坐标系在动;图形永远是在「当前坐标系」的原点附近被画出来。

translate(dx, dy) 的作用是:

  • 把坐标系的原点从当前 (0, 0) 移动到 (dx, dy)
  • 随后以 (dx, dy) 为新的 (0, 0)

常见用法:

  • translate 到目标位置,再在局部坐标系中画图形;
  • 比如绘制一个以 (cx, cy) 为中心的局部坐标系下的图形,就可以:
1ctx.save();
2ctx.translate(cx, cy);
3// 在 (0, 0) 附近绘制局部图形
4ctx.restore();

这样可以避免在每个点的计算中反复加上 (cx, cy) 偏移。

rotate(angle) 会围绕当前原点把整个坐标系旋转一个角度(弧度制):

  • 之后的所有绘制都会在旋转后的坐标轴下进行;
  • 常见语法是先平移再旋转,这样可以围绕特定点旋转。

如果想围绕某个点 (cx, cy) 旋转一个图形:

  • 思路是:
    1. 把坐标系平移到该点(translate(cx, cy));
    2. 在新原点处旋转坐标系(rotate(angle));
    3. 在局部坐标内绘制图形。

示意结构:

1ctx.save();
2ctx.translate(cx, cy);
3ctx.rotate(angle);
4// 在 (0, 0) 周围画图,例如以原点为中心的矩形/图标
5ctx.restore();

这样图形看起来就是「围绕 (cx, cy) 旋转」的。

scale(sx, sy) 会按给定比例拉伸坐标系:

  • sx > 1:水平方向放大;0 < sx < 1:缩小;
  • sy 同理,对垂直方向生效。

后续所有绘制命令中的坐标和尺寸都会被相应缩放:

  • 例如在 scale(2, 2) 后画 fillRect(0, 0, 10, 10),视觉效果是 20×20 的矩形。

常见用法包括:

  • 实现画布整体缩放(常见于画板/地图类应用);
  • 配合 translate 实现以某个点为中心的缩放。

多个变换调用的顺序会直接影响最终效果:

  • translaterotaterotatetranslate 并不等价。

一个形象的理解方式是:

  • 每次调用变换 API,相当于往「当前变换矩阵」右侧再乘一个矩阵;
  • 后调用的变换更靠近绘制命令,对最终结果影响在更内层。

一个常用的安全写法是:

  • 把「平移到中心 → 旋转 → 绘制」这三步的顺序固定下来;
  • 在需要多重变换时,使用 save/restore 把局部变换和全局变换隔离开。

变换和样式是叠加的,使用不当会导致后续绘制全被「带偏」。
save() / restore() 提供了一套简单的状态栈机制:

  • save():把当前所有状态(包括变换矩阵、样式等)压入栈中;
  • restore():从栈顶弹出状态并恢复。

典型用法:

1ctx.save();
2ctx.translate(cx, cy);
3ctx.rotate(angle);
4// 绘制局部对象
5ctx.restore();
6
7// 这里的坐标系和样式又回到了 save() 之前的状态

这使得在复杂场景中可以把每个局部对象的绘制包裹在一对 save / restore 中,避免相互污染。

在画板、地图、流程图等场景中,常常需要:

  • 整体缩放视图(鼠标滚轮放大/缩小);
  • 平移视图(拖拽画布)。

一个常见实现思路是:

  • 把用户的视图变换映射为 Canvas 的全局变换:
    • 维护一个「当前缩放比例」和「当前平移偏移」;
    • 在每次重绘时,先执行:
      • translate(offsetX, offsetY)
      • scale(scale, scale)
    • 然后在「逻辑坐标系」下绘制所有对象。

这样:

  • 内部对象的逻辑坐标不变;
  • 视图放大/缩小和平移都通过变换矩阵来管理。

在交互处理时,需要做逆变换:

  • 把鼠标事件的屏幕坐标,通过当前缩放与平移还原到逻辑坐标系中;
  • 再用逻辑坐标进行命中测试与选中判断。

可以把这一篇压缩为几条记忆点:

  • translate / rotate / scale 并不是直接在「图形」上操作,而是在修改「坐标系本身」;
  • 围绕任意点旋转或缩放,通常需要先 translate 到该点,再做变换,然后绘制;
  • 变换调用的顺序会叠加,后调用的影响在更内层,使用 save / restore 可以把局部变换与全局状态隔离开;
  • 视图级的缩放与平移(画布移动)也可以通过变换矩阵来管理,同时在交互中使用逆变换把事件坐标还原到逻辑空间。

掌握这套心智模型之后,再往上看 Canvas 的交互、动画和复杂场景,就可以始终围绕「坐标系在变,图形在当前坐标系里画」这条主线来思考。