以下の内容はhttps://www.randpy.tokyo/entry/remotion-3-animationより取得しました。


Remotion でアニメーション作成: interpolate と spring でフレーム番号を「動き」に変える【Remotion 第3回】

youtu.be

📚 Remotion シリーズ 全8回の記事一覧

  1. 第1回: 全体像をまとめる
    Composition / AbsoluteFill / Sequence で動画の骨組みを作る
  2. 第2回: Hooks を理解する
    useCurrentFrame / useVideoConfig でフレーム番号と設定を取得する
  3. ▶ 第3回: アニメーションを作る(この記事)
    interpolate / spring でフレーム番号を CSS の値に変換する
  4. 第4回: 素材を使う
    画像・動画・音楽素材を組み込む
  5. 第5回: テロップを入れる
    テキストオーバーレイの作り方
  6. 第6回: シーンを構成する
    Series / TransitionSeries でシーン切り替えとトランジション
  7. 第7回: GitHub Actions で自動レンダリング
    CI/CD で動画レンダリングを自動化する
  8. 第8回: 応用テクニック(準備中)
    GIF・静止画書き出し / FFmpeg カスタマイズ / Lambda / 3D

はじめに

前回の記事では、useCurrentFrame()useVideoConfig() でフレーム番号や動画設定を取得し、それを interpolatespring に渡してアニメーションを作る流れを見ました。

www.randpy.tokyo

前回のまとめで書いた通り、アニメーションをつける流れはこうでした。

  1. useCurrentFrame() でフレーム番号を取得
  2. springinterpolate で styleで用いる変数の値を計算
  3. CSS に反映

でも前回は HelloWorld.tsx の全体像を読み解くのが主眼だったので、interpolatespring 自体の挙動にはあまり踏み込めていませんでした。 今回は、この2つの関数を、色々なパターンを紹介しながらもう少し詳しく掘り下げていきます。

interpolate をもう少し詳しく

interpolate おさらい

前回も紹介しましたが、interpolate は「ある範囲の数値を、別の範囲にマッピングする」関数です。Python でいうと numpy.interp と同じです。

interpolate(入力値, [入力の始点, 入力の終点], [出力の始点, 出力の終点])
const opacity = interpolate(frame, [0, 30], [0, 1]);
// frame=0 であれば opacity = 0、frame=15 であれば opacity = 0.5 のように変換

やっていることは、入力の「どこにいるか(0%〜100%)」を出力の範囲に当てはめているだけです。ここまでは前回やりました。

ここからは、前回触れなかった 範囲外の挙動実例での使い分け を見ていきます。

実例:Subtitle のフェードイン

src/HelloWorld/Subtitle.tsx の実装です。

// src/HelloWorld/Subtitle.tsx
export const Subtitle: React.FC = () => {
  const frame = useCurrentFrame();
  const opacity = interpolate(frame, [0, 30], [0, 1]);
  return <div style={{ opacity }}>...</div>;
};

useCurrentFrame() は Sequence の開始を 0 として返します。 この Subtitle は <Sequence from={75}> の中に置かれているので、動画全体の75フレーム目が この中では frame=0 になるわけです。

動画全体:  0 ---- 75 --- 105 --- 150
Subtitle:        f=0    f=30    f=75
opacity:         0   →→  1      2.5
                         ↑ 30フレームかけてフェードイン

frame=30 以降も interpolate は計算を続けるので、frame=75 では 75/30 = 2.5 が返ります。ただし、CSS の opacity はブラウザが 0〜1 におさめてくれるので 見た目上は 1 と変わりません

今回はたまたま問題になりませんが、これが position や scale だったらレイアウトが崩れますよね。

実例:HelloWorld.tsx のフェードアウト(clamp)

このように、interpolate は範囲外の入力もそのまま計算してしまいます。明示的に範囲を制限したいときは、clamp を指定します。

src/HelloWorld.tsx のフェードアウトがまさにこれです。動画の最後の10フレームで全体を透明にしつつ、それ以外の区間では値を固定しています。

