简介
Anime.js 是一个强大且易用的 JavaScript 动画库,支持对 DOM、SVG、CSS 属性等多种对象进行高性能动画处理。它拥有丰富的缓动函数、时间线控制、序列动画、SVG 路径动画等特性,适用于网页交互动效、数据可视化、图标动画等多种场景。通过 Anime.js,你可以用极简的代码实现复杂的动画效果,提升网页的视觉表现力和用户体验。
安装
npm install animejs
Anime.js 也可以非常方便地为 SVG 元素添加动画,支持路径、形状、颜色等多种属性的动画。下面是一个 SVG 路径动画的示例:
示例
SVG 方 → 星
import { useEffect, useRef } from "react";
import { animate } from "animejs";
const points = 16;
const radius = 70;
const center = 100;
// 生成圆
function getCircle() {
return Array.from({ length: points }, (_, i) => {
const angle = (2 * Math.PI * i) / points - Math.PI / 2;
return [
center + radius * Math.cos(angle),
center + radius * Math.sin(angle),
];
});
}
// 生成“圆角方形”,点分布和圆一致,只是半径在四个角缩小
function getSquare() {
return Array.from({ length: points }, (_, i) => {
const angle = (2 * Math.PI * i) / points - Math.PI / 2;
// 0,4,8,12是边的中点,其他是角
const isCorner = i % 4 !== 0;
const r = isCorner ? radius * 0.55 : radius;
return [center + r * Math.cos(angle), center + r * Math.sin(angle)];
});
}
// 生成星形
function getStar() {
const r1 = radius;
const r2 = radius * 0.45;
return Array.from({ length: points }, (_, i) => {
const angle = (2 * Math.PI * i) / points - Math.PI / 2;
const r = i % 2 === 0 ? r1 : r2;
return [center + r * Math.cos(angle), center + r * Math.sin(angle)];
});
}
// 转换为 path 字符串
function toPath(pointsArr) {
return (
"M" +
pointsArr.map(([x, y]) => `${x.toFixed(2)},${y.toFixed(2)}`).join(" L ") +
" Z"
);
}
const shapes = [toPath(getCircle()), toPath(getSquare()), toPath(getStar())];
export default function ShapeMorphDemo() {
const shapeRef = useRef(null);
const current = useRef(0);
useEffect(() => {
let running = true;
const morph = () => {
if (!running) return;
const from = shapes[current.current % shapes.length];
const to = shapes[(current.current + 1) % shapes.length];
animate(shapeRef.current, {
d: [from, to],
duration: 2200,
ease: "outElastic(1, .7)",
complete: () => {
current.current = (current.current + 1) % shapes.length;
setTimeout(morph, 600);
},
});
};
morph();
return () => {
running = false;
};
}, []);
return (
<div className="flex flex-col items-center justify-center min-h-[60vh]">
<svg width="200" height="200" viewBox="0 0 200 200">
<path
ref={shapeRef}
d={shapes[0]}
fill="#60a5fa"
stroke="#1e3a8a"
strokeWidth="4"
/>
</svg>
<div className="mt-6 text-lg text-gray-700">方 → 星</div>
</div>
);
}

