{T}

Canvas 基础绘图

本文档介绍 Canvas 的基础绘图概念和方法,包括坐标系统、基本形状绘制、路径系统等内容。

Canvas 坐标系统

Canvas 的坐标系是其绘图系统的基础,理解坐标系对于正确绘制图形至关重要。

默认坐标系

code
  0 ──────── X 轴(向右为正)───────→
  │
  │
  │  Canvas 绘图区域
  │
  Y 轴
  (向下为正)
  ↓

特点:

  • 原点位置:(0, 0) 位于画布的左上角
  • X 轴方向:向右为正方向
  • Y 轴方向:向下为正方向
  • 单位:像素(pixel)

画布尺寸

html
<canvas id="myCanvas" width="500" height="300"></canvas>

属性说明:

  • width:画布的实际宽度(像素),默认 300
  • height:画布的实际高度(像素),默认 150
  • 坐标范围:x ∈ [0, width],y ∈ [0, height]
注意

Canvas 尺寸应该通过 HTML 属性或 JavaScript 设置,而不是 CSS。CSS 设置的尺寸只影响显示大小,不影响内部分辨率,会导致图形模糊。

坐标变换方法

方法说明示例
translate(x, y)平移坐标系原点ctx.translate(100, 50)
scale(sx, sy)缩放坐标系ctx.scale(2, 0.5)
rotate(angle)旋转坐标系(弧度)ctx.rotate(Math.PI/4)
transform(a, b, c, d, e, f)应用变换矩阵ctx.transform(1, 0.5, -0.5, 1, 0, 0)
setTransform(a, b, c, d, e, f)设置变换矩阵ctx.setTransform(1, 0, 0, 1, 0, 0)
resetTransform()重置为默认坐标系ctx.resetTransform()

变换矩阵参数说明:

code
| a c e |    a: 水平缩放    c: 垂直倾斜    e: 水平平移
| b d f |    b: 水平倾斜    d: 垂直缩放    f: 垂直平移
| 0 0 1 |

实际应用示例

自定义坐标系

javascript
// 设置数学坐标系(原点在中心,Y轴向上)
function setupMathCoordinateSystem() {
  ctx.save()
  const centerX = canvas.width / 2
  const centerY = canvas.height / 2
  ctx.translate(centerX, centerY)
  ctx.scale(1, -1) // 翻转Y轴,使向上为正方向
}

// 使用自定义坐标系绘制
setupMathCoordinateSystem()
ctx.beginPath()
ctx.arc(0, 0, 50, 0, Math.PI * 2) // 在中心绘制圆
ctx.stroke()
ctx.restore() // 恢复默认坐标系

视口变换

javascript
// 创建视口(只显示画布的一部分)
function setupViewport(x, y, width, height) {
  ctx.save()
  ctx.beginPath()
  ctx.rect(x, y, width, height)
  ctx.clip() // 设置裁剪区域
  ctx.translate(-x, -y) // 移动原点到视口左上角
}

setupViewport(100, 100, 200, 150)
ctx.fillRect(0, 0, 400, 300) // 实际只显示部分内容
ctx.restore()

像素对齐

javascript
// 确保坐标与像素网格对齐(避免模糊)
function drawPixelPerfect(x, y) {
  ctx.save()
  ctx.translate(Math.round(x), Math.round(y))
  ctx.fillRect(0, 0, 10, 10)
  ctx.restore()
}
变换顺序

变换是累积的,顺序不同结果不同。建议遵循:平移 → 旋转 → 缩放 的顺序。


矩形绘制

矩形是 Canvas 中唯一可以直接绘制的基本形状,提供了三个专用方法。

方法概览

方法说明返回值
fillRect(x, y, width, height)绘制填充矩形
strokeRect(x, y, width, height)绘制描边矩形
clearRect(x, y, width, height)清除矩形区域

参数说明

  • x:矩形左上角的 X 坐标
  • y:矩形左上角的 Y 坐标
  • width:矩形的宽度(像素)
  • height:矩形的高度(像素)
注意事项
  1. widthheight 必须为正值
  2. 如果 widthheight 为 0,则不会绘制任何内容
  3. 如果 widthheight 为负值,某些浏览器可能抛出错误

基本用法

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

// 绘制填充矩形
ctx.fillStyle = 'red'
ctx.fillRect(10, 10, 100, 50)

// 绘制描边矩形
ctx.strokeStyle = 'blue'
ctx.lineWidth = 3
ctx.strokeRect(10, 70, 100, 50)

