Skip to content

Canvas 和 SVG:HTML5 图形绘制技术深度对比与应用

图形绘制的两条路径

HTML5 为 Web 开发者提供了两种强大的图形绘制技术:CanvasSVG。它们各有特点,适用于不同的应用场景。

核心区别

Canvas(画布)

  • 基于像素的位图绘制
  • 使用 JavaScript 逐像素绘制
  • 绘制后内容不可直接修改(需要重绘)
  • 性能优秀,适合动态、复杂的图形

SVG(可缩放矢量图形)

  • 基于 XML 的矢量图形
  • 使用标签描述图形
  • 每个图形元素都是 DOM 节点,可以独立操作
  • 无损缩放,适合需要交互的图形

可以这样理解:Canvas 就像一张画布,你用画笔在上面作画;SVG 则像用积木搭建图形,每个积木都可以单独移动、修改。

Canvas 基础

创建 Canvas

html
<canvas id="myCanvas" width="800" height="600">
  您的浏览器不支持 Canvas。
</canvas>

注意

  • 使用 widthheight 属性设置画布大小(而非 CSS)
  • CSS 设置的尺寸会导致图形拉伸变形

获取绘图上下文

javascript
const canvas = document.getElementById("myCanvas");
const ctx = canvas.getContext("2d"); // 获取 2D 绘图上下文

// 检查浏览器是否支持 Canvas
if (!ctx) {
  console.error("浏览器不支持 Canvas");
}

基本图形绘制

1. 矩形

javascript
const ctx = canvas.getContext("2d");

// 填充矩形
ctx.fillStyle = "#3498db"; // 设置填充颜色
ctx.fillRect(50, 50, 200, 100); // fillRect(x, y, width, height)

// 描边矩形
ctx.strokeStyle = "#e74c3c"; // 设置边框颜色
ctx.lineWidth = 5; // 设置线条宽度
ctx.strokeRect(300, 50, 200, 100);

// 清除矩形区域
ctx.clearRect(350, 75, 100, 50);

2. 路径绘制

Canvas 使用路径来绘制复杂图形:

javascript
// 绘制三角形
ctx.beginPath(); // 开始路径
ctx.moveTo(100, 200); // 移动到起点
ctx.lineTo(200, 200); // 画线到第二个点
ctx.lineTo(150, 120); // 画线到第三个点
ctx.closePath(); // 闭合路径(回到起点)

ctx.fillStyle = "#2ecc71";
ctx.fill(); // 填充
ctx.strokeStyle = "#27ae60";
ctx.stroke(); // 描边

3. 圆形和弧线

javascript
// 绘制圆形
ctx.beginPath();
ctx.arc(400, 300, 80, 0, 2 * Math.PI); // arc(x, y, 半径, 起始角度, 结束角度)
ctx.fillStyle = "#9b59b6";
ctx.fill();

// 绘制半圆
ctx.beginPath();
ctx.arc(600, 300, 80, 0, Math.PI); // 0 到 π 是半圆
ctx.strokeStyle = "#8e44ad";
ctx.lineWidth = 3;
ctx.stroke();

// 绘制扇形
ctx.beginPath();
ctx.moveTo(400, 500); // 圆心
ctx.arc(400, 500, 60, 0, Math.PI / 2); // 90度扇形
ctx.lineTo(400, 500); // 回到圆心
ctx.closePath();
ctx.fillStyle = "#f39c12";
ctx.fill();

4. 贝塞尔曲线

javascript
// 二次贝塞尔曲线
ctx.beginPath();
ctx.moveTo(100, 400);
ctx.quadraticCurveTo(200, 300, 300, 400); // 控制点和终点
ctx.strokeStyle = "#e67e22";
ctx.lineWidth = 3;
ctx.stroke();

// 三次贝塞尔曲线
ctx.beginPath();
ctx.moveTo(100, 500);
ctx.bezierCurveTo(150, 450, 250, 550, 300, 500); // 两个控制点和终点
ctx.strokeStyle = "#d35400";
ctx.lineWidth = 3;
ctx.stroke();

文本绘制

javascript
// 填充文本
ctx.font = "48px Arial"; // 设置字体
ctx.fillStyle = "#34495e";
ctx.fillText("Hello Canvas!", 100, 100); // fillText(文本, x, y)

// 描边文本
ctx.font = "bold 36px Georgia";
ctx.strokeStyle = "#2c3e50";
ctx.lineWidth = 2;
ctx.strokeText("Outlined Text", 100, 200);

// 设置文本对齐
ctx.textAlign = "center"; // left, right, center, start, end
ctx.textBaseline = "middle"; // top, bottom, middle, alphabetic, hanging
ctx.fillText("居中文本", canvas.width / 2, canvas.height / 2);

