Canvas与SVG

What — 是什么

Canvas 是基于像素的 2D 绘图 API,通过 JS 逐帧绘制;SVG 是基于矢量的标记语言,通过 XML 描述图形。两者是 Web 图形渲染的两大方案。

Canvas 核心概念:

  • 2D 上下文canvas.getContext('2d'),提供绑制路径、矩形、圆弧、文本等 API
  • 逐帧绘制:每次绘制覆盖上一帧,适合动画和游戏
  • 像素操作getImageData/putImageData 直接操作像素
  • WebGLcanvas.getContext('webgl'),GPU 加速的 3D 渲染

SVG 核心概念:

  • 矢量图形:放大不失真,基于 XML 的 DOM 元素
  • 基本形状<rect><circle><line><path><text>
  • CSS 样式:SVG 元素可用 CSS 控制样式和动画
  • 交互:SVG 元素可绑定事件,每个形状独立可操作

关键特性:

  • Canvas 适合像素级操控、高频动画、图像处理
  • SVG 适合图标、图表、地图、可交互图形
  • Canvas 输出位图,SVG 输出矢量图

Why — 为什么

适用场景:

  • Canvas:游戏、数据可视化热力图、图像编辑器、粒子效果
  • SVG:图标系统、数据图表、地图、Logo、动画插画

对比:

维度CanvasSVG
渲染方式位图(像素)矢量(路径)
缩放模糊清晰
DOM无(单一元素)每个图形是 DOM 节点
事件需手动计算碰撞原生 DOM 事件
性能大量元素更优元素多时 DOM 开销大
动画JS 逐帧控制CSS/SMIL 动画
文件格式PNG/JPEGSVG/XML
可访问性差(无结构)好(有语义)

优缺点:

  • ✅ Canvas 优点:
    • 像素级操控,适合图像处理
    • 大量图形时性能优于 SVG
    • WebGL 支持 3D 渲染
  • ❌ Canvas 缺点:
    • 无 DOM 结构,事件处理复杂
    • 缩放模糊
    • 无障碍性差
  • ✅ SVG 优点:
    • 矢量不失真
    • DOM 事件原生支持
    • CSS 可控样式和动画
  • ❌ SVG 缺点:
    • 复杂图形 DOM 节点多,性能差
    • 像素级操作不便

How — 怎么用

Canvas 基础

<canvas id="canvas" width="800" height="600"></canvas>
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');

// 高清屏适配
function setupHiDPI(canvas, ctx, width, height) {
    const dpr = window.devicePixelRatio || 1;
    canvas.width = width * dpr;
    canvas.height = height * dpr;
    canvas.style.width = width + 'px';
    canvas.style.height = height + 'px';
    ctx.scale(dpr, dpr);
}

// 绘制矩形
ctx.fillStyle = '#3b82f6';
ctx.fillRect(10, 10, 100, 50);

// 绘制圆
ctx.beginPath();
ctx.arc(200, 100, 40, 0, Math.PI * 2);
ctx.fillStyle = '#ef4444';
ctx.fill();

// 绘制文本
ctx.font = '16px sans-serif';
ctx.fillStyle = '#1f2937';
ctx.fillText('Hello Canvas', 300, 100);

// 绘制路径
ctx.beginPath();
ctx.moveTo(400, 50);
ctx.lineTo(450, 100);
ctx.lineTo(350, 100);
ctx.closePath();
ctx.strokeStyle = '#10b981';
ctx.lineWidth = 2;
ctx.stroke();

动画循环:

function drawFrame(time) {
    ctx.clearRect(0, 0, canvas.width, canvas.height);

    // 更新位置
    const x = (time / 10) % canvas.width;

    // 绘制
    ctx.beginPath();
    ctx.arc(x, canvas.height / 2, 20, 0, Math.PI * 2);
    ctx.fillStyle = '#3b82f6';
    ctx.fill();

    requestAnimationFrame(drawFrame);
}
requestAnimationFrame(drawFrame);

图像处理:

// 灰度化
async function grayscale(imageSrc) {
    const img = new Image();
    img.src = imageSrc;
    await img.decode();

    ctx.drawImage(img, 0, 0);
    const imageData = ctx.getImageData(0, 0, img.width, img.height);
    const data = imageData.data;

    for (let i = 0; i < data.length; i += 4) {
        const avg = data[i] * 0.299 + data[i + 1] * 0.587 + data[i + 2] * 0.114;
        data[i] = data[i + 1] = data[i + 2] = avg;
    }

    ctx.putImageData(imageData, 0, 0);
}

SVG 基础

内联 SVG:

<!-- 图标 -->
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
    <circle cx="12" cy="12" r="10"/>
    <path d="M8 12l3 3 5-5"/>
</svg>

