ismail c5f76b3855
Some checks are pending
Build and Push Docker Image / build (push) Waiting to run
updates
2026-05-11 14:45:30 +03:00

7.3 KiB
Raw Permalink Blame History

Animations时间轴动画引擎

做动画/motion design HTML时读这个。原理、用法、典型模式。

核心模式Stage + Sprite

我们的动画系统(assets/animations.jsx)提供一个时间轴驱动的引擎:

  • <Stage>整个动画的容器自动提供auto-scalefit viewport+ scrubber + play/pause/loop控制
  • <Sprite start end>时间片段。一个Sprite只在startend这段时间内显示。内部可以通过useSprite() hook读取自己的本地进度t (0→1)
  • useTime():读当前全局时间(秒)
  • Easing.easeInOut / Easing.easeOut / ...:缓动函数
  • interpolate(t, from, to, easing?)根据t插值

这套模式借鉴Remotion/After Effects思路但轻量、零依赖。

起手

<script type="text/babel" src="animations.jsx"></script>
<script type="text/babel">
  const { Stage, Sprite, useTime, useSprite, Easing, interpolate } = window.Animations;

  function Title() {
    const { t } = useSprite();  // 本地进度 0→1
    const opacity = interpolate(t, [0, 1], [0, 1], Easing.easeOut);
    const y = interpolate(t, [0, 1], [40, 0], Easing.easeOut);
    return (
      <h1 style={{ 
        opacity, 
        transform: `translateY(${y}px)`,
        fontSize: 120,
        fontWeight: 900,
      }}>
        Hello.
      </h1>
    );
  }

  function Scene() {
    return (
      <Stage duration={10}>  {/* 10秒动画 */}
        <Sprite start={0} end={3}>
          <Title />
        </Sprite>
        <Sprite start={2} end={5}>
          <SubTitle />
        </Sprite>
        {/* ... */}
      </Stage>
    );
  }

  const root = ReactDOM.createRoot(document.getElementById('root'));
  root.render(<Scene />);
</script>

常用动画模式

1. Fade In / Fade Out

function FadeIn({ children }) {
  const { t } = useSprite();
  const opacity = interpolate(t, [0, 0.3], [0, 1], Easing.easeOut);
  return <div style={{ opacity }}>{children}</div>;
}

注意范围[0, 0.3]意思是在sprite的前30%时间完成渐入后面保持opacity=1。

2. Slide In

function SlideIn({ children, from = 'left' }) {
  const { t } = useSprite();
  const progress = interpolate(t, [0, 0.4], [0, 1], Easing.easeOut);
  const offset = (1 - progress) * 100;
  const directions = {
    left: `translateX(-${offset}px)`,
    right: `translateX(${offset}px)`,
    top: `translateY(-${offset}px)`,
    bottom: `translateY(${offset}px)`,
  };
  return (
    <div style={{
      transform: directions[from],
      opacity: progress,
    }}>
      {children}
    </div>
  );
}

3. 逐字打字机

function Typewriter({ text }) {
  const { t } = useSprite();
  const charCount = Math.floor(text.length * Math.min(t * 2, 1));
  return <span>{text.slice(0, charCount)}</span>;
}

4. 数字计数

function CountUp({ from = 0, to = 100, duration = 0.6 }) {
  const { t } = useSprite();
  const progress = interpolate(t, [0, duration], [0, 1], Easing.easeOut);
  const value = Math.floor(from + (to - from) * progress);
  return <span>{value.toLocaleString()}</span>;
}

5. 分段解释(典型教学动画)

function Scene() {
  return (
    <Stage duration={20}>
      {/* Phase 1: 展示问题 */}
      <Sprite start={0} end={4}>
        <Problem />
      </Sprite>

      {/* Phase 2: 展示思路 */}
      <Sprite start={4} end={10}>
        <Approach />
      </Sprite>

      {/* Phase 3: 展示结果 */}
      <Sprite start={10} end={16}>
        <Result />
      </Sprite>

      {/* 全程显示的字幕 */}
      <Sprite start={0} end={20}>
        <Caption />
      </Sprite>
    </Stage>
  );
}

Easing函数

预设的easing curves

Easing 特性 用在
linear 匀速 滚动字幕、持续动画
easeIn 慢→快 退场消失
easeOut 快→慢 入场出现
easeInOut 慢→快→慢 位置变化
expoOut 指数缓出 Anthropic 级主 easing(物理重量感)
overshoot 弹性回弹 Toggle / 按钮弹出 / 强调交互
spring 弹簧 交互反馈、几何体归位
anticipation 先反向再正向 强调动作

默认主 easing 用 expoOut(不是 easeOut)—— 见 animation-best-practices.md §2。 入场用 expoOut、出场用 easeIn、toggle 用 overshoot——Anthropic 级动画的基础规律。

节奏和时长指南

微交互0.1-0.3秒)

  • 按钮hover
  • 卡片expand
  • Tooltip出现

UI过渡0.3-0.8秒)

  • 页面切换
  • 模态框出现
  • 列表item加入

叙事动画2-10秒每段

  • 概念解释的一个phase
  • 数据图表的reveal
  • 场景转换

单段叙事动画最长不超过10秒

人类注意力有限。10秒讲一件事讲完换下一件。

设计动画的思考顺序

1. 先有内容/故事,再有动画

错误先想要做fancy动画再塞内容进去 正确先想清楚要传达什么信息再用动画手段serve这个信息

动画是signal,不是装饰。一个fade-in强调的是"这里很重要,请看"——如果什么都fade-insignal就失效。

2. 分Scene写时间轴

0:00 - 0:03   问题出现fade in
0:03 - 0:06   问题放大/展开zoom+pan
0:06 - 0:09   解法出现slide in from right
0:09 - 0:12   解法展开说明typewriter
0:12 - 0:15   结果演示counter up + chart reveal
0:15 - 0:18   总结一句话static读3秒
0:18 - 0:20   CTA或fade out

写完时间轴再写组件。

3. 资源先行

动画要用的图片/图标/字体准备好。不要画到一半去找素材——打断节奏。

常见问题

动画卡顿 → 主要是layout thrashing。用transformopacity,不要动top/left/width/height/margin。浏览器GPU加速transform

动画太快,看不清楚 → 人读一个汉字需要100-150ms一个词300-500ms。如果你用文字讲故事单句至少留3秒。

动画太慢,观众无聊 → 有趣的视觉变化要密集。静态画面超过5秒就会闷。

多个动画互相影响 → 用CSS的will-change: transform提前告诉浏览器这个元素会动减少reflow。

录制成视频 → 用 skill 自带工具链(一条命令出三种格式):见 video-export.md

  • scripts/render-video.js — HTML → 25fps MP4Playwright + ffmpeg
  • scripts/convert-formats.sh — 25fps MP4 → 60fps MP4 + 优化 GIF
  • 想要更精确的帧渲染?让 render(t) 成为 pure functionanimation-pitfalls.md 第 5 条

和视频工具的配合

这个skill做的是HTML动画(在浏览器里跑的)。如果最终产出要作为视频素材:

  • 短动画/concept demo用这里的方法做HTML动画 → 屏幕录制
  • 长视频/叙事:本 skill 专注 HTML 动画,长视频用 AI 视频生成类 skill 或专业视频软件
  • motion graphics专业的After Effects/Motion Canvas更合适

关于Popmotion等库

如果你真的需要物理动画spring、decay、keyframes with precise timing我们的engine搞不定可以fallback到Popmotion

<script src="https://unpkg.com/popmotion@11.0.5/dist/popmotion.min.js"></script>

先试试我们的engine。90%的情况够用。