第一个 Canvas 程序

创建你的第一个Canvas程序,从零开始学习Canvas绑制,包含多个实战案例。 让我们从创建第一个Canvas程序开始,在实践中学习Canvas的基本用法。

创建画布

首先,在HTML中添加canvas元素:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>我的第一个Canvas程序</title>
</head>
<body>
  <canvas id="myCanvas" width="400" height="300">
    您的浏览器不支持Canvas
  </canvas>
</body>
</html>

几个注意点:

  • widthheight属性定义画布尺寸(单位:像素)
  • 标签内的文字在不支持Canvas时显示
  • 建议给canvas添加id,方便JavaScript获取

获取绑制上下文

Canvas本身只是一个容器,绑制操作需要通过"上下文"完成:

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

getContext('2d')返回一个2D绑制上下文对象,它提供了所有绑制方法。

Canvas 初始化器

class CanvasInitializer {
  constructor(options = {}) {
    this.options = {
      id: 'myCanvas',
      width: 400,
      height: 300,
      backgroundColor: '#ffffff',
      ...options
    }
    
    this.canvas = null
    this.ctx = null
    
    this.init()
  }
  
  init() {
    this.canvas = document.getElementById(this.options.id)
    
    if (!this.canvas) {
      this.canvas = document.createElement('canvas')
      this.canvas.id = this.options.id
      document.body.appendChild(this.canvas)
    }
    
    this.canvas.width = this.options.width
    this.canvas.height = this.options.height
    
    this.ctx = this.canvas.getContext('2d')
    
    if (this.options.backgroundColor) {
      this.clear(this.options.backgroundColor)
    }
  }
  
  clear(color = this.options.backgroundColor) {
    this.ctx.fillStyle = color
    this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height)
  }
  
  getCanvas() {
    return this.canvas
  }
  
  getContext() {
    return this.ctx
  }
  
  resize(width, height) {
    this.canvas.width = width
    this.canvas.height = height
    this.options.width = width
    this.options.height = height
  }
  
  toDataURL(type = 'image/png') {
    return this.canvas.toDataURL(type)
  }
  
  download(filename = 'canvas-image.png') {
    const link = document.createElement('a')
    link.download = filename
    link.href = this.toDataURL()
    link.click()
  }
}

案例一:彩色矩形

彩色矩形动画

代码实现

<canvas id="myCanvas" width="400" height="200"></canvas>

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

const colors = ['#e74c3c', '#2ecc71', '#3498db', '#f1c40f', '#9b59b6']
const rectangles = []
let time = 0

const count = 5
const rectWidth = 60
const rectHeight = 80
const gap = 20
const startX = (canvas.width - (count * rectWidth + (count - 1) * gap)) / 2

for (let i = 0; i < count; i++) {
  rectangles.push({
    x: startX + i * (rectWidth + gap),
    y: canvas.height / 2 - rectHeight / 2,
    width: rectWidth,
    height: rectHeight,
    color: colors[i],
    phase: i * 0.5
  })
}

function animate() {
  time += 0.05
  ctx.fillStyle = '#f8f9fa'
  ctx.fillRect(0, 0, canvas.width, canvas.height)
  
  rectangles.forEach((rect) => {
    const offsetY = Math.sin(time + rect.phase) * 20
    
    ctx.fillStyle = rect.color
    ctx.shadowColor = 'rgba(0, 0, 0, 0.2)'
    ctx.shadowBlur = 10
    ctx.shadowOffsetY = 5
    ctx.fillRect(rect.x, rect.y + offsetY, rect.width, rect.height)
  })
  
  ctx.shadowColor = 'transparent'
  
  requestAnimationFrame(animate)
}

animate()
</script>

代码解析

方法说明
fillRect(x, y, w, h)绑制填充矩形
fillStyle设置填充颜色
shadowColor/Blur/OffsetY设置阴影效果
Math.sin()正弦函数,用于创建平滑动画
requestAnimationFrame()浏览器动画帧回调

案例二:动态圆形

动态圆形(移动鼠标交互)

代码实现

<canvas id="myCanvas" width="400" height="300"></canvas>

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

