Skip to content

Canvas 详细教程与面试题集

一、Canvas 基础介绍

1.1 什么是 Canvas

Canvas 是 HTML5 新增的一个元素,用于在网页上绘制图形。它是一个容器,通过 JavaScript 脚本可以在上面绘制各种图形、图像、动画等。

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

1.2 Canvas 的特点

  • 位图绘制:Canvas 是基于像素的即时模式图形系统,绘制的内容是位图
  • 高性能:适合处理大量图形绘制、动画、游戏等场景
  • 灵活性:可以绘制任意形状、图像、文本等
  • 无障碍性:需要提供替代内容给不支持 Canvas 的浏览器

1.3 Canvas 与 SVG 的区别

特性CanvasSVG
渲染方式位图(像素)矢量图
DOM 操作不支持支持
事件处理需要手动实现原生支持
性能适合大量图形适合少量复杂图形
缩放会失真无限缩放不失真
适用场景游戏、图表、图像处理图标、简单动画、数据可视化

1.4 获取绑定上下文

javascript
const canvas = document.getElementById('myCanvas')
const ctx = canvas.getContext('2d') // 获取 2D 渲染上下文

二、Canvas 基础绑定

2.1 绑制矩形

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

// 描边矩形
ctx.strokeStyle = 'blue'
ctx.strokeRect(120, 10, 100, 50)

// 清除矩形区域
ctx.clearRect(15, 15, 50, 30)

2.2 绑制路径

javascript
ctx.beginPath()
ctx.moveTo(50, 50) // 起点
ctx.lineTo(200, 50) // 直线到
ctx.lineTo(200, 150) // 直线到
ctx.closePath() // 闭合路径
ctx.stroke() // 描边
// ctx.fill();         // 填充

2.3 绑制圆形和弧线

javascript
// arc(x, y, radius, startAngle, endAngle, anticlockwise)
ctx.beginPath()
ctx.arc(100, 100, 50, 0, Math.PI * 2) // 完整圆
ctx.fill()

ctx.beginPath()
ctx.arc(200, 100, 50, 0, Math.PI) // 半圆
ctx.stroke()

2.4 绑制文本

javascript
ctx.font = '30px Arial'
ctx.fillStyle = 'black'
ctx.fillText('Hello Canvas', 10, 50)

ctx.strokeStyle = 'red'
ctx.strokeText('Stroke Text', 10, 100)

2.5 绑制图像

javascript
const img = new Image()
img.onload = function () {
  ctx.drawImage(img, 0, 0) // 原尺寸
  ctx.drawImage(img, 0, 0, 200, 150) // 指定尺寸
  ctx.drawImage(img, 50, 50, 100, 100, 0, 0, 100, 100) // 裁剪
}
img.src = 'image.png'

三、Canvas 样式与颜色

3.1 填充与描边样式

javascript
// 颜色值
ctx.fillStyle = '#ff0000'
ctx.fillStyle = 'rgb(255, 0, 0)'
ctx.fillStyle = 'rgba(255, 0, 0, 0.5)'

// 渐变
const linearGradient = ctx.createLinearGradient(0, 0, 200, 0)
linearGradient.addColorStop(0, 'red')
linearGradient.addColorStop(1, 'blue')
ctx.fillStyle = linearGradient

// 径向渐变
const radialGradient = ctx.createRadialGradient(100, 100, 0, 100, 100, 50)
radialGradient.addColorStop(0, 'white')
radialGradient.addColorStop(1, 'black')
ctx.fillStyle = radialGradient

// 图案
const pattern = ctx.createPattern(img, 'repeat')
ctx.fillStyle = pattern

3.2 线条样式

javascript
ctx.lineWidth = 5 // 线宽
ctx.lineCap = 'round' // 线端样式: butt, round, square
ctx.lineJoin = 'round' // 线连接样式: miter, round, bevel
ctx.miterLimit = 10 // 斜接限制
ctx.setLineDash([5, 5]) // 虚线
ctx.lineDashOffset = 0 // 虚线偏移

3.3 透明度与阴影

javascript
ctx.globalAlpha = 0.5 // 全局透明度

ctx.shadowColor = 'rgba(0, 0, 0, 0.5)'
ctx.shadowBlur = 10
ctx.shadowOffsetX = 5
ctx.shadowOffsetY = 5

四、Canvas 变换

4.1 平移、旋转、缩放

javascript
ctx.save() // 保存状态

ctx.translate(100, 100) // 平移
ctx.rotate(Math.PI / 4) // 旋转(弧度)
ctx.scale(2, 2) // 缩放

ctx.fillRect(0, 0, 50, 50)

ctx.restore() // 恢复状态

4.2 变换矩阵

javascript
// transform(a, b, c, d, e, f)
// | a c e |
// | b d f |
// | 0 0 1 |
ctx.transform(1, 0, 0, 1, 0, 0) // 单位矩阵
ctx.setTransform(1, 0, 0, 1, 0, 0) // 重置并设置矩阵

五、Canvas 高级特性

5.1 合成操作

javascript
ctx.globalCompositeOperation = 'source-over' // 默认
// 其他值: source-in, source-out, source-atop,
// destination-over, destination-in, destination-out, destination-atop,
// lighter, copy, xor, multiply, screen, overlay, darken, lighten, etc.

5.2 裁剪路径

javascript
ctx.beginPath()
ctx.arc(100, 100, 50, 0, Math.PI * 2)
ctx.clip() // 设置裁剪区域
// 之后的绑定只在这个圆形区域内可见

5.3 像素操作

javascript
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height)
const data = imageData.data // Uint8ClampedArray

// 修改像素
for (let i = 0; i < data.length; i += 4) {
  data[i] = 255 - data[i] // R
  data[i + 1] = 255 - data[i + 1] // G
  data[i + 2] = 255 - data[i + 2] // B
  // data[i + 3] 是 Alpha
}

ctx.putImageData(imageData, 0, 0)

5.4 状态保存与恢复

javascript
ctx.save() // 保存当前状态到栈
ctx.restore() // 从栈中恢复状态

// 保存的内容包括:
// - 变换矩阵
// - 裁剪区域
// - dash list
// - 样式属性: strokeStyle, fillStyle, globalAlpha, lineWidth, etc.

5.5 路径检测

javascript
// 检测点是否在路径内
ctx.beginPath()
ctx.rect(10, 10, 100, 100)
if (ctx.isPointInPath(50, 50)) {
  console.log('点在路径内')
}

// 检测点是否在描边上
if (ctx.isPointInStroke(50, 10)) {
  console.log('点在描边上')
}

六、Canvas 动画

6.1 基本动画循环

javascript
function animate() {
  ctx.clearRect(0, 0, canvas.width, canvas.height)

  // 更新状态
  x += speed

  // 绑定
  ctx.fillRect(x, y, 50, 50)

  requestAnimationFrame(animate)
}

animate()

6.2 双缓冲技术

javascript
const offscreen = document.createElement('canvas')
offscreen.width = canvas.width
offscreen.height = canvas.height
const offCtx = offscreen.getContext('2d')

function render() {
  // 在离屏 Canvas 上绑定
  offCtx.clearRect(0, 0, offscreen.width, offscreen.height)
  offCtx.fillRect(x, y, 50, 50)

  // 复制到主 Canvas
  ctx.clearRect(0, 0, canvas.width, canvas.height)
  ctx.drawImage(offscreen, 0, 0)

  requestAnimationFrame(render)
}

6.3 性能优化

javascript
// 1. 避免频繁创建对象
const points = []
for (let i = 0; i < 1000; i++) {
  points.push({ x: Math.random() * 500, y: Math.random() * 400 })
}

