Appearance
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 的区别
| 特性 | Canvas | SVG |
|---|---|---|
| 渲染方式 | 位图(像素) | 矢量图 |
| 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 = pattern3.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 有什么区别?
答案:
主要区别如下:
渲染方式:
- Canvas 是基于像素的位图渲染,绘制后不保留图形信息
- SVG 是基于矢量的图形,每个图形都是 DOM 元素
DOM 操作:
- Canvas 绑制的图形不能通过 DOM 操作
- SVG 图形可以通过 DOM API 操作
事件处理:
- Canvas 需要手动计算点击位置来判断点击了哪个图形
- SVG 图形原生支持事件绑定
性能:
- Canvas 适合大量图形绘制(如游戏、粒子效果)
- SVG 适合少量复杂图形,图形过多时性能下降
缩放:
- Canvas 放大后会模糊(位图特性)
- SVG 可以无限缩放不失真
适用场景:
- 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。
设置尺寸有两种方式:
- HTML 属性设置(推荐):
html
<canvas id="myCanvas" width="500" height="400"></canvas>- 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 的优势:
自动匹配刷新率:与显示器刷新率同步(通常是 60fps),避免丢帧或重复绘制
性能优化:页面不可见(如切换标签页、最小化)时自动暂停,节省 CPU/GPU 资源
更流畅:浏览器会在重绘前调用,避免视觉卡顿
时间精度:回调函数接收高精度时间戳参数
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('点在描边上')
}注意:isPointInPath 和 isPointInStroke 只能检测当前路径,需要在 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: *其他解决方案:
- 使用代理服务器转发图片
- 将图片转为 Base64 后使用
- 使用后端处理图片
第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. 图像