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 JSONdelayRender/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:
- lottiefiles.com — 社群網站上有大量免費 / 付費的 Lottie 動畫,下載
.json檔即可 - 設計師從 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 進程會盡可能快。你的 useEffect 裡 fetch 還沒回來,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 JSONtimed out」之類的訊息,方便你 debug。 continueRender(handle)不管成功或失敗都一定要呼叫——失敗時也應該呼叫(或加上catch然後手動continueRender),不然整個 render 會卡在這幀逾時。- 預設逾時是 30 秒,可以在
Composition上用timeoutInMilliseconds調整。
加上 delayRender 之後,render 出來的影片就會正確播放 Lottie 動畫了。
Step 5:把 Lottie 包進進場動畫
Lottie 本身已經會自己動,但你通常還是想要它「淡入 + 縮放」進場,融入整支影片的節奏。範例的做法是把 <Lottie /> 包在一個外層 <div> 裡,外層用 Remotion 的 interpolate 算 opacity 跟 scale:
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 />元件需要animationDataprop,型別是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 同時做兩件事:
- 告訴 render 進程「先別截圖」:每呼叫一次
delayRender()就會回傳一個 handle,render 進程內部維護一個「待解決的 handle 數量」,全部解決前不會把這幀視為完成 - 產生一個逾時計時器:30 秒內必須有對應的
continueRender(handle),不然整個 render 會炸掉 throwdelayRender timed out錯誤
這個機制不只是給 Lottie 用。任何「需要非同步載入」的場景都用它:
- 載入字型
- 預先 fetch API 資料
- 載入圖片並等它 decode
- 把外部 SVG 載進來解析
下次你寫 Remotion 元件遇到「Studio 預覽正常但 render 出來空白 / 缺東西」,第一件事永遠是回頭找:哪裡有非同步邏輯沒套 delayRender。
常見問題
Q: Lottie 檔播一段就停了,後面變靜止?
<Lottie /> 元件有個 loop prop 預設是 true,所以理論上會無限循環。但你的 Remotion Composition 的 durationInFrames 才是真正的影片長度——如果 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 永遠做不到的事。