Remotion の Sequence・Series でアニメーションのタイミングを制御する
Remotion の Sequence・Series でアニメーションのタイミングを制御する
Remotionの動画は、すべて「グローバルフレームカウンター」から始まります。useCurrentFrame() を呼び出せば、コンポーネントツリーのどこからでも現在のフレーム番号を取得できます。最初のフレームは 0、最後のフレームは durationInFrames - 1。シンプルで明快です。
しかし、複数の要素を異なるタイミングで動かそうとした瞬間、この仕組みが足かせになります。「ヘッドラインは0フレームからフェードイン、サブタイトルは30フレームから、グラフィックは90フレームから」という構成を生フレーム演算で実装すると、コードは次のようになります。
const headlineOpacity = Math.min(1, frame / 15);
const subtitleOpacity = frame < 30 ? 0 : Math.min(1, (frame - 30) / 15);
const graphicOpacity = frame < 90 ? 0 : Math.min(1, (frame - 90) / 20);
動作はします。しかし、ヘッドラインの長さを変えればサブタイトルの開始フレームも手で直さなければなりません。要素を別のコンポーネントに切り出してもグローバル時刻を引数として渡し続けるしかなく、再利用性はゼロです。
<Sequence> と <Series> は、この問題を根本から解決するための仕組みです。
<Sequence> — フレームカウンターを局所化する
<Sequence> の本質的な動作は一つだけです。内部で useCurrentFrame() を呼び出したコンポーネントが受け取るフレーム番号を、グローバル時刻ではなくシーケンスの開始点を基準とした値に変換すること。
<Sequence from={30}> と書けば、グローバル時刻が30フレームのとき、内部のコンポーネントには 0 が返ってきます。31フレームなら 1、90フレームなら 60 です。子コンポーネントは「自分の動画は0から始まる」と信じて動作します。
Props一覧
<Sequence
from={number} // 親タイムライン上の開始フレーム(デフォルト: 0)
durationInFrames={number} // 子がマウントされる期間(デフォルト: Infinity)
layout="absolute-fill" // "absolute-fill" | "none"(デフォルト: "absolute-fill")
name="MySequence" // Remotion Studioのタイムラインパネルに表示される名前
style={CSSProperties} // コンテナdivへのCSSスタイル
premountFor={number} // 表示前にN フレーム分だけ先行レンダリングする
>
{children}
</Sequence>
from — 子コンポーネントが表示される開始フレームです。正の値で遅延させ、負の値を指定すると「アニメーションがすでに進行中の状態で表示を開始する」効果が得られます。from={-10} は、ゆっくりしたビルドインの序盤をカットしたいときに便利なテクニックです。
durationInFrames — シーケンスの時間ウィンドウを外れると子はアンマウントされます。不要な計算を避けるためのパフォーマンス最適化であり、シーンが終わった後も要素が画面に残り続けるバグを防ぐ安全弁でもあります。
layout — デフォルトでは <AbsoluteFill> ラッパーが自動的に挿入されます。layout="none" にすると、このラッパーなしで純粋なタイミング制御だけを利用できます。自分でポジショニングを管理したいときや、複数のシーケンスを重ねて配置したいときに使います。
name — レンダリング結果には影響しませんが、Remotion Studioのタイムラインパネルに表示されます。複数のシーケンスを積み上げたときのデバッグに欠かせません。
premountFor — シーケンスが表示されるより N フレーム前から内部コンポーネントをマウントしておきます。重い動画要素やSVGがある場合、表示瞬間に準備が整っていない「ちらつき」を防げます。Remotion v5以降、デフォルト値は fps(1秒分)になっています。
シーケンスをネストして階層的タイミングを作る
<Sequence> は何重にでもネストできます。各階層の from オフセットは加算されますが、ルールは常に「子は親の0を基準とする」というシンプルなものです。
// シーン単体 — 内部タイミングはシーンのフレーム0を基準に書く
const Scene = () => (
<AbsoluteFill style={{ background: '#1a1a2e' }}>
<Sequence from={0} durationInFrames={30} name="Scene/Title">
<TitleSlide />
</Sequence>
<Sequence from={20} durationInFrames={Infinity} name="Scene/Body">
<BodyContent />
</Sequence>
</AbsoluteFill>
);
// 全体構成 — シーンをグローバルタイムラインに配置
export const Presentation = () => (
<AbsoluteFill>
<Sequence from={0} durationInFrames={150} name="Scene1">
<Scene />
</Sequence>
<Sequence from={150} durationInFrames={150} name="Scene2">
<Scene />
</Sequence>
</AbsoluteFill>
);
グローバル時刻が160フレームのとき、Scene2 内部では10フレームとして処理されます。そしてその中の BodyContent(from={20})はまだ開始していないため、表示されません。163フレームのときに初めて BodyContent 内のフレームカウンターが0になります。
このネスト構造の最大のメリットは、シーンの独立開発が可能になることです。シーンを単体でラップした <Composition> を作り、from={0} で動作確認してから全体に組み込む。このワークフローはプロジェクトが大きくなるほど効いてきます。
<Series> — 連続シーンの自動配置
シーンを順番に並べる構成は非常によく登場します。<Series> はこのパターン専用のコンポーネントです。各 <Series.Sequence> の durationInFrames を読み取り、前のシーンが終わった直後に次のシーンが始まるように from を自動計算します。
import { Series, AbsoluteFill } from 'remotion';
export const Slideshow = () => (
<AbsoluteFill>
<Series>
<Series.Sequence durationInFrames={90}>
<SlideOne />
</Series.Sequence>
<Series.Sequence durationInFrames={120}>
<SlideTwo />
</Series.Sequence>
<Series.Sequence durationInFrames={60}>
<SlideThree />
</Series.Sequence>
</Series>
</AbsoluteFill>
);
SlideOne は0〜89フレーム、SlideTwo は90〜209フレーム、SlideThree は210〜269フレームに自動的に配置されます。from を一つも書いていません。スライドの順番を変えたいときは、JSXの順番を入れ替えるだけです。新しいスライドを挿入しても、後続の開始フレームは自動で更新されます。
<Series.Sequence> のProps
<Series.Sequence
durationInFrames={number} // 表示フレーム数(最後の子のみInfinityを許可)
from={number} // 自動計算された開始位置への追加オフセット(省略可)
layout="none" // "absolute-fill" | "none"(Series.Sequenceのデフォルトは"none")
premountFor={number} // 表示前の先行レンダリングフレーム数
>
{children}
</Series.Sequence>
from に注意が必要です。<Series.Sequence> の from は自動計算された開始位置への相対オフセットです。絶対位置ではありません。from={-10} を指定すると、そのシーンが10フレーム早く始まり、前のシーンと重なります。クロスフェードや重なりトランジションを作るときの正しいアプローチです。
<Series.Sequence> と手動 <Sequence from={...}> の使い分け
<Series.Sequence> | <Sequence from={...}> | |
|---|---|---|
| オフセット計算 | 自動 | 手動 |
| シーンの追加・削除 | JSXを変えるだけ | 後続のfromを全て修正 |
| シーンの重なり | fromで相対オフセット | 自由に制御可 |
| 並行・複雑配置 | 不向き | 完全に制御可 |
| 最適な用途 | スライドショー・ステップ系 | 要素が並行・複雑に絡む構成 |
実践的な判断基準は「シーンが一列に並ぶなら <Series>、並行・複雑なら <Sequence>」です。
実装例:3スライドプレゼンテーション
<Series> でスライドを管理し、各スライド内部では <Sequence> でタイトルとボディのタイミングを制御する、実用的な構成例です。
import { Series, Sequence, AbsoluteFill, useCurrentFrame } from 'remotion';
import { spring, useVideoConfig } from 'remotion';
// スライドテンプレート
const Slide = ({
title,
body,
bg,
}: {
title: string;
body: string;
bg: string;
}) => (
<AbsoluteFill style={{ background: bg, padding: '80px' }}>
{/* タイトルは0フレームから登場 */}
<Sequence from={0} durationInFrames={Infinity} name="SlideTitle">
<TitleEntry text={title} />
</Sequence>
{/* ボディは20フレーム遅れで登場 */}
<Sequence from={20} durationInFrames={Infinity} name="SlideBody">
<BodyEntry text={body} />
</Sequence>
</AbsoluteFill>
);
// タイトルのスプリングアニメーション
const TitleEntry = ({ text }: { text: string }) => {
const frame = useCurrentFrame(); // Sequenceのおかげで常に0から始まる
const { fps } = useVideoConfig();
const progress = spring({ frame, fps, config: { damping: 12, stiffness: 80 } });
return (
<h1
style={{
opacity: progress,
transform: `translateY(${(1 - progress) * 40}px)`,
fontSize: 72,
color: '#fff',
fontFamily: '"Yu Gothic", "Hiragino Kaku Gothic ProN", sans-serif',
}}
>
{text}
</h1>
);
};
// ボディのスプリングアニメーション
const BodyEntry = ({ text }: { text: string }) => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const progress = spring({ frame, fps, config: { damping: 14, stiffness: 70 } });
return (
<p
style={{
opacity: progress,
transform: `translateY(${(1 - progress) * 24}px)`,
fontSize: 36,
color: 'rgba(255,255,255,0.85)',
marginTop: 24,
fontFamily: '"Yu Gothic", "Hiragino Kaku Gothic ProN", sans-serif',
}}
>
{text}
</p>
);
};
// 全体構成
export const Presentation = () => (
<AbsoluteFill style={{ background: '#0f0f23' }}>
<Series>
<Series.Sequence durationInFrames={120}>
<Slide title="課題" body="グローバルフレームはタイミング管理を複雑にする。" bg="#1a1a2e" />
</Series.Sequence>
<Series.Sequence durationInFrames={120}>
<Slide title="解決策" body="Sequenceがフレームカウンターを局所化する。" bg="#16213e" />
</Series.Sequence>
<Series.Sequence durationInFrames={Infinity}>
<Slide title="結果" body="クリーンで再利用可能なアニメーション構造が手に入る。" bg="#0f3460" />
</Series.Sequence>
</Series>
</AbsoluteFill>
);
TitleEntry と BodyEntry はグローバルタイムラインを一切知りません。どちらも frame = 0 からスプリングアニメーションを起動します。<Sequence from={20}> が BodyEntry の0を「スライドの20フレーム目」に対応させ、<Series> が各スライドを順番に配置します。この三層の構造がそれぞれ独立して機能するため、どこかを変更しても他への影響が最小限に抑えられます。
<Sequence> とスプリングアニメーションの組み合わせ
Remotionの spring() 関数は常にフレーム 0 からアニメーションを開始します。<Sequence> がなければ、スプリングを遅延させるために毎回 spring({ frame: frame - startFrame, fps }) という式を書き続けなければなりません。この手動オフセットは壊れやすく、startFrame の値が変わるたびに修正が必要です。
<Sequence> を使えば、内部のフレームカウンターはすでにゼロ化されています。
// <Sequence> なし — 壊れやすい
const progress = spring({ frame: frame - 90, fps });
// <Sequence from={90}> の内側 — シンプルでクリーン
const progress = spring({ frame, fps });
シーケンスの中で5フレーム遅らせたいときは、負値のクランプを使います。
// シーケンス開始から5フレーム後にスプリングを起動
const progress = spring({ frame: Math.max(0, frame - 5), fps });
Math.max(0, frame - 5) により、フレーム0〜4ではスプリングの初期値(0)が維持され、フレーム5から動き始めます。グローバルな開始フレームを追跡する必要はまったくありません。
よくある質問
Q: <Sequence> と <AbsoluteFill> の違いは何ですか?
<AbsoluteFill> はレイアウトのプリミティブです。position: absolute; inset: 0 で子要素をコンポジション全体に広げます。<Sequence> はタイミングのプリミティブです。子が表示されるタイミングと受け取るフレーム番号を制御します。デフォルトでは <Sequence> は内部に <AbsoluteFill> を自動生成しますが(layout="absolute-fill" がデフォルト)、目的はまったく異なります。
Q: durationInFrames はコンポジション全体の長さに影響しますか?
影響しません。コンポジションの総フレーム数は、ルートファイルの <Composition> コンポーネントの durationInFrames プロパティだけで決まります。<Sequence> の durationInFrames は子のマウント期間のみを制御します。
Q: <Series> でスライド間に間隔(ポーズ)を入れるにはどうしますか?
最もシンプルな方法は「ポーズ用のシーケンス」を明示的に挿入することです。
<Series>
<Series.Sequence durationInFrames={90}><SlideOne /></Series.Sequence>
<Series.Sequence durationInFrames={30}><HoldFrame /></Series.Sequence>
<Series.Sequence durationInFrames={90}><SlideTwo /></Series.Sequence>
</Series>
HoldFrame は何も表示しないか、前のスライドの最終フレームを静止表示するコンポーネントです。あるいは SlideOne の durationInFrames をアニメーション長より長く設定し、余分なフレームで静止させる方法もあります。
Q: <Sequence> は深くネストしてもパフォーマンスに問題はありませんか?
<Sequence> 自体は非常に軽量です。layout="absolute-fill" のデフォルトではDOMノードが一つ生成されますが、数十個ネストしても問題になることはほとんどありません。不要な div を避けたい場合は layout="none" を使ってください。重い子コンポーネント(動画、複雑なSVG)には premountFor を活用すると表示タイミングのちらつきを防げます。
Q: <Series.Sequence> の中で <Sequence> を使えますか?
使えます。<Series.Sequence> は内部的に <Sequence> を継承しています。その内部でさらに <Sequence> を使ったネストは完全に有効です。各スライドに「タイトルは0フレームから、ボディは20フレームから」という内部タイミング構造を持たせるのは典型的なパターンです。
Q: from に負の値を使う場面を教えてください。
2つの用途があります。ひとつは「ビルドインアニメーションのはじめの遅い部分をカットする」こと。from={-10} にするとアニメーションがすでに10フレーム進んだ状態から表示が始まります。もうひとつは <Series.Sequence> での「前のシーンとの重なり(クロスフェード)」。from={-15} にすると前のシーンの最後の15フレームと重なり、自然なオーバーラップが生まれます。
RenderComp テンプレートで実践する
<Sequence> と <Series> の使い方を理解すると、Remotionのプログラマブル動画制作が格段に扱いやすくなります。すべての RenderComp テンプレートはこの二つのコンポーネントを基盤に設計されており、各シーンは独立したタイミング構造を持ち、スライドの順番変更やデュレーション調整が数行で完結します。
テンプレートのコードを読むことで、実際のプロジェクトにおける <Sequence> と <Series> の使いどころが直感的につかめるはずです。rendercomp.com でテンプレートライブラリをご覧ください。