SVG 圆 ↔ 心形
import { useEffect, useRef, useState } from "react";
import { animate } from "animejs";
const points = 80;
const radius = 70;
const center = 100;
// 标准圆
function getCircle() {
return Array.from({ length: points }, (_, i) => {
const angle = (2 * Math.PI * i) / points;
return [
center + radius * Math.cos(angle),
center + radius * Math.sin(angle),
];
});
}
// 标准心形
function getHeart() {
return Array.from({ length: points }, (_, i) => {
const t = (2 * Math.PI * i) / points;
const x0 = (16 * Math.pow(Math.sin(t), 3)) / 17;
const y0 =
(13 * Math.cos(t) -
5 * Math.cos(2 * t) -
2 * Math.cos(3 * t) -
Math.cos(4 * t)) /
17;
return [center + radius * x0, center - radius * y0];
});
}
// 转换为 path 字符串
function toPath(pointsArr) {
return (
"M" +
pointsArr.map(([x, y]) => `${x.toFixed(2)},${y.toFixed(2)}`).join(" L ") +
" Z"
);
}
// 渐变色组
const gradients = [
// 圆:蓝紫渐变
[
{ offset: "0%", color: "#6EE7F9" },
{ offset: "50%", color: "#A7F3D0" },
{ offset: "100%", color: "#818CF8" },
],
// 心形:粉橙渐变
[
{ offset: "0%", color: "#FDE68A" },
{ offset: "50%", color: "#FCA5A5" },
{ offset: "100%", color: "#F472B6" },
],
];
const shapes = [toPath(getCircle()), toPath(getHeart())];
export default function ShapeMorphHeartDemo() {
const shapeRef = useRef(null);
const [gradientIndex, setGradientIndex] = useState(0);
const current = useRef(0);
useEffect(() => {
let running = true;
const morph = () => {
if (!running) return;
const from = shapes[current.current % shapes.length];
const to = shapes[(current.current + 1) % shapes.length];
// 动态切换渐变色
setGradientIndex((current.current + 1) % gradients.length);
animate(shapeRef.current, {
d: [from, to],
duration: 1800,
ease: "outElastic(1, .7)",
complete: () => {
current.current = (current.current + 1) % shapes.length;
setTimeout(morph, 900);
},
});
};
morph();
return () => {
running = false;
};
}, []);
// 当前渐变色
const stops = gradients[gradientIndex];
return (
<div className="flex flex-col items-center justify-center min-h-[60vh]">
<svg width="220" height="220" viewBox="0 0 200 200">
<defs>
<linearGradient
id="morphGradient"
x1="0%"
y1="0%"
x2="100%"
y2="100%"
>
{stops.map((stop, i) => (
<stop
key={i}
offset={stop.offset}
stopColor={stop.color}
stopOpacity="1"
/>
))}
</linearGradient>
</defs>
<path
ref={shapeRef}
d={shapes[0]}
fill="url(#morphGradient)"
stroke="#2226"
strokeWidth="5"
style={{
filter: "drop-shadow(0 4px 24px #818cf855)",
transition: "filter 0.3s",
}}
/>
</svg>
<div className="mt-6 text-lg text-gray-700 font-semibold">圆 ↔ 心形</div>
</div>
);
}

文字动效
import { useEffect, useRef } from "react";
import { animate, stagger } from "animejs";
const text = "ifrontend.net".split("");
export default function SVGTextAnim() {
const letterRefs = useRef([]);
useEffect(() => {
// 依次弹跳出现
animate(letterRefs.current, {
translateY: [40, 0],
scale: [0.5, 1.1, 1],
opacity: [0, 1],
fill: [
"#818CF8",
"#F472B6",
"#FBBF24",
"#34D399",
"#60A5FA",
"#F472B6",
"#FBBF24",
"#34D399",
"#60A5FA",
"#F472B6",
"#FBBF24",
"#34D399",
],
duration: 1200,
delay: stagger(120),
ease: "outElastic(1, .7)",
});
}, []);
return (
<div className="flex flex-col items-center justify-center min-h-[60vh]">
<svg
width="380"
height="100"
viewBox="0 0 380 100"
style={{ display: "block" }}
>
<defs>
<linearGradient id="textGradient" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stopColor="#818CF8" />
<stop offset="50%" stopColor="#F472B6" />
<stop offset="100%" stopColor="#FBBF24" />
</linearGradient>
</defs>
{text.map((char, i) => (
<text
key={i}
ref={(el) => (letterRefs.current[i] = el)}
x={24 + i * 28} // 步进从40改为28,起始点24
y={68} // y略微上移
fontSize="38" // 字体略小
fontFamily="Fira Mono, Menlo, monospace"
fontWeight="bold"
fill="url(#textGradient)"
stroke="#2226"
strokeWidth="1.2"
opacity="0"
style={{
filter: "drop-shadow(0 2px 8px #818cf855)",
transition: "filter 0.3s",
cursor: "default",
userSelect: "none",
}}
>
{char}
</text>
))}
</svg>
<div className="mt-6 text-lg text-gray-700">文字动效</div>
</div>
);
}

说明
- 通过
getTotalLength()
获取 SVG 路径的总长度。 - 设置
strokeDasharray
和strokeDashoffset
实现路径描边动画。 - 使用 animejs 的
animate
方法让路径从无到有地描绘出来。
你还可以为 SVG 的 fill
、stroke
、transform
等属性添加动画,打造更丰富的 SVG 动效。
原文链接:https://code.ifrontend.net/archives/817,转载请注明出处。
评论0