// 清除矩形区域
ctx.clearRect(30, 20, 60, 30)

完整示例

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>矩形绘制示例</title>
</head>
<body>
  <canvas id="myCanvas" width="400" height="300"></canvas>

  <script>
    const canvas = document.getElementById('myCanvas')
    const ctx = canvas.getContext('2d')

    // 绘制红色填充矩形
    ctx.fillStyle = 'red'
    ctx.fillRect(50, 50, 120, 80)

    // 绘制蓝色描边矩形
    ctx.strokeStyle = 'blue'
    ctx.lineWidth = 3
    ctx.strokeRect(200, 50, 120, 80)

    // 清除部分区域
    ctx.clearRect(80, 70, 60, 40)

    // 绘制半透明矩形
    ctx.fillStyle = 'rgba(0, 255, 0, 0.5)'
    ctx.fillRect(125, 90, 120, 80)
  </script>
</body>
</html>

rect() 方法

rect(x, y, width, height) 方法创建矩形路径,需要配合 fill()stroke() 使用。

与 fillRect/strokeRect 的区别:

特性rect()fillRect()/strokeRect()
返回值
是否创建路径
是否直接绘制否,需要 fill()/stroke()
适用场景复杂路径组合简单矩形绘制
javascript
// 使用 rect() 创建矩形路径
ctx.beginPath()
ctx.rect(50, 50, 200, 100)

// 填充矩形
ctx.fillStyle = 'blue'
ctx.fill()

// 描边矩形
ctx.strokeStyle = 'red'
ctx.lineWidth = 2
ctx.stroke()

路径系统

路径是 Canvas 绘图的核心概念,所有复杂图形都通过路径系统绘制。

路径方法概览

方法描述
beginPath()开始新路径
closePath()闭合路径
moveTo(x, y)移动画笔到指定点
lineTo(x, y)绘制直线到指定点
rect(x, y, width, height)创建矩形路径
arc(x, y, radius, startAngle, endAngle, anticlockwise)创建圆弧路径
fill()填充路径
stroke()描边路径
clip()创建裁剪路径

路径绘制流程

code
beginPath() → moveTo()/lineTo()/arc()... → closePath() → fill()/stroke()

标准流程:

javascript
// 1. 开始新路径
ctx.beginPath()

// 2. 移动到起始点
ctx.moveTo(50, 50)

// 3. 绘制路径
ctx.lineTo(200, 50)
ctx.lineTo(200, 150)
ctx.lineTo(50, 150)

// 4. 闭合路径(可选)
ctx.closePath()

// 5. 填充或描边
ctx.fillStyle = 'blue'
ctx.fill()

ctx.strokeStyle = 'red'
ctx.lineWidth = 2
ctx.stroke()

beginPath() 的重要性

每次开始新的路径绘制时,必须调用 beginPath(),否则新路径会与之前的路径连接。

javascript
// ❌ 错误示例:没有使用 beginPath()
ctx.moveTo(50, 50)
ctx.lineTo(200, 50)
ctx.strokeStyle = 'blue'
ctx.stroke()

// 这条线会与上一条线连接
ctx.moveTo(50, 100)
ctx.lineTo(200, 150)
ctx.strokeStyle = 'green'
ctx.stroke()

// ✅ 正确示例:使用 beginPath()
ctx.beginPath()
ctx.moveTo(50, 50)
ctx.lineTo(200, 50)
ctx.strokeStyle = 'blue'
ctx.stroke()

ctx.beginPath() // 开始新路径
ctx.moveTo(50, 100)
ctx.lineTo(200, 150)
ctx.strokeStyle = 'green'
ctx.stroke()

线条绘制

moveTo 和 lineTo

  • moveTo(x, y):移动画笔到指定坐标,不绘制线条
  • lineTo(x, y):从当前点绘制直线到指定坐标

绘制直线

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

// 绘制一条直线
ctx.beginPath()
ctx.moveTo(50, 50)   // 起点
ctx.lineTo(350, 250) // 终点
ctx.strokeStyle = 'blue'
ctx.lineWidth = 3
ctx.stroke()

绘制折线

javascript
// 绘制折线
ctx.beginPath()
ctx.moveTo(50, 50)
ctx.lineTo(200, 50)
ctx.lineTo(200, 150)
ctx.lineTo(350, 150)
ctx.strokeStyle = 'green'
ctx.lineWidth = 2
ctx.stroke()

