在前端开发中,我们习惯于使用 HTML 标签(如 <img>, <div>, <svg>)来声明我们想要显示的内容,然后由浏览器负责布局和渲染
但 <canvas> 元素截然不同
Canvas 提供的是一个"即时模式"的 2D (或 3D) 绘图 API。它是一块空白的位图画布,你通过 JavaScript 发出绘图指令(如"画个圈"、"填充颜色"),它就立即执行。它不会记住你画了什么对象;一旦像素被涂上,它就只是一堆像素
1. 渲染图片
drawImage() 方法可以接受多种图像源,包括 <img> 元素、另一个 <canvas> 元素、<video> 的当前帧或 ImageBitmap。
渲染流程:
-
等待数据就绪: JavaScript 加载图片是异步 的。在
img.onload事件触发后,确保图片数据(像素)已经完全加载到内存中 -
执行
drawImage:ctx.drawImage(image, dx, dy): 将源图像的像素数据,原封不动地"复制"到 Canvas 画布的(dx, dy)位置。ctx.drawImage(image, dx, dy, dWidth, dHeight): 在复制前,先对源图像的像素进行缩放 (拉伸或压缩)到dWidthxdHeight大小,然后再绘制。ctx.drawImage(image, sx, sy, sWidth, sHeight, ...): 这是最复杂的形式,它先从源图像上"裁剪"出一块矩形区域,然后(可选)缩放,最后"粘贴"到画布上。
js
// 示例:加载并绘制一张图片
const img = new Image();
img.src = 'path/to/image.png';
// 必须等待图片解码完成
img.onload = () => {
// 将图片像素"印"在 (10, 10) 坐标
ctx.drawImage(img, 10, 10);
};
2. 渲染富文本
Canvas 提供了两个基本方法来绘制文本:ctx.fillText() 和 ctx.strokeText()
例如:<span>Hello <b>World</b></span> 的混合样式和自动换行
Canvas 原生 API 并不支持这些
Canvas 的 fillText 命令只是将一个纯文本字符串,根据当前的 ctx.font、ctx.fillStyle 等样式,对这些形状执行一次填充操作
如何渲染真正的"富文本"?
-
自动换行:
- 将长文本分割成单词
- 逐个单词测量其宽度 (
ctx.measureText(word).width) - 如果当前行宽度超过了设定的最大宽度,你就必须手动增加
y坐标(换行),然后从新行开始绘制
-
混合样式(例如
Hello <b>World</b>):ctx.font = '16px Arial';ctx.fillText('Hello ', x, y);- 计算 "Hello " 的宽度:
const w = ctx.measureText('Hello ').width; - 更改状态:
ctx.font = 'bold 16px Arial'; - 在后面继续绘制:
ctx.fillText('World', x + w, y);
3. 渲染 SVG
SVG(可缩放矢量图形)和 Canvas 在某种理解下可以是对立的
- SVG 是"声明式": 用 XML 描述一个"场景"(例如,这里有个圆,那里有条线)。浏览器会记住这些对象
- Canvas 是"命令式": 发出"画圆"的指令,它画完就忘了
因此,Canvas 不能直接渲染 SVG 字符串,你必须在两者之间进行"翻译"
方式一:作为图片"光栅化"(最常用)
这是最简单的方法:把 SVG 当作一张普通的 <img> 图片来处理
-
将 SVG 转换为图像源:
-
如果是
.svg文件:img.src = 'icon.svg'; -
如果是 SVG 字符串:
const svgString = '...';
const svgDataUri = 'data:image/svg+xml;base64,' + btoa(svgString);
img.src = svgDataUri;
-
-
等待加载:
img.onload = () => { ... } -
绘制图像:
ctx.drawImage(img, dx, dy, width, height);
在 drawImage 被调用的那一刻,SVG 的矢量特性就丢失 了。它被"光栅化"成了指定 width 和 height 的像素。如果你在 Canvas 上放大它,它会像普通图片一样变模糊
方式二:作为矢量"解析转译"(复杂)
这个方法会保留矢量特性
-
解析 SVG 的 XML 结构
-
遍历 SVG 节点(如
<rect>,<circle>,<path>) -
将每个 SVG 节点的属性翻译成等效的 Canvas API 调用
<rect x="10" ...>->ctx.rect(10, ...)- 等等
4. 渲染 SVG 中的 Path 路径
SVG 的 <path> 元素使用 d 属性来定义极其复杂的形状,例如:
<path d="M10 10 L100 100 C150 150 200 150 250 100 Z">
M 代表 moveTo(移动到),L 代表 lineTo(画线到),C 代表贝塞尔曲线,Z 代表闭合路径
Canvas 的 ctx.lineTo 等 API 无法直接读取这个字符串,我们如何"翻译"它?
现代方案:Path2D 对象
现代浏览器提供了一个强大的 Path2D 对象,它就是为了解决这个问题而生的。Path2D 构造函数可以直接接收 SVG 的 d 属性字符串
-
创建
Path2D对象:JavaScript
iniconst svgPathData = "M10 10 L100 100 C150 150 200 150 250 100 Z"; const myPath = new Path2D(svgPathData); -
渲染 Path2D 对象:
一旦你有了 myPath 这个对象,Canvas 就可以直接使用它。这个对象已经"预编译"了所有路径指令
JavaScript
inictx.strokeStyle = 'blue'; ctx.lineWidth = 3; ctx.stroke(myPath); // 描边这个路径 ctx.fillStyle = 'rgba(255, 0, 0, 0.5)'; ctx.fill(myPath); // 填充这个路径
Path2D 的优势:
- 简洁: 无需手动解析字符串。
- 高效: 浏览器底层负责解析和优化路径。
- 可重用: 你可以创建一次
Path2D对象,然后在不同的位置、以不同的样式反复绘制它,而无需重新定义路径
传统方案:手动解析(库的实现原理)
在 Path2D 出现之前,或者当需要支持旧版浏览器时,唯一的办法就是手动解析这个 d 字符串。
你需要写一个解析器,逐个读取命令 (M, L, C...) 和坐标,然后调用等效的 Canvas API:
JavaScript
ini
function parseAndDraw(svgPathData) {
// 这是一个极度简化的示例
const commands = svgPathData.split(' '); // 简化的分割
ctx.beginPath(); // **开始 Canvas 路径**
for (let i = 0; i < commands.length; i++) {
const cmd = commands[i];
if (cmd === 'M') {
const x = parseFloat(commands[++i]);
const y = parseFloat(commands[++i]);
ctx.moveTo(x, y); // 等效 M
} else if (cmd === 'L') {
const x = parseFloat(commands[++i]);
const y = parseFloat(commands[++i]);
ctx.lineTo(x, y); // 等效 L
} else if (cmd === 'Z') {
ctx.closePath(); // 等效 Z
}
// ... 还需要处理 C, S, Q, T, A 等等...
}
ctx.stroke(); // **渲染 Canvas 路径**
}
总结
Canvas 本身只是一个低级的"像素绘图仪"。它之所以能渲染高级内容,完全依赖于将这些内容"翻译"成它能理解的指令:
- 渲染图片: 翻译成
drawImage(像素块传输)。 - 渲染富文本: 翻译成
fillText(字形绘制),但布局和换行需要 JavaScript 手动计算。 - 渲染 SVG: 翻译成
drawImage(光栅化)或者解析其所有节点。 - 渲染 SVG Path: 最佳翻译是使用
new Path2D(d)将其转换成 Canvas 原生路径对象,然后再stroke或fill。
理解了这层"翻译"关系,就掌握了 Canvas 渲染万物的核心