“别眨眼,否则你会错过一个像素的一生。”——某不知名前端段子手
0x00 开场白:为什么连“打字中的点点”都要卷?
在 AIGC 时代,每一次交互都是一次小型舞台剧。
当大模型在后台吭哧吭哧地推理,前端必须优雅地告诉用户:
“别急,我正在让硅基大脑跳踢踏舞。”
于是,TypingDots——那三个一跳一跳的小圆点——成了 UI 里最会抢戏的龙套演员。
今天,我们就把它的戏服扒光,看看里头到底藏了多少二进制灵魂。
0x01 硬件级冷知识:一个像素的一生
在 OLED 屏上,每个像素点其实是一位发光二极管社畜。
它从 rgb(0,0,0) 的沉睡中醒来,被 GPU 的电压叫醒,
在 16.67 ms(60Hz)里完成一次“发光→衰减→再发光”的 KPI。
所以,动画的本质,是让这些社畜像素按时打卡,别摸鱼。
0x02 需求拆解:SEE 框架
| 缩写 | 全称 | 翻译成人话 |
|---|---|---|
| S | Skeleton | 先画静态骨架(三个点) |
| E | Easing | 给动画加点“人情味”缓动 |
| E | Event-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 的回复分三段:
- Thinking(推理中)
- Streaming(流式吐字)
- 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 参考资料与八卦
- W3C CSS Animations Level 2:官方鬼画符
- Human Interface Guidelines — Apple:苹果教你别让用户焦虑
- Lottie vs. Pure CSS:一场矢量与像素的宫斗剧
最后,祝你的 TypingDots 早日拿到奥斯卡最佳配角,
毕竟,连 AI 都知道:
“观众只记住最后一帧的情绪。”