Remotion LabRemotion Lab
動畫庫整合Lottie 整合:把 After Effects 動畫直接搬進 Remotion
lottieintegrationdelayRender

Lottie 整合:把 After Effects 動畫直接搬進 Remotion

設計師用 Bodymovin 從 After Effects 匯出 Lottie JSON,開發者用 @remotion/lottie 一行渲染進影片——前提是你要先搞懂 fetch + delayRender 這對組合,不然 render 出來會是空白。

成品預覽

6 秒、單一場景的 Lottie 示範。畫面中央播放一個 loading spinner,這個動畫是直接從 lottiefiles.com 下載的 .json 檔——沒有任何手刻 SVG、沒有用 interpolate 對 path 做動畫,整段動畫的「靈魂」全部都裝在那份 JSON 裡。Remotion 只負責把它放到正確的位置、給它一個進場的縮放與淡入。

這就是 Lottie 整合最迷人的地方:設計師在 After Effects 花一小時調好的動畫,工程師用一個元件就接進來播。沒有「請工程師重刻」這道翻譯成本。


為什麼要用 Lottie?

如果你曾經試過「請工程師用 SVG + interpolate() 把設計稿的動畫重刻一遍」,大概知道那是個多痛苦的過程。設計師花 30 分鐘在 After Effects 裡調的 easing 曲線,工程師可能要花一整天才能逼近——還不一定對得起來。

Lottie 就是為了解決這件事而存在:

  • 設計師熟的工具不是 React。他們熟的是 After Effects、Figma。逼他們學 React 不切實際,逼工程師讀懂 AE 的 keyframe 也一樣不切實際。
  • Lottie 是「動畫的中繼格式」。它把複雜的向量動畫——路徑、形狀、變形、easing、遮罩——打包成一份可跨平台播放的 JSON 檔案。
  • 開發者拿到檔案,一行程式碼就能嵌入。你不需要知道裡面有幾條 path、幾個 keyframe,只要把 JSON 餵給 <Lottie /> 元件,它就會原汁原味播出來。
  • 設計師改了動畫?重新匯出蓋過去就好,程式碼一行不用動。

對 Remotion 影片產線來說,這個整合能力等於是把「動畫設計」這個職能完整接進來——不再受限於工程師會不會手刻動畫。


這篇會用到

  • @remotion/lottie — 官方 Lottie 元件,把 Lottie JSON 動畫渲染進影片
  • fetch + useEffect — 載入 Lottie JSON
  • delayRender / continueRender — 確保 fetch 完成後 Remotion 才開始截圖

核心一句話:Lottie 元件吃的不是檔案路徑,是 parse 好的 JS 物件。理解了這件事,後面所有的眉角都會變直觀。


前置知識

如果你還不熟 Remotion 的 useCurrentFrame / interpolate / Sequence,建議先看 T3:YouTube 片頭動畫。本篇雖然動畫主體交給 Lottie 處理,但容器的進場、縮放、淡入仍然會用到 Remotion 的標準 hook。


Step 1:安裝套件

npm install @remotion/lottie

版本號最好跟你的 remotion 主套件對齊(例如都用 4.0.x),避免 peer dependency 衝突。


Step 2:準備 Lottie 檔案

你有兩個來源可以拿到 Lottie JSON:

  1. lottiefiles.com — 社群網站上有大量免費 / 付費的 Lottie 動畫,下載 .json 檔即可
  2. 設計師從 After Effects 匯出 — 在 AE 裡安裝 Bodymovin 外掛,做好動畫後選擇「Render」匯出成 JSON

下載好的檔案放到 Remotion 專案的 public/ 資料夾下:

public/
└── animations/
    └── loading-spinner.json

public/ 是 Remotion 約定的靜態檔案資料夾,裡面的東西可以用 staticFile() 取得 URL。


Step 3:載入 Lottie JSON(會踩到的第一個坑)

這裡有個容易踩坑的地方:@remotion/lottie<Lottie /> 元件需要的不是「檔案路徑」,而是「已經 parse 好的 JS 物件」。所以你必須自己 fetch 這份 JSON 然後 .json() 解析它:

import {Lottie, type LottieAnimationData} from '@remotion/lottie';
import {staticFile} from 'remotion';
import {useEffect, useState} from 'react';
 
export const LottieScene: React.FC = () => {
  const [animationData, setAnimationData] =
    useState<LottieAnimationData | null>(null);
 
  useEffect(() => {
    fetch(staticFile('animations/loading-spinner.json'))
      .then((r) => r.json())
      .then((data: LottieAnimationData) => setAnimationData(data))
      .catch(() => setAnimationData(null));
  }, []);
 
  if (!animationData) {
    return null;
  }
 
  return (
    <Lottie
      animationData={animationData}
      loop
      playbackRate={1}
      style={{width: 600, height: 500}}
    />
  );
};

