以下の内容はhttps://www.randpy.tokyo/entry/remotion-2-hooksより取得しました。


Remotion のHooks でアニメーションを設定する方法【Remotion 第2回】

youtu.be

📚 Remotion シリーズ 全8回の記事一覧

  1. 第1回: 全体像をまとめる
    Composition / AbsoluteFill / Sequence で動画の骨組みを作る
  2. ▶ 第2回: Hooks を理解する(この記事)
    useCurrentFrame / useVideoConfig でフレーム番号と設定を取得する
  3. 第3回: アニメーションを作る
    interpolate / spring でフレーム番号を CSS の値に変換する
  4. 第4回: 素材を使う
    画像・動画・音楽素材を組み込む
  5. 第5回: テロップを入れる
    テキストオーバーレイの作り方
  6. 第6回: シーンを構成する
    Series / TransitionSeries でシーン切り替えとトランジション
  7. 第7回: GitHub Actions で自動レンダリング
    CI/CD で動画レンダリングを自動化する
  8. 第8回: 応用テクニック(準備中)
    GIF・静止画書き出し / FFmpeg カスタマイズ / Lambda / 3D

はじめに

前回の記事では、Remotion の基本的な仕組み(Composition、AbsoluteFill、Sequence)をまとめました。

www.randpy.tokyo

簡単におさらいすると、以下のイメージです。

  • Composition — 動画の設定(解像度、fps、長さ)を定義する
  • AbsoluteFill — 要素の位置(WHERE)を制御する
  • Sequence — 要素の表示タイミング(WHEN)を制御する

これらは動画の「骨組み」や「配置」を決めるものでした。しかし、実際のアニメーション(フェードイン、スライド、拡大縮小など)を作るには、フレームごとに値を変化させる仕組みが必要です。

それが今回のテーマである 「Hooks」 です。

この記事では、React Hooks の基本は理解している前提で、Remotion 固有の Hooks がどう使われているか、私が学んだことをまとめます。

Remotion が提供する Hooks

Hello World テンプレートで使われている hook は2つだけです。

// src/HelloWorld.tsx
const frame = useCurrentFrame();
const { durationInFrames, fps } = useVideoConfig();
hook 返すもの
useCurrentFrame() 今のフレーム番号(0, 1, 2, ... 149)
useVideoConfig() 動画の設定(fps, durationInFrames, width, height など)

フレームとは?

フレームは動画の 1コマ1コマ のことです。動画はパラパラ漫画のように、静止画を高速で切り替えて動いているように見せています。

// Root.tsx 
<Composition
  id="HelloWorld"
  component={HelloWorld}
  durationInFrames={150}
  fps={30}
 // 省略
/>

HelloWorld プロジェクトでは、Root.tsx で以下のように設定されています。(前回の記事も参照してください)

  • durationInFrames={150} — 全部で150コマ
  • fps={30} — 1秒あたり30コマ

つまり、150 ÷ 30 = 5秒の動画 です。

Remotion の仕組み

Remotion は内部で、フレーム番号を 0 → 1 → 2 → ... → 149 と変化させながら、全コンポーネントを繰り返し実行します。useCurrentFrame() の返り値もそれに合わせて変化するので、「フレームごとに見た目を変える」プログラムを書くことでアニメーションが実現できます。

Frame:  0    30    60    90   120   150
        |-----|-----|-----|-----|-----|
Time:   0秒   1秒   2秒   3秒   4秒   5秒

計算式: 秒 = frame / fps
       例: frame=90, fps=30 → 90/30 = 3秒

30fps なら、プレビュー時は1秒間に30回コンポーネントが再実行されます。レンダリング時は全150フレーム分が順番に処理されて動画ファイルになります。

Remotion には他にも hooks が用意されていますが、基本的にはこの2つだけで動画が作れます。

具体的な使い方

それぞれの hook を使った実践例を見てみましょう。

useCurrentFrame() の使い方

前述した通り、useCurrentFrameは、その瞬間のフレーム番号を返す hooksでした。

// 例1: フレーム番号をそのまま表示
const frame = useCurrentFrame();
return <div>現在のフレーム: {frame}</div>;
// 例2: フレーム番号で条件分岐
const frame = useCurrentFrame();
return (
  <div>
    {frame < 30 ? "イントロ" : frame < 90 ? "メイン" : "アウトロ"}
  </div>
);
// 例3: フレーム番号を使って回転角度を計算(1フレームごとに6度回転)
const frame = useCurrentFrame();
const rotation = frame * 6;
return <div style={{ transform: `rotate(${rotation}deg)` }}>回転</div>;

frame変数は、0から始まって durationInFrames で設定した値まで増えていきます(Hello Worldテンプレートでは、150まで)

このように、フレーム番号を使った表示をすることで、動きをつけることができます。

useVideoConfig() の使い方

前述した通り、useVideoConfigは、動画の設定(fps, durationInFrames, width, height など)を返す hooksでした。

