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

AI向け説明 AI Description

`R-002` は Success / Warning / Error の3種トースト通知が順番にスタックし自動消去されるシーンを生成する Remotion テンプレートです。 1920×1080 / 30fps / 8秒(240フレーム)で、次のシーン進行を自動で再現します: ①「Save Changes」ボタン押下(scale縮小) → ②Successトーストがspring で右からスライドイン → ③「Notify Team」ボタン押下 → ④Warningトーストが下にスタック → ⑤「Delete Record」ボタン押下 → ⑥Errorトーストがさらに下にスタック → ⑦各トーストが時差なしで右へスライドアウト → ⑧クリーン状態に戻りループ。 各トーストにはアイコン・タイトル・メッセージ・自動消去プログレスバーが含まれます。 状態遷移はすべてフレーム番号で管理し、`interpolate` / `spring` を使います(setTimeout 不使用)。 差し替えポイントは TOASTS 配列・theme の2箇所のみです。

`R-002` is a Remotion template that renders a scene of Success / Warning / Error toast notifications stacking in sequence and auto-dismissing. 1920×1080 / 30fps / 8s (240 frames), with this automatic scene progression: ① "Save Changes" button press (scale down) → ② Success toast spring-slides from right → ③ "Notify Team" button press → ④ Warning toast stacks below → ⑤ "Delete Record" button press → ⑥ Error toast stacks below that → ⑦ all toasts slide out right → ⑧ clean state, loop. Each toast includes an icon bubble, title, message, and an auto-dismiss progress bar. All transitions are frame-based using `interpolate` / `spring` — no setTimeout. The two swap points are: the TOASTS 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 { ToastDemo, toastDemoSchema } from './compositions/ToastDemo';

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

export const RemotionRoot: React.FC = () => (
  <>
    {/* ── Landscape 16:9 ──────────────────────────────── */}
    <Composition
      id="ToastDemo"
      component={ToastDemo}
      schema={toastDemoSchema}
      durationInFrames={240}
      fps={30}
      width={1920}
      height={1080}
      defaultProps={{ stackGap: 12, theme: defaultTheme }}
    />

    {/* ── Vertical 9:16 (Stories / TikTok) ───────────── */}
    <Composition
      id="ToastDemoVertical"
      component={ToastDemo}
      schema={toastDemoSchema}
      durationInFrames={240}
      fps={30}
      width={1080}
      height={1920}
      defaultProps={{ stackGap: 12, theme: defaultTheme }}
    />
  </>
);
src/compositions/ToastDemo.tsx
import React from 'react';
import {
  AbsoluteFill,
  interpolate,
  spring,
  useCurrentFrame,
  useVideoConfig,
} from 'remotion';
import { z } from 'zod';
import { Toast } from '../components/Toast';
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 toastDemoSchema = z.object({
  stackGap: z.number().min(4).max(32),
  theme:    themeSchema,
});

type Props = z.infer<typeof toastDemoSchema>;

// ─── Toast data (replace with your own) ─────────────────────────────────────
export type ToastType = 'success' | 'warning' | 'error';

export interface ToastDef {
  id:           number;
  type:         ToastType;
  title:        string;
  message:      string;
  appearFrame:  number;  // frame when toast slides in
  exitFrame:    number;  // frame when toast slides out
  triggerFrame: number;  // frame when button press animation starts
  buttonLabel:  string;
  buttonColor:  string;
}

const TOASTS: ToastDef[] = [
  {
    id: 1, type: 'success',
    title: 'Changes saved',
    message: 'Your profile has been updated successfully.',
    appearFrame: 35,  exitFrame: 190,
    triggerFrame: 25, buttonLabel: 'Save Changes', buttonColor: '#16a34a',
  },
  {
    id: 2, type: 'warning',
    title: 'Session expiring',
    message: 'Your session will expire in 5 minutes.',
    appearFrame: 90,  exitFrame: 205,
    triggerFrame: 80, buttonLabel: 'Notify Team',  buttonColor: '#d97706',
  },
  {
    id: 3, type: 'error',
    title: 'Upload failed',
    message: 'Could not reach the server. Please retry.',
    appearFrame: 145, exitFrame: 220,
    triggerFrame: 135, buttonLabel: 'Delete Record', buttonColor: '#dc2626',
  },
];

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

  // Height of one toast card (px) — adjust if you change toast padding/font size
  const TOAST_H = 108;

  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', 'Profile', 'Settings'].map((label) => (
          <span key={label} style={{ fontSize: 18, color: theme.muted, fontWeight: 500 }}>
            {label}
          </span>
        ))}
      </div>

      {/* ── Content panel (centered below app bar) ──────────────────────── */}
      <div style={{
        position: 'absolute', top: 72, left: 0, right: 0, bottom: 0,
        display: 'flex', alignItems: 'center', justifyContent: 'center',
        padding: '0 80px',
      }}>
        <div style={{
          background: theme.panel,
          border: `1.5px solid ${theme.border}`,
          borderRadius: theme.radius + 6,
          padding: '56px 64px',
          width: 700,
          display: 'flex', flexDirection: 'column', gap: 28,
          boxShadow: '0 4px 32px rgba(0,0,0,0.06)',
        }}>
          <div>
            <h2 style={{ fontSize: 36, fontWeight: 700, color: theme.text, margin: 0, lineHeight: 1.2 }}>
              Account Settings
            </h2>
            <p style={{ fontSize: 20, color: theme.muted, margin: '10px 0 0' }}>
              Manage your profile and notifications.
            </p>
          </div>

          {/* Action buttons — each press triggers a toast */}
          {TOASTS.map((t) => {
            const scale = interpolate(
              frame,
              [t.triggerFrame, t.triggerFrame + 5, t.triggerFrame + 10],
              [1, 0.93, 1],
              { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' },
            );
            return (
              <button key={t.id} style={{
                padding: '18px 32px',
                background: t.buttonColor,
                color: '#fff',
                border: 'none',
                borderRadius: theme.radius,
                fontSize: 20, fontWeight: 600,
                cursor: 'default',
                transform: `scale(${scale})`,
                fontFamily: 'inherit',
                textAlign: 'left',
                boxShadow: '0 2px 12px rgba(0,0,0,0.12)',
              }}>
                {t.buttonLabel}
              </button>
            );
          })}
        </div>
      </div>

      {/* ── Toast stack (top-right corner) ──────────────────────────────── */}
      {TOASTS.map((toast, i) => {
        if (frame < toast.appearFrame) return null;
        // Fixed vertical positions — adjust TOAST_H if needed
        const toastY = 100 + i * (TOAST_H + stackGap);
        return (
          <Toast
            key={toast.id}
            def={toast}
            frame={frame}
            fps={fps}
            toastWidth={480}
            toastY={toastY}
            theme={theme}
          />
        );
      })}

    </AbsoluteFill>
  );
};
src/components/Toast.tsx
import React from 'react';
import { interpolate, spring } from 'remotion';
import type { ToastDef, ToastType } from '../compositions/ToastDemo';

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

