变换操作

Canvas变换操作是图形绑制中极为重要的功能,它允许开发者对绑图上下文进行平移、旋转、缩放等几何变换,从而实现复杂的图形效果和动画。

什么是变换操作

在Canvas中,变换操作实际上是对坐标系的变换,而不是直接变换图形本身。当你调用变换方法时,整个绑图坐标系发生了改变,之后绑制的所有图形都会受到这个变换的影响。

这种设计理念非常重要,理解它可以帮助你正确使用变换:

  • translate(x, y):将坐标系原点移动到新位置
  • rotate(angle):将坐标系绕原点旋转指定角度
  • scale(x, y):缩放坐标系的单位长度

变换的本质

Canvas使用一个3x3的变换矩阵来描述坐标系的变换状态:

| a  c  e |
| b  d  f |
| 0  0  1 |

其中:

  • a, d:控制缩放
  • b, c:控制倾斜/错切
  • e, f:控制平移

每个点(x, y)经过变换后变为:

x' = a*x + c*y + e
y' = b*x + d*y + f

变换的累积效应

变换操作是累积的,每次调用变换方法都会与当前的变换矩阵相乘:

ctx.translate(100, 0)  // 原点移到(100, 0)
ctx.translate(50, 0)   // 原点移到(150, 0),不是(50, 0)

变换顺序的重要性

变换的顺序会直接影响最终结果,因为矩阵乘法不满足交换律:

// 先平移再旋转
ctx.translate(100, 100)
ctx.rotate(Math.PI / 4)
// 结果:图形绕(100, 100)旋转

// 先旋转再平移
ctx.rotate(Math.PI / 4)
ctx.translate(100, 100)
// 结果:图形沿旋转后的方向移动

坐标系思维

理解变换的关键是建立"坐标系思维":

  1. 默认状态:原点(0,0)在画布左上角,X轴向右,Y轴向下
  2. translate后:原点移动到新位置,绑制坐标相对于新原点
  3. rotate后:整个坐标系旋转,X轴和Y轴方向改变
  4. scale后:单位长度改变,同样的坐标值代表不同的实际距离

变换方法概览

方法说明
translate(x, y)平移
rotate(angle)旋转
scale(x, y)缩放
transform(a, b, c, d, e, f)矩阵变换
setTransform(a, b, c, d, e, f)设置变换矩阵
resetTransform()重置变换矩阵

状态管理

方法说明
save()保存当前状态
restore()恢复之前状态

平移变换

translate方法移动画布原点:

const canvas = document.getElementById('canvas')
const ctx = canvas.getContext('2d')

// 原始位置绘制
ctx.fillStyle = '#3498db'
ctx.fillRect(0, 0, 50, 50)

// 平移后绘制
ctx.translate(100, 50)
ctx.fillStyle = '#e74c3c'
ctx.fillRect(0, 0, 50, 50)

// 再次平移
ctx.translate(100, 50)
ctx.fillStyle = '#2ecc71'
ctx.fillRect(0, 0, 50, 50)

平移变换演示

平移变换效果

旋转变换

rotate方法围绕原点旋转画布:

ctx.fillStyle = '#3498db'
ctx.fillRect(50, 50, 100, 30)

// 旋转30度
ctx.rotate(Math.PI / 6)
ctx.fillStyle = '#e74c3c'
ctx.fillRect(50, 50, 100, 30)

// 旋转60度
ctx.rotate(Math.PI / 3)
ctx.fillStyle = '#2ecc71'
ctx.fillRect(50, 50, 100, 30)

旋转变换演示

旋转变换效果

缩放变换

scale方法缩放画布:

ctx.fillStyle = '#3498db'
ctx.fillRect(0, 0, 50, 50)

// 放大2倍
ctx.scale(2, 2)
ctx.fillStyle = '#e74c3c'
ctx.fillRect(60, 0, 50, 50)

// 缩小0.5倍
ctx.scale(0.5, 0.5)
ctx.fillStyle = '#2ecc71'
ctx.fillRect(280, 0, 50, 50)

缩放变换演示

缩放变换效果

变换组合

// 组合变换:先平移,再旋转,最后缩放
ctx.save()
ctx.translate(150, 100)  // 移动到中心
ctx.rotate(Math.PI / 4)  // 旋转45度
ctx.scale(1.5, 1.5)      // 放大1.5倍
ctx.fillStyle = '#3498db'
ctx.fillRect(-25, -25, 50, 50)
ctx.restore()

变换组合演示

变换组合效果

矩阵变换

transform方法使用变换矩阵:

| a c e |
| b d f |
| 0 0 1 |

x' = a*x + c*y + e
y' = b*x + d*y + f
// 水平倾斜
ctx.transform(1, 0, 0.5, 1, 0, 0)
ctx.fillRect(50, 50, 100, 50)

// 垂直倾斜
ctx.transform(1, 0.5, 0, 1, 0, 0)
ctx.fillRect(200, 50, 100, 50)

矩阵变换演示

矩阵变换效果

状态保存与恢复

// 保存当前状态
ctx.save()

// 应用变换
ctx.translate(100, 100)
ctx.rotate(Math.PI / 4)
ctx.scale(2, 2)

// 绘制
ctx.fillStyle = '#3498db'
ctx.fillRect(-25, -25, 50, 50)

// 恢复状态
ctx.restore()

// 此时变换已重置
ctx.fillStyle = '#e74c3c'
ctx.fillRect(0, 0, 50, 50)

状态管理演示

状态保存与恢复

绕点旋转

function rotateAroundPoint(ctx, x, y, angle) {
  ctx.translate(x, y)
  ctx.rotate(angle)
  ctx.translate(-x, -y)
}

// 使用
ctx.save()
rotateAroundPoint(ctx, 100, 100, Math.PI / 4)
ctx.fillRect(75, 75, 50, 50)
ctx.restore()

绕点旋转演示

绕指定点旋转

镜像翻转

// 水平镜像
ctx.save()
ctx.translate(canvas.width, 0)
ctx.scale(-1, 1)
ctx.drawImage(img, 0, 0)
ctx.restore()

// 垂直镜像
ctx.save()
ctx.translate(0, canvas.height)
ctx.scale(1, -1)
ctx.drawImage(img, 0, 0)
ctx.restore()

镜像翻转演示

镜像翻转效果

注意事项

变换顺序

// 变换顺序很重要!
// 先平移再旋转 vs 先旋转再平移 结果不同

// 方式1:先平移再旋转
ctx.translate(100, 100)
ctx.rotate(Math.PI / 4)

// 方式2:先旋转再平移
ctx.rotate(Math.PI / 4)
ctx.translate(100, 100)

累积效应

// 变换会累积
ctx.translate(10, 10)  // 总位移: (10, 10)
ctx.translate(20, 20)  // 总位移: (30, 30)

// 使用save/restore或setTransform重置
ctx.setTransform(1, 0, 0, 1, 0, 0)  // 重置为单位矩阵

性能考虑

// 避免在循环中频繁save/restore
// 不推荐
for (let i = 0; i < 1000; i++) {
  ctx.save()
  ctx.translate(i, 0)
  ctx.fillRect(0, 0, 10, 10)
  ctx.restore()
}

// 推荐:使用setTransform
for (let i = 0; i < 1000; i++) {
  ctx.setTransform(1, 0, 0, 1, i, 0)
  ctx.fillRect(0, 0, 10, 10)
}
ctx.setTransform(1, 0, 0, 1, 0, 0)