這段在 Remotion Studio 的 preview 裡看起來會正常運作——你會看到 spinner 播出來。但是如果直接拿去 npx remotion render,你會踩到一個非常經典的 bug:影片裡那一段是空的。原因下一節解釋。


Step 4:delayRender / continueRender 防止提早截圖

Remotion 的 render 流程是「frame by frame 截圖」,而且 render 進程會盡可能快。你的 useEffectfetch 還沒回來,render 進程就已經把這幀截圖存出去了——結果就是空白。

解法是用 delayRender() 告訴 Remotion「等等!我這裡有非同步資源沒載完」,等資源好了再呼叫 continueRender() 放行:

import {delayRender, continueRender, staticFile} from 'remotion';
import {Lottie, type LottieAnimationData} from '@remotion/lottie';
import {useEffect, useState} from 'react';
 
export const LottieScene: React.FC = () => {
  const [handle] = useState(() => delayRender('Loading Lottie JSON'));
  const [animationData, setAnimationData] =
    useState<LottieAnimationData | null>(null);
 
  useEffect(() => {
    fetch(staticFile('animations/loading-spinner.json'))
      .then((r) => r.json())
      .then((data: LottieAnimationData) => {
        setAnimationData(data);
        continueRender(handle);
      })
      .catch((err) => {
        // 失敗時也要 continueRender,否則整個 render 卡在這幀逾時
        console.error(err);
        continueRender(handle);
      });
  }, [handle]);
 
  if (!animationData) {
    return null;
  }
 
  return <Lottie animationData={animationData} loop playbackRate={1} />;
};

幾個重點:

  • delayRender() 必須在元件首次 render 就呼叫,所以用 useState(() => ...) 包起來確保只跑一次。直接 delayRender() 寫在元件 body 裡會在每次 re-render 都建立新的 handle,完全炸掉。
  • 第一個參數是個可讀的 label,render 失敗時 Remotion 會在 log 顯示「Loading Lottie JSON timed out」之類的訊息,方便你 debug。
  • continueRender(handle) 不管成功或失敗都一定要呼叫——失敗時也應該呼叫(或加上 catch 然後手動 continueRender),不然整個 render 會卡在這幀逾時。
  • 預設逾時是 30 秒,可以在 Composition 上用 timeoutInMilliseconds 調整。

加上 delayRender 之後,render 出來的影片就會正確播放 Lottie 動畫了。


Step 5:把 Lottie 包進進場動畫

Lottie 本身已經會自己動,但你通常還是想要它「淡入 + 縮放」進場,融入整支影片的節奏。範例的做法是把 <Lottie /> 包在一個外層 <div> 裡,外層用 Remotion 的 interpolateopacityscale

import {AbsoluteFill, interpolate, useCurrentFrame} from 'remotion';
 
const LottieShowcase: React.FC = () => {
  // ... animationData 載入邏輯同 Step 4 ...
 
  const frame = useCurrentFrame();
  const scale = interpolate(frame, [10, 50], [0.5, 1], {
    extrapolateLeft: 'clamp',
    extrapolateRight: 'clamp',
  });
  const opacity = interpolate(frame, [10, 35], [0, 1], {
    extrapolateLeft: 'clamp',
    extrapolateRight: 'clamp',
  });
 
  return (
    <div
      style={{
        width: 760,
        height: 760,
        opacity,
        transform: `scale(${scale})`,
        transformOrigin: 'center center',
        filter: 'drop-shadow(0 0 80px rgba(59,130,246,0.6))',
      }}
    >
      {animationData ? (
        <Lottie animationData={animationData} loop playbackRate={1} />
      ) : null}
    </div>
  );
};

這個分工很重要:

  • 內層 <Lottie /> 只負責播動畫——它有自己的時間軸(loop spinner 會永遠循環)
  • 外層 <div> 負責「Remotion 時間軸的進場」——用 useCurrentFrame 算 transform

兩層責任分清楚之後,你可以隨時在外層加上更多進場效果(旋轉、位移、blur),不會影響 Lottie 自己的動畫節奏。


Step 6:渲染

npx remotion render TutorialLottieDemo out/lottie.mp4 --codec=h264

如果 render 在 Lottie 那段卡住或逾時,回頭檢查 Step 4 的 delayRender 有沒有正確呼叫 continueRender。99% 的問題都在這裡。


原理解析:為什麼 Lottie 要 fetch?