// 测量文本宽度
const text = "测量我";
const metrics = ctx.measureText(text);
console.log(`文本宽度: ${metrics.width}px`);

图像处理

javascript
// 加载并绘制图像
const img = new Image();
img.onload = function () {
  // 绘制原图
  ctx.drawImage(img, 0, 0);

  // 绘制并缩放
  ctx.drawImage(img, 200, 0, 150, 100); // drawImage(img, x, y, width, height)

  // 切片绘制
  ctx.drawImage(
    img,
    50,
    50,
    100,
    100, // 源图像的切片位置和大小
    400,
    0,
    200,
    200 // 目标画布的位置和大小
  );
};
img.src = "photo.jpg";

渐变和样式

javascript
// 线性渐变
const linearGradient = ctx.createLinearGradient(0, 0, 200, 0);
linearGradient.addColorStop(0, "#e74c3c");
linearGradient.addColorStop(0.5, "#f39c12");
linearGradient.addColorStop(1, "#f1c40f");

ctx.fillStyle = linearGradient;
ctx.fillRect(50, 50, 200, 100);

// 径向渐变
const radialGradient = ctx.createRadialGradient(400, 300, 20, 400, 300, 100);
radialGradient.addColorStop(0, "#3498db");
radialGradient.addColorStop(1, "#2c3e50");

ctx.fillStyle = radialGradient;
ctx.beginPath();
ctx.arc(400, 300, 100, 0, 2 * Math.PI);
ctx.fill();

// 图案填充
const patternImg = new Image();
patternImg.onload = function () {
  const pattern = ctx.createPattern(patternImg, "repeat"); // repeat, repeat-x, repeat-y, no-repeat
  ctx.fillStyle = pattern;
  ctx.fillRect(0, 400, 300, 150);
};
patternImg.src = "pattern.png";

Canvas 动画

javascript
const canvas = document.getElementById("myCanvas");
const ctx = canvas.getContext("2d");

let x = 0;
let y = 100;
let dx = 2; // x 方向速度
let dy = 1; // y 方向速度
const radius = 20;

function draw() {
  // 清空画布
  ctx.clearRect(0, 0, canvas.width, canvas.height);

  // 绘制小球
  ctx.beginPath();
  ctx.arc(x, y, radius, 0, 2 * Math.PI);
  ctx.fillStyle = "#3498db";
  ctx.fill();
  ctx.closePath();

  // 边界检测
  if (x + dx > canvas.width - radius || x + dx < radius) {
    dx = -dx; // 反向
  }
  if (y + dy > canvas.height - radius || y + dy < radius) {
    dy = -dy;
  }

  // 更新位置
  x += dx;
  y += dy;

  // 循环动画
  requestAnimationFrame(draw);
}

draw();

SVG 基础

创建 SVG

SVG 可以直接嵌入 HTML:

html
<svg width="400" height="300" xmlns="http://www.w3.org/2000/svg">
  <!-- SVG 内容 -->
</svg>

也可以作为独立文件引用:

html
<img src="graphic.svg" alt="SVG 图形" />
<object data="graphic.svg" type="image/svg+xml"></object>

基本图形

1. 矩形

html
<svg width="400" height="300">
  <!-- 矩形 -->
  <rect
    x="50"
    y="50"
    width="200"
    height="100"
    fill="#3498db"
    stroke="#2980b9"
    stroke-width="3"
  />

  <!-- 圆角矩形 -->
  <rect
    x="300"
    y="50"
    width="150"
    height="100"
    rx="15"
    ry="15"
    fill="#e74c3c"
  />
</svg>

2. 圆形和椭圆

html
<svg width="400" height="300">
  <!-- 圆形 -->
  <circle cx="100" cy="100" r="50" fill="#2ecc71" />

  <!-- 椭圆 -->
  <ellipse cx="300" cy="100" rx="80" ry="50" fill="#9b59b6" />
</svg>

3. 线条和折线

html
<svg width="400" height="300">
  <!-- 直线 -->
  <line x1="50" y1="50" x2="200" y2="150" stroke="#34495e" stroke-width="3" />

  <!-- 折线 -->
  <polyline
    points="50,200 100,150 150,180 200,120 250,160"
    fill="none"
    stroke="#e67e22"
    stroke-width="2"
  />

  <!-- 多边形 -->
  <polygon
    points="300,50 350,100 325,150 275,150 250,100"
    fill="#f39c12"
    stroke="#d35400"
    stroke-width="2"
  />
</svg>

4. 路径(Path)

Path 是 SVG 中最强大的元素:

html
<svg width="400" height="300">
  <!-- M = moveto, L = lineto, Z = closepath -->
  <path
    d="M 50 50 L 150 50 L 100 120 Z"
    fill="#3498db"
    stroke="#2980b9"
    stroke-width="2"
  />

  <!-- 曲线:Q = 二次贝塞尔, C = 三次贝塞尔 -->
  <path
    d="M 200 50 Q 250 20 300 50"
    fill="none"
    stroke="#e74c3c"
    stroke-width="3"
  />

  <!-- 弧线:A rx ry x-axis-rotation large-arc-flag sweep-flag x y -->
  <path
    d="M 50 200 A 50 50 0 0 1 150 200"
    fill="none"
    stroke="#2ecc71"
    stroke-width="3"
  />
</svg>

SVG 文本

html
<svg width="400" height="200">
  <!-- 基本文本 -->
  <text x="50" y="50" font-family="Arial" font-size="24" fill="#34495e">
    Hello SVG!
  </text>

  <!-- 沿路径的文本 -->
  <defs>
    <path id="textPath" d="M 50 100 Q 200 50 350 100" />
  </defs>

  <text font-size="18" fill="#e74c3c">
    <textPath href="#textPath">这是沿路径的文本</textPath>
  </text>

  <!-- 多行文本 -->
  <text x="50" y="150" font-size="16">
    <tspan x="50" dy="0">第一行</tspan>
    <tspan x="50" dy="20">第二行</tspan>
    <tspan x="50" dy="20">第三行</tspan>
  </text>
</svg>

SVG 渐变和滤镜

html
<svg width="400" height="300">
  <defs>
    <!-- 线性渐变 -->
    <linearGradient id="grad1" x1="0%" y1="0%" x2="100%" y2="0%">
      <stop offset="0%" style="stop-color:#e74c3c;stop-opacity:1" />
      <stop offset="100%" style="stop-color:#f39c12;stop-opacity:1" />
    </linearGradient>

    <!-- 径向渐变 -->
    <radialGradient id="grad2">
      <stop offset="0%" style="stop-color:#3498db;stop-opacity:1" />
      <stop offset="100%" style="stop-color:#2c3e50;stop-opacity:1" />
    </radialGradient>

    <!-- 阴影滤镜 -->
    <filter id="shadow">
      <feGaussianBlur in="SourceAlpha" stdDeviation="3" />
      <feOffset dx="2" dy="2" result="offsetblur" />
      <feMerge>
        <feMergeNode />
        <feMergeNode in="SourceGraphic" />
      </feMerge>
    </filter>
  </defs>

  <rect x="50" y="50" width="150" height="100" fill="url(#grad1)" />
  <circle cx="300" cy="100" r="50" fill="url(#grad2)" />
  <text x="50" y="200" font-size="36" fill="#34495e" filter="url(#shadow)">
    带阴影的文本
  </text>
</svg>

SVG 动画

html
<svg width="400" height="300">
  <!-- 移动动画 -->
  <circle cx="50" cy="100" r="20" fill="#3498db">
    <animate
      attributeName="cx"
      from="50"
      to="350"
      dur="3s"
      repeatCount="indefinite"
    />
  </circle>

  <!-- 颜色变化 -->
  <rect x="50" y="150" width="100" height="60" fill="#e74c3c">
    <animate
      attributeName="fill"
      values="#e74c3c;#f39c12;#e74c3c"
      dur="2s"
      repeatCount="indefinite"
    />
  </rect>

  <!-- 路径动画 -->
  <circle r="10" fill="#2ecc71">
    <animateMotion dur="4s" repeatCount="indefinite">
      <mpath href="#motionPath" />
    </animateMotion>
  </circle>

  <path
    id="motionPath"
    d="M 50 250 Q 200 200 350 250"
    fill="none"
    stroke="#ccc"
    stroke-width="2"
  />
</svg>

JavaScript 操作 SVG

html
<svg id="mySvg" width="400" height="300">
  <circle id="myCircle" cx="100" cy="100" r="50" fill="#3498db" />
</svg>

<button id="changeColor">改变颜色</button>
<button id="moveCircle">移动圆形</button>

<script>
  const svg = document.getElementById("mySvg");
  const circle = document.getElementById("myCircle");

  // 改变颜色
  document.getElementById("changeColor").addEventListener("click", () => {
    const colors = ["#3498db", "#e74c3c", "#2ecc71", "#f39c12"];
    const randomColor = colors[Math.floor(Math.random() * colors.length)];
    circle.setAttribute("fill", randomColor);
  });

  // 移动圆形
  document.getElementById("moveCircle").addEventListener("click", () => {
    const newX = Math.random() * (400 - 100) + 50;
    const newY = Math.random() * (300 - 100) + 50;
    circle.setAttribute("cx", newX);
    circle.setAttribute("cy", newY);
  });

  // 动态创建 SVG 元素
  function addRectangle() {
    const rect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
    rect.setAttribute("x", Math.random() * 300);
    rect.setAttribute("y", Math.random() * 200);
    rect.setAttribute("width", 80);
    rect.setAttribute("height", 50);
    rect.setAttribute("fill", "#9b59b6");
    svg.appendChild(rect);
  }

  addRectangle();
</script>

Canvas vs SVG:如何选择?

性能对比

特性CanvasSVG
渲染方式基于像素(位图)基于对象(矢量)
DOM 操作无 DOM 节点,性能高每个元素都是 DOM 节点
元素数量适合大量元素(游戏)元素过多会影响性能
缩放会失真(像素化)无损缩放
事件处理需要手动计算坐标元素原生支持事件
内存占用固定(取决于画布大小)随元素数量增加

适用场景

选择 Canvas

✅ 游戏开发(需要高帧率动画)
✅ 实时数据可视化(大量数据点)
✅ 图像处理(滤镜、像素操作)
✅ 视频帧处理
✅ 复杂的粒子系统

选择 SVG

✅ 图标、Logo(需要缩放)
✅ 图表(需要交互)
✅ 地图应用
✅ 需要 SEO 的图形内容
✅ 需要打印的高质量图形
✅ 少量但需要丰富交互的元素

实战示例对比

Canvas 实现柱状图

javascript
const canvas = document.getElementById("chart");
const ctx = canvas.getContext("2d");

const data = [30, 80, 45, 60, 95];
const barWidth = 50;
const barGap = 20;

data.forEach((value, index) => {
  const x = index * (barWidth + barGap) + 50;
  const y = canvas.height - value - 50;
  const height = value;

  ctx.fillStyle = "#3498db";
  ctx.fillRect(x, y, barWidth, height);

  // 绘制数值
  ctx.fillStyle = "#000";
  ctx.font = "14px Arial";
  ctx.textAlign = "center";
  ctx.fillText(value, x + barWidth / 2, y - 5);
});

SVG 实现柱状图

html
<svg width="400" height="300">
  <g id="chart"></g>
</svg>

<script>
  const svg = document.getElementById("chart");
  const data = [30, 80, 45, 60, 95];
  const barWidth = 50;
  const barGap = 20;

  data.forEach((value, index) => {
    const x = index * (barWidth + barGap) + 50;
    const y = 250 - value;

    // 创建矩形
    const rect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
    rect.setAttribute("x", x);
    rect.setAttribute("y", y);
    rect.setAttribute("width", barWidth);
    rect.setAttribute("height", value);
    rect.setAttribute("fill", "#3498db");

    // 添加交互
    rect.addEventListener("mouseover", function () {
      this.setAttribute("fill", "#e74c3c");
    });
    rect.addEventListener("mouseout", function () {
      this.setAttribute("fill", "#3498db");
    });

    svg.appendChild(rect);

    // 添加文本
    const text = document.createElementNS("http://www.w3.org/2000/svg", "text");
    text.setAttribute("x", x + barWidth / 2);
    text.setAttribute("y", y - 5);
    text.setAttribute("text-anchor", "middle");
    text.textContent = value;
    svg.appendChild(text);
  });
</script>

最佳实践

Canvas 优化

javascript
// 1. 避免不必要的状态改变
ctx.fillStyle = "#3498db";
for (let i = 0; i < 1000; i++) {
  ctx.fillRect(x[i], y[i], 10, 10); // 批量绘制相同样式的图形
}

// 2. 使用离屏 Canvas
const offscreenCanvas = document.createElement("canvas");
const offscreenCtx = offscreenCanvas.getContext("2d");
// 在离屏画布上绘制复杂图形
offscreenCtx.drawComplexGraphic();
// 一次性绘制到主画布
ctx.drawImage(offscreenCanvas, 0, 0);

// 3. 减少重绘范围
// 只清除需要更新的区域
ctx.clearRect(x, y, width, height);

SVG 优化

html
<!-- 1. 复用元素 -->
<svg>
  <defs>
    <circle id="dot" r="5" fill="#3498db" />
  </defs>

  <use href="#dot" x="50" y="50" />
  <use href="#dot" x="100" y="100" />
  <use href="#dot" x="150" y="150" />
</svg>

<!-- 2. 使用 CSS 动画而非 SMIL -->
<style>
  .animated-circle {
    animation: move 3s infinite;
  }

  @keyframes move {
    from {
      transform: translateX(0);
    }
    to {
      transform: translateX(300px);
    }
  }
</style>

<svg>
  <circle class="animated-circle" cx="50" cy="100" r="20" fill="#3498db" />
</svg>

总结

Canvas 和 SVG 是 HTML5 提供的两种强大的图形技术:

Canvas

  • 高性能,适合动态、复杂的图形
  • 像素级控制,适合游戏和图像处理
  • 需要更多 JavaScript 代码

SVG

  • 矢量图形,无损缩放
  • DOM 元素,易于交互和操作
  • 适合少量、需要缩放或交互的图形