SVG 进阶与动画
SVG 基础语法请参考 Canvas 与 SVG。本文侧重 SVG 在可视化中的进阶应用。
SVG 坐标系统
viewBox 与视口
<!-- viewBox="minX minY width height" 定义 SVG 的逻辑坐标系 -->
<!-- width/height 定义实际显示尺寸 -->
<svg width="400" height="300" viewBox="0 0 800 600">
<!-- 内容在 800x600 逻辑坐标中绘制,自动缩放到 400x300 显示 -->
<circle cx="400" cy="300" r="100" />
</svg>
preserveAspectRatio 控制缩放行为:
| 值 | 效果 |
|---|---|
xMidYMid meet(默认) | 等比缩放,内容完整显示 |
xMidYMid slice | 等比缩放,填满视口,可能裁切 |
none | 非等比拉伸 |
坐标变换
<g transform="translate(100, 50) rotate(45) scale(1.5)">
<!-- 变换按从右到左的顺序应用:先 scale → 再 rotate → 最后 translate -->
<rect x="0" y="0" width="50" height="30" />
</g>
路径(Path)
<path> 是 SVG 最强大的元素,所有图形都可以用路径表示:
| 命令 | 含义 | 参数 |
|---|---|---|
| M/m | 移动到 | x, y |
| L/l | 直线到 | x, y |
| H/h | 水平线到 | x |
| V/v | 竖直线到 | y |
| C/c | 三次贝塞尔 | x1,y1 x2,y2 x,y |
| S/s | 平滑三次贝塞尔 | x2,y2 x,y |
| Q/q | 二次贝塞尔 | x1,y1 x,y |
| A/a | 椭圆弧 | rx,ry rotation large-arc sweep x,y |
| Z/z | 闭合路径 | - |
大写为绝对坐标,小写为相对坐标。
<!-- 绘制一个心形 -->
<path d="M 150 30
C 100 -20, 0 20, 50 80
L 150 180
L 250 80
C 300 20, 200 -20, 150 30 Z"
fill="red" />
路径动画(沿路径运动)
// 获取路径上某个位置的坐标 — 用于让元素沿路径运动
function animateAlongPath(pathEl: SVGPathElement, duration: number): void {
const totalLength = pathEl.getTotalLength();
let start: number | null = null;
function step(timestamp: number): void {
if (!start) start = timestamp;
const progress = Math.min((timestamp - start) / duration, 1);
const point = pathEl.getPointAtLength(progress * totalLength);
// point.x, point.y 就是路径上当前位置
movingElement.setAttribute('cx', String(point.x));
movingElement.setAttribute('cy', String(point.y));
if (progress < 1) requestAnimationFrame(step);
}
requestAnimationFrame(step);
}
SVG 滤镜
<defs>
<!-- 模糊滤镜 -->
<filter id="blur">
<feGaussianBlur in="SourceGraphic" stdDeviation="3" />
</filter>
<!-- 阴影滤镜 -->
<filter id="shadow">
<feDropShadow dx="2" dy="2" stdDeviation="2" flood-color="rgba(0,0,0,0.3)" />
</filter>
<!-- 发光效果 -->
<filter id="glow">
<feGaussianBlur stdDeviation="4" result="blur" />
<feMerge>
<feMergeNode in="blur" />
<feMergeNode in="SourceGraphic" />
</feMerge>
</filter>
</defs>
<circle cx="100" cy="100" r="40" filter="url(#glow)" fill="#4fc3f7" />
SVG 动画
CSS 动画
/* 描边动画:从无到有 */
.draw-line {
stroke-dasharray: 1000;
stroke-dashoffset: 1000;
animation: draw 2s ease forwards;
}
@keyframes draw {
to { stroke-dashoffset: 0; }
}
/* 呼吸效果 */
.pulse {
animation: pulse 1.5s ease-in-out infinite;
transform-origin: center;
}
@keyframes pulse {
0%, 100% { transform: scale(1); opacity: 1; }
50% { transform: scale(1.1); opacity: 0.7; }
}
SMIL 动画
<!-- 形状变形 -->
<path d="M 10 80 Q 52.5 10, 95 80 T 180 80">
<animate attributeName="d"
values="M 10 80 Q 52.5 10, 95 80 T 180 80;
M 10 80 Q 52.5 150, 95 80 T 180 80;
M 10 80 Q 52.5 10, 95 80 T 180 80"
dur="2s" repeatCount="indefinite" />
</path>
<!-- 沿路径运动 -->
<circle r="5" fill="red">
<animateMotion dur="3s" repeatCount="indefinite">
<mpath href="#motionPath" />
</animateMotion>
</circle>
JavaScript 动画(Web Animations API)
// 使用 WAAPI 控制 SVG 动画
function animateSVGElement(el: SVGElement): Animation {
return el.animate(
[
{ transform: 'scale(1)', opacity: 1 },
{ transform: 'scale(1.5)', opacity: 0.5 },
{ transform: 'scale(1)', opacity: 1 },
],
{ duration: 1000, iterations: Infinity, easing: 'ease-in-out' }
);
}
SVG 在可视化中的应用
响应式图表
// SVG 图表自动适应容器尺寸
function createResponsiveChart(container: HTMLElement): SVGSVGElement {
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.setAttribute('viewBox', '0 0 800 600');
svg.setAttribute('preserveAspectRatio', 'xMidYMid meet');
svg.style.width = '100%';
svg.style.height = '100%';
container.appendChild(svg);
return svg;
}
交互式数据点
// SVG 元素原生支持 DOM 事件
function addDataPointInteraction(circle: SVGCircleElement, data: { label: string; value: number }): void {
circle.addEventListener('mouseenter', () => {
circle.setAttribute('r', String(Number(circle.getAttribute('r')) * 1.3));
showTooltip(circle, `${data.label}: ${data.value}`);
});
circle.addEventListener('mouseleave', () => {
circle.setAttribute('r', String(Number(circle.getAttribute('r')) / 1.3));
hideTooltip();
});
}
SVG 性能优化
| 策略 | 说明 |
|---|---|
| 减少 DOM 节点 | 元素数量 < 1000 为佳 |
使用 <use> 复用 | 重复图形定义一次,多处引用 |
| 简化路径 | 减少路径点数量(工具:SVGO) |
| 避免复杂滤镜 | feGaussianBlur 等非常耗性能 |
| CSS 替代 SMIL | CSS 动画性能更好 |
will-change | 提示浏览器提升动画元素 |
常见面试问题
Q1: SVG 的 viewBox 是什么?有什么作用?
答案:
viewBox 定义 SVG 的逻辑坐标系(用户坐标系),格式为 "minX minY width height"。它与实际显示尺寸(width/height 属性)配合,实现内容的自动缩放。配合 preserveAspectRatio 控制缩放行为。这使 SVG 天然支持响应式。
Q2: SVG 路径的 d 属性如何理解?
答案:
d 属性包含一系列绘图命令:M(移动)、L(直线)、C/Q(贝塞尔曲线)、A(圆弧)、Z(闭合)。大写为绝对坐标,小写为相对坐标。所有 SVG 基本形状(rect、circle 等)都可以转成 path 形式。
Q3: 如何实现 SVG 描边动画?
答案:
利用 stroke-dasharray(线段间距)和 stroke-dashoffset(偏移量):先用 getTotalLength() 获取路径总长度,设置 dasharray 和 dashoffset 都等于总长度(隐藏),然后动画将 dashoffset 从总长度过渡到 0,产生"逐渐画出"的效果。
Q4: SVG 和 Canvas 在事件处理上的差异?
答案:
SVG 每个图形都是 DOM 元素,直接支持 addEventListener、CSS 伪类(:hover)、ARIA 属性。Canvas 是位图无 DOM 结构,需手动实现命中检测(几何计算或颜色拾取),详见 Canvas 2D 进阶。
Q5: SVG 性能瓶颈在哪?如何优化?
答案:
瓶颈在于 DOM 元素数量。每个 SVG 图形都是 DOM 节点,超过几千个会导致渲染变慢。优化方案:减少节点数、用 <use> 复用、简化路径(SVGO)、避免复杂滤镜、合理使用 CSS 动画而非 SMIL。数据量大时应改用 Canvas。