{T}

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)

参数说明

参数类型说明
xNumberX 轴偏移量
yNumberY 轴偏移量

基本用法

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)

参数说明

参数类型说明
angleNumber旋转角度(弧度,顺时针为正)

基本用法

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)

参数说明

参数类型说明
xNumberX 轴缩放因子
yNumberY 轴缩放因子

基本用法

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 教程目录 | 上一篇图像处理