R RenderComp
remotion text-animation animation typography tutorial

Remotionでテキストリビールアニメーションを実装する完全ガイド

Remotionでテキストリビールアニメーションを実装する完全ガイド

テキストがどのように画面に現れるかは、そのテキストが何を言っているかと同じくらい重要だ。テレビのオープニング、映画のクレジット、SNS動画の見出しアニメーション——テキストの登場方法はコンテンツのトーンを決定づける。シャープなワイプは力強さと精度を示し、一文字ずつのスタッガーは丁寧さと演技を感じさせる。

Remotionでは、こうしたテキストアニメーションをReactコンポーネントとして実装する。一度コンポーネントとして作れば、どのコンポジションでも再利用できる。この記事では5つの定番パターン——ワイプリビール、マスクリビール、一文字ずつスタッガー、単語ごとリビール、スクランブルエフェクト——をすべて実装コード付きで解説する。


前提:RemotionのコアAPI

本記事のすべての実装は、3つのRemotionプリミティブに基づいている。

useCurrentFrame() は現在のフレーム番号(0始まりの整数)を返す。<Sequence>の中では、そのシーケンスの開始を0として相対的なフレーム番号が返る。

spring({ frame, fps, config, from, to }) は物理ベースのアニメーション値を生成する。fromからtoに向かって質量・バネ定数・減衰係数の影響を受けながら進む。典型的な使い方は0→1の進捗値として使い、その値をinterpolate()でCSSプロパティに変換することだ。

interpolate(value, inputRange, outputRange, options) は数値を別の数値範囲にマッピングする。spring()の0→1出力をピクセル・度・不透明度などのCSS値に変換するために使う。extrapolateRight: 'clamp'オプションを使えば、入力範囲外でも値が上限を超えないようにできる。

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

パターン1:ワイプリビール(clip-path)

ワイプリビールはブロードキャスト映像で最も多用されるテキストアニメーションだ。テキストが左から右にスライドして現れるように見え、clip-pathの幅を0%から100%にアニメーションさせることで実現する。

clip-path: inset(0 X% 0 0)は要素をクリップし、右側X%を隠す。XをCSSではなくReact側で毎フレーム計算することで、Remotionのレンダリングパイプラインに正確に乗る。

// components/WipeReveal.tsx
import { useCurrentFrame, useVideoConfig, spring, interpolate } from 'remotion';

interface WipeRevealProps {
  text: string;
  fontSize?: number;
  color?: string;
  delay?: number;
}

export const WipeReveal: React.FC<WipeRevealProps> = ({
  text,
  fontSize = 72,
  color = '#ffffff',
  delay = 0,
}) => {
  const frame = useCurrentFrame();
  const { fps } = useVideoConfig();

  const progress = spring({
    frame: Math.max(0, frame - delay),
    fps,
    config: { mass: 0.8, stiffness: 120, damping: 20, overshootClamping: true },
  });

  // 右側クリップ: 100%(全隠し)→ 0%(全表示)
  const clipRight = interpolate(progress, [0, 1], [100, 0]);

  return (
    <div
      style={{
        fontFamily: '"Yu Gothic", "Hiragino Kaku Gothic ProN", "Noto Sans JP", sans-serif',
        fontSize,
        fontWeight: 700,
        color,
        clipPath: `inset(0 ${clipRight}% 0 0)`,
        whiteSpace: 'nowrap',
        lineHeight: 1.2,
      }}
    >
      {text}
    </div>
  );
};

複数行のテキストを段差をつけてワイプさせたい場合は、delayをインデックスに比例させる。

const lines = ['テクノロジーで', '時間価値を', '最大化する。'];

export const MultiLineWipe: React.FC = () => (
  <AbsoluteFill
    style={{
      display: 'flex',
      flexDirection: 'column',
      justifyContent: 'center',
      padding: '0 80px',
      gap: 8,
    }}
  >
    {lines.map((line, i) => (
      <WipeReveal
        key={i}
        text={line}
        delay={i * 8}
        fontSize={80}
      />
    ))}
  </AbsoluteFill>
);

delay: i * 8は30fpsで約267msの間隔。ワイプが次々と走る気持ちのよいリズムになる。


パターン2:単語ごとリビール

単語ごとのリビールは、ナレーションの拍に合わせてテキストが現れるスタイルのSNS動画で多用される。文章を単語に分割し、各単語にspring()のディレイをつけて順番に登場させる。

// components/WordByWordReveal.tsx
import { useCurrentFrame, useVideoConfig, spring, interpolate } from 'remotion';

interface WordByWordRevealProps {
  text: string;
  staggerFrames?: number;
  fontSize?: number;
  color?: string;
}

