R RenderComp
remotion svg animation react tutorial

RemotionでSVGアニメーションを作る完全ガイド

RemotionでSVGアニメーションを作る完全ガイド

映像制作の現場でSVGアニメーションが求められる場面は着実に増えている。ロゴのドロー演出、インフォグラフィックのデータバー、UIのアイコントランジション——これらはいずれも解像度非依存でありながら、コードで完全に制御できるという点でSVGが最も適したフォーマットだ。そしてReactベースの映像プログラミングライブラリであるRemotionは、SVGと極めて高い親和性を持っている。

RemotionではすべてのUIがReactコンポーネントとして記述される。<svg><path>はDOMの一部であり、そのままReactのJSXとして扱える。Remotionが提供するuseCurrentFrame()でフレーム番号を取得し、interpolate()で数値に変換し、SVG属性に渡す——この3ステップが、Remotionにおけるすべてのアニメーションの基本構造だ。


なぜRemotionとSVGは相性が良いのか

従来の映像制作ツールでSVGイラストをアニメーション化するには、タイムライン上でキーフレームを手作業で打つ必要がある。データが変わればキーフレームを打ち直し、スタイルが変わればエクスポートしてインポートし直す。この繰り返しは生産性を大きく損なう。

Remotionのアプローチは根本的に異なる。SVGの形状はデータの関数として定義される。棒グラフの幅はデータ配列の値から計算され、ルートマップのパスは座標配列から生成される。データが変わればコンポーネントを再レンダリングするだけで映像が更新される。バージョン管理も当然テキストファイルとして行える。

この設計思想は、繰り返し更新が発生するコンテンツ——週次レポート動画、顧客ごとに異なるオンボーディング映像、スポーツ中継の統計グラフィック——において特に強力に機能する。


インラインSVGの基本構造

まず最もシンプルな例として、フレームに応じて半径が拡大する円を作成してみよう。

import { useCurrentFrame, interpolate } from "remotion";

export const CircleGrow: React.FC = () => {
  const frame = useCurrentFrame();

  const radius = interpolate(frame, [0, 30], [0, 80], {
    extrapolateRight: "clamp",
  });

  return (
    <svg width={1920} height={1080} viewBox="0 0 1920 1080">
      <circle cx={960} cy={540} r={radius} fill="#6366f1" />
    </svg>
  );
};

useCurrentFrame()が返す値は0から始まる整数のフレーム番号だ。interpolate(frame, [0, 30], [0, 80])は「フレーム0のとき0、フレーム30のとき80」という線形補間を行う。extrapolateRight: "clamp"はフレーム30を超えても値が80を超えないようにする制御だ。

たったこれだけのコードで、フレームに連動する滑らかなアニメーションが完成する。


stroke-dashoffsetによるパス描画アニメーション

SVGアニメーションで最も有名なテクニックのひとつが「パスが自分自身を描くように見せる」演出だ。stroke-dasharraystroke-dashoffsetという2つのSVGプレゼンテーション属性を利用する。

stroke-dasharrayに値を設定すると、ストロークが破線になる。この値をパスの全長と同じ値に設定すると、ちょうど1つの長い線分でパス全体が覆われる状態になる。stroke-dashoffsetはその線分をパスに沿ってずらすオフセット量を指定する。オフセットをパス全長から0まで減少させていくと、線分が現れてくるように見える——これが「描画アニメーション」の仕組みだ。

import { useCurrentFrame, interpolate, Easing } from "remotion";

const PATH_LENGTH = 512; // ブラウザのDevToolsで測定

export const DrawingPath: React.FC = () => {
  const frame = useCurrentFrame();

  const dashOffset = interpolate(frame, [0, 60], [PATH_LENGTH, 0], {
    extrapolateRight: "clamp",
    easing: Easing.out(Easing.cubic),
  });

  return (
    <svg width={1920} height={1080} viewBox="0 0 800 400">
      <path
        d="M 100 200 C 200 50, 600 350, 700 200"
        fill="none"
        stroke="#f43f5e"
        strokeWidth={4}
        strokeDasharray={PATH_LENGTH}
        strokeDashoffset={dashOffset}
      />
    </svg>
  );
};

60フレーム(30fps換算で2秒)かけてパスが描かれる。イージングにはEasing.out(Easing.cubic)を使い、描き始めは速く、終盤は落ち着いて収まる動きにしている。

パス全長の測定方法

PATH_LENGTHの値はブラウザのコンソールで次のコードを実行して取得する。

const p = document.createElementNS("http://www.w3.org/2000/svg", "path");
p.setAttribute("d", "ここにd属性の値を貼る");
document.body.appendChild(p);
console.log(p.getTotalLength());

この値を定数としてコンポーネントファイルに記録しておく。


SVGトランスフォームのアニメーション

SVGのtransform属性はtranslate()rotate()scale()などを受け付ける。Reactでは文字列としてtransformプロップに渡す。

回転

const rotation = interpolate(frame, [0, 60], [0, 360], {
  extrapolateRight: "clamp",
});

<g transform={`rotate(${rotation}, 960, 540)`}>
  {/* 960, 540を中心に回転 */}
