🖼️ Canvas 基础教程
🌟 Canvas 简介
<canvas> 标签只是一个图形容器,而Canvas API 是 HTML5 提供的一个强大的绘图 API,它允许我们使用 JavaScript 在网页上绘制图形、动画和其他视觉效果。
canvas 元素尺寸
关于这部分,详见 Canvas 的两种尺寸
🎯 1. 基础概念
1.1 画布
首先需要在 HTML 中创建 canvas 元素,并通过 JavaScript 获取绘图上下文:
<!-- 1. 创建画布元素 -->
<canvas id="canvas"
style="border: 1px solid #aaaaaa; display: block; margin: 50px auto;"
width="800"
height="600">
<!-- 浏览器不支持 Canvas 时显示的后备内容 -->
你的浏览器不支持 Canvas,请升级浏览器!
</canvas>
<script>
// 2. 获取 Canvas 对象
const canvas = document.getElementById("canvas");
// 3. 获取绘图上下文
const context = canvas.getContext("2d");
console.log(canvas, context);
</script>1.2 🎮 上下文类型与属性
1.2.1 上下文类型(contextType)
| 类型 | 描述 |
|---|---|
| 2d | 创建一个二维渲染上下文 |
| webgl | 创建一个三维渲染上下文(WebGL 版本 1) |
| webgl2 | 创建一个三维渲染上下文(WebGL 版本 2) |
| bitmaprenderer | 创建一个只提供将 canvas 内容替换为指定 ImageBitmap 功能的上下文(Safari 暂不支持) |
1.2.2 上下文属性(contextAttributes)
1.2.2.1 2D 上下文属性
| 属性 | 描述 |
|---|---|
| alpha | 布尔值,表明 canvas 是否包含 alpha 通道。设为 false 时浏览器将认为 canvas 背景总是不透明的,可加速绘制透明内容和图片 |
1.2.2.2 WebGL 上下文属性
| 属性 | 描述 |
|---|---|
| alpha | 布尔值,表明 canvas 是否包含 alpha 通道 |
| antialias | 布尔值,是否开启抗锯齿 |
| depth | 布尔值,绘制缓冲区是否包含至少 16 位的深度缓冲区 |
| failIfMajorPerformanceCaveat | 布尔值,是否在系统性能低的环境创建上下文 |
| powerPreference | 指示浏览器运行 WebGL 上下文时的 GPU 电源配置: • default: 自动选择(默认值) • high-performance: 高性能模式 • low-power: 节能模式 |
| premultipliedAlpha | 布尔值,排版引擎是否假设绘制缓冲区包含预混合 alpha 通道 |
| preserveDrawingBuffer | 布尔值,是否保存缓冲区直到被清除或覆盖 |
| stencil | 布尔值,绘制缓冲区是否包含至少 8 位的模版缓冲区 |
1.3 ⚠️ 重要概念:状态式绘制
Canvas 采用状态式绘制模式,这意味着:
- 先设置绘图状态(颜色、线宽等)
- 然后执行具体的绘制操作
- 状态会影响后续所有绘制操作
1.4 📐 坐标系统
Canvas 使用笛卡尔坐标系:
- 原点(0,0)位于画布左上角
- x 轴向右为正
- y 轴向下为正
🎨 2. 基础绘制操作
<canvas> 只支持两种形式的图形绘制:矩形 和 路径(由一系列点连成的线段)。
2.1 绘制矩形
<canvas> 提供了三种方法绘制矩形,矩形的左上角为(x,y)
2.1.1 fillRect
绘制一个填充的矩形,填充色为当前的 fillStyle。
fillRect(x, y, width, height);2.1.2 strokeRect
绘制一个矩形的边框,边框色为当前的 strokeStyle
strokeRect(x, y, width, height);2.1.3 clearRect
清除画布中指定矩形区域,并重置为透明色,以达到擦除一个矩形区域的效果。 请确保在调用 clearRect() 之后绘制新内容前调用 beginPath() 。
clearRect(x, y, width, height);2.2 ✏️ 路径绘制
使用路径绘制图形的步骤:
- 使用
beginPath()开始新路径。这样做会清空路径列表 - 画出路径。
- (可选)闭合路径,
closePath()绘制一条从当前点到路径起点的直线来闭合路径 - 通过描边或者填充路径区域来渲染图形
- 使用
fill(),所有没有闭合的形状都会自动闭合。 - 调用
stroke(),不会自动闭合。
- 使用
2.2.1 moveTo
moveTo() 方法用于设置路径的起点。
context.moveTo(x, y);// 将笔触移动到坐标 (x,y) 上2.2.2 lineTo
lineTo() 方法用于绘制一条从当前点(之前路径的结束点 / moveTo决定)到指定点(x,y)的线段。
绘制两个三角形:
context.beginPath();
context.moveTo(100, 100);
context.lineTo(600, 600);
context.lineTo(600, 100);
context.fill(); // 自动闭合路径
context.beginPath();
context.moveTo(600, 600);
context.lineTo(100, 600);
context.lineTo(100, 100);
context.closePath();
context.stroke();2.2.3 arc圆弧
绘制圆弧或者圆,可以选择 arc() 或者 arcTo()。两个方法的绘制思路是不一样的,通常选择 arc()
arc()
- 圆心坐标为 (x,y)
- 半径为 radius
- 从 startAngle 到 endAngle ,以弧度计(0°为3点钟方向,正数表示逆时针方向,负数表示顺时针方向)
- anticlockwise 表示绘制方向,true 表示逆时针,false 表示顺时针
context.arc(x, y, radius, startAngle, endAngle, anticlockwise);arcTo()
- 连接当前位置(蓝点)和控制点 1(x1,y1)得到直线 A
- 连接控制点 1(x1,y1)和控制点 2(x2,y2)得到直线 B
- 以 radius 作为半价
- 以直线A 和直线B 作为切线,画出两条切线之前的弧线路径
context.arcTo(x1, y1, x2, y2, radius);2.2.4 rect矩形
rect() 方法绘制一个左上角坐标为 (x,y),宽高为 width 以及 height 的矩形。 在该方法执行前,当前笔触自动重置回默认坐标,即 moveTo(0,0);该方法结束后的笔触停留在矩形左上角 (x,y)。
context.rect(x, y, width, height);2.2.5 Bézier曲线
quadraticCurveTo() 方法绘制一个二次贝塞尔曲线,即从当前位置到指定点(x1,y1)画一条二次贝塞尔曲线,再从指定点(x1,y1)到指定点(x2,y2)画一条直线。
quadraticCurveTo(x1, y1, x2, y2)3. Path2D
Path2D 对象是一个路径对象,它允许你定义一个路径,然后使用它来填充或描边。
3.1 创建和拷贝路径
const path1 = new Path2D();
path1.rect(10, 10, 100, 100);
const path2 = new Path2D(path1);
path2.moveTo(220, 60);
path2.arc(170, 60, 50, 0, 2 * Math.PI);
ctx.stroke(path2);这样就画出了一个圆和一个矩形
3.2 SVG path
Path2D 对象可以解析 SVG path 的字符串,从而创建一个路径对象。
// 移动到(10,10),向右侧移动 80px,向下移动 80px,再向左侧移动 80px,再向上移动 80px,然后回到起点(z)。
const path = new Path2D('M10 10 h 80 v 80 h -80 Z');3.3 addPath
addPath() 方法允许你添加一个路径到当前路径中。
const path1 = new Path2D('M10 10 h 80 v 80 h -80 Z');
path1.addPath(new Path2D('M60 160 h 80 v 80 h -80 Z'), { transform: new DOMMatrix().translate(100, 0) });4. 渲染上色
fillStyle 和 strokeStyle 属性用于设置填充和描边颜色。 两者默认为 #000(黑色),可以接收字符串、颜色对象、渐变对象或者图像对象作为参数。
4.1 色值
context.fillStyle = 'red';
context.fillStyle = "#FFA500";
context.fillStyle = "rgb(255,165,0)";
context.fillStyle = "rgba(255,165,0,0.2)"; // 透明度 0.24.2 渐变对象
用 线性 或者 径向 的两种渐变形式来新建一个 canvasGradient 对象,并且赋给图形的 fillStyle 或 strokeStyle 属性。
线性渐变:
createLinearGradient(x0, y0, x1, y1),x0和y0为起点坐标,x1和y1为终点坐标 径向渐变:createRadialGradient(x0, y0, r0, x1, y1, r1),x0和y0为起点坐标,r0为起点半径,x1和y1为终点坐标,r1为终点半径
添加断点和颜色:创建出 canvasGradient 对象后, 可以使用 addColorStop(position, color) 。
position:断点位置,取值范围从 0 到 1,0 表示起点,1 表示终点color:颜色,可以是字符串、颜色对象、渐变对象或者图像对象
const grd = ctx.createLinearGradient(0, 0, 200, 200);
grd.addColorStop(0, "red");
grd.addColorStop(1, "white");
ctx.fillStyle = grd;
ctx.fillRect(0, 0, 200, 200);4.3 图像对象
createPattern(image, type) 方法创建一个 canvasPattern 对象,并且赋给图形的 fillStyle 或 strokeStyle 属性。
image:图像对象,可以是 一个 Image 对象的引用,或者另一个 canvas 对象。type:填充类型,可选值为repeat、repeat-x、repeat-y、no-repeat**注意:**需要等待图像加载完成,否则会报错。
5. 🎯 线条属性详解
5.1 lineWidth
设置当前绘线的粗细。默认值是 1.0,必须为正数。 绘制十条宽度从 1.0 到 10.0 的线:
for (let i = 1; i <= 10; i++) {
context.lineWidth = i;
context.beginPath();
context.moveTo(i, 0);
context.lineTo(i, 10);
context.stroke();
}注意: 当我们的路径 Y 轴落在网格上时(像下图 2),两边的实际填充区域只占网格的一半,实际渲染中 canvas 会以实际笔触颜色一半色调的颜色来填充整个区域,所以,最左边的以及所有宽度为奇数的线并不能精确呈现。 所以在默认情况下,我们想要获得精确的线条,就要让奇数宽度的中心落在网格中心,偶数宽度的中心落在网格线上。在操作缩放时,这也是不可忽略的一点。
5.2 lineCap
控制线条端点样式:
butt:默认值,平直边缘round:圆形端点square:方形端点
5.3 lineJoin 属性
控制线条连接处样式:
miter:默认值,尖角,受到miterLimit的限制。bevel:斜角round:圆角
5.4 miterLimit
如果夹角无限小,那么外延点会无限远。 miterLimit 设定外延交点与连接点的最大距离(默认为 10.0),如果交点距离大于此值,连接效果会变成了 bevel。
max miterLength(最长交点距离)= miterLimit * lineWidth5.4 虚线
5.4.1 setLineDash
在填充线时使用虚线模式。
context.setLineDash([5, 15]); // 一个数组,描述线段和间距。 5px 线段,15px 间距。
context.setLineDash([]); // 清除虚线模式。
//常用的虚线样式
function drawDashedLine(pattern) {
ctx.beginPath();
ctx.setLineDash(pattern);
ctx.moveTo(0, y);
ctx.lineTo(300, y);
ctx.stroke();
y += 20;
}
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
let y = 15;
drawDashedLine([]); // 实线
drawDashedLine([1, 1]);
drawDashedLine([10, 10]);
drawDashedLine([20, 5]);
drawDashedLine([15, 3, 3, 3]);
drawDashedLine([20, 3, 3, 3, 3, 3, 3, 3]);
drawDashedLine([12, 3, 3]); // Equals [12, 3, 3, 12, 3, 3]5.4.2 lineDashOffset
设置虚线偏移量,默认为 0。 利用这个偏移量,我们可以让虚线"动起来",俗称"蚂蚁线"。
let offset = 0;
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.setLineDash([4, 4, 12, 4]);
ctx.lineDashOffset = offset;
ctx.strokeRect(20, 20, 150, 150);
}
function march() {
offset++;
if (offset > 24) {
offset = 0;
}
draw();
setTimeout(march, 20);
}
march();5.4.3 getLineDash()
getLineDash(),是获取当前线段样式的方法。返回值是一个描述线段和间隔的数组。如果数组元素的数量是奇数,数组元素会被复制并重复。
6. 🎯 阴影
Canvas 提供了四个属性来设置阴影效果:
| 属性 | 描述 | 默认值 | 说明 |
|---|---|---|---|
| shadowOffsetX | 设置阴影在 X 轴的延伸距离 | 0 | • 负值:阴影向左延伸 • 正值:阴影向右延伸 • 不受变换矩阵影响 |
| shadowOffsetY | 设置阴影在 Y 轴的延伸距离 | 0 | • 负值:阴影向上延伸 • 正值:阴影向下延伸 • 不受变换矩阵影响 |
| shadowBlur | 设置阴影的模糊程度 | 0 | 值越大阴影越模糊 |
| shadowColor | 设置阴影的颜色 | transparent black (rgba(0,0,0,0)) | 支持 CSS 颜色值 |
ctx.shadowOffsetX = 10;
ctx.shadowOffsetY = 10;
ctx.shadowBlur = 10;
ctx.shadowColor = "rgba(23,23,23,0.5)";
ctx.fillRect(20, 20, 150, 150);7. 填充规则
Canvas 提供了两种填充规则:nonzero(默认)和 evenodd。这些规则决定了如何填充具有复杂路径(如自交叉或者嵌套路径)的图形。 evenodd:
- 奇偶规则,类似于射线检测,取一点,向任意方向画一条射线,如果射线与路径相交,则计数加一,否则减一。如果计数为奇数,则填充图形。 nonzero:
- 非零 winding 规则,取一点向任意方向发出一条射线,找到那些和射线发生交叉的路径。发生逆时针的偏转 次数减一,发生顺时针的偏转次数加一。如果最终的 winding 数为零,则不填充图形。
8. 绘制文本
Canvas 提供了以下方法来绘制文本:
8.1 绘制文本
fillText(text, x, y [, maxWidth]):绘制文本。 strokeText(text, x, y [, maxWidth]):绘制文本边框。
8.2 文本属性
Canvas 提供了多个属性来控制文本样式:
| 属性 | 描述 | 默认值 | 可选值 |
|---|---|---|---|
| font | 字体样式 | 10px sans-serif | CSS font 属性值 |
| textAlign | 文本水平对齐方式 | start | • left: 左对齐 • right: 右对齐 • center: 居中对齐 • start: 对齐文本起点 • end: 对齐文本终点 |
| textBaseline | 文本基线对齐方式 | alphabetic | • top: 文本块顶部 • hanging: 悬挂基线 • middle: 文本块中间 • alphabetic: 标准字母基线 • ideographic: 表意字基线 • bottom: 文本块底部 |
| direction | 文本方向 | inherit | • ltr: 从左到右 • rtl: 从右到左 • inherit: 继承 |
注意:
- textAlign 的实际效果会受 direction 属性影响:
- direction = ltr 时:left = start,right = end
- direction = rtl 时:left = end,right = start
- textBaseline 中的 ideographic 基线,对于超出 alphabetic 基线的字符,会位于字符底部
8.3 文本测量
measureText 方法将返回一个 TextMetrics 对象,只包含部分能体现文本特性的属性(宽度、所在像素)
9. 绘制图片
绘制图片只需要两个步骤:
- 获取图片资源
- 绘制
9.1 获取图片资源
canvas 支持的图片源类型:
9.1.1 HTMLImageElement
1. new Image()
let img = new Image();
img.onload = function () {
// drawImage to canvas
};
img.src = "XXX.png";2. img 标签
<img id="img" src="XXX.png" />
<script>
let img = document.getElementById("img");
img.onload = function () {
// drawImage to canvas
};
</script>9.1.2 HTMLVideoElement
canvas 会绘制当前视频帧,我们可以搭配 video 事件使用,比如下面的例子:
let video = document.getElementById("video");
// 媒体的第一帧加载完成时,渲染第一帧
video.onloadeddata = function () {};
// 播放暂停时,渲染当前帧
video.onpause = function () {};9.1.3 HTMLCanvasElement
一个常用的应用就是将第二个 canvas 作为第一个 canvas 的缩略图:
let sourceCanvas = document.getElementById("source");
let thumbCanvas = document.getElementById("thumb");
thumbCtx.drawImage(source, 0, 0, 100, 50);9.2 绘制drawImage()
1. 基础用法(三个参数)
ctx.drawImage(img, x, y);2. 缩放(五个参数)
drawImage(image, x, y, width, height);3. 剪切(九参数)
drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight);10. 状态的保存和恢复
Canvas 提供了两个重要的状态管理方法:save() 和 restore()。这两个方法是简单绘制和复杂绘制的分界线,可以帮助我们避免大量重复的状态设置工作。
10.1 状态管理方法
10.1.1 save()
save() 方法将当前的绘图状态推入栈中保存。一个绘图状态包括:
- 变换矩阵信息
- 当前的裁剪区域
- 当前的绘图样式属性
10.1.2 restore()
restore() 方法从栈顶弹出最近保存的状态进行恢复。如果栈为空,此方法不做任何改变。
10.2 可保存的状态属性
1. 样式属性
| 类别 | 属性 |
|---|---|
| 描边/填充 | strokeStyle, fillStyle, globalAlpha |
| 线条样式 | lineWidth, lineCap, lineJoin, miterLimit, lineDashOffset |
| 阴影效果 | shadowOffsetX, shadowOffsetY, shadowBlur, shadowColor |
| 文本样式 | font, textAlign, textBaseline, direction |
| 图像处理 | imageSmoothingEnabled |
| 混合模式 | globalCompositeOperation |
2. 变换与裁剪
- 当前的变换矩阵
- 当前的裁剪路径
11. 合成与裁剪
11.1 globalCompositeOperation
globalCompositeOperation 属性设置图形重叠时的合成方式。
| 值 | 描述 | 效果示例 |
|---|---|---|
| source-over | 默认值。在现有画布上绘制新图形 | 新图形覆盖在原有图形之上 |
| source-in | 仅显示新图形和原有内容重叠的部分,其他区域透明 | 类似 AND 操作 |
| source-out | 仅显示新图形中与原有内容不重叠的部分,其他区域透明 | 类似 NOT AND 操作 |
| source-atop | 新图形仅显示与原有内容重叠的部分,原有内容正常显示 | 保留原内容 |
| destination-over | 在原有内容下方绘制新图形 | 新图形被原内容遮挡 |
| destination-in | 仅保留与新图形重叠的原有内容,其他区域透明 | 反向 AND 操作 |
| destination-out | 原有内容中与新图形重叠的部分变透明 | 反向 NOT AND 操作 |
| destination-atop | 原有内容仅保留与新图形重叠的部分,新图形正常显示 | 保留新内容 |
| lighter | 重叠部分的颜色值相加 | 产生更亮的效果 |
| copy | 只显示新图形 | 完全替换原有内容 |
| xor | 重叠部分变透明 | 类似异或操作 |
| multiply | 将顶层像素与底层像素相乘 | 产生更暗的效果 |
| screen | 像素值反相后相乘,再反相 | 产生更亮的效果 |
| overlay | multiply 和 screen 的结合 | 保留明暗对比 |
| darken | 保留重叠部分中较暗的像素 | 变暗效果 |
| lighten | 保留重叠部分中较亮的像素 | 变亮效果 |
示例:
// 设置合成操作
ctx.globalCompositeOperation = 'xor';
// 绘制红色矩形
ctx.fillStyle = 'red';
ctx.fillRect(50, 50, 100, 100);
// 绘制蓝色圆形
ctx.fillStyle = 'blue';
ctx.beginPath();
ctx.arc(100, 100, 50, 0, Math.PI * 2);
ctx.fill();11.2 裁剪路径
clip() 方法将当前路径转换为裁剪路径。裁剪路径的作用是限制绘图区域,只有裁剪路径内的区域才会被绘制。
裁剪路径的特点:
- 裁剪路径一旦创建,后续所有绘制操作都会被限制在裁剪路径内
- 裁剪路径可以通过
save()和restore()来管理 - 默认的裁剪路径是整个 canvas 区域
示例:
// 创建圆形裁剪区域
ctx.beginPath();
ctx.arc(100, 100, 50, 0, Math.PI * 2);
ctx.clip();
// 绘制图片(只会在圆形区域内显示)
ctx.drawImage(img, 0, 0, 200, 200);常见用例:
// 创建圆形头像
function createCircularAvatar(img, size) {
ctx.save();
// 创建圆形裁剪路径
ctx.beginPath();
ctx.arc(size/2, size/2, size/2, 0, Math.PI * 2);
ctx.clip();
// 绘制图片
ctx.drawImage(img, 0, 0, size, size);
// 恢复原始裁剪路径
ctx.restore();
}
// 创建不规则形状的裁剪区域
function createCustomClip() {
ctx.save();
// 创建星形裁剪路径
ctx.beginPath();
for(let i = 0; i < 5; i++) {
ctx.lineTo(Math.cos(i * 4 * Math.PI / 5) * 50 + 100,
Math.sin(i * 4 * Math.PI / 5) * 50 + 100);
}
ctx.closePath();
ctx.clip();
// 绘制渐变背景
const gradient = ctx.createLinearGradient(0, 0, 200, 200);
gradient.addColorStop(0, 'red');
gradient.addColorStop(1, 'blue');
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, 200, 200);
ctx.restore();
}12. 变形
Canvas 提供了多种变形操作,可以移动原点、旋转和缩放网格。这些变形操作让我们能够更灵活地控制绘制效果。
12.1 基础变形方法
12.1.1 translate(x, y)
移动 Canvas 的原点到指定的点 (x, y)。
- x:水平方向的移动距离
- y:垂直方向的移动距离
// 移动原点到 (50, 50)
ctx.translate(50, 50);
ctx.fillRect(0, 0, 100, 100); // 矩形实际绘制在 (50, 50) 位置12.1.2 rotate(angle)
以原点为中心旋转 Canvas。
- angle:旋转角度(以弧度为单位)
- 正值:顺时针旋转
- 负值:逆时针旋转
// 旋转 45 度
ctx.rotate(Math.PI / 4);
ctx.fillRect(100, 0, 80, 20);12.1.3 scale(x, y)
缩放 Canvas 的坐标系。
- x:水平方向的缩放因子
- y:垂直方向的缩放因子
- 默认值为 1(实际大小)
- 值 < 1:缩小
- 值 > 1:放大
- 负值:镜像反转
// 水平拉伸 2 倍,垂直压缩一半
ctx.scale(2, 0.5);
ctx.fillRect(0, 0, 100, 100);
// 水平镜像
ctx.scale(-1, 1);
ctx.font = "48px serif";
ctx.fillText("Hello", -100, 100);12.2 变形矩阵
12.2.1 transform(a, b, c, d, e, f)
直接修改变形矩阵,参数对应矩阵中的元素:
| a c e |
| b d f |
| 0 0 1 |参数说明:
- a:水平缩放 (scaleX)
- b:水平倾斜 (skewX)
- c:垂直倾斜 (skewY)
- d:垂直缩放 (scaleY)
- e:水平移动 (translateX)
- f:垂直移动 (translateY)
// 实现旋转 45 度的效果
const angle = Math.PI / 4;
ctx.transform(
Math.cos(angle), // a
Math.sin(angle), // b
-Math.sin(angle), // c
Math.cos(angle), // d
0, // e
0 // f
);12.2.2 setTransform(a, b, c, d, e, f)
重置当前变形矩阵为单位矩阵,然后应用 transform() 方法的变形效果。
// 重置并应用新的变形
ctx.setTransform(1, 0.5, 0.5, 1, 30, 10);12.2.3 resetTransform()
将当前变形重置为单位矩阵,相当于:
ctx.setTransform(1, 0, 0, 1, 0, 0);12.3 变形的最佳实践
- 使用 save() 和 restore()
ctx.save();
ctx.translate(50, 50);
ctx.rotate(Math.PI / 4);
ctx.fillRect(0, 0, 100, 100);
ctx.restore(); // 恢复原始状态- 变形的顺序很重要
// 结果不同
ctx.translate(100, 100);
ctx.rotate(Math.PI / 4);
// vs
ctx.rotate(Math.PI / 4);
ctx.translate(100, 100);- 使用相对坐标
function drawRotatedRect(x, y, width, height, angle) {
ctx.save();
ctx.translate(x + width/2, y + height/2); // 移动到矩形中心
ctx.rotate(angle);
ctx.fillRect(-width/2, -height/2, width, height); // 使用相对坐标
ctx.restore();
}- 避免累积变形
// 不好的做法 - 变形会累积
function animate() {
ctx.rotate(0.1);
draw();
requestAnimationFrame(animate);
}
// 好的做法 - 每次都重置变形
function animate() {
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.rotate(currentAngle);
draw();
currentAngle += 0.1;
requestAnimationFrame(animate);
}13. 动画
13.1 基本动画原理
Canvas 动画的核心是清除和重绘。由于 Canvas 没有图层的概念,一旦图形绘制完成,就不能再修改。要实现动画效果,需要按照以下步骤执行:
- 清空 Canvas
- 保存 Canvas 状态
- 绘制动画帧
- 恢复 Canvas 状态
13.2 动画控制方法
13.2.1 定时器
// setInterval - 固定时间间隔重绘
setInterval(function() {
// 清空画布
ctx.clearRect(0, 0, canvas.width, canvas.height);
// 绘制新一帧
draw();
}, 1000/60); // 60fps
// setTimeout - 适合基于用户交互的动画
setTimeout(function() {
// 绘制动画帧
draw();
// 继续下一帧
if (needNextFrame) {
setTimeout(arguments.callee, 1000/60);
}
}, 1000/60);13.2.2 requestAnimationFrame
推荐使用 requestAnimationFrame,它会在浏览器下一次重绘之前调用指定的回调函数,提供更流畅的动画效果。
function animate() {
// 清空画布
ctx.clearRect(0, 0, canvas.width, canvas.height);
// 绘制新一帧
draw();
// 请求下一帧
requestAnimationFrame(animate);
}
// 启动动画
animate();13.3 动画效果
13.3.1 运动轨迹
// 匀速运动的小球
const ball = {
x: 100,
y: 100,
radius: 10,
speedX: 5,
speedY: 3
};
function drawBall() {
ctx.beginPath();
ctx.arc(ball.x, ball.y, ball.radius, 0, Math.PI * 2);
ctx.fillStyle = 'red';
ctx.fill();
ctx.closePath();
}
function animate() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
// 更新位置
ball.x += ball.speedX;
ball.y += ball.speedY;
// 碰撞检测
if (ball.x + ball.radius > canvas.width || ball.x - ball.radius < 0) {
ball.speedX = -ball.speedX;
}
if (ball.y + ball.radius > canvas.height || ball.y - ball.radius < 0) {
ball.speedY = -ball.speedY;
}
drawBall();
requestAnimationFrame(animate);
}13.3.2 长尾效果
通过设置半透明背景覆盖,而不是完全清除画布,可以创建长尾效果:
function animate() {
// 使用半透明背景覆盖
ctx.fillStyle = 'rgba(255, 255, 255, 0.3)';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// 绘制新位置
draw();
requestAnimationFrame(animate);
}13.4 像素操作
13.4.1 ImageData 对象
ImageData 对象用于像素级操作,包含以下属性:
- width:图片宽度(像素)
- height:图片高度(像素)
- data:Uint8ClampedArray 类型的一维数组,包含 RGBA 值
// 创建 ImageData
const imageData = ctx.createImageData(width, height);
// 或基于现有 ImageData
const imageData = ctx.createImageData(anotherImageData);
// 获取像素数据
const imageData = ctx.getImageData(x, y, width, height);
// 写入像素数据
ctx.putImageData(imageData, dx, dy);13.4.2 像素处理示例
// 反相效果
function invertColors() {
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]; // red
data[i + 1] = 255 - data[i + 1]; // green
data[i + 2] = 255 - data[i + 2]; // blue
// data[i + 3] 是 alpha 值
}
ctx.putImageData(imageData, 0, 0);
}13.5 性能优化
- 避免浮点数坐标
// ❌ 不推荐
ctx.drawImage(img, 0.3, 0.5);
// ✅ 推荐
ctx.drawImage(img, Math.floor(0.3), Math.floor(0.5));- 缓存和预渲染
// 预渲染复杂图形
const cacheCanvas = document.createElement('canvas');
const cacheCtx = cacheCanvas.getContext('2d');
// 在缓存画布上绘制
cacheCtx.drawImage(complexImage, 0, 0);
// 主循环中使用缓存的图像
function animate() {
ctx.drawImage(cacheCanvas, x, y);
requestAnimationFrame(animate);
}- 分层渲染
// 创建背景层和动画层
const bgCanvas = document.createElement('canvas');
const bgCtx = bgCanvas.getContext('2d');
const fgCanvas = document.createElement('canvas');
const fgCtx = fgCanvas.getContext('2d');
// 只在需要时重绘背景
function drawBackground() {
bgCtx.drawImage(backgroundImage, 0, 0);
}
// 动画层频繁更新
function animate() {
fgCtx.clearRect(0, 0, fgCanvas.width, fgCanvas.height);
drawAnimatedObjects(fgCtx);
// 合并层
ctx.drawImage(bgCanvas, 0, 0);
ctx.drawImage(fgCanvas, 0, 0);
requestAnimationFrame(animate);
}- 其他优化建议
- 关闭透明度:
canvas.getContext('2d', { alpha: false }) - 避免不必要的状态改变
- 使用适当的清除方法
- 避免频繁使用
shadowBlur等高耗性能的特性 - 合并绘制操作(例如使用路径一次绘制多个图形)
- 适当使用 CSS transforms 进行缩放