export const WordByWordReveal: React.FC<WordByWordRevealProps> = ({
  text,
  staggerFrames = 5,
  fontSize = 64,
  color = '#ffffff',
}) => {
  const frame = useCurrentFrame();
  const { fps } = useVideoConfig();

  const words = text.split(' ');

  return (
    <div
      style={{
        display: 'flex',
        flexWrap: 'wrap',
        gap: '0 16px',
        fontFamily: '"Yu Gothic", "Hiragino Kaku Gothic ProN", "Noto Sans JP", sans-serif',
        fontSize,
        fontWeight: 700,
        color,
        lineHeight: 1.5,
      }}
    >
      {words.map((word, i) => {
        const wordProgress = spring({
          frame: Math.max(0, frame - i * staggerFrames),
          fps,
          config: { mass: 0.9, stiffness: 100, damping: 14 },
        });

        return (
          <span
            key={i}
            style={{
              display: 'inline-block',
              opacity: wordProgress,
              transform: `translateY(${interpolate(
                wordProgress,
                [0, 1],
                [28, 0]
              )}px)`,
            }}
          >
            {word}
          </span>
        );
      })}
    </div>
  );
};

staggerFramesの調整ガイドライン(30fps基準):

  • staggerFrames: 3 — ほぼ同時、高テンポ
  • staggerFrames: 6 — 読みやすいリズム感
  • staggerFrames: 10 — 重厚でドラマチック

パターン3:一文字ずつスタッガー

一文字ずつのアニメーションは、ブランド名や短い見出し(5〜15文字程度)で特に効果的だ。各文字に個別のspringアニメーションを適用し、時間差で登場させる。

// components/LetterByLetterReveal.tsx
import { useCurrentFrame, useVideoConfig, spring, interpolate } from 'remotion';

interface LetterByLetterRevealProps {
  text: string;
  staggerFrames?: number;
  fontSize?: number;
  color?: string;
  direction?: 'up' | 'down';
}

export const LetterByLetterReveal: React.FC<LetterByLetterRevealProps> = ({
  text,
  staggerFrames = 3,
  fontSize = 96,
  color = '#ffffff',
  direction = 'up',
}) => {
  const frame = useCurrentFrame();
  const { fps } = useVideoConfig();

  const characters = text.split('');

  return (
    <div
      style={{
        display: 'flex',
        flexWrap: 'wrap',
        fontFamily: '"Yu Gothic", "Hiragino Kaku Gothic ProN", "Noto Sans JP", sans-serif',
        fontSize,
        fontWeight: 800,
        color,
        letterSpacing: '0.02em',
        overflow: 'hidden',
      }}
    >
      {characters.map((char, i) => {
        const charProgress = spring({
          frame: Math.max(0, frame - i * staggerFrames),
          fps,
          config: { mass: 0.7, stiffness: 140, damping: 12 },
        });

        const yOffset = direction === 'up'
          ? interpolate(charProgress, [0, 1], [40, 0])
          : interpolate(charProgress, [0, 1], [-40, 0]);

        return (
          <span
            key={i}
            style={{
              display: 'inline-block',
              opacity: interpolate(charProgress, [0, 1], [0, 1], {
                extrapolateRight: 'clamp',
              }),
              transform: `translateY(${yOffset}px)`,
              minWidth: char === ' ' || char === ' ' ? '0.5em' : undefined,
            }}
          >
            {char}
          </span>
        );
      })}
    </div>
  );
};

direction: 'up'は文字が下から上へ現れるスタイル(タイトルに適する)。direction: 'down'は上から落ちてくるスタイル(重厚な印象)。日本語の全角スペース( )もminWidthで対応している。


パターン4:マスクリビール

マスクリビールは、物理的なマスクの後ろからテキストが滑り出てくるような演出だ。overflow: hiddenのコンテナにテキストを配置し、translateYをアニメーションさせることで実現する。clip-pathのワイプより立体感があり、ハイエンドのブランド動画や編集系のコンテンツによく合う。

// components/MaskReveal.tsx
import { useCurrentFrame, useVideoConfig, spring, interpolate } from 'remotion';

interface MaskRevealProps {
  text: string;
  fontSize?: number;
  color?: string;
  delay?: number;
}

export const MaskReveal: React.FC<MaskRevealProps> = ({
  text,
  fontSize = 80,
  color = '#ffffff',
  delay = 0,
}) => {
  const frame = useCurrentFrame();
  const { fps } = useVideoConfig();

  const progress = spring({
    frame: Math.max(0, frame - delay),
    fps,
    config: { mass: 1, stiffness: 80, damping: 16, overshootClamping: true },
  });

  const translateY = interpolate(progress, [0, 1], [fontSize * 1.2, 0]);

  return (
    // コンテナがマスクになる
    <div
      style={{
        overflow: 'hidden',
        height: fontSize * 1.4,
        lineHeight: `${fontSize * 1.4}px`,
      }}
    >
      <div
        style={{
          fontFamily: '"Yu Gothic", "Hiragino Kaku Gothic ProN", "Noto Sans JP", sans-serif',
          fontSize,
          fontWeight: 700,
          color,
          transform: `translateY(${translateY}px)`,
          whiteSpace: 'nowrap',
        }}
      >
        {text}
      </div>
    </div>
  );
};

複数行をスタッカーする場合:

const taglines = ['想像する。', '作る。', '届ける。'];

export const StackedMaskReveal: React.FC = () => (
  <AbsoluteFill
    style={{
      display: 'flex',
      flexDirection: 'column',
      justifyContent: 'center',
      padding: '0 80px',
    }}
  >
    {taglines.map((line, i) => (
      <MaskReveal
        key={i}
        text={line}
        delay={i * 10}
        fontSize={88}
      />
    ))}
  </AbsoluteFill>
);

パターン5:スクランブルエフェクト

スクランブルエフェクトは、文字がランダムな記号を経由して正しい文字に解決していくアニメーションだ。技術的・映画的な演出で、ブランド名の登場や数字発表シーンに効果的。

重要な技術的注意点:Remotionは各フレームを独立してレンダリングする。Math.random()を直接使うと、レンダリングのたびに異なる文字が生成され、出力動画がちらつく。シード値を使った決定論的なランダム関数が必須だ。

// utils/seededRandom.ts
export function seededRandom(seed: number): number {
  const x = Math.sin(seed) * 10000;
  return x - Math.floor(x);
}

export function randomChar(seed: number): string {
  const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%';
  return chars[Math.floor(seededRandom(seed) * chars.length)];
}
// components/ScrambleReveal.tsx
import { useCurrentFrame, interpolate } from 'remotion';
import { randomChar } from '../utils/seededRandom';

interface ScrambleRevealProps {
  text: string;
  scrambleDurationFrames?: number;
  fontSize?: number;
  color?: string;
  accentColor?: string;
}

export const ScrambleReveal: React.FC<ScrambleRevealProps> = ({
  text,
  scrambleDurationFrames = 30,
  fontSize = 72,
  color = '#ffffff',
  accentColor = '#00ff88',
}) => {
  const frame = useCurrentFrame();
  const characters = text.toUpperCase().split('');

  return (
    <div
      style={{
        display: 'flex',
        fontFamily: '-apple-system, "Segoe UI", Roboto, sans-serif',
        fontSize,
        fontWeight: 800,
        letterSpacing: '0.08em',
      }}
    >
      {characters.map((targetChar, i) => {
        // 各文字が解決するフレームを等間隔でずらす
        const charResolveFrame = Math.round(
          (i / characters.length) * scrambleDurationFrames
        );
        const isResolved = frame >= charResolveFrame;

        // 解決前はシード付きランダム文字、解決後は正しい文字
        const displayChar = isResolved
          ? targetChar
          : randomChar(frame * 100 + i);

        return (
          <span
            key={i}
            style={{
              color: isResolved ? color : accentColor,
              display: 'inline-block',
              minWidth: targetChar === ' ' ? '0.35em' : undefined,
              opacity: interpolate(frame, [0, 8], [0, 1], {
                extrapolateRight: 'clamp',
              }),
            }}
          >
            {targetChar === ' ' ? ' ' : displayChar}
          </span>
        );
      })}
    </div>
  );
};

seededRandom(frame * 100 + i)はフレーム番号と文字インデックスをシードにしているため、同じフレームを何度レンダリングしても同じ文字が表示される。これが決定論的レンダリングの核心だ。


組み合わせ:ブロードキャスト風タイトルシーケンス

上記のパターンを組み合わせると、本格的なタイトルシーケンスが作れる。メインタイトルにスクランブル、サブタイトルにマスクリビール、キャッチコピーに単語ごとリビールを重ね、それぞれ異なるフレームで始める構成だ。

// compositions/TitleSequence.tsx
import { AbsoluteFill, Sequence } from 'remotion';
import { ScrambleReveal } from '../components/ScrambleReveal';
import { MaskReveal } from '../components/MaskReveal';
import { WordByWordReveal } from '../components/WordByWordReveal';