const circles = []
let mouseX = canvas.width / 2
let mouseY = canvas.height / 2

for (let i = 0; i < 20; i++) {
  circles.push({
    x: Math.random() * canvas.width,
    y: Math.random() * canvas.height,
    radius: Math.random() * 20 + 10,
    color: `hsl(${Math.random() * 360}, 70%, 60%)`,
    vx: (Math.random() - 0.5) * 2,
    vy: (Math.random() - 0.5) * 2
  })
}

canvas.addEventListener('mousemove', function(e) {
  const rect = canvas.getBoundingClientRect()
  mouseX = e.clientX - rect.left
  mouseY = e.clientY - rect.top
})

function animate() {
  ctx.fillStyle = '#1a1a2e'
  ctx.fillRect(0, 0, canvas.width, canvas.height)
  
  circles.forEach(circle => {
    const dx = mouseX - circle.x
    const dy = mouseY - circle.y
    const dist = Math.sqrt(dx * dx + dy * dy)
    
    if (dist < 100) {
      circle.vx -= dx * 0.001
      circle.vy -= dy * 0.001
    }
    
    circle.x += circle.vx
    circle.y += circle.vy
    
    if (circle.x < 0 || circle.x > canvas.width) circle.vx *= -1
    if (circle.y < 0 || circle.y > canvas.height) circle.vy *= -1
    
    ctx.beginPath()
    ctx.arc(circle.x, circle.y, circle.radius, 0, Math.PI * 2)
    ctx.fillStyle = circle.color
    ctx.fill()
    
    ctx.beginPath()
    ctx.arc(circle.x, circle.y, circle.radius + 5, 0, Math.PI * 2)
    ctx.strokeStyle = circle.color.replace('60%', '40%')
    ctx.lineWidth = 2
    ctx.stroke()
  })
  
  requestAnimationFrame(animate)
}

animate()
</script>

代码解析

方法说明
arc(x, y, r, start, end)绑制圆弧/圆形
beginPath()开始新路径
fill()填充当前路径
stroke()描边当前路径
getBoundingClientRect()获取元素位置信息

案例三:绘制笑脸

动态笑脸(眨眼动画)

代码实现

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

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

const centerX = canvas.width / 2
const centerY = canvas.height / 2
const faceRadius = 100
let time = 0

function drawFace() {
  const bounce = Math.sin(time * 2) * 5
  
  ctx.beginPath()
  ctx.arc(centerX, centerY + bounce, faceRadius, 0, Math.PI * 2)
  
  const gradient = ctx.createRadialGradient(
    centerX - 30, centerY - 30 + bounce, 0,
    centerX, centerY + bounce, faceRadius
  )
  gradient.addColorStop(0, '#ffe066')
  gradient.addColorStop(1, '#f1c40f')
  
  ctx.fillStyle = gradient
  ctx.fill()
  
  ctx.strokeStyle = '#d4a800'
  ctx.lineWidth = 3
  ctx.stroke()
}

function drawEyes() {
  const blink = Math.sin(time * 3) > 0.95 ? 0.1 : 1
  const eyeOffsetX = 30
  const eyeOffsetY = -20
  const eyeRadius = 15
  
  ;[-1, 1].forEach(side => {
    const eyeX = centerX + eyeOffsetX * side
    const eyeY = centerY + eyeOffsetY
    
    ctx.beginPath()
    ctx.ellipse(eyeX, eyeY, eyeRadius, eyeRadius * blink, 0, 0, Math.PI * 2)
    ctx.fillStyle = '#2c3e50'
    ctx.fill()
    
    if (blink > 0.5) {
      ctx.beginPath()
      ctx.arc(eyeX + 5, eyeY - 5, 5, 0, Math.PI * 2)
      ctx.fillStyle = '#ffffff'
      ctx.fill()
    }
  })
}

function drawMouth() {
  ctx.beginPath()
  ctx.arc(centerX, centerY + 20, 50, 0.2 * Math.PI, 0.8 * Math.PI)
  ctx.strokeStyle = '#2c3e50'
  ctx.lineWidth = 4
  ctx.lineCap = 'round'
  ctx.stroke()
  
  ctx.beginPath()
  ctx.ellipse(centerX, centerY + 40, 30, 20, 0, 0, Math.PI)
  ctx.fillStyle = '#c0392b'
  ctx.fill()
}

