{T}

Canvas 路径与曲线

本文档详细介绍 Canvas 的路径系统和曲线绘制方法,包括圆弧、贝塞尔曲线等高级路径绘制技术。

圆弧绘制 (arc)

arc() 方法是 Canvas 2D 上下文中用于绘制圆弧或圆形路径的重要方法。

方法签名

javascript
ctx.arc(x, y, radius, startAngle, endAngle, anticlockwise)

参数说明

参数类型说明
xNumber圆心的 X 坐标
yNumber圆心的 Y 坐标
radiusNumber圆的半径
startAngleNumber起始角度(弧度)
endAngleNumber结束角度(弧度)
anticlockwiseBoolean可选,是否逆时针绘制(默认 false,顺时针)

角度系统

code
         -π/2 (270°)
            │
            │
  π (180°) ─┼─ 0 (0°/360°)
            │
            │
          π/2 (90°)

Canvas 角度系统:
- 0° 在右侧(X 轴正方向)
- 角度顺时针增加
- 使用弧度制,不是角度制

角度转换公式:

javascript
// 角度转弧度
const radians = degrees * Math.PI / 180

// 弧度转角度
const degrees = radians * 180 / Math.PI

基本用法

绘制完整圆形

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

// 绘制完整的红色圆形
ctx.beginPath()
ctx.arc(100, 100, 50, 0, Math.PI * 2) // 从 0 到 2π(完整圆)
ctx.fillStyle = 'red'
ctx.fill()

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

绘制扇形

javascript
// 绘制 90 度的蓝色扇形
ctx.beginPath()
ctx.arc(100, 100, 50, 0, Math.PI / 2) // 从 0 到 π/2(90 度)
ctx.lineTo(100, 100) // 回到圆心
ctx.closePath()

ctx.fillStyle = 'blue'
ctx.fill()

绘制半圆

javascript
// 绘制半圆
ctx.beginPath()
ctx.arc(100, 100, 50, 0, Math.PI) // 从 0 到 π(180 度)
ctx.lineTo(100, 100)
ctx.closePath()

ctx.fillStyle = 'green'
ctx.fill()

绘制弧形

javascript
// 绘制弧形(不闭合)
ctx.beginPath()
ctx.arc(100, 100, 50, Math.PI / 4, (Math.PI * 3) / 2) // 从 45° 到 270°
ctx.strokeStyle = 'purple'
ctx.lineWidth = 5
ctx.stroke()

逆时针绘制

javascript
// 逆时针绘制圆弧
ctx.beginPath()
ctx.arc(100, 100, 50, 0, Math.PI / 2, true) // 第 6 个参数为 true
ctx.strokeStyle = 'orange'
ctx.lineWidth = 3
ctx.stroke()