// src/HelloWorld.tsx のフェードアウト部分
const opacity = interpolate(
  frame,
  [durationInFrames - 25, durationInFrames - 15],  // [125, 135]
  [1, 0],
  {
    extrapolateLeft: "clamp",   // frame < 125 なら 1 で固定
    extrapolateRight: "clamp",  // frame > 135 なら 0 で固定
  }
);
frame:    0 -- 125 - 135 - 150
opacity:  1    1  →  0     0

clamp がないと、frame=0 の時点で opacity が 1 よりずっと大きい値になってしまいます。ここでは範囲の両側を固定したいので extrapolateLeftextrapolateRight の両方に "clamp" を指定しています。

実例:Logo の回転

src/HelloWorld/Logo.tsx では、interpolate で動画全体を通じた回転を作っています。

const logoRotation = interpolate(
  frame,
  [0, videoConfig.durationInFrames],  // [0, 150]
  [0, 360],                            // 0度 → 360度
);

// CSS に適用
style={{ transform: `scale(${scale}) rotate(${logoRotation}deg)` }}

150フレームで1回転ですね。等速回転なので interpolate(線形マッピング)だけで実現できます。

spring をもう少し詳しく

前回、spring は「バネの物理シミュレーションで 0→1 を生成する関数」と紹介しました。damping を上げるとバウンスが減ることも触れました。

ここでは、パラメータの使い分けや、テンプレートでの実践的な使い方を掘り下げます。

なぜ interpolate だけでは足りないのか

interpolate は直線的な変化しか作れません。「ポンと出現して少し行き過ぎて戻る」ような自然な動きには、バネの物理シミュレーションで曲線を生成する spring を使います。

パラメータを詳しく見る

前回は damping だけ紹介しましたが、spring には他にもパラメータがあります。

const value = spring({
  frame,       // 現在のフレーム番号
  fps,         // フレームレート(1秒あたりのフレーム数)
  config: {
    damping: 100,  // 減衰(大きいほど揺れが少ない)
    mass: 1,       // 質量(大きいほどゆっくり動く)
    stiffness: 100, // バネの硬さ(大きいほど速く動く)
  },
});

spring0 から始まり、1 に向かって収束する値 を返します。途中の軌道がバネの動きになるんですね。

パラメータ 意味 デフォルト
damping 減衰。大きいとバウンスが消える 10
mass 質量。大きいとゆっくり動き出す 1
stiffness 大きいほど反発が強くなり、動きがキビキビ&跳ねやすくなる 100

ただし実際に使う上では物理の詳細を理解する必要はありません。damping を大きくすれば揺れが減る、mass を大きくすればゆっくりになる、くらいの感覚で十分です。

あとは、実際に値を変えてみて「この動きが欲しい」と思う動きに近づけていくのが良さそうです。

実例:Title の単語が順番に出現

src/HelloWorld/Title.tsx が面白い使い方をしています。

const words = titleText.split(" ");  // "Welcome to Remotion" → ["Welcome", "to", "Remotion"]

return (
  <h1>
    {words.map((t, i) => {
      const delay = i * 5;  // 単語ごとに5フレームずつ遅延

      const scale = spring({
        fps: videoConfig.fps,
        frame: frame - delay,  // ← 遅延分を引く
        config: { damping: 200 },
      });

      return <span style={{ transform: `scale(${scale})` }}>{t}</span>;
    })}
  </h1>
);

ポイントは frame - delay です。各単語(Welcome, to, Remotion)に5フレームずつ遅延を加えて、順番にポップアップする効果を作っています。

Remotionで単語ごとに登場するタイミングが変わるイメージ
単語ごとに登場するタイミングが変わるイメージ

damping: 200 は高めの値で、ほぼ揺れなくスッと出現する設定です。これを damping: 10 にすると、出現後にブルッと震えるような動きになります。

frame にマイナス値を渡したとき

springframe - delay でマイナス値が渡されると、0 が返ります(まだ動き始めていない状態)。なので、delay を使って「まだ始まっていない」状態を自然に表現できるわけですね。

spring + interpolate の組み合わせ

前回の HelloWorld.tsx の読み解きで、spring の出力を interpolate に渡すパターンが出てきました。ここではその仕組みをもう少し分解して見てみます。