function drawCheeks() {
  ;[-1, 1].forEach(side => {
    const gradient = ctx.createRadialGradient(
      centerX + 65 * side, centerY + 15, 0,
      centerX + 65 * side, centerY + 15, 20
    )
    gradient.addColorStop(0, 'rgba(231, 76, 60, 0.5)')
    gradient.addColorStop(1, 'rgba(231, 76, 60, 0)')
    
    ctx.beginPath()
    ctx.arc(centerX + 65 * side, centerY + 15, 20, 0, Math.PI * 2)
    ctx.fillStyle = gradient
    ctx.fill()
  })
}

function animate() {
  time += 0.02
  ctx.clearRect(0, 0, canvas.width, canvas.height)
  
  drawFace()
  drawCheeks()
  drawEyes()
  drawMouth()
  
  requestAnimationFrame(animate)
}

animate()
</script>

代码解析

方法说明
createRadialGradient()创建径向渐变
ellipse()绑制椭圆
lineCap线条端点样式
clearRect()清除矩形区域

案例四:粒子效果

粒子爆炸效果(点击画布触发)

代码实现

<canvas id="myCanvas" width="400" height="300"></canvas>

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

const particles = []

class Particle {
  constructor(x, y) {
    this.x = x
    this.y = y
    const angle = Math.random() * Math.PI * 2
    const speed = Math.random() * 5 + 2
    this.vx = Math.cos(angle) * speed
    this.vy = Math.sin(angle) * speed
    this.radius = Math.random() * 4 + 2
    this.color = `hsl(${Math.random() * 60 + 10}, 100%, 50%)`
    this.life = 1
    this.decay = Math.random() * 0.02 + 0.01
  }
  
  update() {
    this.x += this.vx
    this.y += this.vy
    this.vy += 0.1
    this.life -= this.decay
  }
  
  draw() {
    ctx.globalAlpha = this.life
    ctx.beginPath()
    ctx.arc(this.x, this.y, this.radius * this.life, 0, Math.PI * 2)
    ctx.fillStyle = this.color
    ctx.fill()
    ctx.globalAlpha = 1
  }
}

function createExplosion(x, y) {
  for (let i = 0; i < 30; i++) {
    particles.push(new Particle(x, y))
  }
}

canvas.addEventListener('click', function(e) {
  const rect = canvas.getBoundingClientRect()
  const x = e.clientX - rect.left
  const y = e.clientY - rect.top
  createExplosion(x, y)
})

function animate() {
  ctx.fillStyle = 'rgba(26, 26, 46, 0.2)'
  ctx.fillRect(0, 0, canvas.width, canvas.height)
  
  for (let i = particles.length - 1; i >= 0; i--) {
    particles[i].update()
    particles[i].draw()
    
    if (particles[i].life <= 0) {
      particles.splice(i, 1)
    }
  }
  
  requestAnimationFrame(animate)
}

animate()
</script>

代码解析

概念说明
class Particle粒子类,封装粒子属性和行为
Math.cos/sin计算粒子运动方向
globalAlpha全局透明度
splice()从数组中移除死亡粒子

案例五:渐变背景

动态渐变背景

代码实现

<canvas id="myCanvas" width="400" height="200"></canvas>

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

let time = 0

function animate() {
  time += 0.01
  
  const gradient = ctx.createLinearGradient(0, 0, canvas.width, canvas.height)
  
  const hue1 = (time * 50) % 360
  const hue2 = (hue1 + 60) % 360
  const hue3 = (hue1 + 120) % 360
  
  gradient.addColorStop(0, `hsl(${hue1}, 70%, 60%)`)
  gradient.addColorStop(0.5, `hsl(${hue2}, 70%, 50%)`)
  gradient.addColorStop(1, `hsl(${hue3}, 70%, 60%)`)
  
  ctx.fillStyle = gradient
  ctx.fillRect(0, 0, canvas.width, canvas.height)
  
  for (let i = 0; i < 5; i++) {
    const x = (time * 100 + i * 100) % (canvas.width + 100) - 50
    const y = canvas.height / 2 + Math.sin(time * 2 + i) * 30
    
    ctx.beginPath()
    ctx.arc(x, y, 20, 0, Math.PI * 2)
    ctx.fillStyle = 'rgba(255, 255, 255, 0.3)'
    ctx.fill()
  }
  
  requestAnimationFrame(animate)
}