完整示例:多种圆弧绘制

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>
  <style>
    canvas { display: block; margin: 10px; border: 1px solid #ccc; }
  </style>
</head>
<body>
  <h2>完整圆形</h2>
  <canvas id="circleCanvas" width="200" height="200"></canvas>
  
  <h2>扇形</h2>
  <canvas id="sectorCanvas" width="200" height="200"></canvas>
  
  <h2>半圆</h2>
  <canvas id="semicircleCanvas" width="200" height="200"></canvas>
  
  <h2>弧形</h2>
  <canvas id="arcCanvas" width="200" height="200"></canvas>

  <script>
    // 完整圆形
    const ctx1 = document.getElementById('circleCanvas').getContext('2d')
    ctx1.beginPath()
    ctx1.arc(100, 100, 50, 0, Math.PI * 2)
    ctx1.fillStyle = 'red'
    ctx1.fill()
    ctx1.strokeStyle = 'black'
    ctx1.lineWidth = 2
    ctx1.stroke()

    // 扇形
    const ctx2 = document.getElementById('sectorCanvas').getContext('2d')
    ctx2.beginPath()
    ctx2.arc(100, 100, 50, 0, Math.PI / 2)
    ctx2.lineTo(100, 100)
    ctx2.closePath()
    ctx2.fillStyle = 'blue'
    ctx2.fill()

    // 半圆
    const ctx3 = document.getElementById('semicircleCanvas').getContext('2d')
    ctx3.beginPath()
    ctx3.arc(100, 100, 50, 0, Math.PI)
    ctx3.lineTo(100, 100)
    ctx3.closePath()
    ctx3.fillStyle = 'green'
    ctx3.fill()

    // 弧形
    const ctx4 = document.getElementById('arcCanvas').getContext('2d')
    ctx4.beginPath()
    ctx4.arc(100, 100, 50, Math.PI / 4, (Math.PI * 3) / 2)
    ctx4.strokeStyle = 'purple'
    ctx4.lineWidth = 5
    ctx4.stroke()
  </script>
</body>
</html>

实用应用示例

动态时钟

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="clockCanvas" width="300" height="300"></canvas>

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

    function drawClockFace() {
      // 清除画布
      ctx.clearRect(0, 0, canvas.width, canvas.height)
      
      // 绘制表盘
      ctx.beginPath()
      ctx.arc(150, 150, radius, 0, Math.PI * 2)
      ctx.fillStyle = 'white'
      ctx.fill()
      ctx.strokeStyle = '#333'
      ctx.lineWidth = 4
      ctx.stroke()

      // 绘制刻度
      for (let i = 0; i < 12; i++) {
        const angle = (i * Math.PI) / 6 - Math.PI / 2
        
        ctx.beginPath()
        ctx.moveTo(
          150 + Math.cos(angle) * (radius - 15),
          150 + Math.sin(angle) * (radius - 15)
        )
        ctx.lineTo(
          150 + Math.cos(angle) * radius,
          150 + Math.sin(angle) * radius
        )
        ctx.strokeStyle = i % 3 === 0 ? '#333' : '#999'
        ctx.lineWidth = i % 3 === 0 ? 3 : 1
        ctx.stroke()
      }
    }

    function drawHands(hours, minutes, seconds) {
      // 时针
      const hourAngle = ((hours % 12) * Math.PI) / 6 + (minutes * Math.PI) / 360
      drawHand(hourAngle, radius * 0.5, 6, '#333')

      // 分针
      const minuteAngle = (minutes * Math.PI) / 30
      drawHand(minuteAngle, radius * 0.7, 4, '#666')

      // 秒针
      const secondAngle = (seconds * Math.PI) / 30
      drawHand(secondAngle, radius * 0.9, 2, 'red')
    }

    function drawHand(angle, length, width, color) {
      ctx.save()
      ctx.translate(150, 150)
      ctx.rotate(angle - Math.PI / 2)
      
      ctx.beginPath()
      ctx.moveTo(0, 0)
      ctx.lineTo(length, 0)
      ctx.lineWidth = width
      ctx.strokeStyle = color
      ctx.lineCap = 'round'
      ctx.stroke()
      
      ctx.restore()
    }

    function updateClock() {
      const now = new Date()
      drawClockFace()
      drawHands(now.getHours(), now.getMinutes(), now.getSeconds())
      requestAnimationFrame(updateClock)
    }

    updateClock()
  </script>
</body>
</html>

圆形进度条

javascript
function drawProgressCircle(canvasId, percent) {
  const canvas = document.getElementById(canvasId)
  const ctx = canvas.getContext('2d')
  const radius = 50

  ctx.clearRect(0, 0, canvas.width, canvas.height)

  // 背景圆
  ctx.beginPath()
  ctx.arc(100, 100, radius, 0, Math.PI * 2)
  ctx.strokeStyle = '#eee'
  ctx.lineWidth = 10
  ctx.stroke()

  // 进度圆弧
  ctx.beginPath()
  ctx.arc(
    100, 100, radius,
    -Math.PI / 2, // 从顶部开始
    -Math.PI / 2 + percent * Math.PI * 2
  )
  ctx.strokeStyle = '#4CAF50'
  ctx.lineWidth = 10
  ctx.stroke()

  // 百分比文字
  ctx.fillStyle = '#333'
  ctx.font = '16px Arial'
  ctx.textAlign = 'center'
  ctx.fillText(`${Math.round(percent * 100)}%`, 100, 105)
}

// 动画效果
let progress = 0
function animateProgress() {
  if (progress <= 1) {
    drawProgressCircle('progressCanvas', progress)
    progress += 0.01
    requestAnimationFrame(animateProgress)
  }
}

饼图

javascript
function drawPieChart(canvasId, data) {
  const canvas = document.getElementById(canvasId)
  const ctx = canvas.getContext('2d')
  const centerX = 150
  const centerY = 150
  const radius = 100
  
  const total = data.reduce((sum, item) => sum + item.value, 0)
  let startAngle = -Math.PI / 2 // 从顶部开始

  data.forEach((item) => {
    const sliceAngle = (item.value / total) * Math.PI * 2
    const endAngle = startAngle + sliceAngle

    ctx.beginPath()
    ctx.moveTo(centerX, centerY)
    ctx.arc(centerX, centerY, radius, startAngle, endAngle)
    ctx.closePath()
    ctx.fillStyle = item.color
    ctx.fill()

    // 标签
    const midAngle = startAngle + sliceAngle / 2
    const labelX = centerX + Math.cos(midAngle) * 120
    const labelY = centerY + Math.sin(midAngle) * 120
    
    ctx.fillStyle = '#333'
    ctx.font = '12px Arial'
    ctx.textAlign = 'center'
    ctx.fillText(item.label, labelX, labelY)

    startAngle = endAngle
  })
}

// 使用示例
drawPieChart('pieCanvas', [
  { label: '产品A', value: 30, color: '#3498db' },
  { label: '产品B', value: 25, color: '#e74c3c' },
  { label: '产品C', value: 20, color: '#2ecc71' },
  { label: '产品D', value: 15, color: '#f39c12' },
  { label: '其他', value: 10, color: '#9b59b6' }
])

圆角路径 (arcTo)

arcTo() 方法用于绘制圆角路径,特别适合绘制圆角矩形和圆角多边形。

方法签名

javascript
ctx.arcTo(x1, y1, x2, y2, radius)

参数说明

参数类型说明
x1Number控制点 1 的 X 坐标
y1Number控制点 1 的 Y 坐标
x2Number控制点 2 的 X 坐标
y2Number控制点 2 的 Y 坐标
radiusNumber圆角半径

工作原理

code
当前点 (x0, y0)
    │
    │
    └───→ 控制点 1 (x1, y1)
              │
              │
              └───→ 控制点 2 (x2, y2)

arcTo 会:
1. 从当前点画直线到控制点1的方向
2. 在控制点1处绘制圆角
3. 画直线到控制点2的方向

基本用法

绘制圆角矩形

javascript
function drawRoundedRect(ctx, x, y, width, height, radius) {
  ctx.beginPath()
  ctx.moveTo(x + radius, y)
  ctx.arcTo(x + width, y, x + width, y + height, radius)
  ctx.arcTo(x + width, y + height, x, y + height, radius)
  ctx.arcTo(x, y + height, x, y, radius)
  ctx.arcTo(x, y, x + width, y, radius)
  ctx.closePath()
  
  ctx.fillStyle = 'lightblue'
  ctx.fill()
  ctx.strokeStyle = 'darkblue'
  ctx.lineWidth = 2
  ctx.stroke()
}

drawRoundedRect(ctx, 50, 50, 200, 100, 20)

绘制圆角多边形

javascript
function drawRoundedPolygon(ctx, centerX, centerY, radius, sides, cornerRadius) {
  ctx.beginPath()
  
  for (let i = 0; i < sides; i++) {
    const angle = (i * 2 * Math.PI) / sides - Math.PI / 2
    const nextAngle = ((i + 1) * 2 * Math.PI) / sides - Math.PI / 2
    
    const x1 = centerX + Math.cos(angle) * radius
    const y1 = centerY + Math.sin(angle) * radius
    const x2 = centerX + Math.cos(nextAngle) * radius
    const y2 = centerY + Math.sin(nextAngle) * radius
    
    if (i === 0) {
      ctx.moveTo(x1, y1)
    }
    
    const midAngle = (angle + nextAngle) / 2
    const ctrlX = centerX + Math.cos(midAngle) * (radius - cornerRadius)
    const ctrlY = centerY + Math.sin(midAngle) * (radius - cornerRadius)
    
    ctx.arcTo(ctrlX, ctrlY, x2, y2, cornerRadius)
  }
  
  ctx.closePath()
  ctx.fillStyle = 'gold'
  ctx.fill()
}

drawRoundedPolygon(ctx, 150, 150, 80, 5, 15)

完整示例

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>arcTo 示例</title>
  <style>
    canvas { display: block; margin: 10px; border: 1px solid #ccc; }
  </style>
</head>
<body>
  <h2>圆角矩形</h2>
  <canvas id="roundedRectCanvas" width="250" height="150"></canvas>
  
  <h2>圆角箭头</h2>
  <canvas id="arrowCanvas" width="300" height="150"></canvas>

  <script>
    // 圆角矩形
    const ctx1 = document.getElementById('roundedRectCanvas').getContext('2d')
    const width = 200, height = 100, radius = 20
    
    ctx1.beginPath()
    ctx1.moveTo(25, 10)
    ctx1.arcTo(25 + width, 10, 25 + width, 10 + height, radius)
    ctx1.arcTo(25 + width, 10 + height, 25, 10 + height, radius)
    ctx1.arcTo(25, 10 + height, 25, 10, radius)
    ctx1.arcTo(25, 10, 25 + width, 10, radius)
    ctx1.closePath()
    
    ctx1.fillStyle = 'lightblue'
    ctx1.fill()
    ctx1.strokeStyle = 'darkblue'
    ctx1.lineWidth = 2
    ctx1.stroke()

    // 圆角箭头
    const ctx2 = document.getElementById('arrowCanvas').getContext('2d')
    const startX = 50, startY = 75
    const endX = 250, arrowSize = 20
    
    ctx2.beginPath()
    ctx2.moveTo(startX, startY - 10)
    ctx2.lineTo(endX - arrowSize - 10, startY - 10)
    ctx2.arcTo(endX - arrowSize, startY - 10, endX - arrowSize, startY, 10)
    ctx2.lineTo(endX - arrowSize, startY)
    ctx2.lineTo(endX, startY)
    ctx2.lineTo(endX - arrowSize, startY + 15)
    ctx2.lineTo(endX - arrowSize, startY + 10)
    ctx2.arcTo(endX - arrowSize, startY + 10, startX, startY + 10, 10)
    ctx2.lineTo(startX, startY + 10)
    ctx2.closePath()
    
    ctx2.fillStyle = 'gold'
    ctx2.fill()
    ctx2.strokeStyle = 'orange'
    ctx2.lineWidth = 2
    ctx2.stroke()
  </script>
</body>
</html>

贝塞尔曲线

贝塞尔曲线是一种强大的数学曲线,广泛应用于图形设计、动画和字体设计中。Canvas 支持二次和三次贝塞尔曲线。

二次贝塞尔曲线 (quadraticCurveTo)

使用一个控制点定义曲线形状。

方法签名

javascript
ctx.quadraticCurveTo(cp1x, cp1y, x, y)

参数说明

参数类型说明
cp1xNumber控制点的 X 坐标
cp1yNumber控制点的 Y 坐标
xNumber结束点的 X 坐标
yNumber结束点的 Y 坐标

曲线原理

code
控制点 (cp1x, cp1y)
    ●
   ╱ ╲
  ╱   ╲
 ╱     ╲
●───────●
起点    终点
(x0,y0) (x,y)

曲线被控制点"拉向"但不通过控制点

基本用法

javascript
ctx.beginPath()
ctx.moveTo(50, 200) // 起点
ctx.quadraticCurveTo(150, 50, 250, 200) // 控制点 + 终点
ctx.strokeStyle = 'blue'
ctx.lineWidth = 3
ctx.stroke()

绘制气泡对话框

javascript
function drawSpeechBubble(ctx, x, y, width, height, radius) {
  ctx.beginPath()
  ctx.moveTo(x + radius, y)
  ctx.lineTo(x + width - radius, y)
  ctx.quadraticCurveTo(x + width, y, x + width, y + radius)
  ctx.lineTo(x + width, y + height - radius - 20)
  ctx.quadraticCurveTo(x + width, y + height - 20, x + width - radius, y + height - 20)
  ctx.lineTo(x + width * 0.7, y + height - 20)
  ctx.lineTo(x + width * 0.6, y + height)
  ctx.lineTo(x + width * 0.5, y + height - 20)
  ctx.lineTo(x + radius, y + height - 20)
  ctx.quadraticCurveTo(x, y + height - 20, x, y + height - radius - 20)
  ctx.lineTo(x, y + radius)
  ctx.quadraticCurveTo(x, y, x + radius, y)
  ctx.closePath()
  
  ctx.fillStyle = 'white'
  ctx.fill()
  ctx.strokeStyle = '#333'
  ctx.lineWidth = 2
  ctx.stroke()
}

绘制花瓣

javascript
function drawPetal(ctx, x, y, length, width) {
  ctx.beginPath()
  ctx.moveTo(x, y)
  ctx.quadraticCurveTo(x - width, y + length / 2, x, y + length)
  ctx.quadraticCurveTo(x + width, y + length / 2, x, y)
  ctx.closePath()
  
  ctx.fillStyle = 'pink'
  ctx.fill()
  ctx.strokeStyle = 'hotpink'
  ctx.stroke()
}

// 绘制花朵
const petalCount = 6
for (let i = 0; i < petalCount; i++) {
  const angle = (i / petalCount) * Math.PI * 2
  ctx.save()
  ctx.translate(150, 150)
  ctx.rotate(angle)
  drawPetal(ctx, 0, 0, 60, 20)
  ctx.restore()
}

三次贝塞尔曲线 (bezierCurveTo)

使用两个控制点定义曲线形状,提供更精确的控制。

方法签名

javascript
ctx.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y)

参数说明

参数类型说明
cp1xNumber控制点 1 的 X 坐标
cp1yNumber控制点 1 的 Y 坐标
cp2xNumber控制点 2 的 X 坐标
cp2yNumber控制点 2 的 Y 坐标
xNumber结束点的 X 坐标
yNumber结束点的 Y 坐标

曲线原理

code
控制点1       控制点2
   ●──────────●
  ╱            ╲
 ╱              ╲
●────────────────●
起点            终点

曲线被两个控制点"拉向",产生 S 形曲线

基本用法

javascript
ctx.beginPath()
ctx.moveTo(50, 200) // 起点
ctx.bezierCurveTo(100, 50, 200, 350, 250, 200) // 控制点1 + 控制点2 + 终点
ctx.strokeStyle = 'red'
ctx.lineWidth = 3
ctx.stroke()

绘制心形

javascript
function drawHeart(ctx, x, y, size) {
  ctx.beginPath()
  ctx.moveTo(x, y + size / 4)
  
  // 左半边
  ctx.bezierCurveTo(
    x, y,
    x - size / 2, y,
    x - size / 2, y + size / 4
  )
  ctx.bezierCurveTo(
    x - size / 2, y + size / 2,
    x, y + size * 0.75,
    x, y + size
  )
  
  // 右半边
  ctx.bezierCurveTo(
    x, y + size * 0.75,
    x + size / 2, y + size / 2,
    x + size / 2, y + size / 4
  )
  ctx.bezierCurveTo(
    x + size / 2, y,
    x, y,
    x, y + size / 4
  )
  
  ctx.fillStyle = '#e74c3c'
  ctx.fill()
}

drawHeart(ctx, 150, 50, 120)

绘制波浪

javascript
function drawWave(ctx, startX, startY, width, height, segments) {
  ctx.beginPath()
  ctx.moveTo(startX, startY)
  
  const segmentWidth = width / segments
  let currentX = startX
  
  for (let i = 0; i < segments; i++) {
    const direction = i % 2 === 0 ? -1 : 1
    
    ctx.bezierCurveTo(
      currentX + segmentWidth * 0.25, startY + height * direction,
      currentX + segmentWidth * 0.75, startY + height * direction,
      currentX + segmentWidth, startY
    )
    
    currentX += segmentWidth
  }
  
  ctx.strokeStyle = '#3498db'
  ctx.lineWidth = 3
  ctx.stroke()
}

drawWave(ctx, 50, 150, 300, 50, 4)

二次 vs 三次贝塞尔曲线对比

特性二次贝塞尔三次贝塞尔
控制点数量1 个2 个
控制精度较低较高
计算复杂度较低较高
适用场景简单曲线、弧形复杂曲线、S 形

复杂路径应用

绘制对话框

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="dialogCanvas" width="400" height="300"></canvas>

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

    function drawDialog(x, y, width, height, text) {
      const radius = 15
      const tailWidth = 20
      const tailHeight = 15
      
      ctx.beginPath()
      
      // 右上角
      ctx.moveTo(x + width - radius, y)
      ctx.arcTo(x + width, y, x + width, y + radius, radius)
      
      // 右边
      ctx.lineTo(x + width, y + height - radius)
      ctx.arcTo(x + width, y + height, x + width - radius, y + height, radius)
      
      // 底边(带尾巴)
      ctx.lineTo(x + width * 0.7, y + height)
      ctx.lineTo(x + width * 0.6, y + height + tailHeight)
      ctx.lineTo(x + width * 0.5, y + height)
      ctx.lineTo(x + radius, y + height)
      ctx.arcTo(x, y + height, x, y + height - radius, radius)
      
      // 左边
      ctx.lineTo(x, y + radius)
      ctx.arcTo(x, y, x + radius, y, radius)
      
      ctx.closePath()
      
      // 填充渐变
      const gradient = ctx.createLinearGradient(x, y, x, y + height)
      gradient.addColorStop(0, '#ffffff')
      gradient.addColorStop(1, '#f0f0f0')
      
      ctx.fillStyle = gradient
      ctx.fill()
      ctx.strokeStyle = '#ddd'
      ctx.lineWidth = 2
      ctx.stroke()
      
      // 文字
      ctx.fillStyle = '#333'
      ctx.font = '14px Arial'
      ctx.textAlign = 'center'
      ctx.textBaseline = 'middle'
      ctx.fillText(text, x + width / 2, y + height / 2)
    }

    drawDialog(50, 50, 300, 150, '这是一个对话框示例')
  </script>
</body>
</html>

绘制自定义形状

javascript
// 绘制星形
function drawStar(ctx, cx, cy, spikes, outerRadius, innerRadius) {
  let rot = Math.PI / 2 * 3
  let x = cx
  let y = cy
  const step = Math.PI / spikes

  ctx.beginPath()
  ctx.moveTo(cx, cy - outerRadius)
  
  for (let i = 0; i < spikes; i++) {
    x = cx + Math.cos(rot) * outerRadius
    y = cy + Math.sin(rot) * outerRadius
    ctx.lineTo(x, y)
    rot += step

    x = cx + Math.cos(rot) * innerRadius
    y = cy + Math.sin(rot) * innerRadius
    ctx.lineTo(x, y)
    rot += step
  }
  
  ctx.lineTo(cx, cy - outerRadius)
  ctx.closePath()
  
  ctx.fillStyle = '#f39c12'
  ctx.fill()
  ctx.strokeStyle = '#e67e22'
  ctx.lineWidth = 2
  ctx.stroke()
}

drawStar(ctx, 200, 150, 5, 80, 40)

// 绘制齿轮
function drawGear(ctx, cx, cy, teeth, outerRadius, innerRadius) {
  const toothDepth = (outerRadius - innerRadius) / 2
  const toothWidth = (Math.PI * 2) / teeth / 2

  ctx.beginPath()
  
  for (let i = 0; i < teeth; i++) {
    const angle = (i * Math.PI * 2) / teeth
    
    // 齿根
    ctx.lineTo(
      cx + Math.cos(angle) * innerRadius,
      cy + Math.sin(angle) * innerRadius
    )
    
    // 齿上升
    ctx.lineTo(
      cx + Math.cos(angle + toothWidth * 0.3) * outerRadius,
      cy + Math.sin(angle + toothWidth * 0.3) * outerRadius
    )
    
    // 齿顶
    ctx.lineTo(
      cx + Math.cos(angle + toothWidth * 0.7) * outerRadius,
      cy + Math.sin(angle + toothWidth * 0.7) * outerRadius
    )
    
    // 齿下降
    ctx.lineTo(
      cx + Math.cos(angle + toothWidth) * innerRadius,
      cy + Math.sin(angle + toothWidth) * innerRadius
    )
  }
  
  ctx.closePath()
  
  ctx.fillStyle = '#7f8c8d'
  ctx.fill()
  ctx.strokeStyle = '#2c3e50'
  ctx.lineWidth = 2
  ctx.stroke()
}

drawGear(ctx, 350, 150, 12, 60, 40)

常见问题

1. 弧度与角度转换错误

问题: 使用角度而非弧度导致绘制错误。

解决方案: 始终记住转换公式。

javascript
// 工具函数
const toRadians = degrees => degrees * Math.PI / 180
const toDegrees = radians => radians * 180 / Math.PI

// 使用
ctx.arc(100, 100, 50, toRadians(0), toRadians(90))

2. 贝塞尔曲线控制点位置不理想

问题: 曲线形状不符合预期。

解决方案: 绘制控制点和辅助线来调试。

javascript
function drawBezierWithControls(ctx, x0, y0, cp1x, cp1y, cp2x, cp2y, x, y) {
  // 绘制辅助线
  ctx.beginPath()
  ctx.moveTo(x0, y0)
  ctx.lineTo(cp1x, cp1y)
  ctx.lineTo(cp2x, cp2y)
  ctx.lineTo(x, y)
  ctx.strokeStyle = '#ccc'
  ctx.lineWidth = 1
  ctx.setLineDash([5, 5])
  ctx.stroke()
  ctx.setLineDash([])
  
  // 绘制控制点
  ctx.fillStyle = 'red'
  ctx.beginPath()
  ctx.arc(cp1x, cp1y, 5, 0, Math.PI * 2)
  ctx.fill()
  
  ctx.fillStyle = 'blue'
  ctx.beginPath()
  ctx.arc(cp2x, cp2y, 5, 0, Math.PI * 2)
  ctx.fill()
  
  // 绘制曲线
  ctx.beginPath()
  ctx.moveTo(x0, y0)
  ctx.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y)
  ctx.strokeStyle = 'black'
  ctx.lineWidth = 2
  ctx.stroke()
}

3. 路径未正确闭合

问题: 填充时出现意外的连接线。

解决方案: 确保正确使用 closePath() 或手动连接起点。

javascript
// ❌ 错误示例
ctx.beginPath()
ctx.arc(100, 100, 50, 0, Math.PI * 2)
ctx.fill() // 可能出现问题

// ✅ 正确示例
ctx.beginPath()
ctx.arc(100, 100, 50, 0, Math.PI * 2)
ctx.closePath() // 确保闭合
ctx.fill()

下一步学习


返回Canvas 教程目录 | 上一篇基础绘图