Canvas 如何渲染富文本、图片、SVG 及其 Path 路径?

在前端开发中,我们习惯于使用 HTML 标签(如 <img>, <div>, <svg>)来声明我们想要显示的内容,然后由浏览器负责布局和渲染

<canvas> 元素截然不同

Canvas 提供的是一个"即时模式"的 2D (或 3D) 绘图 API。它是一块空白的位图画布,你通过 JavaScript 发出绘图指令(如"画个圈"、"填充颜色"),它就立即执行。它不会记住你画了什么对象;一旦像素被涂上,它就只是一堆像素

1. 渲染图片

drawImage() 方法可以接受多种图像源,包括 <img> 元素、另一个 <canvas> 元素、<video> 的当前帧或 ImageBitmap

渲染流程:

  1. 等待数据就绪: JavaScript 加载图片是异步 的。在 img.onload 事件触发后,确保图片数据(像素)已经完全加载到内存中

  2. 执行 drawImage

    • ctx.drawImage(image, dx, dy): 将源图像的像素数据,原封不动地"复制"到 Canvas 画布的 (dx, dy) 位置。
    • ctx.drawImage(image, dx, dy, dWidth, dHeight): 在复制前,先对源图像的像素进行缩放 (拉伸或压缩)到 dWidth x dHeight 大小,然后再绘制。
    • 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.fontctx.fillStyle 等样式,对这些形状执行一次填充操作

如何渲染真正的"富文本"?

  1. 自动换行:

    • 将长文本分割成单词
    • 逐个单词测量其宽度 (ctx.measureText(word).width)
    • 如果当前行宽度超过了设定的最大宽度,你就必须手动增加 y 坐标(换行),然后从新行开始绘制
  2. 混合样式(例如 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> 图片来处理

  1. 将 SVG 转换为图像源:

    • 如果是 .svg 文件:img.src = 'icon.svg';

    • 如果是 SVG 字符串:

      const svgString = '...';

      const svgDataUri = 'data:image/svg+xml;base64,' + btoa(svgString);

      img.src = svgDataUri;

  2. 等待加载: img.onload = () => { ... }

  3. 绘制图像: ctx.drawImage(img, dx, dy, width, height);

drawImage 被调用的那一刻,SVG 的矢量特性就丢失 了。它被"光栅化"成了指定 widthheight 的像素。如果你在 Canvas 上放大它,它会像普通图片一样变模糊

方式二:作为矢量"解析转译"(复杂)

这个方法会保留矢量特性

  1. 解析 SVG 的 XML 结构

  2. 遍历 SVG 节点(如 <rect>, <circle>, <path>

  3. 将每个 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 属性字符串

  1. 创建 Path2D 对象:

    JavaScript

    ini 复制代码
    const svgPathData = "M10 10 L100 100 C150 150 200 150 250 100 Z";
    const myPath = new Path2D(svgPathData);
  2. 渲染 Path2D 对象:

    一旦你有了 myPath 这个对象,Canvas 就可以直接使用它。这个对象已经"预编译"了所有路径指令

    JavaScript

    ini 复制代码
    ctx.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 原生路径对象,然后再 strokefill

理解了这层"翻译"关系,就掌握了 Canvas 渲染万物的核心

相关推荐
ywf12151 天前
前端的dist包放到后端springboot项目下一起打包
前端·spring boot·后端
恋猫de小郭1 天前
2026,Android Compose 终于支持 Hot Reload 了,但是收费
android·前端·flutter
hpoenixf1 天前
2026 年前端面试问什么
前端·面试
还是大剑师兰特1 天前
Vue3 中的 defineExpose 完全指南
前端·javascript·vue.js
泯泷1 天前
阶段一:从 0 看懂 JSVMP 架构,先在脑子里搭出一台最小 JSVM
前端·javascript·架构
mengchanmian1 天前
前端node常用配置
前端
华洛1 天前
利好打工人,openclaw不是企业提效工具,而是个人助理
前端·javascript·产品经理
xkxnq1 天前
第六阶段:Vue生态高级整合与优化(第93天)Element Plus进阶:自定义主题(变量覆盖)+ 全局配置与组件按需加载优化
前端·javascript·vue.js
A黄俊辉A1 天前
vue css中 :global的使用
前端·javascript·vue.js
小码哥_常1 天前
被EdgeToEdge适配折磨疯了,谁懂!
前端