animate()
</script>

代码解析

方法说明
createLinearGradient(x0, y0, x1, y1)创建线性渐变
addColorStop(offset, color)添加颜色停止点
hsl()HSL颜色格式,方便动态计算色相

案例六:交互式画笔

交互式画笔

代码实现

<canvas id="myCanvas" width="400" height="300"></canvas>
<div>
  <button id="clearBtn">清除画布</button>
  <input type="color" id="brushColor" value="#e74c3c">
  <input type="range" id="brushSize" min="1" max="30" value="5">
</div>

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

const colorPicker = document.getElementById('brushColor')
const sizePicker = document.getElementById('brushSize')
const clearBtn = document.getElementById('clearBtn')

let isDrawing = false
let lastX = 0
let lastY = 0

ctx.fillStyle = '#ffffff'
ctx.fillRect(0, 0, canvas.width, canvas.height)

ctx.lineCap = 'round'
ctx.lineJoin = 'round'

canvas.addEventListener('mousedown', function(e) {
  isDrawing = true
  const rect = canvas.getBoundingClientRect()
  lastX = e.clientX - rect.left
  lastY = e.clientY - rect.top
})

canvas.addEventListener('mousemove', function(e) {
  if (!isDrawing) return
  
  const rect = canvas.getBoundingClientRect()
  const x = e.clientX - rect.left
  const y = e.clientY - rect.top
  
  ctx.beginPath()
  ctx.moveTo(lastX, lastY)
  ctx.lineTo(x, y)
  ctx.strokeStyle = colorPicker.value
  ctx.lineWidth = sizePicker.value
  ctx.stroke()
  
  lastX = x
  lastY = y
})

canvas.addEventListener('mouseup', function() {
  isDrawing = false
})

canvas.addEventListener('mouseleave', function() {
  isDrawing = false
})

clearBtn.addEventListener('click', function() {
  ctx.fillStyle = '#ffffff'
  ctx.fillRect(0, 0, canvas.width, canvas.height)
})
</script>

代码解析

事件说明
mousedown鼠标按下开始绑制
mousemove鼠标移动时绑制线条
mouseup/mouseleave停止绑制
touchstart/touchmove/touchend触摸事件支持移动端

常见错误

错误1:CSS设置尺寸

// 错误:用CSS设置尺寸会导致图形变形
canvas.style.width = '400px'
canvas.style.height = '300px'

// 正确:使用属性设置
canvas.width = 400
canvas.height = 300

错误2:在canvas加载前执行代码

// 错误:canvas还不存在
const ctx = document.getElementById('myCanvas').getContext('2d')

// 正确:等待DOM加载完成
window.onload = function() {
  const ctx = document.getElementById('myCanvas').getContext('2d')
}

// 或者把script放在canvas后面

错误3:忘记获取上下文

// 错误:canvas元素没有绑制方法
const canvas = document.getElementById('myCanvas')
canvas.fillRect(0, 0, 100, 100)  // 报错!

// 正确:通过上下文绑制
const ctx = canvas.getContext('2d')
ctx.fillRect(0, 0, 100, 100)

调试技巧

查看绑制内容

在浏览器开发者工具中:

  1. 右键点击canvas元素
  2. 选择"检查"
  3. 可以看到canvas的HTML属性

导出画布内容

// 将画布内容转为图片
const dataURL = canvas.toDataURL()
console.log(dataURL)  // base64格式的图片数据

// 在新窗口打开
window.open(dataURL)

小结

创建Canvas程序的基本步骤:

  1. 在HTML中添加<canvas>元素
  2. 用JavaScript获取canvas元素
  3. 调用getContext('2d')获取绑制上下文
  4. 使用上下文的方法绑制图形

接下来,我们将深入学习Canvas的坐标系统和更多绑制方法。