🎬
Remotion テンプレート (R カテゴリ) — このページは /animations/* とページ構造が異なる例外カテゴリです。 ブラウザで動作するUIではなく、Remotion でレンダリングする mp4 動画テンプレートを提供します。 コードをコピーして新規プロジェクトに貼り付けるだけで即動きます。 Remotion Template (R category) — This page follows a different structure from /animations/* pages. It provides an mp4 video template rendered by Remotion, not an interactive browser UI. Copy the code into a new project and it works immediately.

ライブデモ Live Demo

以下はこのテンプレートから Remotion でレンダーした実 mp4 です。手元で同じ動画を再生成できます(→ Commands セクション参照)。

The video below was rendered from this template using Remotion. Regenerate it locally with the commands in the Commands section.

1920 × 1080  ·  30 fps  ·  10 s  ·  H.264

AI向け説明 AI Description

`R-004` は月次売上の棒グラフが左から1本ずつスタガーで伸び上がりながら数値がカウントアップするチャートリビールシーンを生成する Remotion テンプレートです。 1920×1080 / 30fps / 10秒(300フレーム)で、次のシーン進行を自動で再現します: ①ページタイトル「Monthly Revenue」が spring でフェードイン → ②Y軸ラベル・水平グリッドライン・X軸が interpolate でフェードイン → ③Jan($48,200)バーが spring でスライドアップしながら数値がカウントアップ → ④〜⑦ Feb・Mar・Apr・May も順に登場(各 barStagger フレーム差)→ ⑧Jun($89,500・ピーク・バイオレット)バーが登場 → ⑨コールアウト「Jan → Jun ↑ 85.7%」が spring でポップイン → ⑩全バー表示状態でホールド。 ピークバーのみ accent を `#7c3aed` に変えてバイオレット配色にし、値ラベルも同色にします。 差し替えポイントは BARS 配列・MAX_VALUE・theme の3箇所です。

`R-004` is a Remotion template that renders a bar chart reveal scene where monthly revenue bars grow upward one by one in staggered sequence with count-up number animations. 1920×1080 / 30fps / 10s (300 frames), with this automatic scene progression: ① Page title "Monthly Revenue" spring-fades in → ② Y-axis labels, horizontal gridlines, and X-axis fade in via interpolate → ③ Jan ($48,200) bar spring-slides up with number counting up → ④–⑦ Feb, Mar, Apr, May bars appear sequentially (barStagger frames apart) → ⑧ Jun ($89,500, peak, violet) bar appears → ⑨ callout "Jan → Jun ↑ 85.7%" pops in with spring → ⑩ all bars hold visible. Only the peak bar uses accent color `#7c3aed` (violet); its value label is also violet. The three swap points are: the BARS array, MAX_VALUE, and theme.

調整可能パラメータ Adjustable Parameters

実装 Implementation

以下のファイルを新規プロジェクトに配置してください。

Place the following files in a new project.

src/index.ts
import { registerRoot } from 'remotion';
import { RemotionRoot } from './Root';

registerRoot(RemotionRoot);
src/Root.tsx
import React from 'react';
import { Composition } from 'remotion';
import { BarChartReveal, barChartRevealSchema } from './compositions/BarChartReveal';

const defaultTheme = {
  bg:        '#f8fafc',
  panel:     '#ffffff',
  border:    '#e2e8f0',
  text:      '#0f172a',
  muted:     '#64748b',
  gridLine:  '#e2e8f0',
  barColor:  '#4f46e5',
  peakColor: '#7c3aed',
  radius:    8,
};

export const RemotionRoot: React.FC = () => (
  <>
    {/* ── Landscape 16:9 ──────────────────────────────── */}
    <Composition
      id="BarChartReveal"
      component={BarChartReveal}
      schema={barChartRevealSchema}
      durationInFrames={300}
      fps={30}
      width={1920}
      height={1080}
      defaultProps={{ barStagger: 30, countDuration: 50, theme: defaultTheme }}
    />

    {/* ── Vertical 9:16 (Stories / Reels) ─────────────── */}
    <Composition
      id="BarChartRevealVertical"
      component={BarChartReveal}
      schema={barChartRevealSchema}
      durationInFrames={300}
      fps={30}
      width={1080}
      height={1920}
      defaultProps={{ barStagger: 30, countDuration: 50, theme: defaultTheme }}
    />
  </>
);
src/compositions/BarChartReveal.tsx
import React from 'react';
import {
  AbsoluteFill,
  Easing,
  interpolate,
  spring,
  useCurrentFrame,
  useVideoConfig,
} from 'remotion';
import { z } from 'zod';
import { ChartBar } from '../components/ChartBar';
import '../styles.css';

