G6:GSAP + AI 项目实战——从数据可视化到 LLM 流式输出的动态展示
GSAP 动画实战系列 系列总览 | G1 Tween & Timeline | G2 ScrollTrigger | G3 文本动画 | G4 SVG 动画 | G5 GSAP + React | G6 AI 项目实战 ← 本文 | G7 社区案例
AI 项目有一个通用问题:计算过程完全不可见。用户输入 prompt,等了几秒,文本出来了。中间发生了什么?模型为什么这么回答?置信度是多少?
GSAP 不能回答这些问题,但它能让答案的呈现方式降低认知摩擦。这篇文章聚焦四个 AI 典型场景。
场景一:神经网络推理过程可视化
🎬 神经网络可视化演示:CodePen GSAP + SVG | 完整代码见本系列 G4 SVG 动画篇
节点激活扩散动画
Show the flow of activation through network layers:
<svg viewBox="0 0 800 400">
<!-- Layer 1 nodes -->
<circle class="layer1-node" cx="100" cy="100" r="15" />
<circle class="layer1-node" cx="100" cy="200" r="15" />
<circle class="layer1-node" cx="100" cy="300" r="15" />
<!-- Layer 2 nodes -->
<circle class="layer2-node" cx="350" cy="100" r="15" />
<circle class="layer2-node" cx="350" cy="200" r="15" />
<circle class="layer2-node" cx="350" cy="300" r="15" />
<!-- Layer 3 nodes -->
<circle class="layer3-node" cx="600" cy="150" r="15" />
<circle class="layer3-node" cx="600" cy="250" r="15" />
<!-- Connections will be drawn as lines -->
<g class="connections"></g>
<!-- Data flow particles -->
<g class="particles"></g>
</svg>
function animateForwardPass(activations) {
// activations: { layer1: [...], layer2: [...], layer3: [...] }
const colors = ['#3b82f6', '#8b5cf6', '#ec4899']
const masterTL = gsap.timeline()
// Step 1: Activate layer 1 nodes
activations.layer1.forEach((val, i) => {
masterTL.to(`.layer1-node:nth-child(${i+1})`, {
attr: { r: 15 + val * 15 }, // 激活值越高,节点越大
fill: colors[0],
duration: 0.3,
ease: 'power3.out',
}, 0)
})
// Step 2: Animate particles flowing from layer 1 to layer 2
masterTL.to('.particle-l1-l2', {
motionPath: { path: '#conn-l1-l2', autoRotate: true },
duration: 0.8,
stagger: 0.05,
ease: 'power2.inOut',
}, '+=0.2')
// Step 3: Activate layer 2 nodes
activations.layer2.forEach((val, i) => {
masterTL.to(`.layer2-node:nth-child(${i+1})`, {
attr: { r: 15 + val * 15 },
fill: colors[1],
duration: 0.3,
ease: 'power3.out',
}, '-=0.3')
})
// ... continue for remaining layers
}
动态连线绘制
用 DrawSVG 让神经网络的边"生长"出来:
function drawConnections(layerFrom, layerTo) {
const connections = buildConnectionPaths(layerFrom, layerTo)
return gsap.from(connections, {
drawSVG: 0,
duration: 1.5,
stagger: 0.03,
ease: 'power2.inOut',
})
}
数据流粒子
function animateParticlesAlongPath(pathId, count) {
const particles = []
for (let i = 0; i < count; i++) {
const dot = document.createElementNS('http://www.w3.org/2000/svg', 'circle')
dot.setAttribute('r', '3')
dot.setAttribute('fill', '#60a5fa')
dot.style.opacity = 0
document.querySelector('.particles').appendChild(dot)
particles.push(dot)
}
gsap.to(particles, {
duration: 1.5,
motionPath: {
path: `#${pathId}`,
autoRotate: true,
},
opacity: 0.8,
stagger: { each: 0.05, repeat: -1 },
ease: 'none',
onComplete: () => particles.forEach(p => p.remove()),
})
}
场景二:LLM 流式输出的逐 Token 动画
这可能是 AI 项目中最高频的动画需求。用户应该看到 AI 在"想"——不是空白屏幕等着。
2.1 ScrambleText 模拟思考中的输出
function animateThinking(element) {
return gsap.to(element, {
scrambleText: {
text: 'Analyzing context... searching relevant documents... evaluating responses...',
chars: 'abcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*',
revealDelay: 0.5,
speed: 0.3,
},
duration: 2,
ease: 'none',
})
}
2.2 真正的流式输出动画
配合 fetch stream 实现逐 token 打字效果:
class StreamingAIResponse {
constructor(element) {
this.el = element
this.buffer = ''
this.displayed = ''
}
// 当新 token 到达时调用
onToken(token) {
this.buffer += token
this.render()
}
render() {
const target = this.buffer
const newChars = target.length - this.displayed.length
if (newChars > 10) {
// 积压太多,一次性补上
this.displayed = target
gsap.to(this.el, {
text: target,
duration: 0.2,
ease: 'none',
})
} else if (newChars > 0) {
// 逐个字符动画
this.displayed = target
gsap.to(this.el, {
text: target,
duration: 0.1,
ease: 'none',
})
}
}
onComplete() {
// 确保最终文本完全显示
gsap.to(this.el, {
text: this.buffer,
duration: 0.3,
ease: 'none',
})
}
}
// 使用
async function streamResponse(prompt, displayElement) {
const animator = new StreamingAIResponse(displayElement)
const response = await fetch('/api/chat', {
method: 'POST',
body: JSON.stringify({ prompt }),
headers: { 'Content-Type': 'application/json' },
})
const reader = response.body.getReader()
const decoder = new TextDecoder()
while (true) {
const { done, value } = await reader.read()
if (done) break
const text = decoder.decode(value, { stream: true })
// Parse SSE format or plain text
const tokens = parseTokens(text)
tokens.forEach(token => animator.onToken(token))
}
animator.onComplete()
}
2.3 思考状态指示器
在 LLM 思考时显示跳动的圆点:
<div class="thinking-indicator">
<span class="dot"></span>
<span class="dot"></span>
<span class="dot"></span>
</div>
function showThinking() {
return gsap.to('.dot', {
y: -10,
duration: 0.4,
stagger: { each: 0.15, repeat: -1, yoyo: true },
ease: 'power2.inOut',
})
}
function hideThinking(tween) {
tween.kill()
gsap.to('.thinking-indicator', {
opacity: 0,
duration: 0.3,
onComplete: () => {
document.querySelector('.thinking-indicator').remove()
}
})
}
场景三:Embedding 空间 2D 降维投影动画
当展示语义向量从高维→2D(t-SNE/UMAP)投影时,点的运动本身就是信息:
function animateEmbeddingProjection(points, targetPositions) {
// points: [{x, y, label, confidence}]
// targetPositions: 2D 坐标数组
const tl = gsap.timeline()
points.forEach((point, i) => {
// 从随机位置飞到目标位置
tl.to(`#point-${i}`, {
x: targetPositions[i].x,
y: targetPositions[i].y,
attr: {
r: 2 + point.confidence * 8 // 置信度高的点更大
},
duration: 1.5,
ease: 'power3.out',
}, 0) // 所有点同时开始
})
// 点稳定后,显示聚类标签
tl.to('.cluster-label', {
opacity: 1,
duration: 0.5,
stagger: 0.1,
})
}
高亮最近邻
当用户 hover 一个点时,用 GSAP 高亮它和它的最近邻:
function highlightNeighbors(pointId, neighborIds) {
const tl = gsap.timeline()
// 当前点脉冲
tl.to(`#${pointId}`, {
attr: { r: 12 },
duration: 0.3,
ease: 'power2.out',
})
// 邻居飞线连接
neighborIds.forEach(nid => {
const line = createLine(pointId, nid)
tl.from(line, {
drawSVG: 0,
duration: 0.4,
ease: 'power2.inOut',
}, '-=0.2')
})
// 其他点淡出
tl.to('.embedding-point:not(.highlighted)', {
opacity: 0.2,
duration: 0.3,
}, 0)
}
场景四:实时推理指标仪表盘
模型在生产环境运行时,需要一个动态仪表盘显示吞吐量、延迟、准确率等:
4.1 数字递增
function updateMetric(element, value, suffix = '') {
const current = parseFloat(element.textContent) || 0
gsap.to(element, {
text: {
value: value.toFixed(1) + suffix,
speed: Math.abs(value - current) * 2, // 变化大时跳得快
},
duration: Math.min(1.5, Math.abs(value - current) * 0.05),
ease: 'power2.out',
})
}
4.2 动态进度条
function animateProgressBar(selector, targetPercent, color) {
gsap.to(selector, {
width: targetPercent + '%',
backgroundColor: color,
duration: 1,
ease: 'power3.inOut',
})
}
4.3 阈值告警动画
当延迟超过阈值时,仪表盘面板闪烁红色:
function triggerAlert(widgetRef) {
const tl = gsap.timeline({ repeat: 3 })
tl.to(widgetRef.current, {
boxShadow: '0 0 20px rgba(239, 68, 68, 0.6)',
borderColor: '#ef4444',
duration: 0.3,
})
.to(widgetRef.current, {
boxShadow: '0 0 0px rgba(239, 68, 68, 0)',
borderColor: '#333',
duration: 0.3,
})
}
4.4 状态转换动画
模型切换状态时(idle → thinking → responding → done):
const statusFlow = {
idle: { color: '#6b7280', scale: 1 },
thinking: { color: '#f59e0b', scale: 1.1 },
responding: { color: '#3b82f6', scale: 1 },
done: { color: '#10b981', scale: 1 },
}
function transitionStatus(indicatorRef, status) {
const state = statusFlow[status]
gsap.to(indicatorRef.current, {
backgroundColor: state.color,
scale: state.scale,
duration: 0.5,
ease: 'power2.out',
})
}
5. 设计原则
在 AI 项目中做动画,有几个原则值得内化:
5.1 动画服务于认知,而非装饰
一个闪烁的光标告诉用户"系统在工作",这有用。一个花哨的粒子爆炸,没有信息量,只会分散注意力。
5.2 状态转换比"炫"重要
用户关心的是:系统在干嘛?还要等多久?动画应该回答这两个问题——而不是让用户觉得"它是不是卡了"。
5.3 节奏匹配数据频率
- LLM token 流:高频,0.05s 级别——TextPlugin 轻量动画
- 推理指标轮询:低频,1-5s 级别——带 easing 的数字递增
- Embedding 投影:一次性,长 duration(2-3s)——展示降维收敛过程
- 告警:即时的,短 duration(0.3s)——强对比色 + 脉冲
5.4 可中断、可取消
AI 流程可能随时中止(用户取消、超时、错误)。所有动画必须支持中途被 kill:
class CancellableAnimation {
constructor() {
this.tweens = []
}
add(tween) {
this.tweens.push(tween)
return tween
}
cancel() {
this.tweens.forEach(t => t.kill())
this.tweens = []
}
}
6. 实战:一个完整的 AI Dashboard 动画架构
class AIDashboardAnimator {
constructor() {
this.runningAnimations = new Map()
}
// LLM 响应流
streamResponse(id, element) {
const streamer = new StreamingAIResponse(element)
this.runningAnimations.set(id, streamer)
return streamer
}
// 指标更新
updateMetric(id, element, value) {
const existing = this.runningAnimations.get(id)
if (existing) existing.kill()
const tween = gsap.to(element, {
text: { value: value.toFixed(1) },
duration: 0.8,
ease: 'power2.out',
})
this.runningAnimations.set(id, tween)
}
// 网络可视化
visualizeForwardPass(layers) {
const tl = gsap.timeline()
layers.forEach((layer, i) => {
layer.nodes.forEach((node, j) => {
tl.to(`#layer${i}-node${j}`, {
attr: { r: 10 + node.activation * 10 },
fill: activationColor(node.activation),
duration: 0.3,
ease: 'power3.out',
}, i * 0.4)
})
})
return tl
}
// 全部清理
cleanup() {
this.runningAnimations.forEach(anim => {
if (anim.kill) anim.kill()
})
this.runningAnimations.clear()
gsap.killTweensOf('*') // 激进清理
}
}
下一篇:G7 社区案例精选——从 GSAPify 到 gsap-cookbook 的生产级模式
实操清单
- 用 SVG 绘制三层神经网络结构(节点 + 连线),为每个节点添加
class以便 GSAP 选取 - 实现
animateForwardPass(activations)函数,根据激活值动态改变节点半径和颜色 - 用
drawSVG: 0配合gsap.from()让神经网络连线"生长"出来 - 用
motionPath将粒子元素沿连接路径运动,模拟层间数据流动 - 实现
StreamingAIResponse类,在onToken(token)被调用时用 TextPlugin 追加字符 - 接入
fetchstream,循环reader.read()解码 SSE/纯文本 token,逐个喂给StreamingAIResponse - 用 ScrambleText 制作 LLM 思考中的乱码占位动画,并在流式输出开始后
kill()它 - 制作三圆点跳动的 thinking indicator,用
stagger + yoyo + repeat: -1实现无限循环 - 实现
animateEmbeddingProjection(points, targetPositions),将所有点从随机位置同时飞至 t-SNE 目标坐标 - 实现 hover 高亮最近邻:点脉冲放大 + DrawSVG 飞线 + 其余点淡出到
opacity: 0.2 - 封装
CancellableAnimation类,保存所有 tween 引用,在用户取消/超时时统一kill() - 将以上模块整合进
AIDashboardAnimator,验证cleanup()能正确清除所有运行中动画