spring は常に 0→1 の値を返します。でも実際のアニメーションでは「0px → -150px に移動」とか「1 → 0 にフェードアウト」とか、別の範囲の値が欲しいですよね。そこで spring の出力を interpolate の入力に使う という2段構成になります。最初は「なんで2段階?」と思ったのですが、分解して見ると「なるほど」となりました。

改めて src/HelloWorld.tsx の例です。

// ① spring で 0→1 のバネ曲線を作る(25フレーム目から開始)
const logoTranslationProgress = spring({
  frame: frame - 25,  // 25フレーム目から開始
  fps,
  config: { damping: 100 },
});

// ② その 0→1 を interpolate で 0→-150 にマッピング
const logoTranslation = interpolate(
  logoTranslationProgress,  // ← spring の出力が入力になる
  [0, 1],
  [0, -150],
);

// ③ CSS に適用
style={{ transform: `translateY(${logoTranslation}px)` }}

この2段構成がパターンになっています。

  • spring で「動きの曲線(タイミング)」を作ります
  • interpolate で「その曲線をどの値の範囲に適用するか」を決めます

spring 単体でも scale(0→1)のような用途ならそのまま使えますが、移動量や角度のように 0→1 以外の範囲が必要なときは interpolate と組み合わせます。

Logo.tsx:すべてを組み合わせた例

最後に src/HelloWorld/Logo.tsx を見てみると、ここまでの内容がすべて詰まっていて、良い復習になります。

export const Logo: React.FC<...> = ({ logoColor1, logoColor2 }) => {
  const videoConfig = useVideoConfig();
  const frame = useCurrentFrame();

  // ① spring で出現アニメーション(0→1)
  const development = spring({
    config: { damping: 100, mass: 0.5 },
    fps: videoConfig.fps,
    frame,
  });

  // ② spring でスケールアニメーション(0→1)
  const scale = spring({
    frame,
    config: { mass: 0.5 },
    fps: videoConfig.fps,
  });

  // ③ interpolate で回転(0度→360度、等速)
  const logoRotation = interpolate(
    frame,
    [0, videoConfig.durationInFrames],
    [0, 360],
  );

  // ④ CSS に全部適用
  return (
    <AbsoluteFill
      style={{
        transform: `scale(${scale}) rotate(${logoRotation}deg)`,
      }}
    >
      <Arc progress={development} ... />
      <Arc progress={development} ... />
      <Arc progress={development} ... />
      <Atom scale={rotationDevelopment} ... />
    </AbsoluteFill>
  );
};

1つのコンポーネントで spring と interpolate を複数使って、それぞれ異なる CSS プロパティを制御しています。パターンさえ分かれば、何を制御しているか読めるようになりますね。

変数 関数 何を制御 動き
development spring Arc の描画進行 バネ的に展開
scale spring 全体のスケール 0→1 にバネ的に拡大
logoRotation interpolate 全体の回転 0→360度を等速回転

まとめ

今回紹介した interpolatespring の違い。

関数 何をするか 動きの特徴
interpolate 数値を別の範囲にマッピング 直線的(等速)
spring バネ物理で 0→1 を生成 自然な加速・減速・揺れ

使い分けはこれだけ。

  • 等速で変化させたいinterpolate だけ(回転、フェードなど)
  • 自然な動きにしたいspring を使う(出現、移動など)
  • 自然な動き + 0→1 以外の範囲spring + interpolate を組み合わせる

どちらも hook ではなくただの関数で、フレーム番号を入力として受け取り、数値を返すだけです。状態は持ちませんし、呼ぶ場所の制約もありません。

HelloWorld テンプレートのコードを見ても、結局は「フレーム番号 → 計算関数 → CSS」というワンパターンの繰り返しなんですよね。

サンプルアニメーションとソースコード

これを組み合わせると、以下みたいなアニメーションは簡単に作ることができます。

youtu.be

コンポーネント部分だけですが、ソースコードも Gist で一応置いておきます

Remotion の Spring と interpolate を使った例 · GitHub



次の第4回は、画像や動画素材の読み込み方を見ていきます!

www.randpy.tokyo




以上の内容はhttps://www.randpy.tokyo/entry/remotion-3-animationより取得しました。
このページはhttp://font.textar.tv/のウェブフォントを使用してます

不具合報告/要望等はこちらへお願いします。
モバイルやる夫Viewer Ver0.14