</g>

スケール(中心点を固定したい場合)

SVGはtransform-originプロパティがCSSと同様には機能しないため、中心点を固定してスケールする場合は「中心に平行移動→スケール→逆方向に平行移動」という変換順序で対応する。

const scale = interpolate(frame, [0, 30], [0, 1], {
  extrapolateRight: "clamp",
  easing: Easing.out(Easing.back(1.7)),
});

const cx = 960;
const cy = 540;

<g transform={`translate(${cx}, ${cy}) scale(${scale}) translate(${-cx}, ${-cy})`}>
  <circle cx={cx} cy={cy} r={80} fill="#6366f1" />
</g>

Easing.back(1.7)は目標値を少し超えてから戻るオーバーシュートを生み出し、登場演出に弾力感を加える。

spring()との組み合わせ

より有機的な動きが必要な場合はspring()と組み合わせると効果的だ。

import { spring, useCurrentFrame, useVideoConfig } from "remotion";

const { fps } = useVideoConfig();

const scale = spring({
  frame,
  fps,
  config: { stiffness: 200, damping: 15 },
  from: 0,
  to: 1,
});

spring()はバネ物理シミュレーションに基づく補間を行い、stiffness(硬さ)とdamping(減衰)を調整することでオーバーシュートの度合いを制御できる。


データドリブンな棒グラフアニメーション

SVGアニメーションの最も実用的な用途の一つがデータビジュアライゼーションだ。以下はデータ配列から棒グラフを生成し、各バーを時差なしで登場させる例だ。

import { useCurrentFrame, interpolate, Sequence, Easing } from "remotion";

const DATA = [
  { label: "Q1", value: 420 },
  { label: "Q2", value: 680 },
  { label: "Q3", value: 540 },
  { label: "Q4", value: 810 },
];

const MAX_VALUE = Math.max(...DATA.map((d) => d.value));
const BAR_HEIGHT = 60;
const BAR_GAP = 20;
const MAX_WIDTH = 700;

const Bar: React.FC<{ value: number; index: number; label: string }> = ({
  value,
  label,
}) => {
  const frame = useCurrentFrame(); // Sequence内では相対フレーム
  const targetWidth = (value / MAX_VALUE) * MAX_WIDTH;

  const width = interpolate(frame, [0, 40], [0, targetWidth], {
    extrapolateRight: "clamp",
    easing: Easing.out(Easing.cubic),
  });

  return (
    <g>
      <rect x={150} y={0} width={width} height={BAR_HEIGHT} rx={6} fill="#6366f1" />
      <text x={140} y={BAR_HEIGHT / 2} textAnchor="end" dominantBaseline="middle" fill="#fff" fontSize={20}>
        {label}
      </text>
    </g>
  );
};

export const BarChart: React.FC = () => {
  return (
    <svg width={1920} height={1080} viewBox="0 0 1000 400">
      {DATA.map((d, i) => (
        <Sequence key={d.label} from={i * 10} layout="none">
          <g transform={`translate(0, ${i * (BAR_HEIGHT + BAR_GAP)})`}>
            <Bar value={d.value} index={i} label={d.label} />
          </g>
        </Sequence>
      ))}
    </svg>
  );
};

ポイントは<Sequence from={i * 10}>だ。各バーのアニメーション開始を10フレームずつずらしている。<Sequence>内で呼ばれるuseCurrentFrame()はシーケンスの開始フレームからの相対値を返すため、interpolateの入力範囲は常に[0, 40]と書けば良い。


<Img>コンポーネントで外部SVGを使う

複雑なSVGファイルをインラインで記述するのではなく、public/フォルダに置いて読み込みたい場合は<Img>コンポーネントとstaticFile()を使う。

import { Img, staticFile } from "remotion";

export const LogoBrand: React.FC = () => {
  return (
    <Img
      src={staticFile("logo.svg")}
      style={{ width: 400, height: 200 }}
    />
  );
};

<Img>はRemotionが提供するコンポーネントで、アセットが完全に読み込まれるまでそのフレームのレンダリングを待機する。通常のHTML <img>タグではアセット未読み込み状態のフレームが混入するリスクがあるため、Remotion内ではかならず<Img>を使う。

ただし<Img>で読み込んだSVGの内部要素(個別のpathやgグループ)は操作できない。内部のパスを個別にアニメーションしたい場合は、SVGをJSXにインライン展開する必要がある。


チェックマークアイコンのアニメーション実装例

SaaSの成功状態演出でよく見られる「サークルが出現してチェックが描かれる」アニメーションの完成例を示す。

import { useCurrentFrame, interpolate, spring, useVideoConfig } from "remotion";

const CHECKMARK_LENGTH = 45;

