Canvas 变换操作
本文档详细介绍 Canvas 的坐标变换操作,包括平移、旋转、缩放、变换矩阵等内容。
状态管理
Canvas 使用状态栈来管理绘图状态,包括变换矩阵、样式属性、裁剪区域等。
save() 和 restore()
javascript
// 保存当前状态到栈
ctx.save()
// 恢复到上一个保存的状态
ctx.restore()保存的状态包括:
- 变换矩阵(translate、rotate、scale)
- 裁剪区域(clip)
- 样式属性(fillStyle、strokeStyle、lineWidth 等)
- 阴影属性(shadowColor、shadowBlur 等)
- 字体属性(font、textAlign、textBaseline)
- 合成属性(globalAlpha、globalCompositeOperation)
基本用法
javascript
// 保存初始状态
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)状态栈深度
javascript
// 可以多次保存
ctx.save() // 第 1 层
ctx.save() // 第 2 层
ctx.save() // 第 3 层
// 恢复时按栈的顺序
ctx.restore() // 恢复到第 2 层
ctx.restore() // 恢复到第 1 层
ctx.restore() // 恢复到初始状态平移变换
translate(x, y) 方法将坐标系原点移动到指定位置。
方法签名
javascript
ctx.translate(x, y)参数说明
| 参数 | 类型 | 说明 |
|---|---|---|
x | Number | X 轴偏移量 |
y | Number | Y 轴偏移量 |
基本用法
javascript
// 移动原点到 (100, 100)
ctx.translate(100, 100)
// 绘制的图形相对于新原点
ctx.fillRect(0, 0, 50, 50) // 实际位置:(100, 100)应用示例
绘制重复图案
javascript
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()
}居中绘制
javascript
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) 方法将坐标系绕原点旋转指定角度。
方法签名
javascript
ctx.rotate(angle)参数说明
| 参数 | 类型 | 说明 |
|---|---|---|
angle | Number | 旋转角度(弧度,顺时针为正) |
基本用法
javascript
// 旋转 45 度
ctx.rotate(Math.PI / 4)
ctx.fillRect(0, 0, 100, 50)旋转方向
code
-π/2
↑
π ←—— 原点 ——→ 0
↓
π/2
顺时针旋转:正值
逆时针旋转:负值应用示例
绕中心旋转
javascript
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)
})旋转时钟指针
javascript
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) 方法对坐标系进行缩放。
方法签名
javascript
ctx.scale(x, y)参数说明
| 参数 | 类型 | 说明 |
|---|---|---|
x | Number | X 轴缩放因子 |
y | Number | Y 轴缩放因子 |
基本用法
javascript
// 放大 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不等比缩放
javascript
// 水平拉伸
ctx.scale(2, 1)
ctx.fillRect(50, 50, 50, 50)
// 垂直拉伸
ctx.scale(1, 2)
ctx.fillRect(50, 50, 50, 50)镜像翻转
javascript
// 水平镜像
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)应用示例
适应不同 DPI 屏幕
javascript
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 变换矩阵来进行坐标变换。
矩阵形式
code
| a c e |
| b d f |
| 0 0 1 |参数含义:
a:水平缩放b:垂直倾斜c:水平倾斜d:垂直缩放e:水平平移f:垂直平移
transform() 方法
transform() 方法在当前变换矩阵上叠加新的变换。
javascript
ctx.transform(a, b, c, d, e, f)setTransform() 方法
setTransform() 方法重置变换矩阵并设置新的变换。
javascript
ctx.setTransform(a, b, c, d, e, f)resetTransform() 方法
resetTransform() 方法将变换矩阵重置为单位矩阵。
javascript
ctx.resetTransform()
// 等同于
ctx.setTransform(1, 0, 0, 1, 0, 0)变换矩阵应用
倾斜效果
javascript
// 水平倾斜
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)组合变换
javascript
// 使用 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
)
}变换组合
变换的顺序非常重要,不同的顺序会产生不同的结果。
变换顺序原则
建议顺序:平移 → 旋转 → 缩放
javascript
// ❌ 错误示例:先缩放后平移
ctx.scale(2, 2)
ctx.translate(100, 100) // 实际平移 200, 200
// ✅ 正确示例:先平移后缩放
ctx.translate(100, 100)
ctx.scale(2, 2)原地旋转
javascript
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)绕指定点旋转
javascript
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)
})层叠变换
javascript
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()综合示例
变换工具函数
javascript
// 保存状态并应用变换
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)
})动画变换
javascript
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()复杂图案
javascript
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)常见问题
1. 变换累积问题
问题: 多次调用变换导致意外的结果。
解决方案: 使用 save() 和 restore() 管理状态。
javascript
// ❌ 错误:变换累积
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()2. 旋转中心错误
问题: 图形绕错误的原点旋转。
解决方案: 先平移到旋转中心,旋转后再绘制。
javascript
// ❌ 错误:绕左上角旋转
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)3. 缩放影响线宽
问题: 缩放后线条变粗或变细。
解决方案: 缩放后调整线宽。
javascript
ctx.save()
ctx.scale(2, 2)
ctx.lineWidth = 1 / 2 // 补偿缩放
ctx.strokeRect(10, 10, 50, 50)
ctx.restore()下一步学习
返回:Canvas 教程目录 | 上一篇:图像处理