很多人第一次用 @remotion/lottie 都會問:「為什麼不能直接給檔案路徑?」

答案藏在底層的 lottie-web 設計裡:

  • @remotion/lottie<Lottie /> 元件需要 animationData prop,型別是 LottieAnimationData,本質是一個 JS 物件
  • 這個設計是因為 lottie-web(瀏覽器端的 Lottie 渲染器)內部本來就是吃解析好的物件。Remotion 的封裝只是照原本 API 設計,沒有額外幫你做 fetch
  • 為什麼不偷懶幫使用者 fetch? 因為 Remotion 想保持 prop 是「同步且純」的——這樣比較容易做 server-side render、跨 worker 共享、frame 一致性測試⋯⋯等等。你自己 fetch + delayRender 雖然多寫幾行,但行為很明確、可預測

理解了這個設計哲學之後,你會發現很多 Remotion 套件都遵循同樣的模式:有任何「需要等」的東西,使用者要主動告訴 Remotion,而不是讓 Remotion 偷偷處理。這個哲學在 video / audio / image 預載也都一樣。


原理解析:delayRender 的兩個面向

delayRender 同時做兩件事:

  1. 告訴 render 進程「先別截圖」:每呼叫一次 delayRender() 就會回傳一個 handle,render 進程內部維護一個「待解決的 handle 數量」,全部解決前不會把這幀視為完成
  2. 產生一個逾時計時器:30 秒內必須有對應的 continueRender(handle),不然整個 render 會炸掉 throw delayRender timed out 錯誤

這個機制不只是給 Lottie 用。任何「需要非同步載入」的場景都用它:

  • 載入字型
  • 預先 fetch API 資料
  • 載入圖片並等它 decode
  • 把外部 SVG 載進來解析

下次你寫 Remotion 元件遇到「Studio 預覽正常但 render 出來空白 / 缺東西」,第一件事永遠是回頭找:哪裡有非同步邏輯沒套 delayRender。


常見問題

Q: Lottie 檔播一段就停了,後面變靜止?

<Lottie /> 元件有個 loop prop 預設是 true,所以理論上會無限循環。但你的 Remotion CompositiondurationInFrames 才是真正的影片長度——如果 Lottie 動畫本身是 2 秒,但你的 Sequence 是 6 秒,後面 4 秒會繼續循環播放第 1 秒的內容,這是預期行為。如果你不想 loop,把 loop 設成 false 並調整 Sequence 長度匹配 Lottie 原本的時長。

Q: 檔案在 Remotion Studio preview 正常,但 render 時 crash 或畫面空白?

99% 是 Lottie 那段沒加 delayRender。Studio 的 preview 是「等 React 安定再截圖」的模式,很寬容;render CLI 則是「frame 一個一個快速截」,不會等你的 useEffect。回頭把 Step 4 的 delayRender / continueRender 補上即可。

Q: Lottie 檔很大(幾 MB),render 變慢?

每個 frame 都要算 Lottie 的向量繪製,檔案越複雜越吃 CPU。可以考慮:

  • 請設計師簡化路徑、減少 keyframe
  • 改用 Rive(二進位格式 + GPU 加速通常更快)
  • 把 Lottie 那段拉成獨立的 Composition,先 pre-render 成 mp4,再用 <Video /> 嵌進主影片

Q: playbackRate 可以動態調嗎?

可以,但要注意它不接受 useCurrentFrame() 算出來的值動態傳入<Lottie /> 內部對 playbackRate 是「整段時間軸的縮放係數」,不是「每幀的播放速度」。如果你想做「前 2 秒慢放、後面快放」這種效果,正確做法是把 Lottie 那段拆成多個 <Sequence>,每段給不同的固定 playbackRate

Q: 想要 Lottie 動畫跟著 Remotion frame 同步,不要它自己跑?

@remotion/lottie 提供 playbackRate 但沒有暴露「跳到第 N 幀」的 API。如果你需要這種精細控制,下一步就是研究 Rive——Rive runtime 讓你直接 advance(dt) 推進時間,可以從 useCurrentFrame() 完全控制動畫進度。


本篇涵蓋的官方文件


下一步

Rive State Machine:用 Remotion frame 驅動互動動畫 — Lottie 是「純時間軸動畫」的代表:設計師決定動畫怎麼跑,工程師按下播放就好。下一篇要進到「互動式動畫」的世界:Rive state machine。設計師在 Rive editor 裡定義「閒置 → 點擊 → 跳躍」這類狀態轉換,並且暴露 inputs(boolean / number)。Remotion 端用 useCurrentFrame() 算出值丟進去,畫面上的角色就會即時做出對應反應——這是 Lottie 永遠做不到的事。