G6:GSAP + AI 项目实战——从数据可视化到 LLM 流式输出的动态展示

GSAP 动画实战系列 系列总览 | G1 Tween & Timeline | G2 ScrollTrigger | G3 文本动画 | G4 SVG 动画 | G5 GSAP + React | G6 AI 项目实战 ← 本文 | G7 社区案例


# GSAP + AI 项目实战 ## 神经网络可视化 - 节点连线动画 - 层间数据流粒子 - 激活值颜色映射 ## LLM 流式输出 - ScrambleText 逐字乱码 - 打字机 + 闪烁光标 - Token 级动画精度 ## Embedding 可视化 - 3D → 2D 投影动画 - t-SNE 收敛过程 - 聚类边界动态变形 ## 实时指标仪表盘 - 数字递增动画 - 动态进度条 - 状态转换动画 ## 设计原则 - 动画服务于认知 - 而非炫耀技术

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 追加字符
  • 接入 fetch stream,循环 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() 能正确清除所有运行中动画