🎬
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  ·  7 s  ·  H.264

AI向け説明 AI Description

`R-001` は「Load More」ボタン機能のデモ動画を生成する Remotion テンプレートです。 1920×1080 / 30fps / 7秒(210フレーム)で、次のシーン進行を自動で再現します: ①予備動作(ボタンの軽いホバーアニメーション) → ②ボタン押下(scale縮小) → ③ローディング(スピナー) → ④新カード追加(spring stagger) → ⑤ハイライト(青いグロー0.6秒) → ⑥完了状態(落ち着いて静止)。 状態遷移はすべてフレーム番号で管理し、`interpolate` / `spring` を使います(setTimeout 不使用)。 差し替えポイントは ITEMS 配列・clickFrame・batchSize・theme の4箇所のみです。

`R-001` is a Remotion template that generates a demo video for a "Load More" button feature. 1920×1080 / 30fps / 7s (210 frames), with this automatic scene progression: ① hover anticipation (subtle button spring) → ② button press (scale down) → ③ loading (spinner) → ④ new cards appear (spring stagger) → ⑤ highlight (blue glow for 0.6s) → ⑥ settled state (fade to rest). All transitions are frame-based using `interpolate` / `spring` — no setTimeout. The four swap points are: ITEMS array, clickFrame, batchSize, 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 { LoadMoreDemo, loadMoreDemoSchema } from './compositions/LoadMoreDemo';

// Shared theme — reuse across compositions
const defaultTheme = {
  bg:          '#f0f2f5',
  panel:       '#ffffff',
  border:      '#e5e7eb',
  accent:      '#3b82f6',
  accentLight: '#eff6ff',
  text:        '#111827',
  muted:       '#6b7280',
  radius:      12,
};

export const RemotionRoot: React.FC = () => (
  <>
    {/* ── Landscape 16:9 ────────────────────────────── */}
    <Composition
      id="LoadMoreDemo"
      component={LoadMoreDemo}
      schema={loadMoreDemoSchema}
      durationInFrames={210}
      fps={30}
      width={1920}
      height={1080}
      defaultProps={{
        initialCount:    4,
        batchSize:       3,
        clickFrame:      25,
        loadingFrames:   35,
        highlightFrames: 18,
        columns:         4,   // 横4列
        theme: defaultTheme,
      }}
    />

    {/* ── Vertical 4:5 (Instagram / X) ─────────────── */}
    <Composition
      id="LoadMoreDemoVertical"
      component={LoadMoreDemo}
      schema={loadMoreDemoSchema}
      durationInFrames={210}
      fps={30}
      width={1080}
      height={1350}
      defaultProps={{
        initialCount:    4,
        batchSize:       3,
        clickFrame:      25,
        loadingFrames:   35,
        highlightFrames: 18,
        columns:         2,   // 縦動画は2列推奨
        theme: defaultTheme,
      }}
    />
  </>
);
src/compositions/LoadMoreDemo.tsx
import React from 'react';
import {
  AbsoluteFill,
  interpolate,
  spring,
  useCurrentFrame,
  useVideoConfig,
} from 'remotion';
import { z } from 'zod';
import { Card } from '../components/Card';
import '../styles.css';

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

export const loadMoreDemoSchema = z.object({
  initialCount:    z.number().min(1).max(8),
  batchSize:       z.number().min(1).max(6),
  clickFrame:      z.number().min(5).max(60),
  loadingFrames:   z.number().min(15).max(60),
  highlightFrames: z.number().min(10).max(40),
  columns:         z.number().min(1).max(6),
  theme:           themeSchema,
});

type Props = z.infer<typeof loadMoreDemoSchema>;