interface ToastProps {
  def:        ToastDef;
  frame:      number;
  fps:        number;
  toastWidth: number;
  toastY:     number;
  theme:      Theme;
}

// ── Per-type visual config ───────────────────────────────────────────────────
const TYPE_CFG: Record<ToastType, {
  bg: string; border: string; iconBg: string;
  icon: string; label: string; symbol: string; progress: string;
}> = {
  success: {
    bg: '#f0fdf4', border: '#86efac', iconBg: '#dcfce7',
    icon: '#16a34a', label: '#15803d', symbol: '✓', progress: '#22c55e',
  },
  warning: {
    bg: '#fffbeb', border: '#fde68a', iconBg: '#fef3c7',
    icon: '#d97706', label: '#92400e', symbol: '!', progress: '#f59e0b',
  },
  error: {
    bg: '#fef2f2', border: '#fca5a5', iconBg: '#fee2e2',
    icon: '#dc2626', label: '#991b1b', symbol: '✕', progress: '#ef4444',
  },
};

export const Toast: React.FC<ToastProps> = ({
  def, frame, fps, toastWidth, toastY, theme,
}) => {
  const { appearFrame, exitFrame, type, title, message } = def;
  const cfg = TYPE_CFG[type];

  // ── Entrance: spring slide from right ────────────────────────────────────
  const entranceSpring = spring({
    frame: frame - appearFrame,
    fps,
    config: { damping: 22, stiffness: 100, mass: 0.9 },
  });

  // ── Exit: spring slide back right ────────────────────────────────────────
  const isExiting  = frame >= exitFrame;
  const exitSpring = isExiting
    ? spring({ frame: frame - exitFrame, fps,
        config: { damping: 18, stiffness: 120, mass: 0.7 } })
    : 0;

  const slideX = interpolate(entranceSpring, [0, 1], [toastWidth + 60, 0])
               + interpolate(exitSpring,     [0, 1], [0, toastWidth + 80]);
  const opacity = interpolate(entranceSpring, [0, 0.4], [0, 1], { extrapolateRight: 'clamp' })
                * interpolate(exitSpring,     [0, 0.6], [1, 0], { extrapolateLeft:  'clamp' });

  // ── Auto-dismiss progress bar ────────────────────────────────────────────
  const duration  = exitFrame - appearFrame;
  const elapsed   = Math.max(0, Math.min(frame - appearFrame, duration));
  const barWidth  = (1 - elapsed / duration) * toastWidth;

  return (
    <div style={{
      position: 'absolute',
      top: toastY,
      right: 60,
      width: toastWidth,
      opacity,
      transform: `translateX(${slideX}px)`,
      background: cfg.bg,
      border: `2px solid ${cfg.border}`,
      borderRadius: theme.radius,
      padding: '20px 22px 16px',
      display: 'flex',
      gap: 16,
      alignItems: 'flex-start',
      boxShadow: '0 8px 40px rgba(0,0,0,0.10)',
      overflow: 'hidden',
    }}>

      {/* Icon bubble */}
      <div style={{
        width: 44, height: 44,
        borderRadius: '50%',
        background: cfg.iconBg,
        display: 'flex', alignItems: 'center', justifyContent: 'center',
        fontSize: 22, fontWeight: 800, color: cfg.icon,
        flexShrink: 0,
      }}>
        {cfg.symbol}
      </div>

      {/* Text */}
      <div style={{ flex: 1, minWidth: 0 }}>
        <p style={{ fontSize: 20, fontWeight: 700, color: cfg.label, margin: 0, lineHeight: 1.3 }}>
          {title}
        </p>
        <p style={{ fontSize: 16, color: '#374151', margin: '5px 0 0', lineHeight: 1.5 }}>
          {message}
        </p>
      </div>

      {/* Dismiss icon (decorative) */}
      <div style={{ fontSize: 18, color: '#9ca3af', flexShrink: 0 }}>✕</div>

      {/* Auto-dismiss progress bar */}
      <div style={{
        position: 'absolute',
        bottom: 0, left: 0,
        height: 4,
        width: barWidth,
        background: cfg.progress,
      }} />

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

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

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

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

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

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