本文档详细介绍 Canvas 的坐标变换操作,包括平移、旋转、缩放、变换矩阵等内容。
Canvas 使用状态栈来管理绘图状态,包括变换矩阵、样式属性、裁剪区域等。
// 保存当前状态到栈
ctx.save()
// 恢复到上一个保存的状态
ctx.restore()// 保存初始状态
ctx.save()
// 应用变换和样式
ctx.translate(100, 100)
ctx.rotate(Math.PI / 4)
ctx.fillStyle = 'red'
// 绘制图形
ctx.fillRect(-25, -25, 50, 50)
// 恢复初始状态
ctx.restore()
// 继续绘制(使用原始状态)
ctx.fillRect(200, 100, 50, 50)// 可以多次保存
ctx.save() // 第 1 层
ctx.save() // 第 2 层
ctx.save() // 第 3 层
// 恢复时按栈的顺序
ctx.restore() // 恢复到第 2 层
ctx.restore() // 恢复到第 1 层
ctx.restore() // 恢复到初始状态translate(x, y) 方法将坐标系原点移动到指定位置。
ctx.translate(x, y)| 参数 | 类型 | 说明 |
|---|---|---|
x | Number | X 轴偏移量 |
y | Number | Y 轴偏移量 |
// 移动原点到 (100, 100)
ctx.translate(100, 100)
// 绘制的图形相对于新原点
ctx.fillRect(0, 0, 50, 50) // 实际位置:(100, 100)ctx.fillStyle = '#3498db'
for (let i = 0; i < 5; i++) {
ctx.save()
ctx.translate(50 + i * 80, 100)
ctx.fillRect(-25, -25, 50, 50)
ctx.restore()
}function drawAtCenter(drawFn) {
ctx.save()
ctx.translate(canvas.width / 2, canvas.height / 2)
drawFn()
ctx.restore()
}
drawAtCenter(() => {
ctx.beginPath()
ctx.arc(0, 0, 50, 0, Math.PI * 2)
ctx.fill()
})rotate(angle) 方法将坐标系绕原点旋转指定角度。
ctx.rotate(angle)| 参数 | 类型 | 说明 |
|---|---|---|
angle | Number | 旋转角度(弧度,顺时针为正) |
// 旋转 45 度
ctx.rotate(Math.PI / 4)
ctx.fillRect(0, 0, 100, 50) -π/2
↑
π ←—— 原点 ——→ 0
↓
π/2
顺时针旋转:正值
逆时针旋转:负值function rotateAtCenter(x, y, angle, drawFn) {
ctx.save()
ctx.translate(x, y)
ctx.rotate(angle)
drawFn()
ctx.restore()
}
// 绕矩形中心旋转
rotateAtCenter(150, 100, Math.PI / 6, () => {
ctx.fillStyle = '#3498db'
ctx.fillRect(-50, -25, 100, 50)
})function drawClockHand(angle, length, width, color) {
ctx.save()
ctx.translate(200, 200)
ctx.rotate(angle - Math.PI / 2) // 从 12 点方向开始
ctx.beginPath()
ctx.moveTo(0, 0)
ctx.lineTo(length, 0)
ctx.strokeStyle = color
ctx.lineWidth = width
ctx.lineCap = 'round'
ctx.stroke()
ctx.restore()
}
// 时针
drawClockHand(Math.PI / 3, 80, 6, '#333')
// 分针
drawClockHand(Math.PI, 120, 4, '#666')
// 秒针
drawClockHand(Math.PI * 1.5, 140, 2, 'red')scale(x, y) 方法对坐标系进行缩放。
ctx.scale(x, y)| 参数 | 类型 | 说明 |
|---|---|---|
x | Number | X 轴缩放因子 |
y | Number | Y 轴缩放因子 |
// 放大 2 倍
ctx.scale(2, 2)
ctx.fillRect(10, 10, 50, 50) // 实际尺寸:100x100
// 缩小一半
ctx.scale(0.5, 0.5)
ctx.fillRect(10, 10, 50, 50) // 实际尺寸:25x25// 水平拉伸
ctx.scale(2, 1)
ctx.fillRect(50, 50, 50, 50)
// 垂直拉伸
ctx.scale(1, 2)
ctx.fillRect(50, 50, 50, 50)// 水平镜像
ctx.scale(-1, 1)
ctx.translate(-canvas.width, 0)
ctx.drawImage(img, 0, 0)
// 垂直镜像
ctx.scale(1, -1)
ctx.translate(0, -canvas.height)
ctx.drawImage(img, 0, 0)function setupHiDPICanvas(canvas) {
const dpr = window.devicePixelRatio || 1
const rect = canvas.getBoundingClientRect()
canvas.width = rect.width * dpr
canvas.height = rect.height * dpr
const ctx = canvas.getContext('2d')
ctx.scale(dpr, dpr)
canvas.style.width = rect.width + 'px'
canvas.style.height = rect.height + 'px'
return ctx
}Canvas 使用 3x3 变换矩阵来进行坐标变换。
| a c e |
| b d f |
| 0 0 1 |参数含义:
a:水平缩放b:垂直倾斜c:水平倾斜d:垂直缩放e:水平平移f:垂直平移transform() 方法在当前变换矩阵上叠加新的变换。
ctx.transform(a, b, c, d, e, f)setTransform() 方法重置变换矩阵并设置新的变换。
ctx.setTransform(a, b, c, d, e, f)resetTransform() 方法将变换矩阵重置为单位矩阵。
ctx.resetTransform()
// 等同于
ctx.setTransform(1, 0, 0, 1, 0, 0)// 水平倾斜
ctx.transform(1, 0, Math.tan(Math.PI / 6), 1, 0, 0)
ctx.fillRect(100, 100, 100, 50)
// 垂直倾斜
ctx.transform(1, Math.tan(Math.PI / 6), 0, 1, 0, 0)
ctx.fillRect(100, 200, 100, 50)// 使用 transform 实现平移、旋转、缩放
function applyTransform(x, y, angle, scaleX, scaleY) {
const cos = Math.cos(angle)
const sin = Math.sin(angle)
ctx.transform(
cos * scaleX, sin * scaleX,
-sin * scaleY, cos * scaleY,
x, y
)
}变换的顺序非常重要,不同的顺序会产生不同的结果。
建议顺序:平移 → 旋转 → 缩放
// ❌ 错误示例:先缩放后平移
ctx.scale(2, 2)
ctx.translate(100, 100) // 实际平移 200, 200
// ✅ 正确示例:先平移后缩放
ctx.translate(100, 100)
ctx.scale(2, 2)function drawRotatedRect(x, y, width, height, angle) {
ctx.save()
// 1. 移动到目标位置
ctx.translate(x + width / 2, y + height / 2)
// 2. 旋转
ctx.rotate(angle)
// 3. 绘制(相对于中心)
ctx.fillStyle = '#3498db'
ctx.fillRect(-width / 2, -height / 2, width, height)
ctx.restore()
}
drawRotatedRect(100, 100, 150, 80, Math.PI / 6)function rotateAroundPoint(px, py, angle, drawFn) {
ctx.save()
ctx.translate(px, py)
ctx.rotate(angle)
ctx.translate(-px, -py)
drawFn()
ctx.restore()
}
rotateAroundPoint(200, 150, Math.PI / 4, () => {
ctx.fillRect(150, 100, 100, 100)
})function drawSolarSystem() {
const centerX = 300
const centerY = 250
// 太阳
ctx.fillStyle = '#f39c12'
ctx.beginPath()
ctx.arc(centerX, centerY, 30, 0, Math.PI * 2)
ctx.fill()
// 地球轨道
ctx.save()
ctx.translate(centerX, centerY)
const earthAngle = Date.now() / 1000 // 动态角度
ctx.rotate(earthAngle)
ctx.translate(100, 0)
// 地球
ctx.fillStyle = '#3498db'
ctx.beginPath()
ctx.arc(0, 0, 15, 0, Math.PI * 2)
ctx.fill()
// 月球轨道
ctx.save()
ctx.rotate(earthAngle * 12)
ctx.translate(30, 0)
// 月球
ctx.fillStyle = '#95a5a6'
ctx.beginPath()
ctx.arc(0, 0, 5, 0, Math.PI * 2)
ctx.fill()
ctx.restore()
ctx.restore()
}
// 动画循环
function animate() {
ctx.clearRect(0, 0, canvas.width, canvas.height)
drawSolarSystem()
requestAnimationFrame(animate)
}
animate()// 保存状态并应用变换
function withTransform(x, y, angle, scaleX, scaleY, drawFn) {
ctx.save()
ctx.translate(x, y)
if (angle) ctx.rotate(angle)
if (scaleX || scaleY) ctx.scale(scaleX || 1, scaleY || 1)
drawFn()
ctx.restore()
}
// 使用示例
withTransform(200, 150, Math.PI / 4, 1.5, 1.5, () => {
ctx.fillStyle = '#e74c3c'
ctx.fillRect(-25, -25, 50, 50)
})let rotation = 0
function drawRotatingShape() {
ctx.clearRect(0, 0, canvas.width, canvas.height)
ctx.save()
ctx.translate(200, 150)
ctx.rotate(rotation)
// 绘制正方形
ctx.fillStyle = '#3498db'
ctx.fillRect(-40, -40, 80, 80)
ctx.restore()
rotation += 0.02
requestAnimationFrame(drawRotatingShape)
}
drawRotatingShape()function drawFlower(x, y, petals, innerRadius, outerRadius) {
ctx.save()
ctx.translate(x, y)
for (let i = 0; i < petals; i++) {
ctx.save()
ctx.rotate((i / petals) * Math.PI * 2)
ctx.beginPath()
ctx.moveTo(0, 0)
ctx.quadraticCurveTo(innerRadius, -outerRadius / 2, outerRadius, 0)
ctx.quadraticCurveTo(innerRadius, outerRadius / 2, 0, 0)
ctx.fillStyle = `hsl(${(i / petals) * 360}, 70%, 60%)`
ctx.fill()
ctx.restore()
}
// 花心
ctx.fillStyle = '#f39c12'
ctx.beginPath()
ctx.arc(0, 0, innerRadius / 2, 0, Math.PI * 2)
ctx.fill()
ctx.restore()
}
drawFlower(200, 200, 8, 20, 80)问题: 多次调用变换导致意外的结果。
解决方案: 使用 save() 和 restore() 管理状态。
// ❌ 错误:变换累积
ctx.translate(100, 100)
ctx.fillRect(0, 0, 50, 50)
ctx.translate(100, 100) // 实际平移 200, 200
ctx.fillRect(0, 0, 50, 50)
// ✅ 正确:每次变换独立
ctx.save()
ctx.translate(100, 100)
ctx.fillRect(0, 0, 50, 50)
ctx.restore()
ctx.save()
ctx.translate(200, 100)
ctx.fillRect(0, 0, 50, 50)
ctx.restore()问题: 图形绕错误的原点旋转。
解决方案: 先平移到旋转中心,旋转后再绘制。
// ❌ 错误:绕左上角旋转
ctx.rotate(angle)
ctx.fillRect(x, y, width, height)
// ✅ 正确:绕中心旋转
ctx.translate(x + width / 2, y + height / 2)
ctx.rotate(angle)
ctx.fillRect(-width / 2, -height / 2, width, height)问题: 缩放后线条变粗或变细。
解决方案: 缩放后调整线宽。
ctx.save()
ctx.scale(2, 2)
ctx.lineWidth = 1 / 2 // 补偿缩放
ctx.strokeRect(10, 10, 50, 50)
ctx.restore()