// ─── Sample data (replace with your own) ───────────────────────────────────
const ALL_ITEMS = [
  { id: 1, badge: 'Motion',      title: 'Fade Scale Animation',   desc: 'Fade in with a natural scale entrance.' },
  { id: 2, badge: 'Interaction', title: 'Card Flip Effect',        desc: '3D card flip on hover or click.' },
  { id: 3, badge: 'Navigation',  title: 'Sliding Underline Tabs',  desc: 'Indicator glides to the active tab.' },
  { id: 4, badge: 'Layout',      title: 'Masonry Grid',            desc: 'Variable-height cards in CSS Grid.' },
  // ↓ new batch ──────────────────────────────────────────────────────────────
  { id: 5, badge: 'State',       title: 'Skeleton Pulse Rows',     desc: 'Shimmering placeholder for loading.' },
  { id: 6, badge: 'Form',        title: 'Floating Label Input',    desc: 'Label floats above the field on focus.' },
  { id: 7, badge: 'Motion',      title: 'Typewriter Effect',       desc: 'Text appears one character at a time.' },
];

// ─── Composition ────────────────────────────────────────────────────────────
export const LoadMoreDemo: React.FC<Props> = ({
  initialCount,
  batchSize,
  clickFrame,
  loadingFrames,
  highlightFrames,
  columns,
  theme,
}) => {
  const frame = useCurrentFrame();
  const { fps } = useVideoConfig();

  // ── Timeline milestones ──
  const pressFrame    = clickFrame + 4;              // button scale bottoms out
  const releaseFrame  = clickFrame + 9;              // button returns to normal
  const spinnerStart  = clickFrame + 10;             // spinner appears
  const revealFrame   = clickFrame + loadingFrames;  // first new card starts
  const highlightEnd  = revealFrame + highlightFrames;

  // ── Button animation ──
  const btnScale = interpolate(
    frame,
    [clickFrame - 12, clickFrame, pressFrame, releaseFrame],
    [1, 1.03, 0.95, 1],
    { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' }
  );
  const btnShadowOpacity = interpolate(
    frame,
    [clickFrame - 12, clickFrame, pressFrame, releaseFrame],
    [0.25, 0.45, 0.1, 0.25],
    { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' }
  );

  const isLoading  = frame >= spinnerStart  && frame < revealFrame;
  const isRevealed = frame >= revealFrame;
  const isAllDone  = frame >= revealFrame + (batchSize - 1) * 8 + 20;

  const initialItems = ALL_ITEMS.slice(0, initialCount);
  const newItems     = ALL_ITEMS.slice(initialCount, initialCount + batchSize);

  // ── Spinner rotation (frame-driven, no CSS animation) ──
  const spinDeg = ((frame - spinnerStart) / fps) * 360;

  return (
    <AbsoluteFill style={{ background: theme.bg, fontFamily: 'Inter, system-ui, -apple-system, sans-serif' }}>
      <div style={{
        display: 'flex',
        flexDirection: 'column',
        alignItems: 'center',
        justifyContent: 'center',
        height: '100%',
        padding: '60px 140px',
        gap: 36,
      }}>

        {/* ── Header ── */}
        <div style={{ textAlign: 'center' }}>
          <h2 style={{ fontSize: 40, fontWeight: 700, color: theme.text, margin: 0 }}>
            Featured Patterns
          </h2>
          <p style={{ fontSize: 22, color: theme.muted, margin: '8px 0 0' }}>
            Discover UI patterns for modern web apps
          </p>
        </div>

        {/* ── Card grid ── */}
        <div style={{
          display: 'grid',
          gridTemplateColumns: `repeat(${columns}, 1fr)`,
          gap: 24,
          width: '100%',
        }}>
          {/* Initial cards (always visible) */}
          {initialItems.map((item) => (
            <Card key={item.id} item={item} theme={theme} opacity={1} translateY={0} />
          ))}

          {/* New batch cards with entrance animation */}
          {newItems.map((item, i) => {
            const cardFrame  = revealFrame + i * 8;
            const cardSpring = spring({
              frame: frame - cardFrame,
              fps,
              config: { damping: 18, stiffness: 80, mass: 0.8 },
            });
            const opacity    = interpolate(cardSpring, [0, 1], [0, 1]);
            const translateY = interpolate(cardSpring, [0, 1], [36, 0]);

            const hlStart = cardFrame + 4;
            const hlProgress = hlStart < highlightEnd
              ? interpolate(frame, [hlStart, highlightEnd], [1, 0],
                  { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' })
              : 0;

            return (
              <Card
                key={item.id}
                item={item}
                theme={theme}
                opacity={opacity}
                translateY={translateY}
                highlight={hlProgress}
              />
            );
          })}
        </div>

        {/* ── Load More button ── */}
        {!isAllDone && (
          <button style={{
            display: 'flex',
            alignItems: 'center',
            justifyContent: 'center',
            gap: 14,
            padding: '22px 60px',
            background: theme.accent,
            color: '#fff',
            border: 'none',
            borderRadius: theme.radius,
            fontSize: 24,
            fontWeight: 600,
            cursor: 'default',
            transform: `scale(${btnScale})`,
            boxShadow: `0 6px 24px rgba(59,130,246,${btnShadowOpacity})`,
            fontFamily: 'inherit',
            opacity: isLoading ? 0.85 : 1,
          }}>
            {isLoading && (
              <div style={{
                width: 22,
                height: 22,
                border: '3px solid rgba(255,255,255,0.3)',
                borderTopColor: '#fff',
                borderRadius: '50%',
                transform: `rotate(${spinDeg}deg)`,
                flexShrink: 0,
              }} />
            )}
            {isLoading
              ? 'Loading\u2026'
              : `Load More (${ALL_ITEMS.length - initialCount})`
            }
          </button>
        )}

        {isAllDone && (
          <p style={{ fontSize: 22, color: theme.muted, margin: 0 }}>
            All items loaded
          </p>
        )}
      </div>
    </AbsoluteFill>
  );
};
src/components/Card.tsx
import React from 'react';

interface CardItem {
  id: number;
  badge: string;
  title: string;
  desc: string;
}

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

interface CardProps {
  item:        CardItem;
  theme:       Theme;
  opacity:     number;
  translateY:  number;
  highlight?:  number; // 0–1, fades from 1 to 0
}

export const Card: React.FC<CardProps> = ({
  item, theme, opacity, translateY, highlight = 0,
}) => {
  const glowAlpha   = highlight * 0.35;
  const borderColor = highlight > 0
    ? `rgba(59,130,246,${0.25 + highlight * 0.55})`
    : theme.border;
  const boxShadow = highlight > 0
    ? `0 0 0 4px rgba(59,130,246,${glowAlpha}), 0 4px 20px rgba(59,130,246,${glowAlpha * 0.6})`
    : '0 2px 8px rgba(0,0,0,0.06)';

  return (
    <div style={{
      background:    theme.panel,
      border:        `2px solid ${borderColor}`,
      borderRadius:  theme.radius,
      padding:       '28px 24px',
      display:       'flex',
      flexDirection: 'column',
      gap:           10,
      opacity,
      transform:     `translateY(${translateY}px)`,
      boxShadow,
    }}>
      <span style={{
        fontSize:      12,
        fontWeight:    700,
        color:         theme.accent,
        textTransform: 'uppercase',
        letterSpacing: '0.08em',
      }}>
        {item.badge}
      </span>
      <p style={{ fontSize: 20, fontWeight: 600, color: theme.text, margin: 0, lineHeight: 1.4 }}>
        {item.title}
      </p>
      <p style={{ fontSize: 15, color: theme.muted, margin: 0, lineHeight: 1.6 }}>
        {item.desc}
      </p>
    </div>
  );
};
src/styles.css
/* Global reset for Remotion canvas */
* {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

/*
 * Font strategy
 * ─────────────────────────────────────────────────────────────────────────
 * Default: system-ui (-apple-system / Segoe UI / etc.)
 *   → ネットワーク依存なし。Lambda / CI でも安全にレンダーできます。
 *
 * Inter を使いたい場合 (ローカル Remotion Studio のみ):
 *   下の @import のコメントを外してください。
 *   ただし Lambda / サーバーレスレンダーでは @remotion/google-fonts を使うか、
 *   Inter.woff2 を public/ に置いて staticFile() で読み込んでください。
 *
 * @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&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 LoadMoreDemo out/LoadMoreDemo.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 LoadMoreDemo out/R-001.mp4 --codec h264
# → out/R-001.mp4 (1920×1080 / 30fps / H.264)

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

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

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

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

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