// 例1: 動画の中央に要素を配置(width と height を使う)
const { width, height } = useVideoConfig();
return (
  <div style={{
    position: 'absolute',
    left: width / 2,
    top: height / 2,
    transform: 'translate(-50%, -50%)',
  }}>
    中央
  </div>
);
// 例2: 動画の最後のフレームを判定
const frame = useCurrentFrame();
const { durationInFrames } = useVideoConfig();
const isLastFrame = frame === durationInFrames - 1;
return <div>{isLastFrame ? "終了" : "再生中"}</div>;
// 例3: fps を使って「秒」単位で計算
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const seconds = frame / fps;  // フレーム番号を秒に変換
return <div>{seconds.toFixed(1)}秒経過</div>;

このように、useCurrentFrame() でアニメーションのタイミングを制御し、useVideoConfig() で動画の設定を取得して計算に使います。

HelloWorld.tsx を読み解く

ここまでの知識で、テンプレートのメインコンポーネントを読めるようになるはず。

// src/HelloWorld.tsx
export const HelloWorld: React.FC<...> = ({ titleText, titleColor, logoColor1, logoColor2 }) => {
  const frame = useCurrentFrame();                    // ← 今のフレーム番号を取得
  const { durationInFrames, fps } = useVideoConfig(); // ← 動画の設定を取得

  // frame を使ってアニメーションの値を計算
  const logoTranslationProgress = spring({ frame: frame - 25, fps, ... });
  const logoTranslation = interpolate(logoTranslationProgress, [0, 1], [0, -150]);
  const opacity = interpolate(frame, [durationInFrames - 25, durationInFrames - 15], [1, 0], ...);

  // 計算結果を CSS に反映
  return (
    <AbsoluteFill style={{ backgroundColor: "white" }}>
      <AbsoluteFill style={{ opacity }}>
        <AbsoluteFill style={{ transform: `translateY(${logoTranslation}px)` }}>
          <Logo logoColor1={logoColor1} logoColor2={logoColor2} />
        </AbsoluteFill>
        <Sequence from={35}>
          <Title titleText={titleText} titleColor={titleColor} />
        </Sequence>
        <Sequence from={75}>
          <Subtitle />
        </Sequence>
      </AbsoluteFill>
    </AbsoluteFill>
  );
};