export const TitleSequence: React.FC = () => (
  <AbsoluteFill
    style={{
      background: '#0a0a0a',
      display: 'flex',
      flexDirection: 'column',
      justifyContent: 'center',
      padding: '0 80px',
      gap: 24,
    }}
  >
    {/* メインタイトル — スクランブル(frame 0から開始) */}
    <Sequence from={0} name="MainTitle" layout="none">
      <ScrambleReveal
        text="RENDERCOMP"
        scrambleDurationFrames={30}
        fontSize={96}
        accentColor="#00ff88"
      />
    </Sequence>

    {/* サブタイトル — マスクリビール(frame 20から開始) */}
    <Sequence from={20} name="Subtitle" layout="none">
      <MaskReveal
        text="プログラマブルビデオ、大規模に。"
        fontSize={34}
        color="rgba(255,255,255,0.7)"
      />
    </Sequence>

    {/* キャッチコピー — 単語ごと(frame 40から開始) */}
    <Sequence from={40} name="Tagline" layout="none">
      <WordByWordReveal
        text="一度作れば、何度でも。"
        staggerFrames={5}
        fontSize={26}
        color="rgba(255,255,255,0.45)"
      />
    </Sequence>
  </AbsoluteFill>
);

layout="none"<Sequence>に指定していることが重要だ。親のAbsoluteFillがflexboxで3つの要素を縦並びにレイアウトしているため、各<Sequence>がデフォルトのAbsoluteFillposition: absolute)コンテナを生成すると崩れてしまう。layout="none"にすることで、<Sequence>はタイミング制御のみを担い、視覚的なコンテナは生成しない。


springコンフィグ早見表

テキストアニメーションの用途別にspringコンフィグを整理した。

スタイルmassstiffnessdamping特徴
シャープなワイプ0.812020バウンスなし、鋭い
自然な単語入場0.910014軽いバウンス、エネルギッシュ
ラグジュアリー系1.08016なめらか、重量感あり
軽快な文字バウンス0.714010明確なバウンス
重厚なゆっくり入場1.26018重く、決断力のある動き

ワイプとマスクリビールには必ずovershootClamping: trueを設定すること。clip-pathの幅やtranslateYがオーバーシュートすると、テキストがコンテナからはみ出してしまう。


よくある質問

Q: spring()delayパラメータとMath.max(0, frame - delay)の違いは何ですか?

どちらも同じ動作です。spring()delayパラメータは内部的に同じクランプ処理を行います。コードの可読性の観点から、スタッガーが多い場合はMath.max(0, frame - i * staggerFrames)のほうが何が起きているか一目で分かりやすいという意見もあります。プロジェクト内で一貫したスタイルを選べばどちらでも問題ありません。

Q: clip-pathは動画レンダリング後も滑らかに動きますか?

はい。Remotionは各フレームをReactコンポーネントとして個別にレンダリングします。clip-pathの値は毎フレーム正確に計算されるため、CSSトランジションに依存せず、設定したfps通りの滑らかな動きが得られます。

Q: テキストが登場したあとに静止させるには何が必要ですか?

何もしなくてよいです。spring()は最終値に収束したあとは変化しません。テキストは自然に静止します。退場アニメーションが必要な場合は、別のspring()を退場開始フレームからの相対フレームで計算し、入場プログレスから引き算する実装を追加します。

Q: スクランブルエフェクトは日本語テキストでも使えますか?

動作はしますが、スクランブル文字プールを日本語対応のものに置き換える必要があります。英数記号のプールをひらがな・カタカナ・漢数字などに変更すれば、日本語テキストに似合うスクランブルを実現できます。ただし文字幅が均一でないと表示が崩れることがあるため、monospaceフォントか固定幅レイアウトを使うのがおすすめです。

Q: 一文字ずつアニメーションで文字間の空白が崩れます。どう直しますか?

<span>display: 'inline-block'を指定すると、ブラウザのインラインボックス処理で文字間に余計なスペースが入ることがあります。親要素のフォントサイズを0にしてスペースを消し、各<span>でフォントサイズを戻す方法か、flexboxを使って文字を並べる方法が確実です。

Q: テキストアニメーションはSNS動画の視聴維持率に影響しますか?

一般的にそう言われています。テキストが一度に表示されると視聴者は読み終わった瞬間に次の情報を求めますが、段階的に現れるアニメーションは視聴者の目を引きつけ、動画内に留まらせる効果があると言われています。特に最初の3秒間のテキスト演出はアルゴリズムへの評価にも影響するため、冒頭のタイトルアニメーションに力を入れることは理にかなっています。


RenderCompでテキストアニメーションを即戦力化する

この記事で紹介したパターンは、RenderCompテンプレートライブラリのすべてのタイトルカード・ロワーサード・テロップオーバーレイテンプレートで実際に使われているアーキテクチャだ。springinterpolateSequenceの組み合わせで作られた読みやすいコンポーネントが、ブランドカラーとコピーを差し替えるだけで使える状態になっている。

rendercomp.comでテキストアニメーションテンプレートをチェックしてほしい。

販売中

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

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

料金プランを見る →