// 2. 使用 requestAnimationFrame
// 3. 局部重绘
// 4. 减少状态切换
// 5. 使用离屏 Canvas 缓存静态内容

七、Canvas 事件处理

7.1 鼠标事件

javascript
canvas.addEventListener('click', function (e) {
  const rect = canvas.getBoundingClientRect()
  const x = e.clientX - rect.left
  const y = e.clientY - rect.top
  console.log(`点击位置: (${x}, ${y})`)
})

canvas.addEventListener('mousemove', function (e) {
  const rect = canvas.getBoundingClientRect()
  const x = e.clientX - rect.left
  const y = e.clientY - rect.top
  // 处理鼠标移动
})

7.2 触摸事件

javascript
canvas.addEventListener('touchstart', function (e) {
  e.preventDefault()
  const touch = e.touches[0]
  const rect = canvas.getBoundingClientRect()
  const x = touch.clientX - rect.left
  const y = touch.clientY - rect.top
})

7.3 图形拾取

javascript
const shapes = []

function getShapeAt(x, y) {
  for (let i = shapes.length - 1; i >= 0; i--) {
    const shape = shapes[i]
    ctx.beginPath()
    ctx.rect(shape.x, shape.y, shape.width, shape.height)
    if (ctx.isPointInPath(x, y)) {
      return shape
    }
  }
  return null
}

八、Canvas 导出与保存

8.1 导出为图片

javascript
// 导出为 Data URL
const dataURL = canvas.toDataURL('image/png')
const dataURL2 = canvas.toDataURL('image/jpeg', 0.8) // 质量 0-1

// 下载图片
const link = document.createElement('a')
link.download = 'canvas.png'
link.href = dataURL
link.click()

8.2 导出为 Blob

javascript
canvas.toBlob(function (blob) {
  const url = URL.createObjectURL(blob)
  // 使用 URL
  URL.revokeObjectURL(url) // 释放
}, 'image/png')

九、Canvas 实际应用场景

9.1 图表绑定

javascript
function drawBarChart(data) {
  const barWidth = 50
  const gap = 20
  const maxValue = Math.max(...data)

  data.forEach((value, index) => {
    const height = (value / maxValue) * 300
    const x = index * (barWidth + gap)
    const y = canvas.height - height

    ctx.fillStyle = '#3498db'
    ctx.fillRect(x, y, barWidth, height)
  })
}

9.2 简单画板

javascript
let isDrawing = false

canvas.addEventListener('mousedown', () => (isDrawing = true))
canvas.addEventListener('mouseup', () => (isDrawing = false))
canvas.addEventListener('mouseout', () => (isDrawing = false))

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.lineTo(x, y)
  ctx.stroke()
})

9.3 图像滤镜

javascript
function applyFilter(filter) {
  const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height)
  const data = imageData.data

  switch (filter) {
    case 'grayscale':
      for (let i = 0; i < data.length; i += 4) {
        const avg = (data[i] + data[i + 1] + data[i + 2]) / 3
        data[i] = data[i + 1] = data[i + 2] = avg
      }
      break
    case 'invert':
      for (let i = 0; i < data.length; i += 4) {
        data[i] = 255 - data[i]
        data[i + 1] = 255 - data[i + 1]
        data[i + 2] = 255 - data[i + 2]
      }
      break
  }

  ctx.putImageData(imageData, 0, 0)
}

十、Canvas 50道经典面试题及答案详解

第1题:Canvas 和 SVG 有什么区别?

答案:

主要区别如下:

  1. 渲染方式

    • Canvas 是基于像素的位图渲染,绘制后不保留图形信息
    • SVG 是基于矢量的图形,每个图形都是 DOM 元素
  2. DOM 操作

    • Canvas 绑制的图形不能通过 DOM 操作
    • SVG 图形可以通过 DOM API 操作
  3. 事件处理

    • Canvas 需要手动计算点击位置来判断点击了哪个图形
    • SVG 图形原生支持事件绑定
  4. 性能

    • Canvas 适合大量图形绘制(如游戏、粒子效果)
    • SVG 适合少量复杂图形,图形过多时性能下降
  5. 缩放

    • Canvas 放大后会模糊(位图特性)
    • SVG 可以无限缩放不失真
  6. 适用场景

    • Canvas:游戏、实时图表、图像处理、粒子动画
    • SVG:图标、简单动画、数据可视化、可交互图形

第2题:如何获取 Canvas 的 2D 渲染上下文?

答案:

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

getContext('2d') 返回一个 CanvasRenderingContext2D 对象,提供了绑制 2D 图形的方法和属性。

还可以获取其他上下文:

  • getContext('webgl') - WebGL 上下文
  • getContext('webgl2') - WebGL 2 上下文
  • getContext('bitmaprenderer') - ImageBitmapRenderingContext

第3题:Canvas 的默认尺寸是多少?如何设置尺寸?

答案:

Canvas 的默认尺寸是 300px × 150px

设置尺寸有两种方式:

  1. HTML 属性设置(推荐):
html
<canvas id="myCanvas" width="500" height="400"></canvas>
  1. JavaScript 设置
javascript
canvas.width = 500
canvas.height = 400

注意:不要用 CSS 设置 Canvas 尺寸,因为 CSS 设置的是显示尺寸,会导致内容被拉伸或压缩,造成模糊。Canvas 的 width 和 height 属性设置的是绑制缓冲区的实际像素数。

第4题:如何绘制一个实心矩形?

答案:

javascript
// 方法一:fillRect
ctx.fillStyle = 'red'
ctx.fillRect(x, y, width, height)

// 方法二:路径方式
ctx.beginPath()
ctx.rect(x, y, width, height)
ctx.fillStyle = 'red'
ctx.fill()

fillRect(x, y, width, height) 直接绘制填充矩形,不需要 beginPath()fill()

第5题:如何绘制一个空心矩形?

答案:

javascript
// 方法一:strokeRect
ctx.strokeStyle = 'blue'
ctx.lineWidth = 2
ctx.strokeRect(x, y, width, height)

// 方法二:路径方式
ctx.beginPath()
ctx.rect(x, y, width, height)
ctx.strokeStyle = 'blue'
ctx.stroke()

第6题:如何清除 Canvas 上的指定区域?

答案:

javascript
// 清除指定区域
ctx.clearRect(x, y, width, height)

// 清除整个 Canvas
ctx.clearRect(0, 0, canvas.width, canvas.height)

clearRect(x, y, width, height) 会将指定矩形区域的像素变为透明。

第7题:beginPath() 的作用是什么?

答案:

beginPath() 用于开始一个新的路径。调用后,之前的路径会被清空,新的绑制命令会创建一个新的路径。

javascript
// 不使用 beginPath 的问题
ctx.strokeStyle = 'red'
ctx.moveTo(10, 10)
ctx.lineTo(100, 10)
ctx.stroke()

ctx.strokeStyle = 'blue'
ctx.moveTo(10, 50)
ctx.lineTo(100, 50)
ctx.stroke() // 会把第一条线也重新描边为蓝色

// 正确做法
ctx.beginPath()
ctx.strokeStyle = 'red'
ctx.moveTo(10, 10)
ctx.lineTo(100, 10)
ctx.stroke()

ctx.beginPath() // 开始新路径
ctx.strokeStyle = 'blue'
ctx.moveTo(10, 50)
ctx.lineTo(100, 50)
ctx.stroke() // 只描边第二条线

第8题:closePath() 的作用是什么?

答案:

closePath() 用于闭合当前路径,从当前点绘制一条直线到路径的起始点。

javascript
ctx.beginPath()
ctx.moveTo(50, 50)
ctx.lineTo(150, 50)
ctx.lineTo(100, 150)
ctx.closePath() // 连接 (100, 150) 到 (50, 50),形成三角形
ctx.stroke()

注意:closePath() 不会自动调用 fill()stroke(),它只是闭合路径。调用 fill() 时会自动闭合路径。

第9题:如何绘制一个圆形?

答案:

javascript
ctx.beginPath()
ctx.arc(x, y, radius, 0, Math.PI * 2) // 起始角度0,结束角度2π
ctx.fillStyle = 'red'
ctx.fill()

// 或者描边
ctx.beginPath()
ctx.arc(x, y, radius, 0, Math.PI * 2)
ctx.strokeStyle = 'blue'
ctx.stroke()

arc(x, y, radius, startAngle, endAngle, anticlockwise) 参数:

  • x, y: 圆心坐标
  • radius: 半径
  • startAngle: 起始角度(弧度)
  • endAngle: 结束角度(弧度)
  • anticlockwise: 是否逆时针(默认 false,顺时针)

第10题:如何绘制一个半圆?

答案:

javascript
// 上半圆
ctx.beginPath()
ctx.arc(100, 100, 50, 0, Math.PI)
ctx.fill()

// 下半圆
ctx.beginPath()
ctx.arc(100, 100, 50, Math.PI, Math.PI * 2)
ctx.fill()

// 左半圆
ctx.beginPath()
ctx.arc(100, 100, 50, Math.PI / 2, (Math.PI * 3) / 2)
ctx.fill()

// 右半圆
ctx.beginPath()
ctx.arc(100, 100, 50, -Math.PI / 2, Math.PI / 2)
ctx.fill()

第11题:如何绘制一条直线?

答案:

javascript
ctx.beginPath()
ctx.moveTo(x1, y1) // 起点
ctx.lineTo(x2, y2) // 终点
ctx.strokeStyle = 'black'
ctx.lineWidth = 2
ctx.stroke()

moveTo(x, y) 移动画笔到指定位置(不绘制),lineTo(x, y) 从当前位置绘制直线到指定位置。

第12题:如何绘制虚线?

答案:

javascript
ctx.beginPath()
ctx.setLineDash([10, 5]) // [实线长度, 空白长度]
ctx.moveTo(10, 10)
ctx.lineTo(200, 10)
ctx.stroke()

// 复杂虚线模式
ctx.setLineDash([10, 5, 2, 5]) // 实线10,空白5,实线2,空白5,循环

// 动画虚线
ctx.lineDashOffset = offset // 设置虚线偏移

// 清除虚线
ctx.setLineDash([])

第13题:如何设置线条的端点样式?

答案:

使用 lineCap 属性:

javascript
ctx.lineWidth = 20

// butt: 默认,方形端点,不超出端点
ctx.lineCap = 'butt'
ctx.beginPath()
ctx.moveTo(50, 50)
ctx.lineTo(200, 50)
ctx.stroke()

// round: 圆形端点,超出端点半线宽
ctx.lineCap = 'round'
ctx.beginPath()
ctx.moveTo(50, 100)
ctx.lineTo(200, 100)
ctx.stroke()

// square: 方形端点,超出端点半线宽
ctx.lineCap = 'square'
ctx.beginPath()
ctx.moveTo(50, 150)
ctx.lineTo(200, 150)
ctx.stroke()

第14题:如何设置线条连接处的样式?

答案:

使用 lineJoin 属性:

javascript
ctx.lineWidth = 20

// miter: 默认,尖角
ctx.lineJoin = 'miter'
ctx.beginPath()
ctx.moveTo(50, 50)
ctx.lineTo(100, 100)
ctx.lineTo(150, 50)
ctx.stroke()

// round: 圆角
ctx.lineJoin = 'round'

// bevel: 斜角
ctx.lineJoin = 'bevel'

当使用 miter 时,可以用 miterLimit 限制尖角的最大长度:

javascript
ctx.miterLimit = 10

第15题:如何绘制文本?

答案:

javascript
// 设置字体
ctx.font = '30px Arial' // 样式 粗细 大小 字体

// 填充文本
ctx.fillStyle = 'black'
ctx.fillText('Hello Canvas', x, y)

// 描边文本
ctx.strokeStyle = 'red'
ctx.strokeText('Hello Canvas', x, y)

// 文本对齐
ctx.textAlign = 'center' // start, end, left, right, center
ctx.textBaseline = 'middle' // top, hanging, middle, alphabetic, ideographic, bottom

// 测量文本宽度
const metrics = ctx.measureText('Hello Canvas')
console.log(metrics.width)

第16题:如何创建线性渐变?

答案:

javascript
// createLinearGradient(x0, y0, x1, y1)
const gradient = ctx.createLinearGradient(0, 0, 200, 0) // 水平渐变

// 添加颜色停止点 (位置, 颜色),位置范围 0-1
gradient.addColorStop(0, 'red')
gradient.addColorStop(0.5, 'yellow')
gradient.addColorStop(1, 'blue')

ctx.fillStyle = gradient
ctx.fillRect(10, 10, 200, 100)

// 垂直渐变
const verticalGradient = ctx.createLinearGradient(0, 0, 0, 200)

// 对角渐变
const diagonalGradient = ctx.createLinearGradient(0, 0, 200, 200)

第17题:如何创建径向渐变?

答案:

javascript
// createRadialGradient(x0, y0, r0, x1, y1, r1)
// (x0, y0, r0): 内圆
// (x1, y1, r1): 外圆

const gradient = ctx.createRadialGradient(100, 100, 0, 100, 100, 100)
gradient.addColorStop(0, 'white')
gradient.addColorStop(1, 'black')

ctx.fillStyle = gradient
ctx.fillRect(0, 0, 200, 200)

// 偏移径向渐变(产生立体效果)
const offsetGradient = ctx.createRadialGradient(80, 80, 0, 100, 100, 100)

第18题:如何在 Canvas 上绘制图片?

答案:

javascript
const img = new Image()
img.onload = function () {
  // 1. 原尺寸绘制
  ctx.drawImage(img, x, y)

  // 2. 指定尺寸绘制
  ctx.drawImage(img, x, y, width, height)

  // 3. 裁剪绘制
  // drawImage(img, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight)
  ctx.drawImage(img, 0, 0, 100, 100, 50, 50, 200, 200)
}
img.src = 'image.png'

// 也可以使用页面上的图片
const imgElement = document.getElementById('myImage')
ctx.drawImage(imgElement, 0, 0)

第19题:如何保存和恢复 Canvas 状态?

答案:

javascript
// 保存状态到栈
ctx.save()

// 修改状态
ctx.fillStyle = 'red'
ctx.translate(100, 100)
ctx.rotate(Math.PI / 4)

// 绑制
ctx.fillRect(0, 0, 50, 50)

// 恢复之前的状态
ctx.restore()

save() 保存的状态包括:

  • 变换矩阵(translate、rotate、scale、transform)
  • 裁剪区域
  • 虚线列表
  • 样式属性:fillStyle、strokeStyle、globalAlpha、lineWidth、lineCap、lineJoin、miterLimit、shadowOffsetX/Y、shadowBlur、shadowColor、globalCompositeOperation、font、textAlign、textBaseline 等

状态以栈的形式保存,可以多次 save()restore()

第20题:如何实现 Canvas 的平移、旋转、缩放?

答案:

javascript
ctx.save()

// 平移
ctx.translate(100, 100)

// 旋转(弧度)
ctx.rotate(Math.PI / 4) // 45度

// 缩放
ctx.scale(2, 2) // 水平和垂直都放大2倍
// ctx.scale(1, -1); // 垂直翻转

// 绑制(以变换后的坐标系为基准)
ctx.fillRect(0, 0, 50, 50)

ctx.restore()

变换顺序很重要,不同顺序会产生不同结果:

javascript
// 先平移后旋转
ctx.translate(100, 100)
ctx.rotate(Math.PI / 4)

// 先旋转后平移(平移方向也被旋转了)
ctx.rotate(Math.PI / 4)
ctx.translate(100, 100)

第21题:如何实现 Canvas 动画?

答案:

javascript
let x = 0
const speed = 2

function animate() {
  // 清除画布
  ctx.clearRect(0, 0, canvas.width, canvas.height)

  // 更新状态
  x += speed
  if (x > canvas.width) x = 0

  // 绑制
  ctx.fillRect(x, 100, 50, 50)

  // 请求下一帧
  requestAnimationFrame(animate)
}

animate()

// 使用时间差控制动画速度
let lastTime = 0
function animate(currentTime) {
  const deltaTime = currentTime - lastTime
  lastTime = currentTime

  // 使用 deltaTime 控制速度
  x += (speed * deltaTime) / 16.67 // 假设 60fps

  ctx.clearRect(0, 0, canvas.width, canvas.height)
  ctx.fillRect(x, 100, 50, 50)

  requestAnimationFrame(animate)
}

第22题:requestAnimationFrame 相比 setTimeout/setInterval 有什么优势?

答案:

requestAnimationFrame 的优势:

  1. 自动匹配刷新率:与显示器刷新率同步(通常是 60fps),避免丢帧或重复绘制

  2. 性能优化:页面不可见(如切换标签页、最小化)时自动暂停,节省 CPU/GPU 资源

  3. 更流畅:浏览器会在重绘前调用,避免视觉卡顿

  4. 时间精度:回调函数接收高精度时间戳参数

javascript
// setTimeout 方式(不推荐)
setInterval(function () {
  // 即使页面不可见也会执行
  // 可能与刷新率不同步
}, 16)

// requestAnimationFrame 方式(推荐)
function animate(time) {
  // time 是高精度时间戳
  // 页面不可见时自动暂停
  requestAnimationFrame(animate)
}
requestAnimationFrame(animate)

第23题:如何获取 Canvas 上某个点的像素颜色?

答案:

javascript
const imageData = ctx.getImageData(x, y, 1, 1)
const data = imageData.data

const r = data[0]
const g = data[1]
const b = data[2]
const a = data[3]

console.log(`RGB: ${r}, ${g}, ${b}, Alpha: ${a}`)
console.log(`颜色: rgba(${r}, ${g}, ${b}, ${a / 255})`)

如果要获取多个像素:

javascript
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height)
// data 是 Uint8ClampedArray,每4个元素表示一个像素 (R, G, B, A)

第24题:如何实现图片灰度化?

答案:

javascript
function grayscale() {
  const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height)
  const data = imageData.data

  for (let i = 0; i < data.length; i += 4) {
    // 加权平均法(考虑人眼对不同颜色的敏感度)
    const gray = data[i] * 0.299 + data[i + 1] * 0.587 + data[i + 2] * 0.114

    data[i] = gray // R
    data[i + 1] = gray // G
    data[i + 2] = gray // B
    // data[i + 3] 是 Alpha,保持不变
  }

  ctx.putImageData(imageData, 0, 0)
}

// 其他灰度化方法
// 平均值法: (R + G + B) / 3
// 最大值法: max(R, G, B)
// 最小值法: min(R, G, B)

第25题:如何实现图片反色效果?

答案:

javascript
function invert() {
  const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height)
  const data = imageData.data

  for (let i = 0; i < data.length; i += 4) {
    data[i] = 255 - data[i] // R
    data[i + 1] = 255 - data[i + 1] // G
    data[i + 2] = 255 - data[i + 2] // B
  }

  ctx.putImageData(imageData, 0, 0)
}

第26题:如何实现图片模糊效果?

答案:

javascript
// 简单的盒式模糊
function boxBlur(radius) {
  const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height)
  const data = imageData.data
  const width = canvas.width
  const height = canvas.height
  const copy = new Uint8ClampedArray(data)

  for (let y = 0; y < height; y++) {
    for (let x = 0; x < width; x++) {
      let r = 0,
        g = 0,
        b = 0,
        count = 0

      for (let dy = -radius; dy <= radius; dy++) {
        for (let dx = -radius; dx <= radius; dx++) {
          const nx = x + dx
          const ny = y + dy

          if (nx >= 0 && nx < width && ny >= 0 && ny < height) {
            const i = (ny * width + nx) * 4
            r += copy[i]
            g += copy[i + 1]
            b += copy[i + 2]
            count++
          }
        }
      }

      const i = (y * width + x) * 4
      data[i] = r / count
      data[i + 1] = g / count
      data[i + 2] = b / count
    }
  }

  ctx.putImageData(imageData, 0, 0)
}

// 使用 CSS filter(更简单)
canvas.style.filter = 'blur(5px)'

第27题:如何判断一个点是否在绑制的路径内?

答案:

javascript
ctx.beginPath()
ctx.arc(100, 100, 50, 0, Math.PI * 2)

const x = 120
const y = 80

if (ctx.isPointInPath(x, y)) {
  console.log('点在路径内')
} else {
  console.log('点在路径外')
}

// 判断点是否在描边上
if (ctx.isPointInStroke(x, y)) {
  console.log('点在描边上')
}

注意:isPointInPathisPointInStroke 只能检测当前路径,需要在 beginPath() 之后、fill()stroke() 之前调用。

第28题:如何实现图形的点击检测?

答案:

javascript
const shapes = [
  { type: 'rect', x: 10, y: 10, width: 100, height: 80, color: 'red' },
  { type: 'circle', x: 200, y: 100, radius: 50, color: 'blue' }
]

function drawShapes() {
  shapes.forEach(shape => {
    ctx.beginPath()
    if (shape.type === 'rect') {
      ctx.rect(shape.x, shape.y, shape.width, shape.height)
    } else if (shape.type === 'circle') {
      ctx.arc(shape.x, shape.y, shape.radius, 0, Math.PI * 2)
    }
    ctx.fillStyle = shape.color
    ctx.fill()
  })
}

function getShapeAt(x, y) {
  // 从后往前遍历(后绑制的在上层)
  for (let i = shapes.length - 1; i >= 0; i--) {
    const shape = shapes[i]
    ctx.beginPath()

    if (shape.type === 'rect') {
      ctx.rect(shape.x, shape.y, shape.width, shape.height)
    } else if (shape.type === 'circle') {
      ctx.arc(shape.x, shape.y, shape.radius, 0, Math.PI * 2)
    }

    if (ctx.isPointInPath(x, y)) {
      return shape
    }
  }
  return null
}

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

  const shape = getShapeAt(x, y)
  if (shape) {
    console.log('点击了:', shape)
  }
})

第29题:如何设置 Canvas 的阴影效果?

答案:

javascript
ctx.shadowColor = 'rgba(0, 0, 0, 0.5)' // 阴影颜色
ctx.shadowBlur = 10 // 模糊程度
ctx.shadowOffsetX = 5 // 水平偏移
ctx.shadowOffsetY = 5 // 垂直偏移

ctx.fillStyle = 'red'
ctx.fillRect(50, 50, 100, 100)

// 关闭阴影
ctx.shadowColor = 'transparent'
ctx.shadowBlur = 0
ctx.shadowOffsetX = 0
ctx.shadowOffsetY = 0

第30题:如何实现 Canvas 的裁剪?

答案:

javascript
// 创建裁剪路径
ctx.beginPath()
ctx.arc(100, 100, 80, 0, Math.PI * 2)
ctx.clip() // 设置裁剪区域

// 之后绑制的内容只会在圆形区域内显示
ctx.fillStyle = 'red'
ctx.fillRect(0, 0, 300, 300)

// 取消裁剪需要 save/restore
ctx.restore()

完整示例:

javascript
ctx.save() // 保存状态

ctx.beginPath()
ctx.arc(100, 100, 80, 0, Math.PI * 2)
ctx.clip()

ctx.drawImage(img, 0, 0) // 图片只显示圆形区域

ctx.restore() // 恢复状态,取消裁剪

第31题:如何将 Canvas 导出为图片?

答案:

javascript
// 导出为 Data URL
const dataURL = canvas.toDataURL() // 默认 PNG
const pngURL = canvas.toDataURL('image/png')
const jpegURL = canvas.toDataURL('image/jpeg', 0.8) // 质量 0-1

// 创建下载链接
const link = document.createElement('a')
link.download = 'canvas-image.png'
link.href = dataURL
link.click()

// 导出为 Blob
canvas.toBlob(function (blob) {
  // 可以用于上传或其他操作
  const url = URL.createObjectURL(blob)

  // 下载
  const link = document.createElement('a')
  link.download = 'canvas-image.png'
  link.href = url
  link.click()

  // 释放 URL
  URL.revokeObjectURL(url)
}, 'image/png')

注意:如果 Canvas 绑制了跨域图片,导出时会报安全错误。需要设置图片的 crossOrigin 属性。

第32题:Canvas 绑制跨域图片后无法导出怎么办?

答案:

javascript
const img = new Image()
img.crossOrigin = 'anonymous' // 设置跨域属性
img.onload = function () {
  ctx.drawImage(img, 0, 0)

  // 现在可以导出了
  const dataURL = canvas.toDataURL()
}
img.src = 'https://example.com/image.png'

// 如果服务器没有设置正确的 CORS 头,仍然会失败
// 需要服务器设置: Access-Control-Allow-Origin: *

其他解决方案:

  1. 使用代理服务器转发图片
  2. 将图片转为 Base64 后使用
  3. 使用后端处理图片

第33题:如何实现 Canvas 的橡皮擦功能?

答案:

javascript
// 方法一:使用 destination-out 合成模式
ctx.globalCompositeOperation = 'destination-out'
ctx.beginPath()
ctx.arc(x, y, radius, 0, Math.PI * 2)
ctx.fill()
ctx.globalCompositeOperation = 'source-over' // 恢复默认

// 方法二:使用 clearRect(只能擦除矩形区域)
ctx.clearRect(x - radius, y - radius, radius * 2, radius * 2)

// 完整的画板橡皮擦示例
let isErasing = false
const eraserRadius = 20

canvas.addEventListener('mousedown', function (e) {
  isErasing = true
  erase(e)
})

canvas.addEventListener('mousemove', function (e) {
  if (isErasing) erase(e)
})

canvas.addEventListener('mouseup', () => (isErasing = false))

function erase(e) {
  const rect = canvas.getBoundingClientRect()
  const x = e.clientX - rect.left
  const y = e.clientY - rect.top

  ctx.globalCompositeOperation = 'destination-out'
  ctx.beginPath()
  ctx.arc(x, y, eraserRadius, 0, Math.PI * 2)
  ctx.fill()
  ctx.globalCompositeOperation = 'source-over'
}

第34题:globalCompositeOperation 有哪些常用值?

答案:

javascript
// 源(新绑制的内容)和目标(已有内容)的合成方式

// 默认值,新内容覆盖在旧内容之上
ctx.globalCompositeOperation = 'source-over'

// 只显示新内容和旧内容重叠的部分
ctx.globalCompositeOperation = 'source-in'

// 只显示新内容中不与旧内容重叠的部分
ctx.globalCompositeOperation = 'source-out'

// 新内容只在与旧内容重叠处显示,旧内容保留
ctx.globalCompositeOperation = 'source-atop'

// 新内容绘制在旧内容之下
ctx.globalCompositeOperation = 'destination-over'

// 只显示旧内容中与新内容重叠的部分
ctx.globalCompositeOperation = 'destination-in'

// 只显示旧内容中不与新内容重叠的部分
ctx.globalCompositeOperation = 'destination-out'

// 旧内容只在与新内容重叠处显示,新内容保留
ctx.globalCompositeOperation = 'destination-atop'

// 颜色值相加(变亮)
ctx.globalCompositeOperation = 'lighter'

// 只显示新内容
ctx.globalCompositeOperation = 'copy'

// 重叠部分透明
ctx.globalCompositeOperation = 'xor'

// 混合模式
ctx.globalCompositeOperation = 'multiply' // 正片叠底
ctx.globalCompositeOperation = 'screen' // 滤色
ctx.globalCompositeOperation = 'overlay' // 叠加
ctx.globalCompositeOperation = 'darken' // 变暗
ctx.globalCompositeOperation = 'lighten' // 变亮

第35题:如何实现 Canvas 画板功能?

答案:

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

canvas.addEventListener('mousedown', startDrawing)
canvas.addEventListener('mousemove', draw)
canvas.addEventListener('mouseup', stopDrawing)
canvas.addEventListener('mouseout', stopDrawing)

// 触摸支持
canvas.addEventListener('touchstart', handleTouchStart)
canvas.addEventListener('touchmove', handleTouchMove)
canvas.addEventListener('touchend', stopDrawing)

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

function draw(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 = 'black'
  ctx.lineWidth = 2
  ctx.lineCap = 'round'
  ctx.lineJoin = 'round'
  ctx.stroke()

  lastX = x
  lastY = y
}

function stopDrawing() {
  isDrawing = false
}

function handleTouchStart(e) {
  e.preventDefault()
  const touch = e.touches[0]
  const mouseEvent = new MouseEvent('mousedown', {
    clientX: touch.clientX,
    clientY: touch.clientY
  })
  canvas.dispatchEvent(mouseEvent)
}

function handleTouchMove(e) {
  e.preventDefault()
  const touch = e.touches[0]
  const mouseEvent = new MouseEvent('mousemove', {
    clientX: touch.clientX,
    clientY: touch.clientY
  })
  canvas.dispatchEvent(mouseEvent)
}

第36题:如何实现 Canvas 撤销和重做功能?

答案:

javascript
const history = []
let historyIndex = -1
const maxHistory = 50

function saveState() {
  // 删除当前位置之后的历史
  history.splice(historyIndex + 1)

  // 保存当前状态
  history.push(canvas.toDataURL())

  // 限制历史记录数量
  if (history.length > maxHistory) {
    history.shift()
  } else {
    historyIndex++
  }
}

function undo() {
  if (historyIndex > 0) {
    historyIndex--
    loadState(history[historyIndex])
  }
}

function redo() {
  if (historyIndex < history.length - 1) {
    historyIndex++
    loadState(history[historyIndex])
  }
}

function loadState(dataURL) {
  const img = new Image()
  img.onload = function () {
    ctx.clearRect(0, 0, canvas.width, canvas.height)
    ctx.drawImage(img, 0, 0)
  }
  img.src = dataURL
}

// 在每次绑制后保存状态
canvas.addEventListener('mouseup', saveState)

// 键盘快捷键
document.addEventListener('keydown', function (e) {
  if (e.ctrlKey && e.key === 'z') {
    e.preventDefault()
    if (e.shiftKey) {
      redo()
    } else {
      undo()
    }
  }
})

第37题:如何实现 Canvas 的缩放和平移(画布导航)?

