绘制直线

直线是最基础的图形元素。在Canvas中,直线通过路径系统绑制,需要配合moveTo和lineTo两个方法。

基本方法

方法作用
moveTo(x, y)移动画笔到指定位置(不画线)
lineTo(x, y)从当前位置画直线到指定位置

绘制单条直线

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

ctx.beginPath()
ctx.moveTo(50, 50)
ctx.lineTo(250, 150)
ctx.strokeStyle = '#3498db'
ctx.lineWidth = 2
ctx.stroke()

moveTo设置起点,lineTo设置终点,stroke进行描边。

绘制折线

连续使用lineTo可以绑制折线:

ctx.beginPath()
ctx.moveTo(50, 50)
ctx.lineTo(150, 50)
ctx.lineTo(150, 150)
ctx.lineTo(250, 150)
ctx.strokeStyle = '#e74c3c'
ctx.lineWidth = 3
ctx.stroke()

闭合路径

使用closePath可以自动连接终点和起点:

// 不闭合
ctx.beginPath()
ctx.moveTo(50, 200)
ctx.lineTo(100, 250)
ctx.lineTo(50, 300)
ctx.strokeStyle = '#3498db'
ctx.stroke()

// 闭合
ctx.beginPath()
ctx.moveTo(150, 200)
ctx.lineTo(200, 250)
ctx.lineTo(150, 300)
ctx.closePath()
ctx.strokeStyle = '#e74c3c'
ctx.stroke()

绘制网格

function drawGrid(ctx, width, height, step) {
  ctx.strokeStyle = '#ecf0f1'
  ctx.lineWidth = 1
  
  // 垂直线
  for (let x = 0; x <= width; x += step) {
    ctx.beginPath()
    ctx.moveTo(x, 0)
    ctx.lineTo(x, height)
    ctx.stroke()
  }
  
  // 水平线
  for (let y = 0; y <= height; y += step) {
    ctx.beginPath()
    ctx.moveTo(0, y)
    ctx.lineTo(width, y)
    ctx.stroke()
  }
}

drawGrid(ctx, 400, 300, 20)

绘制坐标系

function drawAxis(ctx, x, y, width, height) {
  ctx.strokeStyle = '#333'
  ctx.lineWidth = 2
  
  // X轴
  ctx.beginPath()
  ctx.moveTo(x, y)
  ctx.lineTo(x + width, y)
  ctx.stroke()
  
  // X轴箭头
  ctx.beginPath()
  ctx.moveTo(x + width, y)
  ctx.lineTo(x + width - 10, y - 5)
  ctx.lineTo(x + width - 10, y + 5)
  ctx.closePath()
  ctx.fillStyle = '#333'
  ctx.fill()
  
  // Y轴
  ctx.beginPath()
  ctx.moveTo(x, y)
  ctx.lineTo(x, y - height)
  ctx.stroke()
  
  // Y轴箭头
  ctx.beginPath()
  ctx.moveTo(x, y - height)
  ctx.lineTo(x - 5, y - height + 10)
  ctx.lineTo(x + 5, y - height + 10)
  ctx.closePath()
  ctx.fill()
}

drawAxis(ctx, 50, 250, 300, 200)

绘制折线图

const data = [30, 80, 45, 120, 90, 150, 110]
const startX = 50
const startY = 250
const chartWidth = 300
const chartHeight = 200
const stepX = chartWidth / (data.length - 1)

// 绘制坐标轴
drawAxis(ctx, startX, startY, chartWidth + 30, chartHeight + 30)

// 绘制折线
ctx.beginPath()
ctx.moveTo(startX, startY - data[0])

data.forEach((value, index) => {
  const x = startX + index * stepX
  const y = startY - value
  ctx.lineTo(x, y)
})

ctx.strokeStyle = '#e74c3c'
ctx.lineWidth = 2
ctx.stroke()

// 绘制数据点
data.forEach((value, index) => {
  const x = startX + index * stepX
  const y = startY - value
  
  ctx.beginPath()
  ctx.arc(x, y, 4, 0, Math.PI * 2)
  ctx.fillStyle = '#fff'
  ctx.fill()
  ctx.strokeStyle = '#e74c3c'
  ctx.lineWidth = 2
  ctx.stroke()
})

绘制虚线

通过setLineDash方法设置虚线:

ctx.setLineDash([10, 5])
ctx.beginPath()
ctx.moveTo(50, 50)
ctx.lineTo(350, 50)
ctx.strokeStyle = '#3498db'
ctx.lineWidth = 2
ctx.stroke()

// 重置为实线
ctx.setLineDash([])

绘制带箭头的线

function drawArrow(ctx, fromX, fromY, toX, toY, headLength, headAngle) {
  const angle = Math.atan2(toY - fromY, toX - fromX)
  
  ctx.beginPath()
  ctx.moveTo(fromX, fromY)
  ctx.lineTo(toX, toY)
  ctx.stroke()
  
  // 箭头
  ctx.beginPath()
  ctx.moveTo(toX, toY)
  ctx.lineTo(
    toX - headLength * Math.cos(angle - headAngle),
    toY - headLength * Math.sin(angle - headAngle)
  )
  ctx.lineTo(
    toX - headLength * Math.cos(angle + headAngle),
    toY - headLength * Math.sin(angle + headAngle)
  )
  ctx.closePath()
  ctx.fill()
}

ctx.strokeStyle = '#333'
ctx.fillStyle = '#333'
ctx.lineWidth = 2
drawArrow(ctx, 50, 150, 300, 100, 15, Math.PI / 6)

绘制随机线条图案

for (let i = 0; i < 20; i++) {
  const x1 = Math.random() * 400
  const y1 = Math.random() * 300
  const x2 = Math.random() * 400
  const y2 = Math.random() * 300
  
  ctx.beginPath()
  ctx.moveTo(x1, y1)
  ctx.lineTo(x2, y2)
  ctx.strokeStyle = `rgba(${Math.random() * 255}, ${Math.random() * 255}, ${Math.random() * 255}, 0.5)`
  ctx.lineWidth = Math.random() * 3 + 1
  ctx.stroke()
}

注意事项

1. 必须先调用beginPath

// 错误:没有beginPath会累积之前的路径
ctx.moveTo(50, 50)
ctx.lineTo(100, 100)
ctx.stroke()
ctx.lineTo(200, 100)  // 会从(100,100)继续画
ctx.stroke()

// 正确
ctx.beginPath()
ctx.moveTo(50, 50)
ctx.lineTo(100, 100)
ctx.stroke()

ctx.beginPath()
ctx.moveTo(100, 100)
ctx.lineTo(200, 100)
ctx.stroke()

2. moveTo不只是设置起点

moveTo可以在路径中间"抬起画笔":

ctx.beginPath()
ctx.moveTo(50, 50)
ctx.lineTo(150, 50)
ctx.moveTo(50, 100)  // 移动到新位置
ctx.lineTo(150, 100)
ctx.stroke()

3. 线条端点样式

lineCap影响线条端点外观:

ctx.lineWidth = 15

ctx.lineCap = 'butt'
ctx.beginPath()
ctx.moveTo(50, 50)
ctx.lineTo(200, 50)
ctx.stroke()

ctx.lineCap = 'round'
ctx.beginPath()
ctx.moveTo(50, 100)
ctx.lineTo(200, 100)
ctx.stroke()

ctx.lineCap = 'square'
ctx.beginPath()
ctx.moveTo(50, 150)
ctx.lineTo(200, 150)
ctx.stroke()