R RenderComp
remotion ウェディング イベント動画 アニメーション テンプレート

Remotionでウェディング・イベント動画を作る:招待状アニメーションから一括生成まで

Remotionでウェディング・イベント動画を作る:招待状アニメーションから一括生成まで

ウェディング・イベント映像制作の核心は「パーソナライゼーション」だ。ふたりの名前が正しいフォントで表示され、会場名が適切なトーンで現れ、受け取った人が「自分たちのために作られた」と確信できる映像体験——これこそが結婚式の映像制作が提供すべき価値だ。

問題は、真のパーソナライゼーションはコストがかかることだった。完全オーダーメイドのアニメーション付きセーブザデートを一組ひとりに丁寧に制作するには、数時間の作業が必要になる。スタジオが年間20組のカップルを手がけるなら許容範囲かもしれないが、100組以上を扱うウェディングプランナーやイベントプロデューサーにとっては現実的ではない。

Remotionはこの制約を根本から変える。Remotionのコンポジションはすべてのアニメーション要素がpropsで駆動されるReactコンポーネントだ。ふたりの名前・会場・日付・カラーパレット・写真——これらすべてがデータオブジェクトから流れ込む。データを変えれば映像が変わる。デザインの品質は一定に保たれたまま、コンテンツだけがカップルごとに固有のものになる。


ウェディング動画のprops設計

ウェディング・イベント動画のコンテンツは、一貫した個人データの集合として整理できる。最初にpropsの型定義を丁寧に設計しておくと、コンポジション全体がクリーンで読みやすくなる。

type WeddingVideoProps = {
  couple: {
    partner1: { firstName: string; lastName: string; photoUrl?: string };
    partner2: { firstName: string; lastName: string; photoUrl?: string };
    weddingDate: string;          // ISO形式: "2026-09-19"
    weddingDateFormatted: string; // "2026年9月19日(土)"
    venue: string;
    venueCity: string;
    hashtag?: string;
  };
  photos: Array<{
    url: string;
    caption?: string;
    order: number;
  }>;
  colorPalette: {
    primary: string;    // メインカラー(アクセント)
    secondary: string;  // サブカラー
    background: string; // 背景色またはグラデーション基調色
    text: string;       // 主要テキスト色
  };
  videoType: 'save-the-date' | 'invitation' | 'program' | 'thank-you' | 'slideshow';
};

colorPaletteはウェディング動画において特に重要だ。カップルはウェディングテーマカラーの選定に多大な労力をかける。そのカラーを映像が正確に再現することで、「自分たちのためにカスタムデザインされた」という体験が生まれる。ダスティローズとセージグリーンのパレット、ネイビーとゴールドのパレット、ミニマルなブラック&ホワイトの処理——すべてを同じテンプレートで実現できる。


セーブザデートのアニメーション実装

セーブザデートはカップルが最初に発信するコミュニケーションだ。モーションカードとして設計すると、伝統的な印刷版としての告知機能とSNSシェアの拡散力を兼ね備える。9:16(1080×1920)でInstagram StoriesやLINEのビデオメッセージとして最適化する。

import {
  AbsoluteFill,
  useCurrentFrame,
  useVideoConfig,
  spring,
  interpolate,
  Img,
} from 'remotion';

type SaveTheDateProps = {
  couple: WeddingVideoProps['couple'];
  colorPalette: WeddingVideoProps['colorPalette'];
  backgroundPhotoUrl: string;
};

export const SaveTheDate: React.FC<SaveTheDateProps> = ({
  couple,
  colorPalette,
  backgroundPhotoUrl,
}) => {
  const frame = useCurrentFrame();
  const { fps } = useVideoConfig();

  const photoOpacity = interpolate(frame, [0, 30], [0, 1], {
    extrapolateLeft: 'clamp',
    extrapolateRight: 'clamp',
  });

  const labelEntrance = spring({
    frame: frame - 15,
    fps,
    config: { damping: 20, stiffness: 60, mass: 1 },
  });

  const namesEntrance = spring({
    frame: frame - 30,
    fps,
    config: { damping: 16, stiffness: 50, mass: 1.2 },
  });

  const dateEntrance = spring({
    frame: frame - 50,
    fps,
    config: { damping: 18, stiffness: 55, mass: 1 },
  });

  const dividerWidth = interpolate(
    spring({ frame: frame - 45, fps, config: { damping: 20, stiffness: 70 } }),
    [0, 1],
    [0, 180]
  );

  return (
    <AbsoluteFill style={{ backgroundColor: colorPalette.background }}>
      {/* 背景写真 */}
      <div style={{ opacity: photoOpacity, position: 'absolute', inset: 0 }}>
        <Img
          src={backgroundPhotoUrl}
          style={{ width: '100%', height: '100%', objectFit: 'cover' }}
        />
        {/* テキスト可読性のためのグラデーションオーバーレイ */}
        <div style={{
          position: 'absolute',
          inset: 0,
          background: `linear-gradient(
            to bottom,
            transparent 0%,
            ${colorPalette.background}88 40%,
            ${colorPalette.background}ee 70%,
            ${colorPalette.background} 100%
          )`,
        }} />
      </div>

      {/* コンテンツパネル */}
      <div style={{
        position: 'absolute',
        bottom: 0,
        left: 0,
        right: 0,
        padding: '60px 64px',
        display: 'flex',
        flexDirection: 'column',
        alignItems: 'center',
        gap: 20,
      }}>
        {/* "Save the Date"ラベル */}
        <div style={{
          opacity: labelEntrance,
          transform: `translateY(${interpolate(labelEntrance, [0, 1], [20, 0])}px)`,
          fontSize: 16,
          letterSpacing: '0.2em',
          color: colorPalette.primary,
          fontFamily: '"Yu Gothic", "Hiragino Kaku Gothic ProN", "Noto Sans JP", sans-serif',
          fontWeight: 300,
        }}>
          Save the Date
        </div>

        {/* カップルの名前 */}
        <div style={{
          opacity: namesEntrance,
          transform: `scale(${interpolate(namesEntrance, [0, 1], [0.88, 1])})`,
          textAlign: 'center',
          display: 'flex',
          flexDirection: 'column',
          gap: 8,
        }}>
          <div style={{
            fontSize: 64,
            fontWeight: 300,
            color: colorPalette.text,
            letterSpacing: '-0.01em',
            lineHeight: 1.1,
            fontFamily: '"Yu Gothic", "Hiragino Kaku Gothic ProN", "Noto Sans JP", sans-serif',
          }}>
            {couple.partner1.firstName}
          </div>
          <div style={{
            fontSize: 20,
            color: colorPalette.secondary,
            letterSpacing: '0.15em',
            fontFamily: '-apple-system, "Segoe UI", Roboto, sans-serif',
          }}>
            &
          </div>
          <div style={{
            fontSize: 64,
            fontWeight: 300,
            color: colorPalette.text,
            letterSpacing: '-0.01em',
            lineHeight: 1.1,
            fontFamily: '"Yu Gothic", "Hiragino Kaku Gothic ProN", "Noto Sans JP", sans-serif',
          }}>
            {couple.partner2.firstName}
          </div>
        </div>

        {/* 区切り線 */}
        <div style={{
          width: dividerWidth,
          height: 1,
          backgroundColor: colorPalette.secondary,
          opacity: 0.6,
        }} />

        {/* 日付・会場 */}
        <div style={{
          opacity: dateEntrance,
          transform: `translateY(${interpolate(dateEntrance, [0, 1], [16, 0])}px)`,
          textAlign: 'center',
          display: 'flex',
          flexDirection: 'column',
          gap: 8,
        }}>
          <div style={{
            fontSize: 26,
            fontWeight: 500,
            color: colorPalette.text,
            letterSpacing: '0.04em',
            fontFamily: '"Yu Gothic", "Hiragino Kaku Gothic ProN", "Noto Sans JP", sans-serif',
          }}>
            {couple.weddingDateFormatted}
          </div>
          <div style={{
            fontSize: 18,
            color: `${colorPalette.text}99`,
            fontFamily: '"Yu Gothic", "Hiragino Kaku Gothic ProN", "Noto Sans JP", sans-serif',
          }}>
            {couple.venue}
          </div>
        </div>
      </div>
    </AbsoluteFill>
  );
};

カップル名アニメーション:上品な演出パターン

名前の登場演出はウェディング動画全体の感情トーンを決定する。商業コンテンツやスポーツグラフィックスで有効なパンチの効いた演出は、ウェディングコンテンツでは逆効果になることが多い。品のある演出には以下のパターンが実績を持つ。

ソフトフェードアップ:不透明度0→1と10〜15pxの上方向translateを組み合わせる。スプリングのstiffnessを35〜45程度に低めに設定し、ゆっくりとした収束を実現する。テキストが「静かに現れる」印象を与える。

レタースペーシング収束:広いletter-spacingから始めて通常値に向かって縮めながらフェードインする。文字が虚空から集まってくるような視覚効果が生まれる。

const NameReveal: React.FC<{ name: string; startFrame: number; palette: typeof colorPalette }> = ({
  name,
  startFrame,
  palette,
}) => {
  const frame = useCurrentFrame();
  const { fps } = useVideoConfig();

  const progress = spring({
    frame: frame - startFrame,
    fps,
    config: { damping: 22, stiffness: 42, mass: 1.4 },
  });

  const letterSpacing = interpolate(progress, [0, 1], [0.22, 0.02]);
  const opacity = interpolate(progress, [0, 0.4], [0, 1], {
    extrapolateLeft: 'clamp',
    extrapolateRight: 'clamp',
  });

  return (
    <div style={{
      fontSize: 68,
      fontWeight: 300,
      letterSpacing: `${letterSpacing}em`,
      opacity,
      color: palette.text,
      fontFamily: '"Yu Gothic", "Hiragino Kaku Gothic ProN", "Noto Sans JP", sans-serif',
    }}>
      {name}
    </div>
  );
};

フォトスライドショーと上品なトランジション

ウェディング写真スライドショーでは、機械的に感じさせないトランジション設計が重要だ。クロスディゾルブ(フェードアウトとフェードインの重なり)に、次の写真への控えめなスケールアップを組み合わせると映画的な質感が生まれる。

export const PhotoSlideshow: React.FC<{
  photos: WeddingVideoProps['photos'];
  frameDuration: number;
  transitionFrames: number;
}> = ({ photos, frameDuration, transitionFrames }) => {
  const frame = useCurrentFrame();
  const sorted = [...photos].sort((a, b) => a.order - b.order);

  return (
    <AbsoluteFill>
      {sorted.map((photo, i) => {
        const photoStart = i * (frameDuration - transitionFrames);
        const photoEnd = photoStart + frameDuration;

        if (frame < photoStart || frame > photoEnd + transitionFrames) return null;

        const fadeIn = interpolate(frame - photoStart, [0, transitionFrames], [0, 1], {
          extrapolateLeft: 'clamp',
          extrapolateRight: 'clamp',
        });
        const fadeOut = interpolate(frame, [photoEnd - transitionFrames, photoEnd], [1, 0], {
          extrapolateLeft: 'clamp',
          extrapolateRight: 'clamp',
        });

        const progress = (frame - photoStart) / frameDuration;
        const scale = 1 + Math.min(progress, 1) * 0.05;

        return (
          <div key={photo.url} style={{ position: 'absolute', inset: 0, opacity: Math.min(fadeIn, fadeOut) }}>
            <Img
              src={photo.url}
              style={{ width: '100%', height: '100%', objectFit: 'cover', transform: `scale(${scale})` }}
            />
            {photo.caption && (
              <div style={{
                position: 'absolute',
                bottom: 60,
                left: 0,
                right: 0,
                textAlign: 'center',
                fontSize: 20,
                color: 'rgba(255,255,255,0.82)',
                fontStyle: 'italic',
                fontFamily: '"Yu Gothic", "Hiragino Kaku Gothic ProN", "Noto Sans JP", sans-serif',
                padding: '0 60px',
              }}>
                {photo.caption}
              </div>
            )}
          </div>
        );
      })}
    </AbsoluteFill>
  );
};

式次第・タイムライン表示

フォーマルな結婚式の招待映像では、タイムスケジュールを視覚的に整理して表示することが求められる。各項目を少しずつずらして登場させると、視聴者が自然に上から下へ順番に読む流れが生まれる。

export const EventTimeline: React.FC<{
  events: Array<{ time: string; label: string; location?: string }>;
  startFrame: number;
  palette: typeof colorPalette;
}> = ({ events, startFrame, palette }) => {
  const frame = useCurrentFrame();

  return (
    <div style={{
      display: 'flex',
      flexDirection: 'column',
      gap: 0,
      padding: '0 64px',
      fontFamily: '"Yu Gothic", "Hiragino Kaku Gothic ProN", "Noto Sans JP", sans-serif',
    }}>
      {events.map((event, i) => {
        const delay = startFrame + i * 12;
        const opacity = interpolate(frame - delay, [0, 20], [0, 1], {
          extrapolateLeft: 'clamp',
          extrapolateRight: 'clamp',
        });
        const translateX = interpolate(frame - delay, [0, 20], [28, 0], {
          extrapolateLeft: 'clamp',
          extrapolateRight: 'clamp',
        });

        return (
          <div key={event.time} style={{ opacity, transform: `translateX(${translateX}px)` }}>
            <div style={{
              display: 'flex',
              alignItems: 'flex-start',
              gap: 24,
              padding: '18px 0',
              borderBottom: i < events.length - 1 ? `1px solid ${palette.text}22` : 'none',
            }}>
              <div style={{
                fontSize: 15,
                color: palette.secondary,
                fontWeight: 600,
                letterSpacing: '0.04em',
                minWidth: 80,
                paddingTop: 2,
              }}>
                {event.time}
              </div>
              <div>
                <div style={{ fontSize: 20, color: palette.text, fontWeight: 500 }}>{event.label}</div>
                {event.location && (
                  <div style={{ fontSize: 15, color: `${palette.text}77`, marginTop: 4 }}>
                    {event.location}
                  </div>
                )}
              </div>
            </div>
          </div>
        );
      })}
    </div>
  );
};

お礼動画の一括自動生成

結婚式後のお礼動画は、Remotionのバッチ生成が最も価値を発揮する応用のひとつだ。招待客全員(または家族単位)に、名前と式での共有写真(撮影できている場合)を含んだ個別動画を、ゲストリストのJSONから自動生成する。

type ThankYouProps = {
  recipientName: string;     // "鈴木ご一家様"
  message: string;
  coupleNames: string;       // "陽菜 & 蒼"
  sharedPhotoUrl?: string;
  colorPalette: WeddingVideoProps['colorPalette'];
};

// ゲストリストからの一括生成
async function generateThankYouVideos() {
  const guestList = await import('./data/guest-list.json');

  await Promise.all(
    guestList.default.map(async (guest: any) => {
      const inputProps: ThankYouProps = {
        recipientName: guest.displayName,
        message: guest.hasSharedPhoto
          ? 'この度は素晴らしい一日を共に過ごしてくださり、ありがとうございました。'
          : 'ご参列いただき、心より感謝申し上げます。',
        coupleNames: '陽菜 & 蒼',
        sharedPhotoUrl: guest.photoUrl ?? undefined,
        colorPalette: {
          primary: '#C9A96E',
          secondary: '#8B9467',
          background: '#1A1814',
          text: '#F5F0E8',
        },
      };

      await renderMedia({
        composition: await selectComposition({
          serveUrl: BUNDLE_URL,
          id: 'ThankYouVideo',
          inputProps,
        }),
        outputLocation: `./out/thankyou/${guest.id}.mp4`,
        codec: 'h264',
      });
    })
  );
}

120組のゲストユニット向けにそれぞれ15秒のパーソナライズされたお礼動画を生成するのに、ローカル環境で30分以内に完了する。全件を手作業で編集していたとすれば、少なくとも数週間分の作業に相当する。


複数イベントへの一括展開

年間を通じて複数の結婚式・イベントを担当するプランナーやスタジオにとって、Remotionのバッチ機能は制作スケジュールを根本的に変える。

import events from './data/events-2026.json';

for (const event of events) {
  const videoTypes = ['save-the-date', 'invitation', 'program', 'thank-you'] as const;

  for (const videoType of videoTypes) {
    await renderMedia({
      composition: await selectComposition({
        serveUrl: BUNDLE_URL,
        id: 'WeddingVideo',
        inputProps: { ...event, videoType },
      }),
      outputLocation: `./out/${event.id}/${videoType}.mp4`,
      codec: 'h264',
    });
  }
}

各イベントのフォルダにセーブザデート・招待状・式次第・お礼動画の4種類が出力される。同一カラーパレット・タイポグラフィ・カップル情報で統一されたスイートとして、設計工数ゼロで生成される。40組分のポートフォリオを一夜で処理することも現実的な目標だ。


ウェディング動画SaaSではなくRemotionを選ぶ理由

サブスクリプション型のウェディング動画ツールは導入が速く、技術知識を必要としない。しかし根本的な限界がある。テンプレートのバリエーションはベンダーが用意したものに限られ、カスタマイズはロゴアップロードと色の選択程度だ。動画1本あたりの料金や月額制が、120名分のお礼動画生成では非常にコスト高になる。

Remotionを使えば、すべての設計判断はクリエイターの手にある。アニメーション曲線、タイポグラフィの組み方、各要素の登場タイミング——すべてコードで完全に制御できる。レンダリングコストはコンピューティングコストであり、ライセンス料ではない。120本のお礼動画を生成してもかかるのは電力とディスク容量だけだ。

クライアントの差別化を大切にするプロフェッショナルにとって、この違いは小さくない。データが映像を駆動するため、カップルごとに固有の内容を持つ映像を、実質的にゼロの追加工数で実現できる。


RenderCompウェディング・イベントテンプレート

RenderComp では、このガイドで解説したWeddingVideoProps構造に直接対応したRemotionテンプレートコレクションを提供している。アニメーション付きセーブザデート、フォーマル招待状、式次第アニメーション、フォトスライドショー、パーソナライズドお礼動画のすべてが実装済みで、カラーパレット・カップル名・会場情報・写真配列が完全にパラメータ化されている。イベントデータを差し込んだらすぐにレンダリングを開始できる。


FAQ

Q: セーブザデートをSNSで配信する際、最適な動画サイズはどれですか? A: LINE・Instagram Stories・WhatsAppで送る場合は9:16(1080×1920)が最適です。メールや結婚式Webサイトに埋め込む場合は16:9(1920×1080)か正方形(1080×1080)が汎用性が高い。formatプロップスで9:16と1:1の両バージョンを同一テンプレートから生成するのが最も効率的な運用です。

Q: BGMを追加したいのですが、著作権はどう管理すればよいですか? A: RemotionのAudioコンポーネントにローカルの音声ファイルを指定して使用してください。BGMはEpidemic Sound・Artlist・PremiumBeat等のロイヤリティフリー音楽ライブラリを利用し、ライセンスを取得した上でローカルに保存して参照する形が最も安全です。音量は全体の20〜25%程度に抑え、終了前2〜3秒でフェードアウトすると映像と馴染みます。

Q: 写真の縦横比がカップルごとに異なります(縦長・横長・正方形が混在)。どう対処しますか? A: すべての写真表示にobjectFit: 'cover'を使用してください。コンテナに対してプロポーショナルにトリミングされるため、縦長写真は横方向が、横長写真は縦方向がカットされます。極端な縦横比の写真がある場合は、ぼかし版を背景に敷いてcontain表示した写真をオーバーレイする方法(Instagramのフィル背景に近い処理)が視覚的にきれいです。

Q: クライアント(カップル)に事前確認してもらうワークフローはどう組みますか? A: まず低解像度プレビュー版(720p、CRF 28)をレンダリングしてクライアントに送付します。承認後にフルクオリティ版を最終出力します。プレビュー版のレンダリングは大幅に速く、確認目的には十分な品質です。ファイル名に-preview-finalのサフィックスを付けることでワークフローを整理できます。

Q: 同じデザインで日本語版と英語版(あるいは中国語版など)を並行して出力することはできますか? A: 可能です。languageプロップスをコンポーネントに追加し、テキスト文字列を言語ごとに格納したtranslationsオブジェクトを参照する設計にしてください。アニメーションのロジックはまったく同じで、テキスト内容のみが言語によって切り替わります。日中韓欧語版を同一バッチで同時生成することが技術的に可能です。

Q: カップルが希望するカスタムカラーパレットに対応できますか? A: colorPalette.primary.secondary等はすべてpropsなので、任意のhex値がそのまま機能します。インテイク時にカップルのウェディングカラーをヒアリング(またはインスピレーションボードから抽出)して、イベントのJSONレコードに保存してください。テンプレートのコードを変更せずに、どんなカスタムパレットでも正確に再現されます。

販売中

1,000以上のRemotionテンプレートを一括入手

買い切り(一括払い)・サブスクなし・生涯アップデート無料。TypeScript製。

料金プランを見る →