状态管理

3 阅读5分钟

Canvas 状态管理与变换(移动、缩放、旋转)+ 矩阵变换 超详细解析

Canvas 2D 渲染上下文(CanvasRenderingContext2D)的状态管理坐标变换是绘制复杂图形、动画、图表的核心,也是新手最容易混淆的知识点。

我会用通俗语言 + 代码示例 + 原理拆解,从基础到进阶讲透:

  1. 状态管理(save() / restore()
  2. 基础变换:平移(translate)、缩放(scale)、旋转(rotate)
  3. 高阶:矩阵变换(transform / setTransform
  4. 变换的核心原理与执行顺序

一、Canvas 状态管理:save() & restore()

1. 什么是 Canvas 状态?

Canvas 的绘图状态不是指你画的图形,而是上下文的配置参数,包含:

  • 变换矩阵(平移、缩放、旋转)
  • 样式:fillStylestrokeStylelineWidthfont...
  • 裁剪区域:clip()
  • 阴影、透明度等属性

状态 ≠ 绘制的图形,图形画上去就固定在画布上,状态是控制怎么画的“规则”。

2. save():保存当前状态

把当前所有状态压入状态栈(类似存档)。

ctx.save(); 

3. restore():恢复上一次保存的状态

从状态栈弹出最后一次保存的状态(类似读档)。

ctx.restore();

4. 核心作用:隔离样式/变换,不污染全局

✅ 最佳实践:每次独立绘制前 save,绘制完 restore,避免变换/样式互相影响。

示例:状态隔离

<canvas id="cvs" width="400" height="200"></canvas>
<script>
  const ctx = cvs.getContext('2d');

  // 初始状态:红色、线宽5
  ctx.strokeStyle = 'red';
  ctx.lineWidth = 5;

  ctx.save(); // ✅ 存档:红色+线宽5

  // 修改状态:蓝色、线宽2
  ctx.strokeStyle = 'blue';
  ctx.lineWidth = 2;
  ctx.strokeRect(50,50,50,50); // 蓝色小矩形

  ctx.restore(); // ✅ 读档:回到红色+线宽5

  ctx.strokeRect(150,50,50,50); // 红色大矩形
</script>

结果:第二个矩形自动恢复为红色、线宽5,没有被蓝色样式污染。


二、Canvas 坐标变换:核心三剑客

Canvas 变换不是移动图形,而是移动/旋转/缩放整个坐标系! 这是 99% 新手的误区:

  • 你没动图形,动的是画布的坐标系
  • 所有变换都是累积生效

前置知识:Canvas 默认坐标系

  • 原点 (0,0):画布左上角
  • X 轴:向右为正
  • Y 轴:向下为正(和数学坐标系相反)

1. 平移:translate(x, y)

把坐标系原点移动到 (x, y) 位置

ctx.translate(x, y);
示例:平移坐标系
ctx.fillStyle = 'red';
ctx.fillRect(0,0,50,50); // 画在左上角(0,0)

ctx.translate(100, 50);   // 原点移到 (100,50)

ctx.fillStyle = 'blue';
ctx.fillRect(0,0,50,50);  // 现在的(0,0) = 原来的(100,50)

2. 旋转:rotate(angle)

坐标系围绕当前原点旋转指定弧度

ctx.rotate(弧度);

⚠️ 注意:

  1. 参数是弧度,不是角度 → 角度转弧度:弧度 = 角度 * Math.PI / 180
  2. 默认顺时针旋转(Y轴向下导致)
  3. 旋转中心 = 当前坐标系原点
示例:旋转矩形(先平移再旋转,让矩形绕中心转)
// 想让矩形绕自己中心旋转
ctx.translate(200, 100);   // 原点移到画布中心
ctx.rotate(Math.PI / 4);    // 旋转45度
ctx.fillRect(-25, -25, 50,50); // 中心对齐原点

✅ 标准旋转用法:先平移到目标点 → 再旋转 → 再绘制


3. 缩放:scale(xScale, yScale)

坐标系按比例缩放

ctx.scale(scaleX, scaleY);
  • >1:放大
  • 0~1:缩小
  • -1:翻转(水平/垂直镜像)

⚠️ 副作用:

  • 缩放会同时放大线宽、阴影、坐标位置
  • 缩放是累积的:scale(2,2) 执行两次 = 放大4倍
示例:缩放 + 镜像
ctx.scale(2, 2);    // 放大2倍
ctx.fillRect(50,50,50,50); // 实际大小 100x100

ctx.scale(-1, 1);  // 水平翻转
ctx.fillText('翻转文字', -200, 200);

4. 变换的执行顺序(超级重要)

Canvas 变换是矩阵乘法顺序不可逆

标准正确顺序(固定口诀)

  1. translate(x, y) → 移动原点到目标位置
  2. rotate(angle) → 绕原点旋转
  3. scale(sx, sy) → 缩放
  4. 绘制图形

错误顺序会导致完全不同的结果


三、状态管理 + 变换 综合实战

这是最常用的组合:save → 变换 → 绘制 → restore

const ctx = cvs.getContext('2d');

// 绘制第一个图形:红色旋转矩形
ctx.save();
ctx.translate(100, 100);
ctx.rotate(Math.PI/6);
ctx.fillStyle = 'red';
ctx.fillRect(-30,-30,60,60);
ctx.restore(); // 恢复初始状态

// 绘制第二个图形:蓝色矩形,不受上面变换影响
ctx.fillStyle = 'blue';
ctx.fillRect(200,100,60,60);

结果:两个图形完全独立,变换互不干扰。


四、高阶:矩阵变换(transform / setTransform)

所有 translaterotatescale 本质都是矩阵变换的语法糖。 掌握矩阵,你可以实现任意线性变换(斜切、组合变换等)。

1. 变换矩阵公式

Canvas 使用 3x3 仿射变换矩阵

[ a  c  e ]
[ b  d  f ]
[ 0  0  1 ]

坐标变换规则:

x = a * 旧x + c * 旧y + e
新y = b * 旧x + d * 旧y + f

2. 两个核心 API

① transform(a, b, c, d, e, f)

在当前变换矩阵基础上,叠加新矩阵(累积变换)

ctx.transform(a,b,c,d,e,f);
② setTransform(a, b, c, d, e, f)

重置矩阵为单位矩阵,再应用新变换(覆盖式,不累积)

ctx.setTransform(a,b,c,d,e,f);

3. 基础变换 对应 矩阵参数

这是最实用的对照表,直接背:

变换对应矩阵参数 (a,b,c,d,e,f)
无变换(单位矩阵)1,0,0,1,0,0
平移 tx,ty1,0,0,1,tx,ty
缩放 sx,sysx,0,0,sy,0,0
旋转 θcosθ,sinθ,-sinθ,cosθ,0,0
水平斜切 α1,0,tanα,1,0,0

4. 矩阵变换示例

示例1:用矩阵实现平移+缩放
// 等价于 ctx.translate(100,50); ctx.scale(2,2);
ctx.transform(2, 0, 0, 2, 100, 50);
ctx.fillRect(0,0,50,50);
示例2:斜切变换(基础变换做不到)
// 水平斜切 30度
ctx.transform(1, 0, Math.tan(Math.PI/6), 1, 100, 100);
ctx.fillRect(0,0,100,50);

五、核心总结(必背)

  1. 状态管理

    • save():保存当前所有样式+变换
    • restore():恢复到上一次保存的状态
    • 作用:隔离绘制,避免污染
  2. 基础变换

    • translate(x,y):移动坐标系原点
    • rotate(弧度):绕原点旋转
    • scale(sx,sy):缩放坐标系
    • 顺序:平移 → 旋转 → 缩放 → 绘制
  3. 矩阵变换

    • 所有变换本质都是矩阵运算
    • transform():叠加变换
    • setTransform():重置变换
    • 参数:(a,b,c,d,e,f) 对应缩放、旋转、平移

一句话记住 Canvas 变换

你从来没有移动过图形,你只是在移动、旋转、缩放用来画图的坐标系。

如果需要,我可以给你做一个可交互的 Canvas 变换演示页面,直观看到每一步变换效果!