DOM、SVG 与 Canvas 怎么选
Web 画图有三条路:DOM/CSS(简单但元素一多就卡)、SVG(矢量、可编辑、每个图形是 DOM 节点,适合几百个元素)、Canvas(一块像素画布、用命令式 API 直接画,适合成千上万个图形、游戏、高性能图表)。
选型原则:元素少、要交互和无障碍,用 SVG;元素多、要动画和高性能,用 Canvas。数据可视化里,几百个数据点的趋势图用 SVG 更省心;几万条粒子、实时波形、热力图,必须 Canvas。像 ECharts 这类库内部也会按数据量自动在 SVG 和 Canvas 之间切换。
- getContext拿到 2d 画笔,一切绘制都从这里开始。
- rAFrequestAnimationFrame 是动画的正确节拍器。
- 坐标系鼠标事件要转成画布内部坐标。
- 高清适配处理 devicePixelRatio 避免模糊。
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 区间,点击时遍历这些区间判断落在哪个。大场景则用离屏画布或空间索引加速。
性能与高清适配
高分屏(Retina)上直接画会模糊,因为一个 CSS 像素对应多个物理像素。正确做法是按 devicePixelRatio 放大画布的实际像素、再用 scale 把坐标系缩回来:
const dpr = window.devicePixelRatio || 1;
canvas.width = cssWidth * dpr;
canvas.height = cssHeight * dpr;
ctx.scale(dpr, dpr);
性能上:尽量减少每帧的状态切换(fillStyle 频繁改会拖慢)、离屏预渲染静态背景、数据量大时做分层画布(静态层一张、动态层一张),别每帧全量重画所有东西。
实战清单
落地清单:
- 元素多用 Canvas,元素少且要无障碍用 SVG。
- 动画一律用 requestAnimationFrame,别用 setInterval。
- 每次画路径记得 beginPath,避免线连在一起。
- 交互先做鼠标坐标到画布坐标的换算。
- 高分屏按 devicePixelRatio 适配,防止模糊。
- 大数据量做离屏分层,别每帧全量重画。
Canvas 的价值在于把成千上万个图形从 DOM 的束缚里解放出来,用像素级的掌控力画出高性能可视化与动画。把坐标换算、动画节拍和高清适配这三件事用对,复杂图表和实时效果都能稳稳跑起来。