ライブデモ 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.
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
- fps — フレームレート(デフォルト: 30)Frame rate (default: 30)
- durationSec — 動画の長さ(デフォルト: 8秒 = 240フレーム)Video duration (default: 8s = 240 frames)
- stackGap — トースト間の縦方向スペース px(デフォルト: 12)Vertical gap between toasts in px (default: 12)
- TOASTS[].appearFrame — 各トーストが入場するフレーム番号Frame at which each toast slides in
- TOASTS[].exitFrame — 各トーストが退場するフレーム番号Frame at which each toast slides out
- TOASTS[].triggerFrame — ボタン押下アニメーションが始まるフレーム番号(appearFrame より前)Frame when the button press animation starts (before appearFrame)
- TOASTS[].type — 'success' / 'warning' / 'error'(色・アイコン・プログレスバー色を決定)'success' / 'warning' / 'error' — controls colors, icon, and progress bar
- theme.bg — 動画の背景色(デフォルト: #f8fafc)Video background color (default: #f8fafc)
- theme.panel — アプリバー・コンテンツカードの背景色(デフォルト: #ffffff)App bar and card background color (default: #ffffff)
- theme.radius — トースト・ボタンの角丸 px(デフォルト: 12)Corner radius for toasts and buttons in px (default: 12)
実装 Implementation
以下のファイルを新規プロジェクトに配置してください。
Place the following files in a new project.
import { registerRoot } from 'remotion';
import { RemotionRoot } from './Root';
registerRoot(RemotionRoot);
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 }}
/>
</>
);
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>
);
};
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>
);
};
/* 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');
*/
import { Config } from '@remotion/cli/config';
Config.setVideoImageFormat('jpeg');
Config.setOverwriteOutput(true);
{
"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
# Remotion 公式スキャフォルドから始める
npx create-video@latest
# プロンプトで "Hello World" か "Blank" を選択し、上記ファイルを上書きする
npm install remotion @remotion/cli zod
npx remotion studio
# ブラウザで http://localhost:3000 が開きます
# フレームをスクラブしてアニメーションを確認できます
# ── 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 で配信されます
# 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
- トースト内容の差し替え: `TOASTS` 配列の各オブジェクトを変更するだけで内容を入れ替えられます。`type`・`title`・`message`・各フレーム番号・`buttonLabel`・`buttonColor` の6フィールドが差し替え対象です。 Swapping toast content: change entries in the TOASTS array to customize the video. The six fields to swap are: `type`, `title`, `message`, frame numbers, `buttonLabel`, and `buttonColor`.
- トースト数の変更: TOASTS 配列の要素を増減するだけです。固定 Y 位置は `TOAST_H + stackGap` の倍数で自動計算されます。追加した分だけ durationInFrames を伸ばしてください(目安: トースト1枚あたり +55フレーム)。 Changing the number of toasts: add or remove items in the TOASTS array. Fixed Y positions auto-calculate from `TOAST_H + stackGap`. Extend durationInFrames proportionally (approx. +55 frames per additional toast).
- 動的スタック(後入り前出し): 本テンプレートはシンプルさを優先し固定 Y を使っています。本物のスタッキング(新しいトーストが上に積まれ古いものを押し下げる)を実装したい場合は、`toastY` をフレームごとに現在可視中のトースト数で計算し直してください。 Dynamic stacking (LIFO): this template uses fixed Y positions for simplicity. For real stacking where new toasts push older ones down, recompute `toastY` per frame based on the count of currently visible toasts above each one.
-
フォント: デフォルトは `system-ui`(ネットワーク依存なし)。ローカル Remotion Studio で Inter を使いたい場合は styles.css の `@import` コメントを外してください。Lambda 等のサーバーレスレンダーでは
@remotion/google-fontsパッケージか、public/にフォントファイルを置いてstaticFile()で参照する方法を推奨します。 Fonts: defaults tosystem-ui(no network dependency). Uncomment the@importin styles.css for Inter in Remotion Studio. For Lambda or serverless rendering, use the@remotion/google-fontspackage or place a font file inpublic/and reference it viastaticFile(). - Remotion Studio でのパラメータ調整: `toastDemoSchema` を zod で定義しているため、Remotion Studio の GUI から `stackGap` や `theme` をリアルタイムに変更できます。コードを書かなくてもカスタマイズ可能です。 Parameter tuning in Remotion Studio: because `toastDemoSchema` is defined with zod, Remotion Studio automatically generates a GUI panel where you can tweak `stackGap`, `theme`, and other props in real time without touching code.