export const AnimatedCheckmark: React.FC = () => {
  const frame = useCurrentFrame();
  const { fps } = useVideoConfig();

  const circleScale = spring({
    frame,
    fps,
    config: { stiffness: 180, damping: 14 },
    from: 0,
    to: 1,
    durationInFrames: 25,
  });

  const dashOffset = interpolate(frame, [20, 50], [CHECKMARK_LENGTH, 0], {
    extrapolateLeft: "clamp",
    extrapolateRight: "clamp",
  });

  return (
    <svg width={1920} height={1080} viewBox="0 0 200 200">
      <g transform="translate(100, 100)">
        <circle r={70} fill="#22c55e" transform={`scale(${circleScale})`} />
        <path
          d="M -25 0 L -8 18 L 30 -20"
          fill="none"
          stroke="white"
          strokeWidth={6}
          strokeLinecap="round"
          strokeLinejoin="round"
          strokeDasharray={CHECKMARK_LENGTH}
          strokeDashoffset={dashOffset}
        />
      </g>
    </svg>
  );
};

サークルはspring()で弾力感のある登場(フレーム0〜25)、チェックマークのストロークはフレーム20〜50で描画される。フレーム20〜25はサークルとチェックの演出が重なるため、2段階に見えず連続した動作として知覚される。


パフォーマンスの最適化

パスの複雑さを削減する

ノード数が多いパスはフレームごとに多くの計算コストがかかる。SVGエディタ(Inkscapeなど)の「パスの簡略化」機能でノード数を削減してからRemotionに持ち込む習慣をつけよう。

SVGフィルタは最小限に

<feGaussianBlur><feDropShadow>はピクセルベースのラスタライズを行うため、特に高解像度(1080p以上)で非常に重い。広い領域に適用する場合はCSSのfilterプロパティで代替できないか検討する。

--log=verboseでプロファイリング

Remotion CLIの--log=verboseオプションを付けてレンダリングすると、フレームごとの処理時間が表示される。特定フレームで時間が跳ね上がっている場合、そのフレームでのSVG描画が原因であることが多い。


RenderCompのSVGテンプレートを活用する

SVGアニメーションの仕組みを理解したうえで、実際の制作をより速く進めたいならRenderCompのテンプレートライブラリが役立つ。棒グラフ、円グラフ、パスドロー演出、アイコンアニメーションなど、本記事で解説したテクニックをプロダクションレベルで実装済みのRemotionコンポーネントを提供している。rendercomp.comでテンプレート一覧を確認してほしい。


FAQ

Q1: GSAPやAnime.jsをRemotionで使えますか?

requestAnimationFrameを前提とするタイムラインベースのアニメーションライブラリは、Remotionのフレームバイフレームレンダリングとは相性が悪く、期待通りに動作しません。RemotionではuseCurrentFrame()interpolate()spring()を使ってアニメーション状態をフレームの関数として表現するのが正しいアプローチです。

Q2: パスの全長(PATH_LENGTH)をコードから自動取得できますか?

開発時はブラウザのDevToolsでelement.getTotalLength()を実行して取得するのが最も簡単です。自動化したい場合はNode.jsのsvgpathライブラリ等を使ってビルド時に計算し、定数として出力するスクリプトを書く方法があります。

Q3: <Img>で読み込んだSVGの内部パスをアニメーションできますか?

できません。<Img>はSVGを不透明な画像として扱うため、内部のDOM要素には一切アクセスできません。内部要素を個別にアニメーションしたい場合は、SVGコードをJSXに直接インライン展開する必要があります。

Q4: CSSアニメーション(@keyframes)はRemotionで使えますか?

Remotionはフレームごとにスナップショットとしてレンダリングするため、@keyframesアニメーションの時間軸はRemotionのフレームタイムラインと同期しません。見た目上は動いても、実際のレンダリング結果では正しく再現されない場合があります。アニメーションは必ずuseCurrentFrame()を起点に記述してください。

Q5: 4K解像度でもSVGは綺麗に出力されますか?

はい。SVGは解像度非依存なので4K(3840×2160)でも完全にシャープに出力されます。コンポジションのwidthheightを4K解像度に設定し、SVGにviewBoxを定義しておくことで、Remotionが自動的に適切なスケールでレンダリングします。

Q6: SVGテキストのアニメーションでフォント埋め込みは必要ですか?

Remotionはシステムフォントを参照します。レンダリング環境(ローカルマシンやLambda)にフォントがインストールされていれば問題ありません。Lambda環境での日本語フォントが必要な場合は、@remotion/google-fontsパッケージまたは自前のフォントファイルをRemotionのフォント読み込みAPIで登録する方法があります。

Q7: 複雑なSVGイラストを多数のレイヤーに分けてアニメーションするベストプラクティスは?

関連する要素を<g>タグでグループ化し、グループレベルのtransformでまとめて動かすアプローチが効率的です。個別の要素アニメーションは本当に必要なものだけに限定することで、フレームあたりの計算量を抑えられます。

Q8: stroke-dashoffsetアニメーションが動かない場合のチェックポイントを教えてください。

最も多い原因はstroke-dasharrayの値がパス全長と一致していないことです。またfill="none"の指定を忘れると塗りが出てしまい意図した見た目になりません。加えてextrapolateLeft: "clamp"の指定がない場合、フレーム0以前の外挿値が意図しない位置になることがあります。上記3点をまず確認してください。

販売中

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

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

料金プランを見る →