DOM、SVG 与 Canvas 怎么选

Web 画图有三条路:DOM/CSS(简单但元素一多就卡)、SVG(矢量、可编辑、每个图形是 DOM 节点,适合几百个元素)、Canvas(一块像素画布、用命令式 API 直接画,适合成千上万个图形、游戏、高性能图表)。

选型原则:元素少、要交互和无障碍,用 SVG;元素多、要动画和高性能,用 Canvas。数据可视化里,几百个数据点的趋势图用 SVG 更省心;几万条粒子、实时波形、热力图,必须 Canvas。像 ECharts 这类库内部也会按数据量自动在 SVG 和 Canvas 之间切换。

本文核心知识点
  • getContext拿到 2d 画笔,一切绘制都从这里开始。
  • rAFrequestAnimationFrame 是动画的正确节拍器。
  • 坐标系鼠标事件要转成画布内部坐标。
  • 高清适配处理 devicePixelRatio 避免模糊。
Canvas 可视化配图:蓝色渐变背景上一块发光的画布面板,面板内有上升折线、竖立柱状与环形饼图形状,旁边有帧格方块,扁平科技插画
一块画布面板上同时呈现折线、柱状与环形图,旁边帧格表示动画序列。

getContext 与基本绘制

Canvas 就是一个位图画布,你先拿到它的 2d 上下文,然后用一系列“绘制指令”往上画:画矩形、描路径、填文字。它是命令式的——画完就变成像素,没有“那个矩形”这个对象供你之后点选。

const canvas = document.getElementById('c');
const ctx = canvas.getContext('2d');

// 填一个蓝色矩形
ctx.fillStyle = '#3b82f6';
ctx.fillRect(20, 20, 160, 80);

// 描一条折线
ctx.beginPath();
ctx.moveTo(20, 120);
ctx.lineTo(180, 60);
ctx.lineTo(320, 140);
ctx.stroke();

记住流程:beginPath() 开新路径 → moveTo/lineTo 定义形状 → fill() 或 stroke() 上色。忘了 beginPath,新画的线会和上次的路径连在一起,这是新手最常见的 bug。

requestAnimationFrame 动画

做动画不能用 setInterval,要交给浏览器的 requestAnimationFrame(rAF):它跟着屏幕刷新节奏(约 60 次/秒)触发,页面切到后台会自动暂停,省电。每一帧先清画布、再重画一遍最新状态,就形成了连续动画。

let x = 0;
function draw() {
  ctx.clearRect(0, 0, canvas.width, canvas.height); // 清屏
  ctx.fillStyle = '#3b82f6';
  ctx.fillRect(x, 60, 40, 40);                     // 重画
  x += 2;
  requestAnimationFrame(draw);                      // 下一帧
}
requestAnimationFrame(draw);

动画的本质是“每帧清屏 + 按时间重绘”。数据变了、时间推进了,就把同一套数据在新位置画一遍。

画折线、柱状与饼图

三类基础图形的画法:

  • 柱状图:把每个数据值按比例换算成高度,循环 fillRect 画一排柱子。
  • 折线图:把各点用 moveTo/lineTo 连成路径,stroke 出线,可用 quadraticCurveTo 让线平滑。
  • 饼图:按每块占比算出起始和结束角度,用 arc 画扇形,fill 上色。
// 饼图一块
const start = 0, end = Math.PI * 2 * 0.4; // 40%
ctx.beginPath();
ctx.moveTo(200, 200);
ctx.arc(200, 200, 100, start, end);
ctx.fill();

核心都是数据到像素的换算:先定坐标系范围(数据的 min/max),再把每个值线性映射到画布上的 x/y。

交互与坐标转换

Canvas 没有 DOM 节点,所以“点中哪根柱子”不会自动告诉你,得自己算。鼠标事件给的是视口坐标,要先用 getBoundingClientRect 减去画布偏移,转成画布内部坐标:

canvas.addEventListener('click', (e) => {
  const rect = canvas.getBoundingClientRect();
  const x = e.clientX - rect.left;
  const y = e.clientY - rect.top;
  // 再判断 x 落在哪个柱子的区间里
});

命中检测常见做法:画每根柱子时记下它的 x 区间,点击时遍历这些区间判断落在哪个。大场景则用离屏画布或空间索引加速。