动机
周末闲暇时,我在做个人网站时想到一个有趣的需求:想让 SVG 图标有个描边动画效果。市面上有很多成熟的动画库,但大多功能繁杂、体积庞大。其实我只需要一个轻量的 SVG 动画解决方案,于是决定自己动手写一个。
这就是svg-animate-web的由来 —— 一个完全为个人兴趣开发的小工具,纯粹是解决自己的需求,顺便分享给有类似需求的同学。
核心理念
作为一个周末玩具项目,我给自己设定了几个简单规则:
- 只实现自己需要的功能,不追求大而全
- 无第三方依赖,纯原生实现
- 尽量保持 API 简洁明了
实现原理剖析
与其展示大量使用示例,我想和大家分享下这个小工具的核心实现原理和一些有趣的技术点。
演示
效果
描边动画的实现
SVG 描边动画的核心是利用stroke-dasharray和stroke-dashoffset这两个属性。源码中的核心实现如下:
// 路径动画应用函数核心逻辑
function applyPathAnimation(pathElement, options) {
// 计算路径长度
let pathLength = 0;
try {
pathLength = pathElement.getTotalLength(); // 获取路径总长度
} catch (e) {
// 降级处理
const bbox = pathElement.getBBox();
pathLength = 2 * (bbox.width + bbox.height);
}
// 生成唯一ID
const id = Math.random().toString(36).substring(2, 10);
// 设置初始样式
setStyle(pathElement, {
stroke: options.stroke,
strokeWidth: options.strokeWidth,
strokeDasharray: pathLength,
strokeDashoffset: pathLength,
animation: `animation${id} ${options.duration}s ${options.easing} ${options.delay}s ${options.count} forwards`,
});
// 插入动画关键帧
insertKeyframes(`
@keyframes animation${id} {
0% { stroke-dashoffset: ${pathLength}; }
100% { stroke-dashoffset: 0; }
}
`);
}
这里的技术要点有:
- 使用
getTotalLength()获取路径长度,同时提供降级方案 - 为每个动画生成唯一 ID,避免 CSS 冲突
- 动态创建和插入 CSS 关键帧,而非使用 JavaScript 动画
动态样式和关键帧注入
为了避免污染全局 CSS 命名空间,我采用了动态生成和注入 CSS 的方式:
function insertKeyframes(keyframes) {
let styleEl = document.getElementById("svg-animate-keyframes");
if (!styleEl) {
styleEl = document.createElement("style");
styleEl.id = "svg-animate-keyframes";
document.head.appendChild(styleEl);
}
try {
const sheet = styleEl.sheet;
if (sheet) {
sheet.insertRule(keyframes, sheet.cssRules.length);
} else {
throw new Error("Style sheet not available");
}
} catch (e) {
// 回退处理
styleEl.textContent += keyframes;
}
}
这段代码有个有趣的地方:我先尝试使用 CSSOM API 直接操作样式表,如果失败则回退到直接修改textContent。这增加了兼容性,同时尽量使用更高效的 API。
不同元素类型的处理
使用了简单的条件判断区分不同 SVG 元素:
export function setPathAnimation(element, options) {
if (!element || !(element instanceof SVGElement)) return;
// 检查是否为矩形元素
const isRect = element.tagName.toLowerCase() === "rect";
if (isRect) {
// 应用矩形特有动画
applyRectAnimation(element, {
// 配置参数
});
} else {
// 应用路径元素动画
applyPathAnimation(element, {
// 配置参数
});
}
}
这种朴素的方式虽然不够优雅,但对于一个个人玩具项目来说足够简单明了,也便于理解和修改。
一些有趣的实现细节
自动处理 SVG 元素原始样式
在应用动画时,需要考虑 SVG 元素可能已有的样式。比如如何优雅处理已有的 fill 和 stroke 属性:
function getElementFillColor(element, defaultColor, userColor) {
if (userColor) return userColor;
const inlineFill = element.getAttribute("fill");
if (inlineFill && inlineFill !== "none") return inlineFill;
try {
const computedFill = window.getComputedStyle(element).fill;
if (
computedFill &&
computedFill !== "none" &&
computedFill !== "rgb(0, 0, 0)"
) {
return computedFill;
}
} catch (e) {
// 忽略计算样式错误
}
return defaultColor;
}
这个函数会按优先级依次检查:
- 用户指定的颜色
- 元素的内联 fill 属性
- 元素的计算样式
- 默认颜色
这种细节处理让库在实际使用时更加健壮。
性能考量
即使是个人玩具项目,我也注重性能。比如在处理多个 SVG 元素时,采用了延迟执行的策略:
export function setSvgAnimation(svgElement, options) {
if (!svgElement) return;
const pathElements = svgElement.querySelectorAll(
"path, line, polyline, polygon, rect, circle, ellipse"
);
Array.from(pathElements).forEach((element, index) => {
if (!(element instanceof SVGElement)) return;
const elementOptions = { ...options };
elementOptions.delay = (options?.delay ?? 0) + index * 0.1; // 错开动画开始时间
setPathAnimation(element, elementOptions);
});
}
通过为每个元素设置递增的延迟,既创造了序列动画效果,又避免了同时执行大量动画带来的性能问题。
未来可能的改进
虽然作为一个个人玩具项目,但确实有一些有趣的改进点:
- 添加更多元素类型的专用动画
- 优化动画性能,特别是对于复杂 SVG
- 增加更多动画控制选项
如果你对这个小工具感兴趣,欢迎在 GitHub 上提交 issue 或 PR,一起完善它。