绘制三角形

javascript
// 绘制填充三角形
ctx.beginPath()
ctx.moveTo(200, 50)
ctx.lineTo(100, 150)
ctx.lineTo(300, 150)
ctx.closePath() // 自动闭合路径

ctx.fillStyle = 'rgba(255, 0, 0, 0.5)'
ctx.fill()

ctx.strokeStyle = 'black'
ctx.lineWidth = 2
ctx.stroke()

绘制多边形

javascript
// 绘制五边形
function drawPolygon(ctx, centerX, centerY, radius, sides) {
  ctx.beginPath()
  
  for (let i = 0; i < sides; i++) {
    const angle = (i * 2 * Math.PI / sides) - Math.PI / 2
    const x = centerX + radius * Math.cos(angle)
    const y = centerY + radius * Math.sin(angle)
    
    if (i === 0) {
      ctx.moveTo(x, y)
    } else {
      ctx.lineTo(x, y)
    }
  }
  
  ctx.closePath()
  ctx.fillStyle = 'gold'
  ctx.fill()
  ctx.strokeStyle = 'orange'
  ctx.lineWidth = 2
  ctx.stroke()
}

drawPolygon(ctx, 200, 150, 80, 5)

线条样式

Canvas 提供了多种线条样式属性来控制线条的外观。

基本属性

属性说明默认值可选值
lineWidth线条宽度1正数(像素)
lineCap线条端点样式'butt''butt', 'round', 'square'
lineJoin线条连接样式'miter''miter', 'round', 'bevel'
miterLimit斜接限制10正数
lineDashOffset虚线偏移量0数字

lineCap 线条端点样式

code
butt:    ───────  (平直端点,默认)
round:   ╭─────╮  (圆形端点)
square:  █─────█  (方形端点,延伸半个线宽)
javascript
const lineWidths = ['butt', 'round', 'square']

lineWidths.forEach((cap, index) => {
  ctx.beginPath()
  ctx.lineCap = cap
  ctx.lineWidth = 20
  ctx.moveTo(50, 50 + index * 60)
  ctx.lineTo(350, 50 + index * 60)
  ctx.stroke()
  
  // 绘制参考线
  ctx.beginPath()
  ctx.strokeStyle = 'red'
  ctx.lineWidth = 1
  ctx.moveTo(50, 50 + index * 60)
  ctx.lineTo(350, 50 + index * 60)
  ctx.stroke()
  ctx.strokeStyle = 'black'
})

lineJoin 线条连接样式

code
miter:  尖角连接(默认)
round:  圆角连接
bevel:  斜角连接
javascript
const joins = ['miter', 'round', 'bevel']

joins.forEach((join, index) => {
  ctx.beginPath()
  ctx.lineJoin = join
  ctx.lineWidth = 20
  ctx.moveTo(50 + index * 120, 50)
  ctx.lineTo(100 + index * 120, 150)
  ctx.lineTo(150 + index * 120, 50)
  ctx.stroke()
})

虚线

使用 setLineDash() 方法设置虚线样式。

javascript
// 设置虚线样式 [实线长度, 空白长度]
ctx.setLineDash([10, 5])

ctx.beginPath()
ctx.moveTo(50, 50)
ctx.lineTo(350, 50)
ctx.lineWidth = 2
ctx.stroke()

// 复杂虚线样式
ctx.setLineDash([20, 5, 5, 5])
ctx.beginPath()
ctx.moveTo(50, 100)
ctx.lineTo(350, 100)
ctx.stroke()

// 取消虚线
ctx.setLineDash([])

// 虚线动画
let offset = 0
function animate() {
  ctx.clearRect(0, 0, canvas.width, canvas.height)
  
  ctx.setLineDash([10, 5])
  ctx.lineDashOffset = offset
  
  ctx.beginPath()
  ctx.moveTo(50, 150)
  ctx.lineTo(350, 150)
  ctx.stroke()
  
  offset++
  if (offset > 15) offset = 0
  
  requestAnimationFrame(animate)
}

综合示例

