不要在 Remotion 中使用 CSS 動畫
說明為何 CSS animation、transition 和 JavaScript 計時器在 Remotion 中無法正常運作,以及如何改用 useCurrentFrame 驅動動畫
問題說明
在 Remotion 中,下列寫法是無法正常運作的:
// 不要這樣做
const myMarkup = (
<div
style={{
animation: "fadeIn 1s forwards",
}}
>
Hello World!
</div>
);同樣,以下做法在 Remotion 中也是錯誤的:
- 使用 CSS
transition屬性 - 使用 CSS
@keyframes規則 - 使用 JavaScript 計時器(如
setTimeout、setInterval)來驅動動畫
根本原因
Remotion 的渲染模式與瀏覽器的正常顯示模式根本不同:
- 每幀獨立渲染:Remotion 不是按照時間順序播放影片,而是將每一幀獨立截圖。
- 渲染順序不保證:第 30 幀可能比第 10 幀先被渲染,第 50 幀可能被渲染兩次。
- 時間軸無關性:CSS 動畫依賴瀏覽器的時間軸(
animation-duration、transition-duration等),而 Remotion 的渲染過程中這些時間軸不會以正確的方式推進。
因此,當 CSS 動畫不知道 Remotion 目前正在渲染哪一幀時,你會看到:
- 渲染過程中出現閃爍或空白幀
- 動畫進度不正確(每幀的動畫狀態錯誤)
- 最終影片的動畫不連貫或跳幀
解決方案
使用 useCurrentFrame() 驅動所有動畫
將所有動畫狀態都從幀號碼推導出來,而不是依賴時間軸。這樣動畫狀態是決定性的(deterministic),確保相同的幀號碼永遠產生相同的畫面。
以下是一個使用 interpolate() 實作淡入效果的範例:
import { AbsoluteFill, interpolate, useCurrentFrame } from "remotion";
export const FadeIn = () => {
const frame = useCurrentFrame();
// 將幀 0~30 對應到透明度 0~1
const opacity = interpolate(frame, [0, 30], [0, 1], {
extrapolateRight: "clamp",
});
return (
<AbsoluteFill
style={{
justifyContent: "center",
alignItems: "center",
backgroundColor: "white",
fontSize: 80,
}}
>
<div style={{ opacity }}>Hello World!</div>
</AbsoluteFill>
);
};移動動畫的實作範例
import { interpolate, useCurrentFrame } from "remotion";
export const SlideIn = () => {
const frame = useCurrentFrame();
// 幀 0~20 從左側 -200px 移動到 0px
const translateX = interpolate(frame, [0, 20], [-200, 0], {
extrapolateRight: "clamp",
});
return (
<div
style={{
transform: `translateX(${translateX}px)`,
fontSize: 60,
color: "black",
}}
>
Slide In Text
</div>
);
};取代 CSS transition 的做法
// 不要這樣做(依賴 CSS transition)
// style={{ transition: "opacity 0.5s", opacity: isVisible ? 1 : 0 }}
// 改成這樣(使用 useCurrentFrame)
const frame = useCurrentFrame();
const opacity = interpolate(frame, [0, 15], [0, 1], {
extrapolateRight: "clamp",
});
// style={{ opacity }}常見動畫模式對照表
| 原本的做法 | Remotion 的正確做法 |
|---|---|
animation: fadeIn 1s | interpolate(frame, [0, 30], [0, 1]) |
transition: transform 0.3s | spring({ frame, fps, config }) |
@keyframes bounce | 使用 spring() 搭配 useCurrentFrame() |
setTimeout(() => setState(...), 1000) | 根據 frame >= fps * 1 判斷狀態 |