<!-- 渐变 -->
<svg width="200" height="100">
    <defs>
        <linearGradient id="grad" x1="0%" y1="0%" x2="100%" y2="0%">
            <stop offset="0%" style="stop-color:#3b82f6"/>
            <stop offset="100%" style="stop-color:#8b5cf6"/>
        </linearGradient>
    </defs>
    <rect width="200" height="100" rx="8" fill="url(#grad)"/>
</svg>

SVG 图标系统:

<!-- sprite.svg(合并所有图标) -->
<svg xmlns="http://www.w3.org/2000/svg" style="display:none">
    <symbol id="icon-home" viewBox="0 0 24 24">
        <path d="M3 12l9-9 9 9M5 10v10a1 1 0 001 1h3m10-11v10a1 1 0 01-1 1h-3m-4 0v-6a1 1 0 011-1h2a1 1 0 011 1v6"/>
    </symbol>
    <symbol id="icon-user" viewBox="0 0 24 24">
        <circle cx="12" cy="8" r="4"/>
        <path d="M4 21v-1a6 6 0 0112 0v1"/>
    </symbol>
</svg>
<!-- 使用图标 -->
<svg class="icon" width="20" height="20">
    <use href="sprite.svg#icon-home"/>
</svg>

SVG CSS 动画:

<svg viewBox="0 0 100 100" width="100" height="100">
    <circle cx="50" cy="50" r="40" fill="none" stroke="#3b82f6" stroke-width="4"
            stroke-dasharray="251" stroke-dashoffset="251" class="spinner"/>
</svg>

<style>
.spinner {
    animation: spin 1.5s linear infinite;
}
@keyframes spin {
    to {
        stroke-dashoffset: 0;
        transform: rotate(360deg);
    }
}
</style>

SVG + React:

// 可配置的图标组件
interface IconProps {
    name: string;
    size?: number;
    color?: string;
    className?: string;
}

function Icon({ name, size = 20, color = 'currentColor', className }: IconProps) {
    return (
        <svg
            width={size}
            height={size}
            fill="none"
            stroke={color}
            strokeWidth={2}
            strokeLinecap="round"
            strokeLinejoin="round"
            className={className}
        >
            <use href={`sprite.svg#icon-${name}`} />
        </svg>
    );
}

// 使用
<Icon name="home" size={24} color="#3b82f6" />

常见问题与踩坑

问题原因解决方案
Canvas 模糊高清屏 DPR 未处理canvas 尺寸 × DPR + ctx.scale(dpr)
Canvas resize 丢失内容改变 canvas 尺寸会清空resize 前保存 imageData 或重新绘制
SVG 图标加载闪烁sprite 文件未预加载<link rel="preload"> 或内联关键图标
SVG 复杂图形卡顿DOM 节点太多超过 1000 个元素考虑 Canvas
Canvas 事件难做只有一个 DOM 元素记录图形位置,点击时遍历判断碰撞

最佳实践

  • 图标用 SVG(矢量清晰 + CSS 可控),动画/游戏/图像处理用 Canvas
  • Canvas 高清屏必做 DPR 适配
  • SVG 图标用 sprite 合并,减少请求
  • SVG 图标用 currentColor 继承颜色,方便主题切换
  • 大量图形(>1000)场景选 Canvas,交互丰富的选 SVG

面试题

Q1: Canvas和SVG的核心区别是什么?

Canvas是基于像素的位图绘图,通过JS逐帧绘制,输出位图,无DOM结构,事件需手动计算碰撞;SVG是基于矢量的XML标记,每个图形是DOM节点,原生支持事件和CSS样式,缩放不失真。Canvas适合大量图形和高频动画,SVG适合图标和交互丰富的图形。

Q2: Canvas如何适配高清屏?

高清屏设备像素比(DPR)>1时,需将canvas的width/height属性设为CSS尺寸乘以DPR,再通过style设置CSS显示尺寸,最后调用ctx.scale(dpr, dpr)缩放绘制上下文。否则canvas会在高清屏上模糊,因为1个CSS像素对应多个物理像素。

Q3: 什么时候选Canvas,什么时候选SVG?

选型原则:需要像素级操作、图像处理、大量图形(>1000个元素)、高频动画/游戏选Canvas;需要缩放不失真、CSS样式控制、DOM事件交互、图标/图表/地图选SVG。实际项目中常结合使用,如图标用SVG,动画效果用Canvas。

Q4: Canvas的事件处理为什么复杂?如何解决?

Canvas只是一个DOM元素,内部绘制的图形不是独立DOM节点,无法直接绑定事件。解决方案:1) 维护图形列表记录位置,点击时遍历判断碰撞(hit testing);2) 使用isPointInPath()检测点击是否在路径内;3) 复杂场景可用Canvas库(Fabric.js/Konva.js)提供对象模型和事件系统。


相关链接: