Canvas 和 SVG:HTML5 图形绘制技术深度对比与应用
图形绘制的两条路径
HTML5 为 Web 开发者提供了两种强大的图形绘制技术:Canvas 和 SVG。它们各有特点,适用于不同的应用场景。
核心区别
Canvas(画布):
- 基于像素的位图绘制
- 使用 JavaScript 逐像素绘制
- 绘制后内容不可直接修改(需要重绘)
- 性能优秀,适合动态、复杂的图形
SVG(可缩放矢量图形):
- 基于 XML 的矢量图形
- 使用标签描述图形
- 每个图形元素都是 DOM 节点,可以独立操作
- 无损缩放,适合需要交互的图形
可以这样理解:Canvas 就像一张画布,你用画笔在上面作画;SVG 则像用积木搭建图形,每个积木都可以单独移动、修改。
Canvas 基础
创建 Canvas
html
<canvas id="myCanvas" width="800" height="600">
您的浏览器不支持 Canvas。
</canvas>注意:
- 使用
width和height属性设置画布大小(而非 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:如何选择?
性能对比
| 特性 | Canvas | SVG |
|---|---|---|
| 渲染方式 | 基于像素(位图) | 基于对象(矢量) |
| 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 元素,易于交互和操作
- 适合少量、需要缩放或交互的图形