Text 文本动画
GSAP 的文本动画功能,用于创建文字拆分、打字机效果、数字计数等文本动画效果。
📚 文本动画概述
为什么需要文本动画?
- 吸引注意力:动态文字比静态文字更吸引人
- 增强体验:提升用户交互体验
- 信息传达:通过动画强调重要信息
GSAP 文本动画方式
- SplitText 插件:文字拆分动画(付费)
- TextPlugin:文字内容动画
- 自定义实现:使用 JavaScript 拆分文字
🚀 TextPlugin(文字内容动画)
基础用法
gsap.to(".text", {
textContent: "新文字内容",
duration: 1
});数字计数动画
gsap.to(".counter", {
textContent: 100,
duration: 2,
snap: { textContent: 1 }, // 整数显示
ease: "power2.out"
});格式化数字
gsap.to(".price", {
textContent: 999.99,
duration: 2,
snap: { textContent: 0.01 }, // 保留两位小数
ease: "power2.out"
});🎯 SplitText 文字拆分
安装 SplitText
SplitText 是 GSAP 的付费插件,需要 Club GreenSock 会员。
基础拆分
// 引入 SplitText
import { SplitText } from "gsap/SplitText";
gsap.registerPlugin(SplitText);
// 拆分文字
const split = new SplitText(".text", {
type: "chars" // 按字符拆分
});
// 动画每个字符
gsap.from(split.chars, {
opacity: 0,
y: 50,
duration: 0.5,
stagger: 0.05
});拆分类型
// 按字符拆分
const split = new SplitText(".text", {
type: "chars"
});
// 按单词拆分
const split = new SplitText(".text", {
type: "words"
});
// 按行拆分
const split = new SplitText(".text", {
type: "lines"
});
// 组合拆分
const split = new SplitText(".text", {
type: "chars,words,lines"
});🎨 自定义文字拆分(免费方案)
按字符拆分
function splitTextToChars(element) {
const text = element.textContent;
const chars = text.split("").map(char => {
const span = document.createElement("span");
span.textContent = char === " " ? "\u00A0" : char; // 保留空格
span.style.display = "inline-block";
return span;
});
element.textContent = "";
chars.forEach(char => element.appendChild(char));
return Array.from(element.children);
}
// 使用
const chars = splitTextToChars(document.querySelector(".text"));
gsap.from(chars, {
opacity: 0,
y: 20,
duration: 0.3,
stagger: 0.02
});按单词拆分
function splitTextToWords(element) {
const text = element.textContent;
const words = text.split(" ").map(word => {
const span = document.createElement("span");
span.textContent = word;
span.style.display = "inline-block";
span.style.marginRight = "0.3em";
return span;
});
element.textContent = "";
words.forEach(word => element.appendChild(word));
return Array.from(element.children);
}
// 使用
const words = splitTextToWords(document.querySelector(".text"));
gsap.from(words, {
opacity: 0,
y: 30,
duration: 0.5,
stagger: 0.1
});🎬 实战案例
案例 1:打字机效果
function typewriter(element, text, speed = 50) {
element.textContent = "";
let i = 0;
function type() {
if (i < text.length) {
element.textContent += text.charAt(i);
i++;
setTimeout(type, speed);
}
}
type();
}
// 使用 GSAP 实现
function typewriterGSAP(element, text, duration = 1) {
gsap.to(element, {
textContent: text,
duration: duration,
ease: "none",
snap: { textContent: 1 }
});
}案例 2:文字依次出现
const text = document.querySelector(".text");
const chars = splitTextToChars(text);
gsap.from(chars, {
opacity: 0,
y: 50,
rotationX: -90,
duration: 0.5,
stagger: 0.03,
ease: "back.out(1.7)"
});案例 3:文字波浪效果
const text = document.querySelector(".text");
const chars = splitTextToChars(text);
chars.forEach((char, i) => {
gsap.to(char, {
y: -20,
duration: 0.3,
repeat: -1,
yoyo: true,
ease: "sine.inOut",
delay: i * 0.05
});
});案例 4:数字计数动画
function animateCounter(element, target, duration = 2) {
gsap.to(element, {
textContent: target,
duration: duration,
snap: { textContent: 1 },
ease: "power2.out",
onUpdate: function() {
element.textContent = Math.floor(this.targets()[0].textContent);
}
});
}
// 使用
animateCounter(document.querySelector(".counter"), 100);案例 5:价格动画
function animatePrice(element, target, duration = 1.5) {
gsap.to(element, {
textContent: target,
duration: duration,
snap: { textContent: 0.01 },
ease: "power2.out",
onUpdate: function() {
const value = parseFloat(this.targets()[0].textContent);
element.textContent = `¥${value.toFixed(2)}`;
}
});
}案例 6:滚动触发文字动画
const text = document.querySelector(".text");
const words = splitTextToWords(text);
gsap.from(words, {
opacity: 0,
y: 30,
duration: 0.5,
stagger: 0.1,
scrollTrigger: {
trigger: text,
start: "top 80%",
toggleActions: "play none none reverse"
}
});案例 7:文字高亮效果
const text = document.querySelector(".text");
const words = splitTextToWords(text);
words.forEach((word, i) => {
word.addEventListener("mouseenter", () => {
gsap.to(word, {
scale: 1.2,
color: "#ff6b6b",
duration: 0.3,
ease: "back.out(1.7)"
});
});
word.addEventListener("mouseleave", () => {
gsap.to(word, {
scale: 1,
color: "inherit",
duration: 0.3
});
});
});案例 8:文字擦除效果
const text = document.querySelector(".text");
const chars = splitTextToChars(text);
// 设置初始状态
gsap.set(chars, { opacity: 1 });
// 擦除动画
function eraseText() {
gsap.to(chars, {
opacity: 0,
x: 50,
duration: 0.3,
stagger: 0.02,
ease: "power2.in"
});
}🛠️ 高级技巧
保留 HTML 结构
function splitTextPreserveHTML(element) {
const walker = document.createTreeWalker(
element,
NodeFilter.SHOW_TEXT,
null,
false
);
const textNodes = [];
let node;
while (node = walker.nextNode()) {
textNodes.push(node);
}
textNodes.forEach(textNode => {
const parent = textNode.parentNode;
const text = textNode.textContent;
const chars = text.split("").map(char => {
const span = document.createElement("span");
span.textContent = char === " " ? "\u00A0" : char;
span.style.display = "inline-block";
return span;
});
textNode.remove();
chars.forEach(char => parent.appendChild(char));
});
return Array.from(element.querySelectorAll("span"));
}结合 Timeline
const text = document.querySelector(".text");
const words = splitTextToWords(text);
const tl = gsap.timeline();
words.forEach((word, i) => {
tl.from(word, {
opacity: 0,
y: 20,
duration: 0.3
}, i * 0.1);
});💡 最佳实践
1. 性能优化
// ✅ 推荐:使用 transform 和 opacity
gsap.from(chars, {
opacity: 0,
y: 20, // 使用 y 而不是 top
duration: 0.3
});
// ❌ 不推荐:触发重排
gsap.from(chars, {
opacity: 0,
top: 20, // 触发重排
duration: 0.3
});2. 合理使用 stagger
// ✅ 推荐:适度的延迟
gsap.from(chars, {
opacity: 0,
stagger: 0.02 // 20ms 延迟
});
// ❌ 不推荐:延迟太长
gsap.from(chars, {
opacity: 0,
stagger: 0.2 // 200ms 延迟,太慢
});3. 处理空格
// ✅ 推荐:保留空格
span.textContent = char === " " ? "\u00A0" : char; // 不间断空格
// ❌ 不推荐:丢失空格
span.textContent = char; // 空格可能被忽略