// ─── Schemas ────────────────────────────────────────────────────────────────
const themeSchema = z.object({
  bg:        z.string(),
  panel:     z.string(),
  border:    z.string(),
  text:      z.string(),
  muted:     z.string(),
  gridLine:  z.string(),
  barColor:  z.string(),
  peakColor: z.string(),
  radius:    z.number(),
});

export const barChartRevealSchema = z.object({
  barStagger:    z.number().min(10).max(60),
  countDuration: z.number().min(20).max(90),
  theme:         themeSchema,
});

type Props = z.infer<typeof barChartRevealSchema>;

// ─── Chart data (replace with your own) ─────────────────────────────────────
export const MAX_VALUE = 100000;

export interface BarDef {
  month:  string;
  value:  number;
  isPeak: boolean;
}

const BARS: BarDef[] = [
  { month: 'Jan', value: 48200, isPeak: false },
  { month: 'Feb', value: 52800, isPeak: false },
  { month: 'Mar', value: 61400, isPeak: false },
  { month: 'Apr', value: 55900, isPeak: false },
  { month: 'May', value: 72300, isPeak: false },
  { month: 'Jun', value: 89500, isPeak: true  },
];

// Y-axis: gridlines at these percentages of MAX_VALUE (from the bottom)
const GRIDLINE_PCTS = [20, 40, 60, 80];
// Y-axis labels (top to bottom)
const Y_LABELS = [100000, 80000, 60000, 40000, 20000, 0];

const FIRST_BAR_FRAME = 45;
const X_AXIS_HEIGHT   = 60;  // px reserved for x-axis labels

