Remotionでレスポンシブ動画を作る:縦横スクエア、1つのコードベースから複数フォーマットを出力
Remotionでレスポンシブ動画を作る:縦横スクエア、1つのコードベースから複数フォーマットを出力
動画コンテンツを複数のプラットフォームに展開するとき、必ず直面するのが「アスペクト比の問題」です。YouTubeには16:9の横向き、TikTokやInstagramリールには9:16の縦向き、Instagramフィードには1:1のスクエア。同じキャンペーン素材を3種類のフォーマットで用意しなければならない現場では、毎回手作業でサイズを調整し直すのが当たり前になっています。
Remotionはこの問題をエレガントに解決します。コンポジションはReactコンポーネントそのものなので、同じコンポーネントにキャンバスサイズの情報を渡すだけで、どんなフォーマットにも適応させることができます。アニメーションロジックは一度書けばよく、3フォーマット分の重複メンテナンスは不要になります。
この記事では、その具体的な実装方法を体系的に解説します。Root.tsxへの複数コンポジション登録、useVideoConfig()によるレイアウトの動的制御、スケール計算のパターン、そして縦横それぞれに対応したレイアウト切り替えまで、プロダクションで使えるレベルの実装を目指します。
基本構造:1つのコンポーネントを複数登録する
RemotionのエントリーポイントであるRoot.tsxには、<Composition>要素を通じてすべてのコンポジションを登録します。同じコンポーネントを異なるwidthとheightで複数登録することが、マルチフォーマット対応の起点です。
// Root.tsx
import { Composition } from "remotion";
import { PromoBanner } from "./PromoBanner";
export const RemotionRoot = () => {
return (
<>
<Composition
id="PromoBanner-Landscape"
component={PromoBanner}
durationInFrames={150}
fps={30}
width={1920}
height={1080}
/>
<Composition
id="PromoBanner-Portrait"
component={PromoBanner}
durationInFrames={150}
fps={30}
width={1080}
height={1920}
/>
<Composition
id="PromoBanner-Square"
component={PromoBanner}
durationInFrames={150}
fps={30}
width={1080}
height={1080}
/>
</>
);
};
これだけです。3つのコンポジション、1つのコンポーネント。次のステップは、PromoBannerコンポーネントが自分のキャンバスサイズを認識できるようにすることです。
useVideoConfig()でキャンバスサイズを読み取る
useVideoConfig()フックは、現在レンダリング中のコンポジションのwidth、height、fps、durationInFramesを返します。コンポーネントのどこからでも呼び出すことができ、コンポジションIDに応じて正しい値が返ってきます。
import { useVideoConfig } from "remotion";
export const PromoBanner: React.FC = () => {
const { width, height } = useVideoConfig();
const isPortrait = height > width;
const isSquare = width === height;
const isLandscape = width > height;
return (
<div style={{ width, height, position: "relative" }}>
{/* ここからレイアウトを構築 */}
</div>
);
};
isPortrait、isSquare、isLandscapeという真偽値でレイアウトを切り替えることができますが、これだけでは要素のサイズをどうするかという問題が残ります。
スケール計算:すべてのサイズを1つの数値から導く
マルチフォーマット対応で最も重要なパターンが「スケール計算」です。Math.min(width, height)を使って、キャンバスの短辺を「スケール単位」として決定します。
const { width, height } = useVideoConfig();
const scale = Math.min(width, height);
// テキストサイズ
const titleFontSize = scale * 0.08; // 短辺の8%
const bodyFontSize = scale * 0.04; // 短辺の4%
// 余白
const padding = scale * 0.05;
// ロゴや画像のサイズ
const logoSize = scale * 0.15;
なぜMath.minなのか。1080×1920の縦向きキャンバスと1920×1080の横向きキャンバスでは、どちらも短辺が1080です。scaleは同じ1080になるので、文字サイズや余白の物理的な大きさがほぼ一致します。1080×1080のスクエアも短辺は1080なので同様です。
この計算により、3フォーマットの見た目の調和が保たれます。文字が小さすぎたり大きすぎたりする問題が根本的に解消されます。
レイアウトの切り替え:縦向きと横向きに対応する
実際のコンポーネント全体を見てみましょう。スケール計算とレイアウト切り替えを組み合わせた完成形です。
import {
useCurrentFrame,
useVideoConfig,
interpolate,
spring,
} from "remotion";
type Props = {
headline: string;
subline: string;
logoSrc: string;
accentColor: string;
};
export const PromoBanner: React.FC<Props> = ({
headline,
subline,
logoSrc,
accentColor,
}) => {
const frame = useCurrentFrame();
const { width, height, fps } = useVideoConfig();
const scale = Math.min(width, height);
const isPortrait = height > width;
// アニメーション値
const slideIn = interpolate(frame, [0, 18], [40, 0], {
extrapolateRight: "clamp",
});
const fadeIn = interpolate(frame, [0, 18], [0, 1], {
extrapolateRight: "clamp",
});
const logoProgress = spring({
frame,
fps,
config: { damping: 14, mass: 0.9 },
});
const logoScale = interpolate(logoProgress, [0, 1], [0.5, 1]);
return (
<div
style={{
width,
height,
backgroundColor: "#0f172a",
display: "flex",
flexDirection: isPortrait ? "column" : "row",
alignItems: "center",
justifyContent: "center",
gap: scale * 0.05,
padding: scale * 0.06,
boxSizing: "border-box",
fontFamily: '"Yu Gothic", "Hiragino Kaku Gothic ProN", "Noto Sans JP", sans-serif',
overflow: "hidden",
}}
>
{/* ロゴ */}
<img
src={logoSrc}
style={{
width: scale * 0.2,
height: scale * 0.2,
objectFit: "contain",
transform: `scale(${logoScale})`,
flexShrink: 0,
}}
/>
{/* テキストブロック */}
<div
style={{
display: "flex",
flexDirection: "column",
gap: scale * 0.025,
textAlign: isPortrait ? "center" : "left",
maxWidth: isPortrait ? "85%" : "60%",
opacity: fadeIn,
transform: `translateY(${slideIn}px)`,
}}
>
<h1
style={{
fontSize: scale * 0.075,
fontWeight: 700,
color: "#f8fafc",
margin: 0,
lineHeight: 1.25,
}}
>
{headline}
</h1>
<p
style={{
fontSize: scale * 0.038,
color: "#94a3b8",
margin: 0,
lineHeight: 1.6,
}}
>
{subline}
</p>
<div
style={{
width: scale * 0.08,
height: 3,
backgroundColor: accentColor,
marginTop: scale * 0.02,
marginLeft: isPortrait ? "auto" : 0,
marginRight: isPortrait ? "auto" : 0,
}}
/>
</div>
</div>
);
};
ポイントを整理します。
flexDirectionを縦向きではcolumn、横向きではrowに切り替えることで、縦向きはロゴの下にテキスト、横向きはロゴの右にテキストという自然なレイアウトになりますtextAlignも縦向きではcenter、横向きではleftに切り替えています- すべてのサイズは
scaleの倍数として表現されているため、3フォーマットで一貫した視覚的比率が保たれます - フォントスタックは日本語対応済みのシステムフォントを使用しています
defaultPropsでフォーマット別に内容を変える
同じコンポーネントでも、フォーマットによってコピーを変えたい場合があります。縦向きではキャッチコピーを短く、横向きでは詳細な説明を入れる、といったケースです。<Composition>のdefaultPropsを使えばこれが実現できます。
<Composition
id="PromoBanner-Portrait"
component={PromoBanner}
durationInFrames={150}
fps={30}
width={1080}
height={1920}
defaultProps={{
headline: "短いキャッチコピー",
subline: "スワイプして詳細をチェック",
logoSrc: "/logo-white.png",
accentColor: "#6366f1",
}}
/>
<Composition
id="PromoBanner-Landscape"
component={PromoBanner}
durationInFrames={150}
fps={30}
width={1920}
height={1080}
defaultProps={{
headline: "サービス名・プロダクト名",
subline: "詳細な説明文をここに入れることができます。横向きでは行数の余裕があります。",
logoSrc: "/logo-color.png",
accentColor: "#6366f1",
}}
/>
このdefaultPropsはRemotion Studioのプレビューでも反映されるため、フォーマットごとの見た目を開発中に確認することができます。
CLIから3フォーマットを一括レンダリング
コンポジションが登録されたら、CLIからそれぞれをレンダリングするだけです。シェルスクリプトにまとめると運用が楽になります。
#!/bin/bash
# render-all-formats.sh
PROPS='{"headline":"夏のキャンペーン","subline":"最大50%オフ","logoSrc":"/logo.png","accentColor":"#f59e0b"}'
for FORMAT in Landscape Portrait Square; do
npx remotion render src/index.ts "PromoBanner-$FORMAT" \
"out/campaign-$FORMAT.mp4" \
--props="$PROPS"
echo "Rendered: $FORMAT"
done
--propsフラグでJSONをインジェクトできるため、キャンペーンごとにコピーを変えながら繰り返しレンダリングするのが簡単です。このJSONをCMSやスプレッドシートから自動生成すれば、コンテンツ担当者がコードを触らずに動画を量産できる体制が作れます。
テキストのはみ出し対策
マルチフォーマット対応で必ず検討が必要なのがテキストのオーバーフローです。横向きでは余裕があっても、縦向きでは画面幅が狭いため文字が折り返したり、コンテナからはみ出したりします。
最小フォントサイズを設ける:
const titleFontSize = Math.max(scale * 0.07, 28);
Math.maxで下限を設けることで、小さいキャンバスでも読めないサイズにはなりません。
CSSで行数を制限する:
style={{
display: "-webkit-box",
WebkitBoxOrient: "vertical",
WebkitLineClamp: isPortrait ? 3 : 2,
overflow: "hidden",
}}
フォーマットに応じて最大行数を変えることで、レイアウト崩れを防ぎます。
フォーマット別にテキストを変える:最終的には、縦向き専用の短いコピーを用意するのが最も確実な解決策です。defaultPropsでフォーマットごとに異なるテキストを設定してください。
RenderCompのマルチフォーマットテンプレート
このパターンをゼロから実装するのが大変な場合は、RenderCompのテンプレートライブラリが役に立ちます。横向き・縦向き・スクエアのコンポジション登録が済んでいる状態でスタートでき、scale計算とレイアウト切り替えも組み込み済みです。SNS広告、告知動画、製品紹介など用途別のテンプレートを用意しています。rendercomp.comでラインナップをご確認ください。
よくある質問
Q1: 3フォーマットでアニメーションのタイミングを少しずつ変えることはできますか?
はい。各<Composition>は独立したdurationInFramesとfpsを持てます。defaultPropsでフォーマット別のタイミングパラメータを渡すことも可能です。またcalculateMetadataを使えば、入力プロップスに応じてデュレーションを動的に算出することもできます。
Q2: useVideoConfig()はコンポーネントのどこからでも呼べますか?
はい。Reactのコンテキストフックとして実装されているため、コンポジション内のどのコンポーネントからでも呼び出せます。widthとheightをプロップスとして親から子へ渡し続ける必要はありません。
Q3: TikTok向けの縦向きの推奨サイズは何ですか? 1080×1920(9:16)、30fpsが標準です。Instagram Reelsも同様です。YouTube Shortsは60fpsが推奨されています。ただし30fpsはすべてのプラットフォームで対応しており、ファイルサイズも抑えられます。
Q4: スクエアフォーマットではscaleはどうなりますか?
1080×1080のスクエアではMath.min(1080, 1080) = 1080となります。横向きの短辺(1080)、縦向きの短辺(1080)と同じ値になるため、3フォーマットでscaleが揃い、要素のサイズ比率が統一されます。
Q5: Remotion Studioで3つのコンポジションを切り替えてプレビューするには?
Studioの左側のパネルにすべての登録済みコンポジションが表示されます。名前をクリックするだけで切り替えられます。フォーマットごとにdefaultPropsが異なる場合も、それぞれのデフォルト値でプレビューされます。
Q6: 3フォーマットを並列でレンダリングして時間を短縮できますか?
はい。3つのnpx remotion renderコマンドをバックグラウンドプロセスとして並列起動することができます。ただし、並列実行するとCPUとメモリの負荷が集中します。マシンのスペックに応じて並列数を調整してください。Remotion Lambdaを使えば、クラウドで並列レンダリングを行い大幅に時間を短縮できます。
Q7: 縦向きのみで表示したい要素を、横向きでは非表示にするには?
Reactの条件レンダリングを使えば簡単です。{isPortrait && <SwipeUpCTA />}のように記述するだけで、縦向きコンポジションでのみ表示される要素を作れます。