流れを整理するとこうなります:

  1. useCurrentFrame() で今のフレーム番号を取得(Context から読み出し)
  2. useVideoConfig() で fps や総フレーム数を取得(同上)
  3. springinterpolate でフレーム番号から CSS の値を計算(これらは hook ではなく、ただの関数です
  4. 計算結果を CSS に反映して、その瞬間の「画面の見た目」を返します

これが30fpsなら1秒間に30回実行されます。 フレームが進むたびに Context の value が変わり、全コンポーネントが再実行され、新しい見た目を返します。パラパラ漫画と同じ原理ですね。

spring / interpolate は hook ではない

ここは最初勘違いしたのですが、springinterpolate は、中で基本 hook を呼んでもいません。フレーム番号を受け取って数値を返すだけの 純粋な計算関数 です。

const progress = spring({ frame, fps, config: { damping: 100 } });
const position = interpolate(progress, [0, 1], [0, -150]);

Pythonで言うと

interpolate は、Python の numpy.interp() や線形補間の関数と同じです。

# Python での線形補間
import numpy as np

frame = 30
opacity = np.interp(frame, [0, 60], [0, 1])  # ← frame=30 なら opacity=0.5

入力値(フレーム番号)を受け取って、指定した範囲で値をなめらかに変換する計算をしているだけです。Remotion の interpolate も同じで、「フレーム 0 のとき opacity は 0、フレーム 60 のとき opacity は 1、その間は線形補間」という計算を行っています。

interpolate の動作イメージ

interpolate(frame, [0, 60], [0, 1]) の動きを図で表すと、こんな感じです:

Input (frame):
0     10    20    30    40    50    60
|-----|-----|-----|-----|-----|-----|
                    ↓ interpolate
Output (opacity):
0    0.17  0.33  0.5  0.67  0.83   1
|-----|-----|-----|-----|-----|-----|

・frame=0  → opacity=0(透明)
・frame=30 → opacity=0.5(半透明)
・frame=60 → opacity=1(完全表示)

※ 0と60の間は線形補間で自動計算される

このように、入力範囲 [0, 60] を出力範囲 [0, 1] にマッピングして、中間の値も自動で計算してくれます。

spring とは

spring は、物理的なバネの動きを再現する関数です。interpolate が直線的に値を変化させるのに対し、spring は加速と減速を伴う自然な動きを作ります。

const frame = useCurrentFrame();
const { fps } = useVideoConfig();

const progress = spring({
  frame,
  fps,
  config: { damping: 100 }
});
  • 0 から 1 に変化:frame が進むにつれて、0 から 1 へなめらかに変化
  • 物理シミュレーション:実際のバネの動きを数式で再現しているので、自然な動きになる
  • damping で調整damping を小さくすると揺れが大きくなり、大きくすると揺れがなくなる

damping のパラメータ(デフォルト: 10)

  • damping: 10(デフォルト)→ ブルブル震えながら止まる(バウンスが大きい)
  • damping: 100 → なめらかに減速して止まる(バウンスがほぼない)
  • damping: 200 → バウンスが完全に消え、スッと止まる

HelloWorld.tsx では、Logo を上に移動させるときに spring を使っています。これにより、Logo が勢いよくスライドして、最後にスッと止まる自然な動きになります。

HelloWorld のアニメーション:spring と interpolate の組み合わせ

HelloWorld.tsx のコードを改めて見てみましょう。springinterpolate を組み合わせて、Logo を上に移動させながらフェードアウトしています。

// src/HelloWorld.tsx(抜粋)
const frame = useCurrentFrame();
const { durationInFrames, fps } = useVideoConfig();

// Logo の移動アニメーション
const logoTranslationProgress = spring({
  frame: frame - 25,  // 25フレーム目から開始
  fps,
  config: { damping: 100 },
});
const logoTranslation = interpolate(logoTranslationProgress, [0, 1], [0, -150]);

// 全体のフェードアウト
const opacity = interpolate(
  frame,
  [durationInFrames - 25, durationInFrames - 15],
  [1, 0],
  { extrapolateRight: "clamp" }
);

動きの流れ:

  1. frame 0〜24:Logo は静止(frame - 25 が負なので、spring は 0 を返す)
  2. frame 25〜spring が 0→1 に変化開始
    • logoTranslationProgress が 0→1 に変化(バネの動き)
    • interpolate でそれを 0→-150px にマッピング
    • Logo が下から上へスライド(Y軸方向に -150px 移動)
  3. frame 125〜140(最後の 25〜15 フレーム):全体がフェードアウト
    • opacity が 1→0 に変化
    • 画面全体が透明になっていく
Timeline (150フレーム、30fps = 5秒):
0 -------- 25 -------- 125 ---- 140 ---- 150
[  静止  ] [  Logo移動  ] [ フェードアウト ]
           ↑ spring       ↑ interpolate
           Y: 0→-150px    opacity: 1→0

なぜ spring と interpolate を両方使うのか?

  • spring:物理的な動き(スライド、回転など)に使うと自然
  • interpolate:透明度や色など、物理的な意味がない値の変化に使う

この例では、「Logo の移動」には spring を使って自然な動きにし、「フェードアウト」には interpolate を使って直線的に透明度を下げています。

子コンポーネントでの hook の使い方

HelloWorld.tsx だけでなく、子コンポーネントでもそれぞれ useCurrentFrame() を呼んでいますね。

// src/HelloWorld/Title.tsx
export const Title: React.FC<...> = ({ titleText, titleColor }) => {
  const videoConfig = useVideoConfig();
  const frame = useCurrentFrame();

  const words = titleText.split(" ");
  return (
    <h1>
      {words.map((t, i) => {
        const delay = i * 5;
        const scale = spring({
          fps: videoConfig.fps,
          frame: frame - delay,    // ← 単語ごとに遅延させている
          config: { damping: 200 },
        });
        return <span style={{ transform: `scale(${scale})` }}>{t}</span>;
      })}
    </h1>
  );
};
// src/HelloWorld/Subtitle.tsx
export const Subtitle: React.FC = () => {
  const frame = useCurrentFrame();
  const opacity = interpolate(frame, [0, 30], [0, 1]);
  return <div style={{ opacity }}>Edit src/Root.tsx and save to reload.</div>;
};

どのコンポーネントでも同じパターンです

  1. useCurrentFrame() でフレーム番号を取得
  2. そのフレーム番号を springinterpolate に渡してアニメーション値を計算
  3. CSS に反映

Sequence と hook の関係

<Sequence from={35}> の中にあるコンポーネントでは、useCurrentFrame()0 から数え直した値 を返します。

グローバルフレーム:     0 -------- 35 -------- 75 -------- 150
                     |           |           |           |
HelloWorld:          frame=0    frame=35    frame=75     frame=149
Title (Sequence from=35):       frame=0     frame=40     frame=114
Subtitle (Sequence from=75):                frame=0      frame=74

まとめ

ここまで見てきて分かったのは、Remotion の Hooks は思った以上にシンプル ということです。

使うのは基本的に2つだけ

  • useCurrentFrame() で今のフレーム番号を取得
  • useVideoConfig() で動画の設定(fps、長さ、解像度)を取得

そして、これらと interpolate()spring() を組み合わせることで、フェードイン、スライド、回転など、あらゆるアニメーションが作れます。

次は、interpolate()spring() を使った様々なアニメーションパターンを実践的に見ていきます!

www.randpy.tokyo




以上の内容はhttps://www.randpy.tokyo/entry/remotion-2-hooksより取得しました。
このページはhttp://font.textar.tv/のウェブフォントを使用してます

不具合報告/要望等はこちらへお願いします。
モバイルやる夫Viewer Ver0.14