// ─── Composition ─────────────────────────────────────────────────────────────
export const BarChartReveal: React.FC<Props> = ({ barStagger, countDuration, theme }) => {
  const frame = useCurrentFrame();
  const { fps } = useVideoConfig();

  // Title entrance (spring)
  const titleSpring  = spring({ frame, fps, config: { damping: 22, stiffness: 90, mass: 0.8 } });
  const titleOpacity = interpolate(titleSpring, [0, 0.5], [0, 1], { extrapolateRight: 'clamp' });
  const titleY       = interpolate(titleSpring, [0, 1], [24, 0]);

  // Subtitle fade-in
  const subOpacity = interpolate(frame, [10, 30], [0, 1], {
    extrapolateLeft:  'clamp',
    extrapolateRight: 'clamp',
    easing: Easing.out(Easing.quad),
  });

  // Axes + gridlines fade-in
  const gridOpacity = interpolate(frame, [20, 45], [0, 1], {
    extrapolateLeft:  'clamp',
    extrapolateRight: 'clamp',
    easing: Easing.out(Easing.quad),
  });

  // Per-bar appear frames
  const barsWithFrames = BARS.map((b, i) => ({
    ...b,
    appearFrame: FIRST_BAR_FRAME + i * barStagger,
  }));

  // Callout appears after last bar + 30 frames
  const calloutFrame    = FIRST_BAR_FRAME + (BARS.length - 1) * barStagger + 30;
  const calloutRelFrame = Math.max(0, frame - calloutFrame);
  const calloutSpring   = spring({
    frame: calloutRelFrame,
    fps,
    config: { damping: 18, stiffness: 80, mass: 1 },
  });
  const calloutOpacity = interpolate(calloutSpring, [0, 0.4], [0, 1], { extrapolateRight: 'clamp' });
  const calloutScale   = interpolate(calloutSpring, [0, 1], [0.88, 1]);

  return (
    <AbsoluteFill style={{
      background: theme.bg,
      fontFamily: 'Inter, system-ui, -apple-system, sans-serif',
    }}>

      {/* ── App bar ─────────────────────────────────────────────────────── */}
      <div style={{
        position: 'absolute', top: 0, left: 0, right: 0,
        height: 72,
        background: theme.panel,
        borderBottom: `1.5px solid ${theme.border}`,
        display: 'flex', alignItems: 'center',
        padding: '0 80px', gap: 40,
        zIndex: 1,
      }}>
        <span style={{ fontSize: 26, fontWeight: 800, color: theme.text, letterSpacing: '-0.03em' }}>
          ◆ MyApp
        </span>
        {['Dashboard', 'Reports', 'Settings'].map((label) => (
          <span key={label} style={{ fontSize: 18, color: theme.muted, fontWeight: 500 }}>
            {label}
          </span>
        ))}
      </div>

      {/* ── Page content ─────────────────────────────────────────────────── */}
      <div style={{
        position: 'absolute', top: 124, left: 80, right: 80, bottom: 52,
        display: 'flex', flexDirection: 'column', gap: 32,
      }}>

        {/* Title */}
        <div style={{
          opacity: titleOpacity,
          transform: `translateY(${titleY}px)`,
          flexShrink: 0,
        }}>
          <h1 style={{
            fontSize: 42, fontWeight: 800, color: theme.text,
            margin: 0, letterSpacing: '-0.02em',
          }}>
            Monthly Revenue
          </h1>
          <p style={{
            fontSize: 20, color: theme.muted, margin: '8px 0 0',
            opacity: subOpacity,
          }}>
            Jan – Jun 2025
          </p>
        </div>

        {/* Chart container */}
        <div style={{ flex: 1, position: 'relative' }}>

          {/* Y-axis labels (right-aligned, aligned to bars area height) */}
          <div style={{
            position: 'absolute',
            left: 0, width: 90,
            top: 0, bottom: X_AXIS_HEIGHT,
            opacity: gridOpacity,
          }}>
            {Y_LABELS.map((v) => (
              <div key={v} style={{
                position: 'absolute',
                right: 20,
                bottom: `${(v / MAX_VALUE) * 100}%`,
                transform: 'translateY(50%)',
                fontSize: 16,
                color: theme.muted,
                textAlign: 'right',
                whiteSpace: 'nowrap',
                fontVariantNumeric: 'tabular-nums',
              }}>
                {v === 0 ? '$0' : `$${v / 1000}k`}
              </div>
            ))}
          </div>

          {/* Chart body */}
          <div style={{ position: 'absolute', left: 100, right: 0, top: 0, bottom: 0 }}>

            {/* Bars area (above x-axis) — also contains gridlines */}
            <div style={{
              position: 'absolute',
              left: 0, right: 0,
              top: 0, bottom: X_AXIS_HEIGHT,
            }}>
              {/* Horizontal gridlines */}
              {GRIDLINE_PCTS.map((pct) => (
                <div key={pct} style={{
                  position: 'absolute',
                  left: 0, right: 0,
                  bottom: `${pct}%`,
                  height: 1.5,
                  background: theme.gridLine,
                  opacity: gridOpacity,
                }} />
              ))}

              {/* Bars row */}
              <div style={{
                position: 'absolute', inset: 0,
                display: 'flex', alignItems: 'flex-end',
                gap: 28, padding: '0 24px',
              }}>
                {barsWithFrames.map((bar) => (
                  frame >= bar.appearFrame ? (
                    <ChartBar
                      key={bar.month}
                      frame={frame}
                      fps={fps}
                      appearFrame={bar.appearFrame}
                      value={bar.value}
                      maxValue={MAX_VALUE}
                      isPeak={bar.isPeak}
                      countDuration={countDuration}
                      theme={theme}
                    />
                  ) : (
                    <div key={bar.month} style={{ flex: 1 }} />
                  )
                ))}
              </div>
            </div>

            {/* X-axis line */}
            <div style={{
              position: 'absolute',
              left: 0, right: 0,
              bottom: X_AXIS_HEIGHT,
              height: 1.5,
              background: '#94a3b8',
              opacity: gridOpacity,
            }} />

            {/* X-axis labels */}
            <div style={{
              position: 'absolute',
              left: 0, right: 0,
              bottom: 0, height: X_AXIS_HEIGHT,
              display: 'flex', padding: '0 24px',
              opacity: gridOpacity,
            }}>
              {BARS.map((bar) => (
                <div key={bar.month} style={{
                  flex: 1,
                  display: 'flex', alignItems: 'center', justifyContent: 'center',
                }}>
                  <span style={{ fontSize: 18, color: theme.muted, fontWeight: 500 }}>
                    {bar.month}
                  </span>
                </div>
              ))}
            </div>

            {/* Callout annotation */}
            <div style={{
              position: 'absolute',
              top: 0, right: 0,
              background: `${theme.peakColor}14`,
              border: `1.5px solid ${theme.peakColor}44`,
              borderRadius: 14,
              padding: '20px 28px',
              opacity: calloutOpacity,
              transform: `scale(${calloutScale})`,
              transformOrigin: 'top right',
            }}>
              <div style={{ fontSize: 17, color: theme.muted, marginBottom: 6 }}>
                Jan → Jun
              </div>
              <div style={{
                fontSize: 38, fontWeight: 800,
                color: theme.peakColor,
                letterSpacing: '-0.02em',
              }}>
                ↑ 85.7%
              </div>
            </div>

          </div>{/* /.chart-body */}
        </div>{/* /.chart-container */}

      </div>{/* /.page-content */}
    </AbsoluteFill>
  );
};
src/components/ChartBar.tsx
import React from 'react';
import { Easing, interpolate, spring } from 'remotion';

