ライブデモ 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-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
- fps — フレームレート(デフォルト: 30)Frame rate (default: 30)
- durationSec — 動画の長さ(デフォルト: 7秒 = 210フレーム)Video duration (default: 7s = 210 frames)
- initialCount — 初期表示カード数(デフォルト: 4)Initial card count (default: 4)
- batchSize — 追加バッチのカード数(デフォルト: 3)New cards per batch (default: 3)
- clickFrame — ボタン押下が始まるフレーム番号(デフォルト: 25)Frame at which button press starts (default: 25)
- loadingFrames — ローディング表示のフレーム数(デフォルト: 35 ≈ 1.2秒)Duration of loading spinner in frames (default: 35 ≈ 1.2s)
- highlightFrames — ハイライトの持続フレーム数(デフォルト: 18 = 0.6秒)Highlight glow duration in frames (default: 18 = 0.6s)
- theme.bg — 動画の背景色(デフォルト: #f0f2f5)Background color (default: #f0f2f5)
- theme.accent — ボタン・バッジのアクセントカラー(デフォルト: #3b82f6)Accent color for button and badges (default: #3b82f6)
- theme.radius — カード・ボタンの角丸(デフォルト: 12px)Corner radius for cards and button (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 { 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,
}}
/>
</>
);
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>
);
};
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>
);
};
/* 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');
*/
import { Config } from '@remotion/cli/config';
Config.setVideoImageFormat('jpeg');
Config.setOverwriteOutput(true);
{
"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
# 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 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 で配信されます
# 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
- 実データへの差し替え: `ALL_ITEMS` 配列の各オブジェクトを変更するだけで、デモに表示するカード内容を完全に入れ替えられます。badge・title・desc の3フィールドのみが必須です。 Swapping real data: replace entries in the `ALL_ITEMS` array to change the cards shown in the video. Only the three fields badge, title, and desc are required.
- 押下→ローディング間のリアリティ: `clickFrame` と `loadingFrames` のバランスが重要です。押下が速すぎると不自然に感じます。目安: clickFrame=25(F0から約0.8秒)、loadingFrames=35(約1.2秒)が自然です。 Making press-to-loading feel real: balance `clickFrame` and `loadingFrames` carefully. Clicking too early feels unnatural. Recommended starting values: clickFrame=25 (~0.8s from start), loadingFrames=35 (~1.2s of loading).
-
縦動画 (1080×1350 / 4:5): Root.tsx に `LoadMoreDemoVertical` Composition が定義済みです。
columns: 2を設定しているので、縦幅が狭いキャンバスでもカードが適切な大きさで表示されます。横動画は `columns: 4`、縦動画は `columns: 2` が基本値です。 Vertical video (1080×1350 / 4:5):LoadMoreDemoVerticalis already defined in Root.tsx withcolumns: 2, which prevents cards from becoming too small on a narrow canvas. Usecolumns: 4for landscape andcolumns: 2for vertical as a baseline. -
フォント: デフォルトは `system-ui`(ネットワーク依存なし)。ローカル Remotion Studio で Inter を使いたい場合は styles.css の `@import` コメントを外してください。Lambda 等のサーバーレスレンダーでは
@remotion/google-fontsパッケージか、public/にフォントファイルを置いてstaticFile()で参照する方法を推奨します。 Fonts: defaults tosystem-ui(no network dependency). For Inter in local Remotion Studio, uncomment the@importline in styles.css. For Lambda or serverless rendering, use the@remotion/google-fontspackage or place a font file inpublic/and reference it viastaticFile(). - Remotion Studio でのパラメータ調整: `loadMoreDemoSchema` を zod で定義しているため、Remotion Studio の GUI でスライダーからリアルタイムに `clickFrame` 等を変更できます。コードを書かなくてもタイミング調整が可能です。 Adjusting parameters in Remotion Studio: because `loadMoreDemoSchema` is defined with zod, Remotion Studio automatically generates a GUI panel where you can tweak `clickFrame` and other values in real time without editing code.