Canvas变换操作是图形绑制中极为重要的功能,它允许开发者对绑图上下文进行平移、旋转、缩放等几何变换,从而实现复杂的图形效果和动画。
在Canvas中,变换操作实际上是对坐标系的变换,而不是直接变换图形本身。当你调用变换方法时,整个绑图坐标系发生了改变,之后绑制的所有图形都会受到这个变换的影响。
这种设计理念非常重要,理解它可以帮助你正确使用变换:
Canvas使用一个3x3的变换矩阵来描述坐标系的变换状态:
| a c e |
| b d f |
| 0 0 1 |
其中:
每个点(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)
// 结果:图形沿旋转后的方向移动
理解变换的关键是建立"坐标系思维":
| 方法 | 说明 |
|---|---|
| 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)