interface Theme {
  text:      string;
  barColor:  string;
  peakColor: string;
  radius:    number;
}

interface ChartBarProps {
  frame:         number;
  fps:           number;
  appearFrame:   number;
  value:         number;
  maxValue:      number;
  isPeak:        boolean;
  countDuration: number;
  theme:         Theme;
}

// Value label: "$48k", "$89k", etc.
function formatK(raw: number): string {
  return '$' + Math.round(raw / 1000) + 'k';
}

export const ChartBar: React.FC<ChartBarProps> = ({
  frame, fps, appearFrame,
  value, maxValue, isPeak,
  countDuration, theme,
}) => {
  const relFrame = frame - appearFrame;

  // ── Entrance: spring grow ─────────────────────────────────────────────────
  const growSpring = spring({
    frame: relFrame,
    fps,
    config: { damping: 18, stiffness: 60, mass: 1 },
  });

  const targetPct   = (value / maxValue) * 100;
  const barHeightPct = interpolate(growSpring, [0, 1], [0, targetPct]);

  // ── Label opacity ─────────────────────────────────────────────────────────
  const labelOpacity = interpolate(growSpring, [0, 0.25], [0, 1], { extrapolateRight: 'clamp' });

  // ── Count-up ──────────────────────────────────────────────────────────────
  const countProgress = interpolate(relFrame, [0, countDuration], [0, 1], {
    extrapolateLeft:  'clamp',
    extrapolateRight: 'clamp',
    easing:           Easing.out(Easing.quad),
  });
  const displayValue = formatK(value * countProgress);

  const accent     = isPeak ? theme.peakColor : theme.barColor;
  const accentLight = isPeak ? '#a78bfa' : '#818cf8';

  return (
    <div style={{
      flex:    1,
      height:  '100%',
      display: 'flex',
      alignItems: 'flex-end',
      position: 'relative',
    }}>

      {/* Value label above bar top */}
      <div style={{
        position:  'absolute',
        bottom:    `${barHeightPct}%`,
        left:      '50%',
        transform: 'translateX(-50%) translateY(-10px)',
        fontSize:  22,
        fontWeight: 700,
        color:     isPeak ? theme.peakColor : theme.text,
        opacity:   labelOpacity,
        whiteSpace: 'nowrap',
        fontVariantNumeric: 'tabular-nums',
      }}>
        {displayValue}
      </div>

      {/* Bar */}
      <div style={{
        width:        '100%',
        height:       `${barHeightPct}%`,
        background:   `linear-gradient(to top, ${accent} 0%, ${accentLight} 100%)`,
        borderRadius: `${theme.radius}px ${theme.radius}px 0 0`,
        boxShadow:    isPeak
          ? `0 -4px 20px ${theme.peakColor}44`
          : `0 -2px 12px ${theme.barColor}22`,
      }} />

    </div>
  );
};
src/styles.css
/* Global reset for Remotion canvas */
* {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

/*
 * Font strategy
 * ──────────────────────────────────────────────────────────────────────────
 * Default: system-ui — no network dependency, safe for Lambda / CI rendering.
 *
 * To use Inter in local Remotion Studio, uncomment the @import below.
 * For Lambda / serverless rendering, use @remotion/google-fonts or place
 * Inter.woff2 in public/ and reference it via staticFile().
 *
 * @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700;800&display=swap');
 */
remotion.config.ts
import { Config } from '@remotion/cli/config';

Config.setVideoImageFormat('jpeg');
Config.setOverwriteOutput(true);
package.json (scripts & dependencies to add)
{
  "scripts": {
    "remotion:preview": "remotion studio",
    "remotion:render":  "remotion render BarChartReveal out/BarChartReveal.mp4 --codec h264"
  },
  "dependencies": {
    "remotion":      "^4.0.0",
    "@remotion/cli": "^4.0.0",
    "react":         "^18.0.0",
    "react-dom":     "^18.0.0",
    "zod":           "^3.22.0"
  },
  "devDependencies": {
    "@types/react":     "^18.0.0",
    "@types/react-dom": "^18.0.0",
    "typescript":       "^5.0.0"
  }
}

コマンド Commands

① 新規プロジェクト作成(推奨) ① Scaffold a new project (recommended)
# Remotion 公式スキャフォルドから始める
npx create-video@latest
# プロンプトで "Hello World" か "Blank" を選択し、上記ファイルを上書きする
② 既存 React プロジェクトに追加 ② Add to an existing React project
npm install remotion @remotion/cli zod
③ プレビュー(Remotion Studio) ③ Preview in Remotion Studio
npx remotion studio
# ブラウザで http://localhost:3000 が開きます
# フレームをスクラブしてアニメーションを確認できます
④ mp4 レンダリング → DevSnips に配置 ④ Render to mp4 → place in DevSnips
# ── Step 1: Remotion プロジェクト内でレンダー
npx remotion render BarChartReveal out/R-004.mp4 --codec h264
# → out/R-004.mp4 (1920×1080 / 30fps / H.264)

# ── Step 2: DevSnips の assets/ に配置
#    (DevSnips リポジトリのルートで実行)
mkdir -p src/assets/remotion
cp /path/to/remotion-project/out/R-004.mp4 src/assets/remotion/R-004.mp4

# ── Step 3: poster 画像を生成(ffmpeg がある場合)
ffmpeg -i src/assets/remotion/R-004.mp4 \
       -ss 00:00:06.000 -frames:v 1 \
       src/assets/remotion/R-004.png

# ── Step 4: DevSnips をビルド & デプロイ
npm run build
# → dist/assets/remotion/R-004.mp4 として
#   https://devsnips.dev/assets/remotion/R-004.mp4 で配信されます
⑤ 縦動画 (9:16) をレンダリング ⑤ Render the vertical (9:16) version
# Root.tsx に BarChartRevealVertical が定義済み (1080×1920)
npx remotion render BarChartRevealVertical out/R-004-vertical.mp4 --codec h264

# poster 生成
ffmpeg -i out/R-004-vertical.mp4 \
       -ss 00:00:06.000 -frames:v 1 \
       out/R-004-vertical.png

注意とバリエーション Notes & Variations