答案:

javascript
let scale = 1
let offsetX = 0
let offsetY = 0
let isDragging = false
let lastX, lastY

function render() {
  ctx.clearRect(0, 0, canvas.width, canvas.height)

  ctx.save()
  ctx.translate(offsetX, offsetY)
  ctx.scale(scale, scale)

  // 绑制内容
  drawContent()

  ctx.restore()
}

// 鼠标滚轮缩放
canvas.addEventListener('wheel', function (e) {
  e.preventDefault()

  const rect = canvas.getBoundingClientRect()
  const mouseX = e.clientX - rect.left
  const mouseY = e.clientY - rect.top

  const zoom = e.deltaY > 0 ? 0.9 : 1.1
  const newScale = scale * zoom

  // 限制缩放范围
  if (newScale < 0.1 || newScale > 10) return

  // 以鼠标位置为中心缩放
  offsetX = mouseX - (mouseX - offsetX) * zoom
  offsetY = mouseY - (mouseY - offsetY) * zoom
  scale = newScale

  render()
})

// 拖拽平移
canvas.addEventListener('mousedown', function (e) {
  if (e.button === 1 || e.button === 2) {
    // 中键或右键
    isDragging = true
    lastX = e.clientX
    lastY = e.clientY
  }
})

canvas.addEventListener('mousemove', function (e) {
  if (isDragging) {
    offsetX += e.clientX - lastX
    offsetY += e.clientY - lastY
    lastX = e.clientX
    lastY = e.clientY
    render()
  }
})

canvas.addEventListener('mouseup', () => (isDragging = false))
canvas.addEventListener('contextmenu', e => e.preventDefault())

第38题:如何绘制贝塞尔曲线?

答案:

javascript
// 二次贝塞尔曲线
// quadraticCurveTo(cpx, cpy, x, y)
// cpx, cpy: 控制点
// x, y: 终点
ctx.beginPath()
ctx.moveTo(50, 100)
ctx.quadraticCurveTo(150, 20, 250, 100) // 控制点(150, 20),终点(250, 100)
ctx.stroke()

// 三次贝塞尔曲线
// bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y)
// cp1x, cp1y: 控制点1
// cp2x, cp2y: 控制点2
// x, y: 终点
ctx.beginPath()
ctx.moveTo(50, 100)
ctx.bezierCurveTo(100, 20, 200, 180, 250, 100)
ctx.stroke()

// 绘制心形示例
ctx.beginPath()
ctx.moveTo(100, 50)
ctx.bezierCurveTo(50, 0, 0, 50, 100, 150)
ctx.bezierCurveTo(200, 50, 150, 0, 100, 50)
ctx.fillStyle = 'red'
ctx.fill()

第39题:如何实现 Canvas 的离屏渲染?

答案:

javascript
// 创建离屏 Canvas
const offscreen = document.createElement('canvas')
offscreen.width = canvas.width
offscreen.height = canvas.height
const offCtx = offscreen.getContext('2d')

// 在离屏 Canvas 上预渲染静态内容
function prerender() {
  offCtx.clearRect(0, 0, offscreen.width, offscreen.height)

  // 绑制复杂的静态背景
  offCtx.fillStyle = '#f0f0f0'
  offCtx.fillRect(0, 0, offscreen.width, offscreen.height)

  // 绑制网格
  offCtx.strokeStyle = '#ddd'
  for (let x = 0; x < offscreen.width; x += 20) {
    offCtx.beginPath()
    offCtx.moveTo(x, 0)
    offCtx.lineTo(x, offscreen.height)
    offCtx.stroke()
  }
  for (let y = 0; y < offscreen.height; y += 20) {
    offCtx.beginPath()
    offCtx.moveTo(0, y)
    offCtx.lineTo(offscreen.width, y)
    offCtx.stroke()
  }
}

// 主渲染循环
function render() {
  // 复制离屏 Canvas 到主 Canvas
  ctx.clearRect(0, 0, canvas.width, canvas.height)
  ctx.drawImage(offscreen, 0, 0)

  // 绑制动态内容
  drawDynamicContent()

  requestAnimationFrame(render)
}

prerender()
render()

第40题:如何优化 Canvas 的性能?

答案:

javascript
// 1. 减少绑制调用
// 不好:多次绘制
for (let i = 0; i < 1000; i++) {
  ctx.beginPath()
  ctx.arc(i, 100, 5, 0, Math.PI * 2)
  ctx.fill()
}

// 好:一次绘制
ctx.beginPath()
for (let i = 0; i < 1000; i++) {
  ctx.moveTo(i + 5, 100)
  ctx.arc(i, 100, 5, 0, Math.PI * 2)
}
ctx.fill()

// 2. 使用离屏 Canvas 缓存
const cachedCanvas = document.createElement('canvas')
// ... 预渲染

// 3. 避免频繁状态切换
// 不好
for (let i = 0; i < 100; i++) {
  ctx.fillStyle = colors[i]
  ctx.fillRect(i * 10, 0, 10, 10)
}

// 好:按颜色分组
const colorGroups = {}
// ... 分组
for (const color in colorGroups) {
  ctx.fillStyle = color
  colorGroups[color].forEach(rect => {
    ctx.fillRect(rect.x, rect.y, rect.w, rect.h)
  })
}

// 4. 局部重绘
// 只清除和重绘变化区域
ctx.clearRect(x, y, width, height)
// ... 重绘该区域

// 5. 减少浮点数坐标
ctx.fillRect(Math.round(x), Math.round(y), w, h)

// 6. 使用 CSS transform 代替 Canvas 变换(对于静态缩放)

// 7. 关闭透明度(如果不需要)
const ctx = canvas.getContext('2d', { alpha: false })

// 8. 使用 requestAnimationFrame
// 9. 避免在动画中创建对象
// 10. 使用 Web Worker 处理复杂计算

第41题:如何实现 Canvas 的截图功能?

答案:

javascript
// 截取整个 Canvas
function captureCanvas() {
  const dataURL = canvas.toDataURL('image/png')
  return dataURL
}

// 截取指定区域
function captureArea(x, y, width, height) {
  // 创建临时 Canvas
  const tempCanvas = document.createElement('canvas')
  tempCanvas.width = width
  tempCanvas.height = height
  const tempCtx = tempCanvas.getContext('2d')

  // 复制指定区域
  tempCtx.drawImage(canvas, x, y, width, height, 0, 0, width, height)

  return tempCanvas.toDataURL('image/png')
}

// 下载截图
function downloadScreenshot() {
  const link = document.createElement('a')
  link.download = 'screenshot.png'
  link.href = canvas.toDataURL('image/png')
  link.click()
}

// 复制到剪贴板(需要 HTTPS)
async function copyToClipboard() {
  try {
    const blob = await new Promise(resolve => {
      canvas.toBlob(resolve, 'image/png')
    })
    await navigator.clipboard.write([new ClipboardItem({ 'image/png': blob })])
    console.log('已复制到剪贴板')
  } catch (err) {
    console.error('复制失败:', err)
  }
}

第42题:如何处理 Canvas 的高 DPI 显示?

答案:

javascript
function setupHiDPICanvas(canvas) {
  const dpr = window.devicePixelRatio || 1
  const rect = canvas.getBoundingClientRect()

  // 设置 Canvas 实际尺寸
  canvas.width = rect.width * dpr
  canvas.height = rect.height * dpr

  // 设置 CSS 显示尺寸
  canvas.style.width = rect.width + 'px'
  canvas.style.height = rect.height + 'px'

  // 缩放上下文以匹配 DPI
  const ctx = canvas.getContext('2d')
  ctx.scale(dpr, dpr)

  return ctx
}

