跳到主要内容

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 替代 SMILCSS 动画性能更好
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。

相关链接