Skip to content

🖼️ Canvas 基础教程 ​

🌟 Canvas 简介 ​

<canvas> 标签只是一个图形容器,而Canvas API 是 HTML5 提供的一个强大的绘图 API,它允许我们使用 JavaScript 在网页上绘制图形、动画和其他视觉效果。

canvas 元素尺寸 ​

关于这部分,详见 Canvas 的两种尺寸

🎯 1. 基础概念 ​

1.1 画布 ​

首先需要在 HTML 中创建 canvas 元素,并通过 JavaScript 获取绘图上下文:

html
<!-- 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。

js
fillRect(x, y, width, height);

2.1.2 strokeRect ​

绘制一个矩形的边框,边框色为当前的 strokeStyle

js
strokeRect(x, y, width, height);

2.1.3 clearRect ​

清除画布中指定矩形区域,并重置为透明色,以达到擦除一个矩形区域的效果。 请确保在调用 clearRect() 之后绘制新内容前调用 beginPath() 。

js
clearRect(x, y, width, height);

2.2 ✏️ 路径绘制 ​

使用路径绘制图形的步骤:

  1. 使用 beginPath() 开始新路径。这样做会清空路径列表
  2. 画出路径。
  3. (可选)闭合路径,closePath() 绘制一条从当前点到路径起点的直线来闭合路径
  4. 通过描边或者填充路径区域来渲染图形
    • 使用 fill() ,所有没有闭合的形状都会自动闭合。
    • 调用 stroke(),不会自动闭合。

2.2.1 moveTo ​

moveTo() 方法用于设置路径的起点。

js
context.moveTo(x, y);// 将笔触移动到坐标 (x,y) 上

2.2.2 lineTo ​

lineTo() 方法用于绘制一条从当前点(之前路径的结束点 / moveTo决定)到指定点(x,y)的线段。

绘制两个三角形:

js
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 表示顺时针
js
context.arc(x, y, radius, startAngle, endAngle, anticlockwise);

arcTo()

  • 连接当前位置(蓝点)和控制点 1(x1,y1)得到直线 A
  • 连接控制点 1(x1,y1)和控制点 2(x2,y2)得到直线 B
  • 以 radius 作为半价
  • 以直线A 和直线B 作为切线,画出两条切线之前的弧线路径
js
context.arcTo(x1, y1, x2, y2, radius);

2.2.4 rect矩形 ​

rect() 方法绘制一个左上角坐标为 (x,y),宽高为 width 以及 height 的矩形。 在该方法执行前,当前笔触自动重置回默认坐标,即 moveTo(0,0);该方法结束后的笔触停留在矩形左上角 (x,y)。

js
context.rect(x, y, width, height);

2.2.5 Bézier曲线 ​

quadraticCurveTo() 方法绘制一个二次贝塞尔曲线,即从当前位置到指定点(x1,y1)画一条二次贝塞尔曲线,再从指定点(x1,y1)到指定点(x2,y2)画一条直线。

js
quadraticCurveTo(x1, y1, x2, y2)

3. Path2D ​

Path2D 对象是一个路径对象,它允许你定义一个路径,然后使用它来填充或描边。

3.1 创建和拷贝路径 ​

js
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 的字符串,从而创建一个路径对象。

js
// 移动到(10,10),向右侧移动 80px,向下移动 80px,再向左侧移动 80px,再向上移动 80px,然后回到起点(z)。
const path = new Path2D('M10 10 h 80 v 80 h -80 Z');

3.3 addPath ​

addPath() 方法允许你添加一个路径到当前路径中。

js
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 色值 ​

js
context.fillStyle = 'red';
context.fillStyle = "#FFA500";
context.fillStyle = "rgb(255,165,0)";
context.fillStyle = "rgba(255,165,0,0.2)"; // 透明度 0.2

4.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:颜色,可以是字符串、颜色对象、渐变对象或者图像对象
js
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 的线:

js
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。

js
max miterLength(最长交点距离)= miterLimit * lineWidth

5.4 虚线 ​

5.4.1 setLineDash ​

在填充线时使用虚线模式。

js
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。 利用这个偏移量,我们可以让虚线"动起来",俗称"蚂蚁线"。

js
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 颜色值
js
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-serifCSS 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()

js
let img = new Image();
img.onload = function () {
  // drawImage to canvas
};
img.src = "XXX.png";

2. img 标签

html
<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 事件使用,比如下面的例子:

js
 let video = document.getElementById("video");

// 媒体的第一帧加载完成时,渲染第一帧
video.onloadeddata = function () {};

// 播放暂停时,渲染当前帧
video.onpause = function () {};

9.1.3 HTMLCanvasElement ​

一个常用的应用就是将第二个 canvas 作为第一个 canvas 的缩略图:

js
let sourceCanvas = document.getElementById("source");
let thumbCanvas = document.getElementById("thumb");
thumbCtx.drawImage(source, 0, 0, 100, 50);

9.2 绘制drawImage() ​

1. 基础用法(三个参数)

js
ctx.drawImage(img, x, y);

2. 缩放(五个参数)

js
drawImage(image, x, y, width, height);

3. 剪切(九参数)

js
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像素值反相后相乘,再反相产生更亮的效果
overlaymultiply 和 screen 的结合保留明暗对比
darken保留重叠部分中较暗的像素变暗效果
lighten保留重叠部分中较亮的像素变亮效果

示例:

js
// 设置合成操作
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 区域

示例:

js
// 创建圆形裁剪区域
ctx.beginPath();
ctx.arc(100, 100, 50, 0, Math.PI * 2);
ctx.clip();

// 绘制图片(只会在圆形区域内显示)
ctx.drawImage(img, 0, 0, 200, 200);

常见用例:

js
// 创建圆形头像
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:垂直方向的移动距离
js
// 移动原点到 (50, 50)
ctx.translate(50, 50);
ctx.fillRect(0, 0, 100, 100); // 矩形实际绘制在 (50, 50) 位置

12.1.2 rotate(angle) ​

以原点为中心旋转 Canvas。

  • angle:旋转角度(以弧度为单位)
  • 正值:顺时针旋转
  • 负值:逆时针旋转
js
// 旋转 45 度
ctx.rotate(Math.PI / 4);
ctx.fillRect(100, 0, 80, 20);

12.1.3 scale(x, y) ​

缩放 Canvas 的坐标系。

  • x:水平方向的缩放因子
  • y:垂直方向的缩放因子
  • 默认值为 1(实际大小)
  • 值 < 1:缩小
  • 值 > 1:放大
  • 负值:镜像反转
js
// 水平拉伸 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)
js
// 实现旋转 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() 方法的变形效果。

js
// 重置并应用新的变形
ctx.setTransform(1, 0.5, 0.5, 1, 30, 10);

12.2.3 resetTransform() ​

将当前变形重置为单位矩阵,相当于:

js
ctx.setTransform(1, 0, 0, 1, 0, 0);

12.3 变形的最佳实践 ​

  1. 使用 save() 和 restore()
js
ctx.save();
ctx.translate(50, 50);
ctx.rotate(Math.PI / 4);
ctx.fillRect(0, 0, 100, 100);
ctx.restore(); // 恢复原始状态
  1. 变形的顺序很重要
js
// 结果不同
ctx.translate(100, 100);
ctx.rotate(Math.PI / 4);

// vs

ctx.rotate(Math.PI / 4);
ctx.translate(100, 100);
  1. 使用相对坐标
js
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();
}
  1. 避免累积变形
js
// 不好的做法 - 变形会累积
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 没有图层的概念,一旦图形绘制完成,就不能再修改。要实现动画效果,需要按照以下步骤执行:

  1. 清空 Canvas
  2. 保存 Canvas 状态
  3. 绘制动画帧
  4. 恢复 Canvas 状态

13.2 动画控制方法 ​

13.2.1 定时器 ​

js
// 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,它会在浏览器下一次重绘之前调用指定的回调函数,提供更流畅的动画效果。

js
function animate() {
    // 清空画布
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    
    // 绘制新一帧
    draw();
    
    // 请求下一帧
    requestAnimationFrame(animate);
}

// 启动动画
animate();

13.3 动画效果 ​

13.3.1 运动轨迹 ​

js
// 匀速运动的小球
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 长尾效果 ​

通过设置半透明背景覆盖,而不是完全清除画布,可以创建长尾效果:

js
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 值
js
// 创建 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 像素处理示例 ​

js
// 反相效果
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 性能优化 ​

  1. 避免浮点数坐标
js
// ❌ 不推荐
ctx.drawImage(img, 0.3, 0.5);

// ✅ 推荐
ctx.drawImage(img, Math.floor(0.3), Math.floor(0.5));
  1. 缓存和预渲染
js
// 预渲染复杂图形
const cacheCanvas = document.createElement('canvas');
const cacheCtx = cacheCanvas.getContext('2d');

// 在缓存画布上绘制
cacheCtx.drawImage(complexImage, 0, 0);

// 主循环中使用缓存的图像
function animate() {
    ctx.drawImage(cacheCanvas, x, y);
    requestAnimationFrame(animate);
}
  1. 分层渲染
js
// 创建背景层和动画层
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);
}
  1. 其他优化建议
  • 关闭透明度:canvas.getContext('2d', { alpha: false })
  • 避免不必要的状态改变
  • 使用适当的清除方法
  • 避免频繁使用 shadowBlur 等高耗性能的特性
  • 合并绘制操作(例如使用路径一次绘制多个图形)
  • 适当使用 CSS transforms 进行缩放