绘制简单图形

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>基础图形绘制</title>
</head>
<body>
  <canvas id="myCanvas" width="500" height="400"></canvas>

  <script>
    const canvas = document.getElementById('myCanvas')
    const ctx = canvas.getContext('2d')

    // 绘制矩形
    ctx.fillStyle = '#3498db'
    ctx.fillRect(20, 20, 100, 80)

    ctx.strokeStyle = '#e74c3c'
    ctx.lineWidth = 3
    ctx.strokeRect(150, 20, 100, 80)

    // 绘制三角形
    ctx.beginPath()
    ctx.moveTo(350, 20)
    ctx.lineTo(300, 100)
    ctx.lineTo(400, 100)
    ctx.closePath()
    ctx.fillStyle = '#2ecc71'
    ctx.fill()

    // 绘制五边形
    ctx.beginPath()
    const centerX = 100
    const centerY = 200
    const radius = 50
    const sides = 5

    for (let i = 0; i < sides; i++) {
      const angle = (i * 2 * Math.PI / sides) - Math.PI / 2
      const x = centerX + radius * Math.cos(angle)
      const y = centerY + radius * Math.sin(angle)
      
      if (i === 0) {
        ctx.moveTo(x, y)
      } else {
        ctx.lineTo(x, y)
      }
    }
    ctx.closePath()
    ctx.fillStyle = '#f39c12'
    ctx.fill()
    ctx.strokeStyle = '#d35400'
    ctx.lineWidth = 2
    ctx.stroke()

    // 绘制星形
    ctx.beginPath()
    const starCenterX = 250
    const starCenterY = 200
    const outerRadius = 50
    const innerRadius = 25
    const points = 5

    for (let i = 0; i < points * 2; i++) {
      const r = i % 2 === 0 ? outerRadius : innerRadius
      const angle = (i * Math.PI / points) - Math.PI / 2
      const x = starCenterX + r * Math.cos(angle)
      const y = starCenterY + r * Math.sin(angle)
      
      if (i === 0) {
        ctx.moveTo(x, y)
      } else {
        ctx.lineTo(x, y)
      }
    }
    ctx.closePath()
    ctx.fillStyle = '#9b59b6'
    ctx.fill()

    // 绘制虚线矩形
    ctx.setLineDash([10, 5])
    ctx.strokeStyle = '#34495e'
    ctx.lineWidth = 2
    ctx.strokeRect(350, 150, 120, 100)
    ctx.setLineDash([])

    // 绘制不同线条样式
    const caps = ['butt', 'round', 'square']
    caps.forEach((cap, index) => {
      ctx.beginPath()
      ctx.lineCap = cap
      ctx.lineWidth = 15
      ctx.strokeStyle = '#1abc9c'
      ctx.moveTo(50, 320 + index * 30)
      ctx.lineTo(450, 320 + index * 30)
      ctx.stroke()
    })
  </script>
</body>
</html>

坐标系可视化

javascript
// 绘制坐标系网格
function drawGrid(ctx, width, height, gridSize) {
  ctx.strokeStyle = '#e0e0e0'
  ctx.lineWidth = 1

  // 绘制垂直线
  for (let x = 0; x <= width; x += gridSize) {
    ctx.beginPath()
    ctx.moveTo(x, 0)
    ctx.lineTo(x, height)
    ctx.stroke()
  }

  // 绘制水平线
  for (let y = 0; y <= height; y += gridSize) {
    ctx.beginPath()
    ctx.moveTo(0, y)
    ctx.lineTo(width, y)
    ctx.stroke()
  }

  // 绘制坐标轴
  ctx.strokeStyle = '#333'
  ctx.lineWidth = 2

  // X 轴
  ctx.beginPath()
  ctx.moveTo(0, height - 1)
  ctx.lineTo(width, height - 1)
  ctx.stroke()

  // Y 轴
  ctx.beginPath()
  ctx.moveTo(1, 0)
  ctx.lineTo(1, height)
  ctx.stroke()
}

常见问题

1. 图形显示模糊

原因: Canvas 实际尺寸与 CSS 显示尺寸不匹配。

解决方案:

javascript
// 设置高 DPI 支持
function setupCanvas(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
}

2. 线条看起来有"毛边"

原因: 线条坐标在像素边界上。

解决方案: 将坐标偏移 0.5 像素。

javascript
// 绘制清晰的 1px 线条
ctx.moveTo(50.5, 10.5)
ctx.lineTo(200.5, 10.5)

3. beginPath() 调用时机

问题: 什么时候需要调用 beginPath()

答案: 每次绘制新的独立图形时都需要调用。以下情况需要调用:

  • 开始绘制新图形
  • 改变填充或描边样式前
  • 需要独立的图形不受之前路径影响时

下一步学习


返回Canvas 教程目录