当 AIGC 遇见 UI:SEE TypingDots 动画的奇幻漂流

94 阅读3分钟

“别眨眼,否则你会错过一个像素的一生。”——某不知名前端段子手


0x00 开场白:为什么连“打字中的点点”都要卷?

在 AIGC 时代,每一次交互都是一次小型舞台剧
当大模型在后台吭哧吭哧地推理,前端必须优雅地告诉用户:

“别急,我正在让硅基大脑跳踢踏舞。”

于是,TypingDots——那三个一跳一跳的小圆点——成了 UI 里最会抢戏的龙套演员。
今天,我们就把它的戏服扒光,看看里头到底藏了多少二进制灵魂


0x01 硬件级冷知识:一个像素的一生

在 OLED 屏上,每个像素点其实是一位发光二极管社畜
它从 rgb(0,0,0) 的沉睡中醒来,被 GPU 的电压叫醒,
在 16.67 ms(60Hz)里完成一次“发光→衰减→再发光”的 KPI。

所以,动画的本质,是让这些社畜像素按时打卡,别摸鱼。


0x02 需求拆解:SEE 框架

缩写全称翻译成人话
SSkeleton先画静态骨架(三个点)
EEasing给动画加点“人情味”缓动
EEvent-Driven让动画听命于 AI 状态机

0x03 Skeleton:用最少的 div,装最多的逼

<!-- TypingDots.vue / TypingDots.tsx / TypingDots.whatever -->
<div class="typing-dots">
  <span class="dot"></span>
  <span class="dot"></span>
  <span class="dot"></span>
</div>
.typing-dots {
  display: flex;
  gap: 4px;
  align-items: center;
}

.dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: #60a5fa; /* Tailwind blue-400 */
  /* 初始状态:社畜还没上班 */
  transform: scale(0);
}

0x04 Easing:让动画有“呼吸感”

“线性动画就像机器人打太极,没有灵魂。”
——某被甲方逼疯的动画师

我们用 cubic-bezier 伪造“肺部起伏”:

.dot {
  animation: breathe 1.4s infinite;
  transform-origin: bottom;
}

.dot:nth-child(1) { animation-delay: 0s; }
.dot:nth-child(2) { animation-delay: 0.16s; }
.dot:nth-child(3) { animation-delay: 0.32s; }

@keyframes breathe {
  0%   { transform: scale(0)  translateY(0);   opacity: 0.6; }
  50%  { transform: scale(1.2) translateY(-6px); opacity: 1; }
  100% { transform: scale(0)  translateY(0);   opacity: 0.6; }
}

注意:这里把“位移”也加进关键帧,让点有“脚尖踮起”的错觉。
数学上,你可以把 translateY 想成重力势能的瞬移。


0x05 Event-Driven:让动画听懂 AI 的暗号

AI 的回复分三段:

  1. Thinking(推理中)
  2. Streaming(流式吐字)
  3. Done(收工)

我们用 WebSocket 驱动动画的生死:

// 伪代码,可直接塞进 React/Vue/Svelte
const [phase, setPhase] = useState('idle');

useEffect(() => {
  const ws = new WebSocket('wss://api.aigc.example/stream');

  ws.onmessage = (ev) => {
    const { type } = JSON.parse(ev.data);
    setPhase(type); // 'thinking' | 'streaming' | 'done'
  };
}, []);

然后,让动画随状态自动启停:

// TypingDots.jsx
export default function TypingDots({ phase }) {
  return (
    <div className={`typing-dots ${phase === 'thinking' ? 'animate' : ''}`}>
      <span className="dot" />
      <span className="dot" />
      <span className="dot" />
    </div>
  );
}
.typing-dots.animate .dot {
  animation-play-state: running;
}
.typing-dots:not(.animate) .dot {
  animation: none;
  transform: scale(0); /* 一键下班 */
}

0x06 彩蛋:用 Canvas 把像素玩到飞起

如果你想让社畜像素加班更狠,可以用 OffscreenCanvas 在 Web Worker 里算轨迹:

// worker.js
const canvas = new OffscreenCanvas(60, 20);
const ctx = canvas.getContext('2d');

function drawDots(t) {
  ctx.clearRect(0, 0, 60, 20);
  for (let i = 0; i < 3; i++) {
    const offset = Math.sin(t * 0.006 + i * 0.5) * 6 + 10;
    ctx.beginPath();
    ctx.arc(10 + i * 20, offset, 4, 0, 2 * Math.PI);
    ctx.fillStyle = '#60a5fa';
    ctx.fill();
  }
  requestAnimationFrame(drawDots);
}
drawDots(0);

主线程每秒 60 次从 Worker 里把 ImageBitmap 捞回来,
像从鸽子笼里掏信一样,塞进 <canvas>

worker.onmessage = ({ data }) => {
  ctx.drawImage(data.bitmap, 0, 0);
};

这样做 CPU 偷了个懒,GPU 多跑两步,双赢(指老板)


0x07 尾声:当点点熄灭时

动画的最后一帧,别忘了让点点“温柔落地”:

.dot {
  transition: transform 0.2s ease-out, opacity 0.2s ease-out;
}

这样,当 AI 说完“以上就是我的看法”时,
三个点会像喝完咖啡的打工人一样,
轻轻缩回黑暗,留给你一个礼貌的沉默。


0x08 参考资料与八卦

最后,祝你的 TypingDots 早日拿到奥斯卡最佳配角,
毕竟,连 AI 都知道:
“观众只记住最后一帧的情绪。”