// 使用
const canvas = document.getElementById('myCanvas')
const ctx = setupHiDPICanvas(canvas)

// 绑制时使用 CSS 像素坐标
ctx.fillRect(10, 10, 100, 100)

// 监听窗口大小变化
window.addEventListener('resize', function () {
  setupHiDPICanvas(canvas)
  render()
})

第43题:如何实现 Canvas 的粒子效果?

答案:

javascript
class Particle {
  constructor(x, y) {
    this.x = x
    this.y = y
    this.vx = (Math.random() - 0.5) * 4
    this.vy = (Math.random() - 0.5) * 4
    this.radius = Math.random() * 3 + 1
    this.color = `hsl(${Math.random() * 360}, 70%, 60%)`
    this.alpha = 1
    this.decay = Math.random() * 0.02 + 0.01
  }

  update() {
    this.x += this.vx
    this.y += this.vy
    this.alpha -= this.decay
  }

  draw(ctx) {
    ctx.save()
    ctx.globalAlpha = this.alpha
    ctx.fillStyle = this.color
    ctx.beginPath()
    ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2)
    ctx.fill()
    ctx.restore()
  }
}

const particles = []

function animate() {
  ctx.clearRect(0, 0, canvas.width, canvas.height)

  // 更新和绘制粒子
  for (let i = particles.length - 1; i >= 0; i--) {
    particles[i].update()
    particles[i].draw(ctx)

    // 移除消失的粒子
    if (particles[i].alpha <= 0) {
      particles.splice(i, 1)
    }
  }

  requestAnimationFrame(animate)
}

// 点击创建粒子
canvas.addEventListener('click', function (e) {
  const rect = canvas.getBoundingClientRect()
  const x = e.clientX - rect.left
  const y = e.clientY - rect.top

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

animate()

第44题:如何实现 Canvas 的碰撞检测?

答案:

javascript
// 矩形碰撞检测
function rectCollision(rect1, rect2) {
  return (
    rect1.x < rect2.x + rect2.width &&
    rect1.x + rect1.width > rect2.x &&
    rect1.y < rect2.y + rect2.height &&
    rect1.y + rect1.height > rect2.y
  )
}

// 圆形碰撞检测
function circleCollision(circle1, circle2) {
  const dx = circle1.x - circle2.x
  const dy = circle1.y - circle2.y
  const distance = Math.sqrt(dx * dx + dy * dy)
  return distance < circle1.radius + circle2.radius
}

// 点与矩形碰撞
function pointInRect(point, rect) {
  return (
    point.x >= rect.x &&
    point.x <= rect.x + rect.width &&
    point.y >= rect.y &&
    point.y <= rect.y + rect.height
  )
}

// 点与圆形碰撞
function pointInCircle(point, circle) {
  const dx = point.x - circle.x
  const dy = point.y - circle.y
  const distance = Math.sqrt(dx * dx + dy * dy)
  return distance < circle.radius
}

// 矩形与圆形碰撞
function rectCircleCollision(rect, circle) {
  const closestX = Math.max(rect.x, Math.min(circle.x, rect.x + rect.width))
  const closestY = Math.max(rect.y, Math.min(circle.y, rect.y + rect.height))

  const dx = circle.x - closestX
  const dy = circle.y - closestY
  const distance = Math.sqrt(dx * dx + dy * dy)

  return distance < circle.radius
}

// 使用示例
const objects = [
  { type: 'rect', x: 10, y: 10, width: 50, height: 50 },
  { type: 'circle', x: 100, y: 100, radius: 30 }
]

function checkCollisions() {
  for (let i = 0; i < objects.length; i++) {
    for (let j = i + 1; j < objects.length; j++) {
      const obj1 = objects[i]
      const obj2 = objects[j]

      if (obj1.type === 'rect' && obj2.type === 'rect') {
        if (rectCollision(obj1, obj2)) {
          console.log('矩形碰撞')
        }
      } else if (obj1.type === 'circle' && obj2.type === 'circle') {
        if (circleCollision(obj1, obj2)) {
          console.log('圆形碰撞')
        }
      }
    }
  }
}

第45题:如何实现 Canvas 的拖拽功能?

答案:

javascript
const shapes = [
  { x: 50, y: 50, width: 100, height: 80, color: 'red' },
  { x: 200, y: 100, width: 80, height: 80, color: 'blue' }
]

let selectedShape = null
let offsetX, offsetY
let isDragging = false

function draw() {
  ctx.clearRect(0, 0, canvas.width, canvas.height)
  shapes.forEach(shape => {
    ctx.fillStyle = shape.color
    ctx.fillRect(shape.x, shape.y, shape.width, shape.height)
  })
}

function getShapeAt(x, y) {
  for (let i = shapes.length - 1; i >= 0; i--) {
    const shape = shapes[i]
    if (
      x >= shape.x &&
      x <= shape.x + shape.width &&
      y >= shape.y &&
      y <= shape.y + shape.height
    ) {
      return shape
    }
  }
  return null
}

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

  selectedShape = getShapeAt(x, y)
  if (selectedShape) {
    isDragging = true
    offsetX = x - selectedShape.x
    offsetY = y - selectedShape.y

    // 将选中的图形移到最上层
    const index = shapes.indexOf(selectedShape)
    shapes.splice(index, 1)
    shapes.push(selectedShape)
    draw()
  }
})

canvas.addEventListener('mousemove', function (e) {
  if (!isDragging || !selectedShape) return

  const rect = canvas.getBoundingClientRect()
  const x = e.clientX - rect.left
  const y = e.clientY - rect.top

  selectedShape.x = x - offsetX
  selectedShape.y = y - offsetY
  draw()
})

canvas.addEventListener('mouseup', function () {
  isDragging = false
  selectedShape = null
})

draw()

第46题:如何实现 Canvas 的缩放动画?

答案:

javascript
function animateScale(from, to, duration, callback) {
  const startTime = performance.now()

  function animate(currentTime) {
    const elapsed = currentTime - startTime
    const progress = Math.min(elapsed / duration, 1)

    // 缓动函数(ease-out)
    const easeProgress = 1 - Math.pow(1 - progress, 3)

    const currentScale = from + (to - from) * easeProgress

    ctx.clearRect(0, 0, canvas.width, canvas.height)
    ctx.save()
    ctx.translate(canvas.width / 2, canvas.height / 2)
    ctx.scale(currentScale, currentScale)
    ctx.translate(-canvas.width / 2, -canvas.height / 2)

    // 绑制内容
    ctx.fillStyle = 'red'
    ctx.fillRect(100, 100, 100, 100)

    ctx.restore()

    if (progress < 1) {
      requestAnimationFrame(animate)
    } else if (callback) {
      callback()
    }
  }

  requestAnimationFrame(animate)
}

// 使用
animateScale(0.5, 2, 1000)

// 常用缓动函数
const easings = {
  linear: t => t,
  easeIn: t => t * t,
  easeOut: t => 1 - Math.pow(1 - t, 2),
  easeInOut: t => (t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2),
  bounce: t => {
    const n1 = 7.5625
    const d1 = 2.75
    if (t < 1 / d1) return n1 * t * t
    if (t < 2 / d1) return n1 * (t -= 1.5 / d1) * t + 0.75
    if (t < 2.5 / d1) return n1 * (t -= 2.25 / d1) * t + 0.9375
    return n1 * (t -= 2.625 / d1) * t + 0.984375
  }
}

第47题:如何实现 Canvas 的旋转动画?

答案:

javascript
let angle = 0
let rotationSpeed = 0.02

