📚 Remotion シリーズ 全8回の記事一覧
- 第1回: 全体像をまとめる
Composition / AbsoluteFill / Sequence で動画の骨組みを作る - 第2回: Hooks を理解する
useCurrentFrame / useVideoConfig でフレーム番号と設定を取得する - ▶ 第3回: アニメーションを作る(この記事)
interpolate / spring でフレーム番号を CSS の値に変換する - 第4回: 素材を使う
画像・動画・音楽素材を組み込む - 第5回: テロップを入れる
テキストオーバーレイの作り方 - 第6回: シーンを構成する
Series / TransitionSeries でシーン切り替えとトランジション - 第7回: GitHub Actions で自動レンダリング
CI/CD で動画レンダリングを自動化する - 第8回: 応用テクニック(準備中)
GIF・静止画書き出し / FFmpeg カスタマイズ / Lambda / 3D
はじめに
前回の記事では、useCurrentFrame() と useVideoConfig() でフレーム番号や動画設定を取得し、それを interpolate や spring に渡してアニメーションを作る流れを見ました。
前回のまとめで書いた通り、アニメーションをつける流れはこうでした。
useCurrentFrame()でフレーム番号を取得springやinterpolateで styleで用いる変数の値を計算- CSS に反映
でも前回は HelloWorld.tsx の全体像を読み解くのが主眼だったので、interpolate や spring 自体の挙動にはあまり踏み込めていませんでした。
今回は、この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 よりずっと大きい値になってしまいます。ここでは範囲の両側を固定したいので extrapolateLeft と extrapolateRight の両方に "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, // バネの硬さ(大きいほど速く動く) }, });
spring は 0 から始まり、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フレームずつ遅延を加えて、順番にポップアップする効果を作っています。

damping: 200 は高めの値で、ほぼ揺れなくスッと出現する設定です。これを damping: 10 にすると、出現後にブルッと震えるような動きになります。
frame にマイナス値を渡したとき
spring に frame - 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度を等速回転 |
まとめ
今回紹介した interpolate と spring の違い。
| 関数 | 何をするか | 動きの特徴 |
|---|---|---|
interpolate |
数値を別の範囲にマッピング | 直線的(等速) |
spring |
バネ物理で 0→1 を生成 | 自然な加速・減速・揺れ |
使い分けはこれだけ。
- 等速で変化させたい →
interpolateだけ(回転、フェードなど) - 自然な動きにしたい →
springを使う(出現、移動など) - 自然な動き + 0→1 以外の範囲 →
spring+interpolateを組み合わせる
どちらも hook ではなくただの関数で、フレーム番号を入力として受け取り、数値を返すだけです。状態は持ちませんし、呼ぶ場所の制約もありません。
HelloWorld テンプレートのコードを見ても、結局は「フレーム番号 → 計算関数 → CSS」というワンパターンの繰り返しなんですよね。
サンプルアニメーションとソースコード
これを組み合わせると、以下みたいなアニメーションは簡単に作ることができます。
コンポーネント部分だけですが、ソースコードも Gist で一応置いておきます
Remotion の Spring と interpolate を使った例 · GitHub
次の第4回は、画像や動画素材の読み込み方を見ていきます!
