RemotionのTransitionSeries完全ガイド — シーントランジションの実装とカスタム演出
RemotionのTransitionSeries完全ガイド — シーントランジションの実装とカスタム演出
複数シーンで構成される動画には、必ず「カット点で何が起きるか」という問いがついて回ります。ハードカットで十分な場面も多いですが、クロスフェード・スライド・ワイプ、あるいは完全オリジナルの演出を入れたい場合は、2つのシーンを時間的に重ね合わせながらエフェクトでブレンドする仕組みが必要です。タイムライン型の編集ソフトならカット点にトランジションをドラッグするところを、Remotionではコードで表現します。
公式パッケージ @remotion/transitions は、これを <TransitionSeries> というひとつのコンポーネントと、2つの組み合わせ可能なパーツで解決します。プレゼンテーション(トランジションの見た目)とタイミング(進行の速度カーブ)です。5種類の組み込みエフェクトをそのまま使うこともできますし、30行程度で自作のプレゼンテーションを書くこともできます。
この記事では、TransitionSeries のセットアップ、組み込みプレゼンテーション、springとlinearのタイミング関数、カスタムプレゼンテーションの自作、初見でほぼ全員がつまずく尺の計算、そして音声クロスフェードまでを体系的に解説します。
@remotion/transitionsとは何か・いつ使うか
@remotion/transitions はRemotionのファーストパーティパッケージです。プロジェクトの他のパッケージとバージョンを揃えるため、Remotion CLI経由でインストールします。
npx remotion add @remotion/transitions
このパッケージがやってくれることは3つです。
- シーケンシング —
<Series>のように、手動のfromオフセットなしでシーンを順に並べる - オーバーラップ — 各トランジションで次のシーンを時間的に手前へずらし、トランジション期間中は両シーンを同時に描画する
- プレゼンテーション — 退場シーンと登場シーンをラップし、0から1の進捗値を受け取って視覚エフェクトを適用する
ハードカットだけで足りる動画なら、このパッケージは不要です。素の <Series> や <Sequence> の方がシンプルで、タイミングモデルはSequenceとSeriesのタイミング制御ガイドで解説しています。<Sequence> を重ねて自力でトランジションを作ることも可能ですが、オーバーラップの計算・進捗値の算出・登場側と退場側の区別といった、TransitionSeries が既に提供している仕組みを再発明することになります。
TransitionSeriesのセットアップ: SequenceとTransition
<TransitionSeries> には2種類の子要素を交互に並べます。<TransitionSeries.Sequence>(シーン本体。durationInFrames を明示する)と <TransitionSeries.Transition>(2シーン間のエフェクト)です。最小構成の2シーンクロスフェードはこうなります。
import { TransitionSeries, linearTiming } from "@remotion/transitions";
import { fade } from "@remotion/transitions/fade";
import { AbsoluteFill } from "remotion";
const Scene: React.FC<{ label: string; background: string }> = ({
label,
background,
}) => {
return (
<AbsoluteFill
style={{
background,
justifyContent: "center",
alignItems: "center",
fontFamily: '-apple-system, "Segoe UI", Roboto, sans-serif',
fontSize: 100,
fontWeight: 700,
color: "#ffffff",
}}
>
{label}
</AbsoluteFill>
);
};
export const CrossfadeDemo: React.FC = () => {
return (
<TransitionSeries>
<TransitionSeries.Sequence durationInFrames={60}>
<Scene label="Scene A" background="#0b84ff" />
</TransitionSeries.Sequence>
<TransitionSeries.Transition
presentation={fade()}
timing={linearTiming({ durationInFrames: 15 })}
/>
<TransitionSeries.Sequence durationInFrames={60}>
<Scene label="Scene B" background="#111827" />
</TransitionSeries.Sequence>
</TransitionSeries>
);
};
構造上のルールを押さえておきましょう。
- 子要素は絶対配置される。 各
<TransitionSeries.Sequence>は<AbsoluteFill>のように振る舞い、オーバーラップ中はシーン同士が重なって描画されます。 - トランジションは必ず2つのSequenceに挟まれる。 シリーズの先頭・末尾にトランジションを置くことも、トランジションを2つ連続させることもできません。
presentationとtimingは両方必須。 見た目と進行カーブそれぞれにデフォルトはありません。- トランジションは隣接する両シーンより短くなければならない。 15フレームのトランジションが10フレームのシーンに接すると、そのシーンが登場と退場を同時に行うことになるため、Remotionはエラーを投げます。
組み込みプレゼンテーション: fade・slide・wipe・flip・clockWipe
組み込みプレゼンテーションはそれぞれ専用のサブパスに分かれており、バンドルサイズを小さく保てます。
import { fade } from "@remotion/transitions/fade";
import { slide } from "@remotion/transitions/slide";
import { wipe } from "@remotion/transitions/wipe";
import { flip } from "@remotion/transitions/flip";
import { clockWipe } from "@remotion/transitions/clock-wipe";
各プレゼンテーションの効果と主なオプションは次の通りです。
| プレゼンテーション | 効果 | 主なオプション |
|---|---|---|
fade() | 登場シーンが退場シーンの上にフェードイン | — |
slide({ direction }) | 登場シーンが退場シーンを押し出す | "from-left" / "from-right" / "from-top" / "from-bottom" |
wipe({ direction }) | 登場シーンが退場シーンの上をスライドして覆う | 上記4方向 + "from-top-left" 等の斜め4方向(計8方向) |
flip({ direction, perspective? }) | 3Dカードフリップで次のシーンを表示 | 4方向。perspective のデフォルトは 1000 |
clockWipe({ width, height }) | 円形のスイープで次のシーンを表示 | コンポジションの寸法が必須 |
clockWipe だけは注意が必要で、コンポジションの width と height を要求します。ハードコードせず useVideoConfig() から取得しましょう。
import { TransitionSeries, springTiming, linearTiming } from "@remotion/transitions";
import { slide } from "@remotion/transitions/slide";
import { clockWipe } from "@remotion/transitions/clock-wipe";
import { useVideoConfig } from "remotion";
export const MultiTransitionDemo: React.FC = () => {
const { width, height } = useVideoConfig();
return (
<TransitionSeries>
<TransitionSeries.Sequence durationInFrames={90}>
<Scene label="Intro" background="#0b84ff" />
</TransitionSeries.Sequence>
<TransitionSeries.Transition
presentation={slide({ direction: "from-right" })}
timing={springTiming({ config: { damping: 200 } })}
/>
<TransitionSeries.Sequence durationInFrames={90}>
<Scene label="Features" background="#111827" />
</TransitionSeries.Sequence>
<TransitionSeries.Transition
presentation={clockWipe({ width, height })}
timing={linearTiming({ durationInFrames: 25 })}
/>
<TransitionSeries.Sequence durationInFrames={90}>
<Scene label="Outro" background="#065f46" />
</TransitionSeries.Sequence>
</TransitionSeries>
);
};
slide と wipe の違いは動くシーンの数です。slide は両方のシーンが動き(新シーンが旧シーンをフレーム外に押し出す)、wipe は旧シーンを固定したまま新シーンがカードのように上を滑ります。
もうひとつ、none()(@remotion/transitions/none)という視覚エフェクトなしのプレゼンテーションもあります。映像はハードカットのまま、時間的なオーバーラップだけがほしい場面——たとえば音声だけをクロスフェードしたい場合に役立ちます。
タイミング関数: springTiming vs linearTiming
timing プロップは presentationProgress が0から1へどう進むかを制御します。
linearTiming は指定フレーム数のあいだ一定速度で進行します。イージングカーブを追加することもできます。
import { linearTiming } from "@remotion/transitions";
import { Easing } from "remotion";
// 一定速度
linearTiming({ durationInFrames: 20 });
// イージングあり——ゆっくり始まり加速する
linearTiming({
durationInFrames: 20,
easing: Easing.in(Easing.ease),
});
springTiming はスプリング物理演算で進捗を駆動し、自然な加速と収束を与えます。
import { springTiming } from "@remotion/transitions";
springTiming({
config: { damping: 200 },
durationInFrames: 30,
durationRestThreshold: 0.001,
});
springTiming について知っておくべきことが3つあります。
durationInFramesを省略すると、トランジションの長さはスプリングが収束するまでの時間になります。これはconfigとコンポジションのfpsに依存します。durationInFramesを渡すと、スプリングカーブがその長さにぴったり収まるよう伸縮されます。次のセクションの尺計算で予測可能な数字が必要なときに便利です。- デフォルトの収束しきい値では、アニメーションの最後の見える部分が切り落とされることがあります。
durationRestThreshold: 0.001を指定すると終端の「カクッ」というスナップを回避できます(トランジションはわずかに長くなります)。
damping: 200 の設定はオーバーシュートのない滑らかなカーブで、ほとんどのシーントランジションに適しています。dampingを下げるとオーバーシュートが発生し、slide では気持ちよく見えますが fade では不自然になります。スプリング設定の感覚をつかむにはスプリングアニメーション完全ガイドを参照してください。
カスタムプレゼンテーションをゼロから作る
プレゼンテーションの実体は、Reactの component とそれに渡す props という2フィールドを持つオブジェクトにすぎません。コンポーネントはトランジションの仕組みから4つのプロップを受け取ります。
children— ラップされるシーンのマークアップpresentationDirection—"entering"または"exiting"。このインスタンスが2シーンのどちらをラップしているかを示すpresentationProgress— タイミング関数が駆動する0〜1の数値passedProps— 自分で定義する型安全な設定値
重要なメンタルモデルはこれです: トランジション中、あなたのコンポーネントは2回描画されます。1回は退場シーンをラップして、もう1回は登場シーンをラップして。それぞれの側で何をするかはあなたが決めます。以下は、新しいシーンが中央の円から広がって現れる「サークルリビール」の完全な実装です。
import type {
TransitionPresentation,
TransitionPresentationComponentProps,
} from "@remotion/transitions";
import React, { useMemo } from "react";
import { AbsoluteFill } from "remotion";
type CircleRevealProps = {
width: number;
height: number;
};
const CircleRevealPresentation: React.FC<
TransitionPresentationComponentProps<CircleRevealProps>
> = ({ children, presentationDirection, presentationProgress, passedProps }) => {
// progress = 1 で画面全体を覆いきる半径
const finishedRadius =
Math.sqrt(passedProps.width ** 2 + passedProps.height ** 2) / 2;
const radius = finishedRadius * presentationProgress;
const style: React.CSSProperties = useMemo(() => {
return {
width: "100%",
height: "100%",
// 退場シーンは下層にそのまま描画し、
// 登場シーンだけを広がる円形にクリップする
clipPath:
presentationDirection === "exiting"
? undefined
: `circle(${radius}px at 50% 50%)`,
};
}, [presentationDirection, radius]);
return (
<AbsoluteFill>
<AbsoluteFill style={style}>{children}</AbsoluteFill>
</AbsoluteFill>
);
};
export const circleReveal = (
props: CircleRevealProps
): TransitionPresentation<CircleRevealProps> => {
return { component: CircleRevealPresentation, props };
};
使い方は組み込みプレゼンテーションとまったく同じです。
const { width, height } = useVideoConfig();
<TransitionSeries.Transition
presentation={circleReveal({ width, height })}
timing={springTiming({ config: { damping: 200 }, durationInFrames: 25 })}
/>;
このスケルトンからほぼ何でも作れます。clipPath を進捗に応じて解像していく filter: blur() に差し替える、scale と opacity を組み合わせてズームパンチカットにする、登場側と退場側で別々のエフェクトを適用する——。進捗値はタイミング関数から供給されるため、ひとつのプレゼンテーションがlinearとspringの両方のタイミングでそのまま動きます。
尺の計算: オーバーラップで動画が短くなる理由と対策
TransitionSeries で最も混乱を招くポイントがここです。トランジションは隣接シーンをオーバーラップさせるため、シリーズ全体の長さはシーンの合計より短くなります。
全体の長さ = シーンの合計 − トランジションの合計
60フレームのシーン2つを15フレームのフェードでつなぐと、120フレームではなく105フレームの動画になります。90フレームのシーン3つを20フレームのトランジション2つでつなぐと、270ではなく230フレームです。
<Composition> の durationInFrames に単純合計を宣言してしまうと、シリーズが早く終わってレンダリング末尾に空フレームが残ります。逆に目分量で短めの数字を入れると、最後のシーンが切れるリスクがあります。正解は、推測ではなく計算することです。すべてのタイミングオブジェクトが持つ getDurationInFrames() メソッドを使います。
import { CalculateMetadataFunction, Composition } from "remotion";
import { linearTiming } from "@remotion/transitions";
const SCENE_DURATIONS = [90, 120, 90];
const TRANSITION_DURATION = 20;
const transitionTiming = linearTiming({
durationInFrames: TRANSITION_DURATION,
});
const calculateMetadata: CalculateMetadataFunction<
Record<string, unknown>
> = () => {
const fps = 30;
const transitionFrames = transitionTiming.getDurationInFrames({ fps });
const scenes = SCENE_DURATIONS.reduce((sum, d) => sum + d, 0);
const transitions = transitionFrames * (SCENE_DURATIONS.length - 1);
return {
fps,
durationInFrames: scenes - transitions, // 300 − 40 = 260
};
};
export const RemotionRoot: React.FC = () => {
return (
<Composition
id="TransitionsDemo"
component={MultiTransitionDemo}
width={1920}
height={1080}
fps={30}
durationInFrames={260}
calculateMetadata={calculateMetadata}
/>
);
};
getDurationInFrames({ fps }) が最も重要になるのは springTiming を使う場合です。durationInFrames を明示しない限り、実際の長さは物理演算とフレームレートから導出されるため、このメソッドが唯一の確実な取得手段になります。シーンの長さがデータ由来——音声ファイルの長さやAPIレスポンス——の場合も同じパターンで自然に拡張できます。詳しくはcalculateMetadataガイドを参照してください。
もうひとつ計算時の制約があります。両側にトランジションを持つ中間シーンは、2つのトランジションの合計より長くなければなりません。そうでないとオーバーラップ同士が衝突します。
トランジション中の音声クロスフェード
映像はクロスフェードしているのに音声がハードカット——これは非常に耳障りです。幸い、トランジション期間中は両シーンが実際にマウントされているため、各 <TransitionSeries.Sequence> の中に置いた音声も自然にオーバーラップします。あとは音量の形を整えるだけです。
パターンはこうです: 各シーンの音声を、末尾でフェードアウト・先頭でフェードインさせ、その長さをトランジションの長さに合わせます。<Audio> の volume プロップは、音声の再生開始を基準とした相対フレームを受け取るコールバックを受け付けます。
import { Audio } from "@remotion/media";
import { interpolate, staticFile } from "remotion";
const TRANSITION_FRAMES = 20;
const SceneAudio: React.FC<{
src: string;
sceneDuration: number;
}> = ({ src, sceneDuration }) => {
return (
<Audio
src={staticFile(src)}
volume={(f) =>
interpolate(
f,
[
0,
TRANSITION_FRAMES,
sceneDuration - TRANSITION_FRAMES,
sceneDuration,
],
[0, 1, 1, 0],
{ extrapolateLeft: "clamp", extrapolateRight: "clamp" }
)
}
/>
);
};
各Sequenceの中に映像と並べて <SceneAudio> を置くだけです。20フレームのオーバーラップ中、退場側の音声は 1 → 0 へ、登場側は 0 → 1 へランプし、追加のオーケストレーションなしにきれいなクロスフェードが成立します。最初のシーンはフェードインを、最後のシーンはフェードアウトを省略してください。
トランジション効果音——スライドのヒュッという音、クロックワイプのチクタク音——は、プレゼンテーション自体に音声を組み込むのが便利です。そのトランジションを使う場所すべてに音が自動的についてきます。
import type {
TransitionPresentation,
TransitionPresentationComponentProps,
} from "@remotion/transitions";
import { Audio } from "@remotion/media";
export function addSound<T extends Record<string, unknown>>(
transition: TransitionPresentation<T>,
src: string
): TransitionPresentation<T> {
const { component: Component, ...other } = transition;
const C = Component as React.FC<TransitionPresentationComponentProps<T>>;
const NewComponent: React.FC<TransitionPresentationComponentProps<T>> = (
p
) => {
return (
<>
{p.presentationDirection === "entering" ? <Audio src={src} /> : null}
<C {...p} />
</>
);
};
return { component: NewComponent, ...other };
}
// 使い方:
// presentation={addSound(slide({ direction: "from-right" }), staticFile("whoosh.mp3"))}
presentationDirection === "entering" のガードが重要です。これがないと、ラップされる2つのシーンそれぞれで音が鳴り、二重再生になります。映像はハードカットのまま音だけのトランジションにしたい場合は、このラッパーを前述の none() プレゼンテーションと組み合わせてください。Remotionの音声制御全般は音声完全ガイドで詳しく解説しています。
よくある質問(FAQ)
Q: TransitionSeries を通常の <Sequence> の中にネストできますか?
できます。普通のReactコンポーネントなので、<Sequence>・<AbsoluteFill> などのプリミティブと自由に組み合わせられます。よくあるレイアウトは、常時表示の背景レイヤーの上に、前景シーンの TransitionSeries を重ねる構成です。
Q: トランジションはレンダリングパフォーマンスに影響しますか?
オーバーラップ期間中は両シーンが毎フレーム描画されるため、トランジション内のフレームは外のフレームのおよそ2倍のコストがかかります。一般的なシーンでは無視できる差ですが、重いシーン(大きな動画素材・複雑なSVG)ではトランジションを短めにしましょう。
Q: 同じシリーズ内でトランジションごとに異なるタイミング関数を使えますか?
使えます。各 <TransitionSeries.Transition> は独立した timing と presentation を取るため、スプリングのスライドとリニアのフェードをひとつのシリーズに混在させて問題ありません。尺計算に各トランジションの実際の長さを含めることだけ忘れずに。
まとめ
@remotion/transitionsが提供する<TransitionSeries>は、Sequence(シーン)とTransition(エフェクト)の子要素を交互に並べる- 組み込みプレゼンテーションは
fade・slide・wipe・flip・clockWipeの5種 + 視覚効果なしでオーバーラップだけ作るnone() - タイミングは差し替え可能: 一定速度・イージングの
linearTiming、物理的な動きのspringTiming - カスタムプレゼンテーションは約30行:
presentationProgressとpresentationDirectionを受け取るコンポーネントをファクトリ関数で包むだけ - トランジションはシーンをオーバーラップさせるため、全体の長さ = シーン合計 − トランジション合計。推測せず
getDurationInFrames()で計算する - 音声クロスフェードはオーバーラップから自然に導かれる。各シーンの音声を
volumeコールバックで整形すればよい
既製のトランジションパックで開発を加速する
カスタムプレゼンテーションを1つ作るのは、ちょうどいい午後の演習です。しかし一貫したセットを作る——イージングを揃え、長さを統一し、横型・縦型の両フォーマットで機能する方向設計にする——のは、本格的なデザインワークになります。
RenderComp では、調整済みのスプリング設定・TypeScriptのプロップインターフェース・calculateMetadata に組み込み済みの尺計算を備えた、本番品質のRemotionトランジションコンポーネントとマルチシーンテンプレートを公開しています。TransitionSeries にそのまま組み込んで、シーンを子要素として渡すだけでレンダリングできます。