- はじめに
- Remotion が提供する Hooks
- HelloWorld.tsx を読み解く
- HelloWorld のアニメーション:spring と interpolate の組み合わせ
- 子コンポーネントでの hook の使い方
- Sequence と hook の関係
- まとめ
📚 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
はじめに
前回の記事では、Remotion の基本的な仕組み(Composition、AbsoluteFill、Sequence)をまとめました。
簡単におさらいすると、以下のイメージです。
- Composition — 動画の設定(解像度、fps、長さ)を定義する
- AbsoluteFill — 要素の位置(WHERE)を制御する
- Sequence — 要素の表示タイミング(WHEN)を制御する
これらは動画の「骨組み」や「配置」を決めるものでした。しかし、実際のアニメーション(フェードイン、スライド、拡大縮小など)を作るには、フレームごとに値を変化させる仕組みが必要です。
それが今回のテーマである 「Hooks」 です。
この記事では、React Hooks の基本は理解している前提で、Remotion 固有の Hooks がどう使われているか、私が学んだことをまとめます。
Remotion が提供する Hooks
Hello World テンプレートで使われている hook は2つだけです。
// src/HelloWorld.tsx const frame = useCurrentFrame(); const { durationInFrames, fps } = useVideoConfig();
| hook | 返すもの |
|---|---|
useCurrentFrame() |
今のフレーム番号(0, 1, 2, ... 149) |
useVideoConfig() |
動画の設定(fps, durationInFrames, width, height など) |
フレームとは?
フレームは動画の 1コマ1コマ のことです。動画はパラパラ漫画のように、静止画を高速で切り替えて動いているように見せています。
// Root.tsx <Composition id="HelloWorld" component={HelloWorld} durationInFrames={150} fps={30} // 省略 />
HelloWorld プロジェクトでは、Root.tsx で以下のように設定されています。(前回の記事も参照してください)
durationInFrames={150}— 全部で150コマfps={30}— 1秒あたり30コマ
つまり、150 ÷ 30 = 5秒の動画 です。
Remotion の仕組み
Remotion は内部で、フレーム番号を 0 → 1 → 2 → ... → 149 と変化させながら、全コンポーネントを繰り返し実行します。useCurrentFrame() の返り値もそれに合わせて変化するので、「フレームごとに見た目を変える」プログラムを書くことでアニメーションが実現できます。
Frame: 0 30 60 90 120 150
|-----|-----|-----|-----|-----|
Time: 0秒 1秒 2秒 3秒 4秒 5秒
計算式: 秒 = frame / fps
例: frame=90, fps=30 → 90/30 = 3秒
30fps なら、プレビュー時は1秒間に30回コンポーネントが再実行されます。レンダリング時は全150フレーム分が順番に処理されて動画ファイルになります。
Remotion には他にも hooks が用意されていますが、基本的にはこの2つだけで動画が作れます。
具体的な使い方
それぞれの hook を使った実践例を見てみましょう。
useCurrentFrame() の使い方
前述した通り、useCurrentFrameは、その瞬間のフレーム番号を返す hooksでした。
// 例1: フレーム番号をそのまま表示 const frame = useCurrentFrame(); return <div>現在のフレーム: {frame}</div>;
// 例2: フレーム番号で条件分岐 const frame = useCurrentFrame(); return ( <div> {frame < 30 ? "イントロ" : frame < 90 ? "メイン" : "アウトロ"} </div> );
// 例3: フレーム番号を使って回転角度を計算(1フレームごとに6度回転) const frame = useCurrentFrame(); const rotation = frame * 6; return <div style={{ transform: `rotate(${rotation}deg)` }}>回転</div>;
frame変数は、0から始まって durationInFrames で設定した値まで増えていきます(Hello Worldテンプレートでは、150まで)
このように、フレーム番号を使った表示をすることで、動きをつけることができます。
useVideoConfig() の使い方
前述した通り、useVideoConfigは、動画の設定(fps, durationInFrames, width, height など)を返す hooksでした。
// 例1: 動画の中央に要素を配置(width と height を使う) const { width, height } = useVideoConfig(); return ( <div style={{ position: 'absolute', left: width / 2, top: height / 2, transform: 'translate(-50%, -50%)', }}> 中央 </div> );
// 例2: 動画の最後のフレームを判定 const frame = useCurrentFrame(); const { durationInFrames } = useVideoConfig(); const isLastFrame = frame === durationInFrames - 1; return <div>{isLastFrame ? "終了" : "再生中"}</div>;
// 例3: fps を使って「秒」単位で計算 const frame = useCurrentFrame(); const { fps } = useVideoConfig(); const seconds = frame / fps; // フレーム番号を秒に変換 return <div>{seconds.toFixed(1)}秒経過</div>;
このように、useCurrentFrame() でアニメーションのタイミングを制御し、useVideoConfig() で動画の設定を取得して計算に使います。
HelloWorld.tsx を読み解く
ここまでの知識で、テンプレートのメインコンポーネントを読めるようになるはず。
// src/HelloWorld.tsx export const HelloWorld: React.FC<...> = ({ titleText, titleColor, logoColor1, logoColor2 }) => { const frame = useCurrentFrame(); // ← 今のフレーム番号を取得 const { durationInFrames, fps } = useVideoConfig(); // ← 動画の設定を取得 // frame を使ってアニメーションの値を計算 const logoTranslationProgress = spring({ frame: frame - 25, fps, ... }); const logoTranslation = interpolate(logoTranslationProgress, [0, 1], [0, -150]); const opacity = interpolate(frame, [durationInFrames - 25, durationInFrames - 15], [1, 0], ...); // 計算結果を CSS に反映 return ( <AbsoluteFill style={{ backgroundColor: "white" }}> <AbsoluteFill style={{ opacity }}> <AbsoluteFill style={{ transform: `translateY(${logoTranslation}px)` }}> <Logo logoColor1={logoColor1} logoColor2={logoColor2} /> </AbsoluteFill> <Sequence from={35}> <Title titleText={titleText} titleColor={titleColor} /> </Sequence> <Sequence from={75}> <Subtitle /> </Sequence> </AbsoluteFill> </AbsoluteFill> ); };
流れを整理するとこうなります:
useCurrentFrame()で今のフレーム番号を取得(Context から読み出し)useVideoConfig()で fps や総フレーム数を取得(同上)springやinterpolateでフレーム番号から CSS の値を計算(これらは hook ではなく、ただの関数です)- 計算結果を CSS に反映して、その瞬間の「画面の見た目」を返します
これが30fpsなら1秒間に30回実行されます。 フレームが進むたびに Context の value が変わり、全コンポーネントが再実行され、新しい見た目を返します。パラパラ漫画と同じ原理ですね。
spring / interpolate は hook ではない
ここは最初勘違いしたのですが、spring と interpolate は、中で基本 hook を呼んでもいません。フレーム番号を受け取って数値を返すだけの 純粋な計算関数 です。
const progress = spring({ frame, fps, config: { damping: 100 } }); const position = interpolate(progress, [0, 1], [0, -150]);
Pythonで言うと
interpolate は、Python の numpy.interp() や線形補間の関数と同じです。
# Python での線形補間 import numpy as np frame = 30 opacity = np.interp(frame, [0, 60], [0, 1]) # ← frame=30 なら opacity=0.5
入力値(フレーム番号)を受け取って、指定した範囲で値をなめらかに変換する計算をしているだけです。Remotion の interpolate も同じで、「フレーム 0 のとき opacity は 0、フレーム 60 のとき opacity は 1、その間は線形補間」という計算を行っています。
interpolate の動作イメージ
interpolate(frame, [0, 60], [0, 1]) の動きを図で表すと、こんな感じです:
Input (frame):
0 10 20 30 40 50 60
|-----|-----|-----|-----|-----|-----|
↓ interpolate
Output (opacity):
0 0.17 0.33 0.5 0.67 0.83 1
|-----|-----|-----|-----|-----|-----|
・frame=0 → opacity=0(透明)
・frame=30 → opacity=0.5(半透明)
・frame=60 → opacity=1(完全表示)
※ 0と60の間は線形補間で自動計算される
このように、入力範囲 [0, 60] を出力範囲 [0, 1] にマッピングして、中間の値も自動で計算してくれます。
spring とは
spring は、物理的なバネの動きを再現する関数です。interpolate が直線的に値を変化させるのに対し、spring は加速と減速を伴う自然な動きを作ります。
const frame = useCurrentFrame(); const { fps } = useVideoConfig(); const progress = spring({ frame, fps, config: { damping: 100 } });
- 0 から 1 に変化:frame が進むにつれて、0 から 1 へなめらかに変化
- 物理シミュレーション:実際のバネの動きを数式で再現しているので、自然な動きになる
- damping で調整:
dampingを小さくすると揺れが大きくなり、大きくすると揺れがなくなる
damping のパラメータ(デフォルト: 10)
- damping: 10(デフォルト)→ ブルブル震えながら止まる(バウンスが大きい)
- damping: 100 → なめらかに減速して止まる(バウンスがほぼない)
- damping: 200 → バウンスが完全に消え、スッと止まる
HelloWorld.tsx では、Logo を上に移動させるときに spring を使っています。これにより、Logo が勢いよくスライドして、最後にスッと止まる自然な動きになります。
HelloWorld のアニメーション:spring と interpolate の組み合わせ
HelloWorld.tsx のコードを改めて見てみましょう。spring と interpolate を組み合わせて、Logo を上に移動させながらフェードアウトしています。
// src/HelloWorld.tsx(抜粋) const frame = useCurrentFrame(); const { durationInFrames, fps } = useVideoConfig(); // Logo の移動アニメーション const logoTranslationProgress = spring({ frame: frame - 25, // 25フレーム目から開始 fps, config: { damping: 100 }, }); const logoTranslation = interpolate(logoTranslationProgress, [0, 1], [0, -150]); // 全体のフェードアウト const opacity = interpolate( frame, [durationInFrames - 25, durationInFrames - 15], [1, 0], { extrapolateRight: "clamp" } );
動きの流れ:
- frame 0〜24:Logo は静止(
frame - 25が負なので、springは 0 を返す) - frame 25〜:
springが 0→1 に変化開始logoTranslationProgressが 0→1 に変化(バネの動き)interpolateでそれを 0→-150px にマッピング- Logo が下から上へスライド(Y軸方向に -150px 移動)
- frame 125〜140(最後の 25〜15 フレーム):全体がフェードアウト
opacityが 1→0 に変化- 画面全体が透明になっていく
Timeline (150フレーム、30fps = 5秒):
0 -------- 25 -------- 125 ---- 140 ---- 150
[ 静止 ] [ Logo移動 ] [ フェードアウト ]
↑ spring ↑ interpolate
Y: 0→-150px opacity: 1→0
なぜ spring と interpolate を両方使うのか?
- spring:物理的な動き(スライド、回転など)に使うと自然
- interpolate:透明度や色など、物理的な意味がない値の変化に使う
この例では、「Logo の移動」には spring を使って自然な動きにし、「フェードアウト」には interpolate を使って直線的に透明度を下げています。
子コンポーネントでの hook の使い方
HelloWorld.tsx だけでなく、子コンポーネントでもそれぞれ useCurrentFrame() を呼んでいますね。
// src/HelloWorld/Title.tsx export const Title: React.FC<...> = ({ titleText, titleColor }) => { const videoConfig = useVideoConfig(); const frame = useCurrentFrame(); const words = titleText.split(" "); return ( <h1> {words.map((t, i) => { const delay = i * 5; const scale = spring({ fps: videoConfig.fps, frame: frame - delay, // ← 単語ごとに遅延させている config: { damping: 200 }, }); return <span style={{ transform: `scale(${scale})` }}>{t}</span>; })} </h1> ); };
// src/HelloWorld/Subtitle.tsx export const Subtitle: React.FC = () => { const frame = useCurrentFrame(); const opacity = interpolate(frame, [0, 30], [0, 1]); return <div style={{ opacity }}>Edit src/Root.tsx and save to reload.</div>; };
どのコンポーネントでも同じパターンです
useCurrentFrame()でフレーム番号を取得- そのフレーム番号を
springやinterpolateに渡してアニメーション値を計算 - CSS に反映
Sequence と hook の関係
<Sequence from={35}> の中にあるコンポーネントでは、useCurrentFrame() が 0 から数え直した値 を返します。
グローバルフレーム: 0 -------- 35 -------- 75 -------- 150
| | | |
HelloWorld: frame=0 frame=35 frame=75 frame=149
Title (Sequence from=35): frame=0 frame=40 frame=114
Subtitle (Sequence from=75): frame=0 frame=74
まとめ
ここまで見てきて分かったのは、Remotion の Hooks は思った以上にシンプル ということです。
使うのは基本的に2つだけ
useCurrentFrame()で今のフレーム番号を取得useVideoConfig()で動画の設定(fps、長さ、解像度)を取得
そして、これらと interpolate() や spring() を組み合わせることで、フェードイン、スライド、回転など、あらゆるアニメーションが作れます。
次は、interpolate() と spring() を使った様々なアニメーションパターンを実践的に見ていきます!