🎬
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-003` は 4 枚の KPI スタットカードがスタガーで登場しながら数値がカウントアップするダッシュボード紹介シーンを生成する Remotion テンプレートです。 1920×1080 / 30fps / 10秒(300フレーム)で、次のシーン進行を自動で再現します: ①ページタイトル「Dashboard Overview」が spring でフェードイン → ②「Total Revenue($124,500)」カードが spring でスライドアップしながら数値がカウントアップ → ③「Active Users(8,340)」カードが同様に登場 → ④「Conversion Rate(3.7%)」カードが登場 → ⑤「Monthly Orders(2,847)」カードが登場 → ⑥全4カードが表示された状態でホールド。 各カードには左端のアクセントカラーストリップ・アイコン・ラベル・数値・トレンドバッジが含まれます。 数値カウントアップは ease-out quad で補間し、`interpolate` + `Easing.out(Easing.quad)` で実装しています。 差し替えポイントは STATS 配列と theme の 2 箇所のみです。

`R-003` is a Remotion template that renders a dashboard introduction scene where 4 KPI stat cards reveal in staggered sequence with number count-up animations. 1920×1080 / 30fps / 10s (300 frames), with this automatic scene progression: ① Page title "Dashboard Overview" spring-fades in → ② "Total Revenue ($124,500)" card spring-slides up with number counting up → ③ "Active Users (8,340)" card appears similarly → ④ "Conversion Rate (3.7%)" card appears → ⑤ "Monthly Orders (2,847)" card appears → ⑥ all 4 cards held visible. Each card includes a left-edge accent color strip, icon, label, large value, and trend badge. Count-up uses ease-out quad interpolation via `interpolate` + `Easing.out(Easing.quad)`. The two swap points are: the STATS array 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 { DashboardReveal, dashboardRevealSchema } from './compositions/DashboardReveal';

const defaultTheme = {
  bg:     '#f8fafc',
  panel:  '#ffffff',
  border: '#e2e8f0',
  text:   '#0f172a',
  muted:  '#64748b',
  radius: 16,
};

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

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

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

export const dashboardRevealSchema = z.object({
  cardStagger:   z.number().min(10).max(90),
  countDuration: z.number().min(20).max(120),
  theme:         themeSchema,
});

type Props = z.infer<typeof dashboardRevealSchema>;

// ─── KPI data (replace with your own) ───────────────────────────────────────
export type StatFormat = 'currency' | 'number' | 'percent';

export interface StatDef {
  id:          number;
  icon:        string;
  label:       string;
  value:       number;
  format:      StatFormat;
  trend:       string;
  trendUp:     boolean;
  accentColor: string;
}

const STATS: StatDef[] = [
  { id: 1, icon: '💰', label: 'Total Revenue',   value: 124500, format: 'currency', trend: '12.4%', trendUp: true,  accentColor: '#4f46e5' },
  { id: 2, icon: '👥', label: 'Active Users',    value: 8340,   format: 'number',  trend: '5.2%',  trendUp: true,  accentColor: '#0891b2' },
  { id: 3, icon: '📈', label: 'Conversion Rate', value: 3.7,    format: 'percent', trend: '0.8%',  trendUp: true,  accentColor: '#16a34a' },
  { id: 4, icon: '📦', label: 'Monthly Orders',  value: 2847,   format: 'number',  trend: '18.3%', trendUp: true,  accentColor: '#ea580c' },
];

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

  // Title entrance
  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 fades in slightly after title
  const subOpacity = interpolate(frame, [10, 30], [0, 1], {
    extrapolateLeft:  'clamp',
    extrapolateRight: 'clamp',
    easing: Easing.out(Easing.quad),
  });

  // Compute per-card appear frames from cardStagger
  const statsWithFrames = STATS.map((s, i) => ({
    ...s,
    appearFrame: 20 + i * cardStagger,
  }));

  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: 72, left: 0, right: 0, bottom: 0,
        padding: '52px 80px',
        display: 'flex', flexDirection: 'column', gap: 36,
      }}>

        {/* Page title */}
        <div style={{ opacity: titleOpacity, transform: `translateY(${titleY}px)` }}>
          <h1 style={{
            fontSize: 44, fontWeight: 800, color: theme.text,
            margin: 0, letterSpacing: '-0.02em',
          }}>
            Dashboard Overview
          </h1>
          <p style={{
            fontSize: 20, color: theme.muted, margin: '8px 0 0',
            opacity: subOpacity,
          }}>
            Q1 2025 — Updated just now
          </p>
        </div>

        {/* 2×2 stat card grid */}
        <div style={{
          display: 'grid',
          gridTemplateColumns: '1fr 1fr',
          gridTemplateRows: '1fr 1fr',
          gap: 28,
          flex: 1,
        }}>
          {statsWithFrames.map((stat) =>
            frame >= stat.appearFrame ? (
              <StatCard
                key={stat.id}
                frame={frame}
                fps={fps}
                appearFrame={stat.appearFrame}
                icon={stat.icon}
                label={stat.label}
                value={stat.value}
                format={stat.format}
                trend={stat.trend}
                trendUp={stat.trendUp}
                accentColor={stat.accentColor}
                countDuration={countDuration}
                theme={theme}
              />
            ) : (
              <div key={stat.id} />
            )
          )}
        </div>

      </div>
    </AbsoluteFill>
  );
};
src/components/StatCard.tsx
import React from 'react';
import { Easing, interpolate, spring } from 'remotion';
import type { StatFormat } from '../compositions/DashboardReveal';

interface Theme {
  panel:  string;
  border: string;
  text:   string;
  muted:  string;
  radius: number;
}

interface StatCardProps {
  frame:         number;
  fps:           number;
  appearFrame:   number;
  icon:          string;
  label:         string;
  value:         number;
  format:        StatFormat;
  trend:         string;
  trendUp:       boolean;
  accentColor:   string;
  countDuration: number;
  theme:         Theme;
}

// ── Value formatting ──────────────────────────────────────────────────────────
function formatValue(raw: number, format: StatFormat): string {
  if (format === 'currency') return '$' + Math.floor(raw).toLocaleString('en-US');
  if (format === 'percent')  return raw.toFixed(1) + '%';
  return Math.floor(raw).toLocaleString('en-US');
}

export const StatCard: React.FC<StatCardProps> = ({
  frame, fps, appearFrame,
  icon, label, value, format,
  trend, trendUp, accentColor,
  countDuration, theme,
}) => {
  const relFrame = frame - appearFrame;

  // ── Entrance: spring slide-up ─────────────────────────────────────────────
  const entranceSpring = spring({
    frame: relFrame,
    fps,
    config: { damping: 20, stiffness: 80, mass: 0.85 },
  });

  const slideY  = interpolate(entranceSpring, [0, 1], [40, 0]);
  const opacity = interpolate(entranceSpring, [0, 0.4], [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 = formatValue(value * countProgress, format);

  // ── Trend badge colors ────────────────────────────────────────────────────
  const trendBg    = trendUp ? '#dcfce7' : '#fee2e2';
  const trendColor = trendUp ? '#15803d' : '#991b1b';

  return (
    <div style={{
      opacity,
      transform:    `translateY(${slideY}px)`,
      background:   theme.panel,
      border:       `1.5px solid ${theme.border}`,
      borderRadius: theme.radius,
      padding:      '36px 44px 36px 48px',
      display:      'flex',
      flexDirection: 'column',
      gap:          20,
      boxShadow:    '0 4px 24px rgba(0,0,0,0.06)',
      position:     'relative',
      overflow:     'hidden',
    }}>

      {/* Left accent strip */}
      <div style={{
        position: 'absolute',
        top: 0, left: 0,
        width: 6, height: '100%',
        background: accentColor,
      }} />

      {/* Icon + label */}
      <div style={{ display: 'flex', alignItems: 'center', gap: 18 }}>
        <div style={{
          width: 52, height: 52,
          borderRadius: 14,
          background: accentColor + '1a',
          display: 'flex', alignItems: 'center', justifyContent: 'center',
          fontSize: 28,
          flexShrink: 0,
        }}>
          {icon}
        </div>
        <span style={{ fontSize: 20, color: theme.muted, fontWeight: 500 }}>
          {label}
        </span>
      </div>

      {/* Big value (count-up) */}
      <div style={{
        fontSize: 56, fontWeight: 800,
        color: theme.text,
        lineHeight: 1,
        letterSpacing: '-0.03em',
        fontVariantNumeric: 'tabular-nums',
      }}>
        {displayValue}
      </div>

      {/* Trend badge */}
      <div style={{
        display:      'inline-flex',
        alignItems:   'center',
        gap:          6,
        background:   trendBg,
        color:        trendColor,
        borderRadius: 20,
        padding:      '6px 16px',
        fontSize:     18,
        fontWeight:   600,
        alignSelf:    'flex-start',
      }}>
        {trendUp ? '↑' : '↓'} {trend}
      </div>

    </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 DashboardReveal out/DashboardReveal.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 DashboardReveal out/R-003.mp4 --codec h264
# → out/R-003.mp4 (1920×1080 / 30fps / H.264)

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

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

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

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

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