function animate() {
  ctx.clearRect(0, 0, canvas.width, canvas.height)

  ctx.save()
  ctx.translate(canvas.width / 2, canvas.height / 2)
  ctx.rotate(angle)

  // 绑制一个矩形,中心在原点
  ctx.fillStyle = 'red'
  ctx.fillRect(-50, -50, 100, 100)

  ctx.restore()

  angle += rotationSpeed
  requestAnimationFrame(animate)
}

animate()

// 围绕特定点旋转
function rotateAroundPoint(x, y, angle) {
  ctx.save()
  ctx.translate(x, y)
  ctx.rotate(angle)
  ctx.translate(-x, -y)

  // 绑制内容
  ctx.fillRect(x - 50, y - 50, 100, 100)

  ctx.restore()
}

// 旋转到指定角度的动画
function animateToAngle(targetAngle, duration) {
  const startAngle = angle
  const startTime = performance.now()

  function animate(currentTime) {
    const elapsed = currentTime - startTime
    const progress = Math.min(elapsed / duration, 1)

    angle = startAngle + (targetAngle - startAngle) * progress

    ctx.clearRect(0, 0, canvas.width, canvas.height)
    ctx.save()
    ctx.translate(canvas.width / 2, canvas.height / 2)
    ctx.rotate(angle)
    ctx.fillRect(-50, -50, 100, 100)
    ctx.restore()

    if (progress < 1) {
      requestAnimationFrame(animate)
    }
  }

  requestAnimationFrame(animate)
}

第48题:如何实现 Canvas 的渐变动画?

答案:

javascript
// 颜色渐变动画
function animateColor(fromColor, toColor, duration, callback) {
  const startTime = performance.now()

  function animate(currentTime) {
    const elapsed = currentTime - startTime
    const progress = Math.min(elapsed / duration, 1)

    const currentColor = interpolateColor(fromColor, toColor, progress)

    ctx.clearRect(0, 0, canvas.width, canvas.height)
    ctx.fillStyle = currentColor
    ctx.fillRect(50, 50, 200, 200)

    if (progress < 1) {
      requestAnimationFrame(animate)
    } else if (callback) {
      callback()
    }
  }

  requestAnimationFrame(animate)
}

function interpolateColor(color1, color2, progress) {
  const r1 = parseInt(color1.slice(1, 3), 16)
  const g1 = parseInt(color1.slice(3, 5), 16)
  const b1 = parseInt(color1.slice(5, 7), 16)

  const r2 = parseInt(color2.slice(1, 3), 16)
  const g2 = parseInt(color2.slice(3, 5), 16)
  const b2 = parseInt(color2.slice(5, 7), 16)

  const r = Math.round(r1 + (r2 - r1) * progress)
  const g = Math.round(g1 + (g2 - g1) * progress)
  const b = Math.round(b1 + (b2 - b1) * progress)

  return `rgb(${r}, ${g}, ${b})`
}

// 使用
animateColor('#ff0000', '#0000ff', 2000)

// 渐变位置动画
function animateGradient() {
  let offset = 0

  function animate() {
    ctx.clearRect(0, 0, canvas.width, canvas.height)

    const gradient = ctx.createLinearGradient(offset, 0, offset + 200, 0)
    gradient.addColorStop(0, 'red')
    gradient.addColorStop(1, 'blue')

    ctx.fillStyle = gradient
    ctx.fillRect(0, 100, 400, 100)

    offset += 2
    if (offset > 200) offset = -200

    requestAnimationFrame(animate)
  }

  animate()
}

第49题:如何实现 Canvas 的路径动画(绘制过程动画)?

答案:

javascript
// 使用 setLineDash 和 lineDashOffset 实现路径动画
function animatePath(points, duration) {
  ctx.beginPath()
  ctx.moveTo(points[0].x, points[0].y)

  for (let i = 1; i < points.length; i++) {
    ctx.lineTo(points[i].x, points[i].y)
  }

  // 计算路径总长度
  let totalLength = 0
  for (let i = 1; i < points.length; i++) {
    const dx = points[i].x - points[i - 1].x
    const dy = points[i].y - points[i - 1].y
    totalLength += Math.sqrt(dx * dx + dy * dy)
  }

  ctx.setLineDash([totalLength])
  ctx.lineDashOffset = totalLength
  ctx.strokeStyle = 'black'
  ctx.lineWidth = 2

  const startTime = performance.now()

  function animate(currentTime) {
    const elapsed = currentTime - startTime
    const progress = Math.min(elapsed / duration, 1)

    ctx.clearRect(0, 0, canvas.width, canvas.height)
    ctx.lineDashOffset = totalLength * (1 - progress)
    ctx.stroke()

    if (progress < 1) {
      requestAnimationFrame(animate)
    }
  }

  requestAnimationFrame(animate)
}

// 使用
const points = [
  { x: 50, y: 200 },
  { x: 150, y: 50 },
  { x: 250, y: 200 },
  { x: 350, y: 50 }
]
animatePath(points, 2000)

// 另一种方法:逐步绘制
function animatePathProgress(points, duration) {
  const startTime = performance.now()

  function animate(currentTime) {
    const elapsed = currentTime - startTime
    const progress = Math.min(elapsed / duration, 1)

    const pointsToDraw = Math.floor(points.length * progress)

    ctx.clearRect(0, 0, canvas.width, canvas.height)
    ctx.beginPath()
    ctx.moveTo(points[0].x, points[0].y)

    for (let i = 1; i <= pointsToDraw; i++) {
      ctx.lineTo(points[i].x, points[i].y)
    }

    // 绘制到当前进度的中间点
    if (pointsToDraw < points.length - 1) {
      const subProgress = points.length * progress - pointsToDraw
      const lastPoint = points[pointsToDraw]
      const nextPoint = points[pointsToDraw + 1]
      const x = lastPoint.x + (nextPoint.x - lastPoint.x) * subProgress
      const y = lastPoint.y + (nextPoint.y - lastPoint.y) * subProgress
      ctx.lineTo(x, y)
    }

    ctx.stroke()

    if (progress < 1) {
      requestAnimationFrame(animate)
    }
  }

  requestAnimationFrame(animate)
}

第50题:Canvas 有哪些常见的应用场景?请举例说明。

答案:

Canvas 的常见应用场景包括:

1. 数据可视化图表

javascript
// 柱状图
function drawBarChart(data) {
  const barWidth = 40
  const gap = 20
  const maxValue = Math.max(...data.map(d => d.value))

  data.forEach((item, index) => {
    const height = (item.value / maxValue) * 300
    const x = index * (barWidth + gap)
    const y = canvas.height - height - 30

    ctx.fillStyle = item.color
    ctx.fillRect(x, y, barWidth, height)

    ctx.fillStyle = 'black'
    ctx.font = '12px Arial'
    ctx.textAlign = 'center'
    ctx.fillText(item.label, x + barWidth / 2, canvas.height - 10)
  })
}

2. 游戏开发

javascript
// 简单的游戏循环
class Game {
  constructor() {
    this.player = { x: 100, y: 100, width: 50, height: 50 }
    this.enemies = []
  }

  update() {
    this.player.x += 1
  }

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

    ctx.fillStyle = 'blue'
    ctx.fillRect(
      this.player.x,
      this.player.y,
      this.player.width,
      this.player.height
    )

    this.enemies.forEach(enemy => {
      ctx.fillStyle = 'red'
      ctx.fillRect(enemy.x, enemy.y, enemy.width, enemy.height)
    })
  }

  loop() {
    this.update()
    this.render()
    requestAnimationFrame(() => this.loop())
  }
}

**3. 图像

基于 VitePress 的本地知识库