Remotion LabRemotion Lab
疑難排解不要在 Remotion 中使用 CSS 動畫

不要在 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 計時器(如 setTimeoutsetInterval)來驅動動畫

根本原因

Remotion 的渲染模式與瀏覽器的正常顯示模式根本不同

  1. 每幀獨立渲染:Remotion 不是按照時間順序播放影片,而是將每一幀獨立截圖。
  2. 渲染順序不保證:第 30 幀可能比第 10 幀先被渲染,第 50 幀可能被渲染兩次。
  3. 時間軸無關性:CSS 動畫依賴瀏覽器的時間軸(animation-durationtransition-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 1sinterpolate(frame, [0, 30], [0, 1])
transition: transform 0.3sspring({ frame, fps, config })
@keyframes bounce使用 spring() 搭配 useCurrentFrame()
setTimeout(() => setState(...), 1000)根據 frame >= fps * 1 判斷狀態

延伸閱讀