Remotion の useCurrentFrame・useVideoConfig 完全解説
Remotion の useCurrentFrame・useVideoConfig 完全解説
Remotionで動画を作るとき、ほぼすべてのアニメーションは最終的に useCurrentFrame と useVideoConfig という2つのフックに行き着きます。フェードイン、スライドイン、カウントダウン、プログレスバー——どれを実装するにしても、これらのフックが返す値を起点に計算を組み立てていきます。
この2つを本当に理解すると、Remotionの「動画をコードで書く」という設計思想が腑に落ちます。本記事では基本的な使い方から実践的なパターンまで、順を追って解説します。
Remotionの核心:動画は「時間を受け取る関数」
まずメンタルモデルを確認しておきましょう。Remotionにおける動画とは、フレーム番号を受け取ってReactツリーを返す関数です。
f(フレーム番号) → Reactのレンダリング結果 → PNG → 動画の1フレーム
レンダラーはコンポジションの全フレームに対してコンポーネントを1回ずつ呼び出し、各フレームの見た目をキャプチャします。After Effectsのように「キーフレームを打ってトゥイーンさせる」のではなく、「このフレーム番号のとき、この要素はどう見えるべきか」を計算で記述するのがRemotionのやり方です。
useCurrentFrame はその「今何フレーム目か」を、useVideoConfig は「このコンポジションはどんな仕様か」を提供します。
useCurrentFrame() — 現在のフレーム番号を取得する
import { useCurrentFrame } from "remotion";
const frame = useCurrentFrame();
このフックが返すのは整数1つ、現在のフレーム番号です。コンポジションの先頭が 0 で、150フレームのコンポジションなら末尾は 149 になります。
動作のポイント
常に整数。 Remotionは離散フレームを扱うため、小数点以下の値は返しません。
<Sequence> の内側では相対フレームになる。 コンポーネントを <Sequence from={30}> で囲んだ場合、その内側で useCurrentFrame() を呼ぶと、グローバルのフレームが 30 のとき 0 を返します。サブアニメーションを独立したコンポーネントとして設計できるのはこの仕組みのおかげです。
コンテキスト経由で注入される。 フレーム番号をpropsとして渡す必要はありません。必要な場所でフックを呼ぶだけです。
いつ使うか
「時間とともに値が変化する」ものすべてに使います。透明度、位置、スケール、色、回転——すべてフレーム番号から計算します。
フェードインアニメーションを実装する
最もシンプルなアニメーションであるフェードインを例に、基本パターンを見てみましょう。
import { useCurrentFrame, interpolate } from "remotion";
export const FadeIn: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const frame = useCurrentFrame();
const opacity = interpolate(frame, [0, 30], [0, 1], {
extrapolateRight: "clamp",
});
return <div style={{ opacity }}>{children}</div>;
};
interpolate はRemotionが提供するマッピングユーティリティです。入力値(フレーム番号)と入力範囲 [0, 30]、出力範囲 [0, 1] を受け取り、線形補間した値を返します。extrapolateRight: "clamp" を指定することで、フレームが 30 を超えても透明度が 1 で止まります。
ほぼすべてのプロパティアニメーションはこの3ステップで組み立てられます。
useCurrentFrame()でフレーム番号を取得するinterpolate()でフレーム範囲を値の範囲にマッピングする- 結果をスタイルやpropsに適用する
useVideoConfig() — コンポジションの仕様を取得する
import { useVideoConfig } from "remotion";
const { width, height, fps, durationInFrames } = useVideoConfig();
このフックは現在のコンポジションに関するメタデータを返します。
| プロパティ | 型 | 内容 |
|---|---|---|
width | number | コンポジションの横幅(px) |
height | number | コンポジションの縦幅(px) |
fps | number | フレームレート(fps) |
durationInFrames | number | コンポジションの総フレーム数 |
他にも id(コンポジションID)や defaultProps / props(データ駆動用)が含まれますが、日常的に使うのは上記の4つです。
なぜ重要か
フレーム番号を直接ハードコードするのは避けるべきです。たとえば「2秒後に登場するアニメーション」を 60 フレームと書いてしまうと、30fpsのときは正しく動きますが、60fpsに変更した瞬間に1秒後の登場になります。fps を使って 2 * fps と書いておけば、フレームレートが変わっても「2秒後」という意図が保たれます。
同様に、要素の位置やサイズに width / height を使うことで、解像度の変更にも自動で対応できます。
2つのフックを組み合わせる
useCurrentFrame と useVideoConfig を組み合わせることで、コンポジションの仕様に依存しない堅牢なアニメーションが書けます。
例:コンポジションをぴったり満たすプログレスバー
import { useCurrentFrame, useVideoConfig, interpolate } from "remotion";
export const ProgressBar: React.FC = () => {
const frame = useCurrentFrame();
const { fps, width } = useVideoConfig();
const twoSeconds = 2 * fps;
const barWidth = interpolate(frame, [0, twoSeconds], [0, width], {
extrapolateRight: "clamp",
});
return (
<div style={{ width, height: 8, background: "#1e293b" }}>
<div style={{ width: barWidth, height: 8, background: "#38bdf8" }} />
</div>
);
};
2 * fps でフレームレートに依存しない「2秒間」を表現し、バーの最大幅に width を使うことでキャンバスサイズに追従します。フレームレートや解像度が変わっても、このコンポーネントは常に意図どおり動きます。
フレームと秒の変換
フレーム番号と秒数の変換は、Remotionを触り始めたときに最初にぶつかる壁のひとつです。計算式はシンプルです。
秒数 = フレーム番号 / fps
フレーム数 = 秒数 × fps
実際のコードでよく使うパターンをまとめます。
const { fps, durationInFrames } = useVideoConfig();
const frame = useCurrentFrame();
// 現在の再生位置(秒)
const currentSeconds = frame / fps;
// 動画全体の長さ(秒)
const totalSeconds = durationInFrames / fps;
// 0〜1の進行率
const progress = frame / durationInFrames;
// 1.5秒後に開始するアニメーション用の遅延フレーム
const delayedFrame = Math.max(0, frame - Math.round(1.5 * fps));
delayedFrame のパターンは特に便利です。フレームから遅延分を引いてゼロ以下にならないようにクランプするだけで、アニメーションの開始タイミングを自由にずらせます。
よく使うアニメーションパターン
登場アニメーション
登場アニメーションの基本形は「初期状態をキープ → 短い時間でアニメーション → 最終状態をキープ」の3段構成です。
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
// 20フレームかけてY方向に40pxスライドイン
const translateY = interpolate(frame, [0, 20], [40, 0], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
});
カウントダウンタイマー
残り時間を表示するカウントダウンは、総フレーム数から経過フレームを引いて秒に変換するだけです。
const frame = useCurrentFrame();
const { fps, durationInFrames } = useVideoConfig();
const remainingFrames = durationInFrames - frame;
const remainingSeconds = Math.ceil(remainingFrames / fps);
Math.ceil を使うことで、最後の1秒間は「1」を表示し続け、0 への突然の切り替わりを防ぎます。
音声のビート・効果音に同期する
音声ファイルの特定の秒数に合わせてアニメーションを発火させる場合、秒数をフレームに変換してトリガーとして使います。
const { fps } = useVideoConfig();
const frame = useCurrentFrame();
// 2.4秒地点のビートに合わせてスケールアップ
const beatFrame = Math.round(2.4 * fps);
const scale = interpolate(frame, [beatFrame, beatFrame + 6], [1, 1.15], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
});
フレームレートが変わっても beatFrame は自動的に再計算されるため、音とアニメーションのズレが発生しません。
パフォーマンスとフックのルール
Remotionは通常のReactのフックルール(条件分岐内での呼び出し禁止、ループ内での呼び出し禁止など)をすべて踏襲します。加えて、Remotion固有の注意点があります。
CSSトランジション・CSSアニメーションを使わない。 Remotionは各フレームを独立してレンダリングするため、transition: opacity 0.3s のようなCSSアニメーションは意味を持ちません。アニメーションの値はすべて現在のフレーム番号から決定論的に計算できなければなりません。
useCurrentFrame() だけを時間のソースにする。 Date.now()、performance.now()、setTimeout、setInterval などを使ってはいけません。これらは非決定論的であり、同じフレームを2回レンダリングしたときに異なる結果を返す可能性があります。Remotionは並列・順不同でフレームをレンダリングするため、決定論的な計算が必須です。
レンダリング中のサイドエフェクトを避ける。 useEffect や useLayoutEffect はRemotionのレンダリングパイプラインでは期待どおりに動作しません。「時間の経過で何かを起こす」ロジックは、フレーム番号を引数に取る純粋な計算として表現してください。
useVideoConfig() は軽量。 コンテキストを読むだけなので、どのコンポーネントで何回呼んでもパフォーマンスへの影響は無視できます。
よくある質問
Q: 複数のコンポーネントで useCurrentFrame() を呼んでもいいですか?
はい、問題ありません。同一レンダーパス内のすべてのコンポーネントが同じフレーム番号を参照します。コンテキスト経由の取得なので、どこで呼んでも同じ値が返ります。
Q: useCurrentFrame() は小数を返すことがありますか?
いいえ、常に整数を返します。Remotionは離散フレームを扱います。
Q: Remotionのコンポジション外でこれらのフックを呼んだらどうなりますか?
エラーになります。これらのフックはRemotionのコンテキストプロバイダーの内側でしか使えません。<Player> コンポーネントで埋め込む場合は、その内側のコンポジション内で呼ぶ限り正常に動作します。
Q: コンポジションの末尾からさかのぼってアニメーションを組みたい場合は?
durationInFrames から現在のフレームを引いて「末尾からの経過フレーム」を計算します。
const { durationInFrames } = useVideoConfig();
const frame = useCurrentFrame();
const frameFromEnd = durationInFrames - frame;
この値を interpolate の入力として使えば、終わりに向かうアニメーションが組めます。
Q: <Sequence> の内側で useCurrentFrame() を使うと値がずれる気がします。バグですか?
バグではなく意図した仕様です。<Sequence from={N}> の内側では、フレーム番号がシーケンス開始を 0 として相対化されます。これにより、サブコンポーネントをオフセットを意識せずに再利用できます。グローバルフレームが必要な場合は、シーケンスの開始フレームを明示的に加算してください。
Q: frame をpropsとして子コンポーネントに渡す書き方は?
できますが、アンチパターンです。propsドリリングが生じ、コンポーネントの再利用性が下がります。必要な場所で useCurrentFrame() を直接呼ぶのがRemotionのイディオムです。
実践例:フェードイン・フェードアウトするタイトルカード
最後に、両フックを使ったまとまった例を示します。最初の0.5秒でフェードイン、最後の0.5秒でフェードアウトするタイトルカードです。
import { useCurrentFrame, useVideoConfig, interpolate } from "remotion";
export const TitleCard: React.FC<{ title: string }> = ({ title }) => {
const frame = useCurrentFrame();
const { fps, durationInFrames, width, height } = useVideoConfig();
const fadeInEnd = Math.round(0.5 * fps);
const fadeOutStart = durationInFrames - Math.round(0.5 * fps);
const opacity = interpolate(
frame,
[0, fadeInEnd, fadeOutStart, durationInFrames],
[0, 1, 1, 0],
{ extrapolateLeft: "clamp", extrapolateRight: "clamp" }
);
return (
<div
style={{
width,
height,
display: "flex",
alignItems: "center",
justifyContent: "center",
opacity,
background: "#0f172a",
}}
>
<h1 style={{ color: "#f8fafc", fontSize: 72, fontFamily: "sans-serif" }}>
{title}
</h1>
</div>
);
};
fps と durationInFrames を使っているので、フレームレートや尺を変更してもフェードの秒数は0.5秒に保たれます。width と height でキャンバスをぴったり埋めるため、解像度変更にも自動対応します。
ボイラープレートを飛ばして、すぐ制作へ
useCurrentFrame と useVideoConfig の仕組みを理解したら、次は実際に動画を作り込む段階です。登場アニメーション、プログレスバー、カウントダウン、タイトルカード——こういった汎用パターンを毎回ゼロから実装するのは時間の無駄です。
RenderCompのテンプレートは、本記事で解説したパターンをすべて内包した本番品質のRemotionコンポジションです。コンテンツをpropsで渡すだけで動作し、タイミング計算はテンプレート内部で処理されます。
RenderCompテンプレートでボイラープレートを省略する → rendercomp.com