ライブデモ 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-004` は月次売上の棒グラフが左から1本ずつスタガーで伸び上がりながら数値がカウントアップするチャートリビールシーンを生成する Remotion テンプレートです。 1920×1080 / 30fps / 10秒(300フレーム)で、次のシーン進行を自動で再現します: ①ページタイトル「Monthly Revenue」が spring でフェードイン → ②Y軸ラベル・水平グリッドライン・X軸が interpolate でフェードイン → ③Jan($48,200)バーが spring でスライドアップしながら数値がカウントアップ → ④〜⑦ Feb・Mar・Apr・May も順に登場(各 barStagger フレーム差)→ ⑧Jun($89,500・ピーク・バイオレット)バーが登場 → ⑨コールアウト「Jan → Jun ↑ 85.7%」が spring でポップイン → ⑩全バー表示状態でホールド。 ピークバーのみ accent を `#7c3aed` に変えてバイオレット配色にし、値ラベルも同色にします。 差し替えポイントは BARS 配列・MAX_VALUE・theme の3箇所です。
`R-004` is a Remotion template that renders a bar chart reveal scene where monthly revenue bars grow upward one by one in staggered sequence with count-up number animations. 1920×1080 / 30fps / 10s (300 frames), with this automatic scene progression: ① Page title "Monthly Revenue" spring-fades in → ② Y-axis labels, horizontal gridlines, and X-axis fade in via interpolate → ③ Jan ($48,200) bar spring-slides up with number counting up → ④–⑦ Feb, Mar, Apr, May bars appear sequentially (barStagger frames apart) → ⑧ Jun ($89,500, peak, violet) bar appears → ⑨ callout "Jan → Jun ↑ 85.7%" pops in with spring → ⑩ all bars hold visible. Only the peak bar uses accent color `#7c3aed` (violet); its value label is also violet. The three swap points are: the BARS array, MAX_VALUE, and theme.
調整可能パラメータ Adjustable Parameters
- fps — フレームレート(デフォルト: 30)Frame rate (default: 30)
- durationSec — 動画の長さ(デフォルト: 10秒 = 300フレーム)。バーを追加する場合は +1〜2秒が目安Video duration (default: 10s = 300 frames). Add ~1–2s per extra bar
- barStagger — バー間の登場フレーム差(デフォルト: 30 = 1秒)。増やすとゆっくり順に登場、減らすと素早く登場Frames between each bar's appearance (default: 30 = 1s). Increase for slower stagger, decrease for faster
- countDuration — 数値カウントアップにかけるフレーム数(デフォルト: 50 = 1.67秒)Frames for the count-up animation (default: 50 = 1.67s)
- BARS[].month — X軸のラベル文字列(例: 'Jan', 'Q1')X-axis label string (e.g. 'Jan', 'Q1')
- BARS[].value — 各バーの数値(カウントアップの最終値)Target value for each bar's count-up
- BARS[].isPeak — true にするとバイオレット配色に。ピークバーに使用Set to true for violet accent — use on the peak bar
- MAX_VALUE — Y軸の最大値。グリッドラインと棒高さの基準Y-axis maximum. Used as the reference for gridlines and bar heights
- theme.bg — 動画の背景色(デフォルト: #f8fafc)Video background color (default: #f8fafc)
- theme.barColor — 通常バーのグラデーション開始色(デフォルト: #4f46e5 インディゴ)Gradient start color for regular bars (default: #4f46e5 indigo)
- theme.peakColor — ピークバーのグラデーション開始色(デフォルト: #7c3aed バイオレット)Gradient start color for the peak bar (default: #7c3aed violet)
- theme.radius — バー上端の角丸 px(デフォルト: 8)Bar top-corner radius in px (default: 8)
実装 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 { BarChartReveal, barChartRevealSchema } from './compositions/BarChartReveal';
const defaultTheme = {
bg: '#f8fafc',
panel: '#ffffff',
border: '#e2e8f0',
text: '#0f172a',
muted: '#64748b',
gridLine: '#e2e8f0',
barColor: '#4f46e5',
peakColor: '#7c3aed',
radius: 8,
};
export const RemotionRoot: React.FC = () => (
<>
{/* ── Landscape 16:9 ──────────────────────────────── */}
<Composition
id="BarChartReveal"
component={BarChartReveal}
schema={barChartRevealSchema}
durationInFrames={300}
fps={30}
width={1920}
height={1080}
defaultProps={{ barStagger: 30, countDuration: 50, theme: defaultTheme }}
/>
{/* ── Vertical 9:16 (Stories / Reels) ─────────────── */}
<Composition
id="BarChartRevealVertical"
component={BarChartReveal}
schema={barChartRevealSchema}
durationInFrames={300}
fps={30}
width={1080}
height={1920}
defaultProps={{ barStagger: 30, countDuration: 50, theme: defaultTheme }}
/>
</>
);
import React from 'react';
import {
AbsoluteFill,
Easing,
interpolate,
spring,
useCurrentFrame,
useVideoConfig,
} from 'remotion';
import { z } from 'zod';
import { ChartBar } from '../components/ChartBar';
import '../styles.css';
// ─── Schemas ────────────────────────────────────────────────────────────────
const themeSchema = z.object({
bg: z.string(),
panel: z.string(),
border: z.string(),
text: z.string(),
muted: z.string(),
gridLine: z.string(),
barColor: z.string(),
peakColor: z.string(),
radius: z.number(),
});
export const barChartRevealSchema = z.object({
barStagger: z.number().min(10).max(60),
countDuration: z.number().min(20).max(90),
theme: themeSchema,
});
type Props = z.infer<typeof barChartRevealSchema>;
// ─── Chart data (replace with your own) ─────────────────────────────────────
export const MAX_VALUE = 100000;
export interface BarDef {
month: string;
value: number;
isPeak: boolean;
}
const BARS: BarDef[] = [
{ month: 'Jan', value: 48200, isPeak: false },
{ month: 'Feb', value: 52800, isPeak: false },
{ month: 'Mar', value: 61400, isPeak: false },
{ month: 'Apr', value: 55900, isPeak: false },
{ month: 'May', value: 72300, isPeak: false },
{ month: 'Jun', value: 89500, isPeak: true },
];
// Y-axis: gridlines at these percentages of MAX_VALUE (from the bottom)
const GRIDLINE_PCTS = [20, 40, 60, 80];
// Y-axis labels (top to bottom)
const Y_LABELS = [100000, 80000, 60000, 40000, 20000, 0];
const FIRST_BAR_FRAME = 45;
const X_AXIS_HEIGHT = 60; // px reserved for x-axis labels
// ─── Composition ─────────────────────────────────────────────────────────────
export const BarChartReveal: React.FC<Props> = ({ barStagger, countDuration, theme }) => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
// Title entrance (spring)
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 fade-in
const subOpacity = interpolate(frame, [10, 30], [0, 1], {
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp',
easing: Easing.out(Easing.quad),
});
// Axes + gridlines fade-in
const gridOpacity = interpolate(frame, [20, 45], [0, 1], {
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp',
easing: Easing.out(Easing.quad),
});
// Per-bar appear frames
const barsWithFrames = BARS.map((b, i) => ({
...b,
appearFrame: FIRST_BAR_FRAME + i * barStagger,
}));
// Callout appears after last bar + 30 frames
const calloutFrame = FIRST_BAR_FRAME + (BARS.length - 1) * barStagger + 30;
const calloutRelFrame = Math.max(0, frame - calloutFrame);
const calloutSpring = spring({
frame: calloutRelFrame,
fps,
config: { damping: 18, stiffness: 80, mass: 1 },
});
const calloutOpacity = interpolate(calloutSpring, [0, 0.4], [0, 1], { extrapolateRight: 'clamp' });
const calloutScale = interpolate(calloutSpring, [0, 1], [0.88, 1]);
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: 124, left: 80, right: 80, bottom: 52,
display: 'flex', flexDirection: 'column', gap: 32,
}}>
{/* Title */}
<div style={{
opacity: titleOpacity,
transform: `translateY(${titleY}px)`,
flexShrink: 0,
}}>
<h1 style={{
fontSize: 42, fontWeight: 800, color: theme.text,
margin: 0, letterSpacing: '-0.02em',
}}>
Monthly Revenue
</h1>
<p style={{
fontSize: 20, color: theme.muted, margin: '8px 0 0',
opacity: subOpacity,
}}>
Jan – Jun 2025
</p>
</div>
{/* Chart container */}
<div style={{ flex: 1, position: 'relative' }}>
{/* Y-axis labels (right-aligned, aligned to bars area height) */}
<div style={{
position: 'absolute',
left: 0, width: 90,
top: 0, bottom: X_AXIS_HEIGHT,
opacity: gridOpacity,
}}>
{Y_LABELS.map((v) => (
<div key={v} style={{
position: 'absolute',
right: 20,
bottom: `${(v / MAX_VALUE) * 100}%`,
transform: 'translateY(50%)',
fontSize: 16,
color: theme.muted,
textAlign: 'right',
whiteSpace: 'nowrap',
fontVariantNumeric: 'tabular-nums',
}}>
{v === 0 ? '$0' : `$${v / 1000}k`}
</div>
))}
</div>
{/* Chart body */}
<div style={{ position: 'absolute', left: 100, right: 0, top: 0, bottom: 0 }}>
{/* Bars area (above x-axis) — also contains gridlines */}
<div style={{
position: 'absolute',
left: 0, right: 0,
top: 0, bottom: X_AXIS_HEIGHT,
}}>
{/* Horizontal gridlines */}
{GRIDLINE_PCTS.map((pct) => (
<div key={pct} style={{
position: 'absolute',
left: 0, right: 0,
bottom: `${pct}%`,
height: 1.5,
background: theme.gridLine,
opacity: gridOpacity,
}} />
))}
{/* Bars row */}
<div style={{
position: 'absolute', inset: 0,
display: 'flex', alignItems: 'flex-end',
gap: 28, padding: '0 24px',
}}>
{barsWithFrames.map((bar) => (
frame >= bar.appearFrame ? (
<ChartBar
key={bar.month}
frame={frame}
fps={fps}
appearFrame={bar.appearFrame}
value={bar.value}
maxValue={MAX_VALUE}
isPeak={bar.isPeak}
countDuration={countDuration}
theme={theme}
/>
) : (
<div key={bar.month} style={{ flex: 1 }} />
)
))}
</div>
</div>
{/* X-axis line */}
<div style={{
position: 'absolute',
left: 0, right: 0,
bottom: X_AXIS_HEIGHT,
height: 1.5,
background: '#94a3b8',
opacity: gridOpacity,
}} />
{/* X-axis labels */}
<div style={{
position: 'absolute',
left: 0, right: 0,
bottom: 0, height: X_AXIS_HEIGHT,
display: 'flex', padding: '0 24px',
opacity: gridOpacity,
}}>
{BARS.map((bar) => (
<div key={bar.month} style={{
flex: 1,
display: 'flex', alignItems: 'center', justifyContent: 'center',
}}>
<span style={{ fontSize: 18, color: theme.muted, fontWeight: 500 }}>
{bar.month}
</span>
</div>
))}
</div>
{/* Callout annotation */}
<div style={{
position: 'absolute',
top: 0, right: 0,
background: `${theme.peakColor}14`,
border: `1.5px solid ${theme.peakColor}44`,
borderRadius: 14,
padding: '20px 28px',
opacity: calloutOpacity,
transform: `scale(${calloutScale})`,
transformOrigin: 'top right',
}}>
<div style={{ fontSize: 17, color: theme.muted, marginBottom: 6 }}>
Jan → Jun
</div>
<div style={{
fontSize: 38, fontWeight: 800,
color: theme.peakColor,
letterSpacing: '-0.02em',
}}>
↑ 85.7%
</div>
</div>
</div>{/* /.chart-body */}
</div>{/* /.chart-container */}
</div>{/* /.page-content */}
</AbsoluteFill>
);
};
import React from 'react';
import { Easing, interpolate, spring } from 'remotion';
interface Theme {
text: string;
barColor: string;
peakColor: string;
radius: number;
}
interface ChartBarProps {
frame: number;
fps: number;
appearFrame: number;
value: number;
maxValue: number;
isPeak: boolean;
countDuration: number;
theme: Theme;
}
// Value label: "$48k", "$89k", etc.
function formatK(raw: number): string {
return '$' + Math.round(raw / 1000) + 'k';
}
export const ChartBar: React.FC<ChartBarProps> = ({
frame, fps, appearFrame,
value, maxValue, isPeak,
countDuration, theme,
}) => {
const relFrame = frame - appearFrame;
// ── Entrance: spring grow ─────────────────────────────────────────────────
const growSpring = spring({
frame: relFrame,
fps,
config: { damping: 18, stiffness: 60, mass: 1 },
});
const targetPct = (value / maxValue) * 100;
const barHeightPct = interpolate(growSpring, [0, 1], [0, targetPct]);
// ── Label opacity ─────────────────────────────────────────────────────────
const labelOpacity = interpolate(growSpring, [0, 0.25], [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 = formatK(value * countProgress);
const accent = isPeak ? theme.peakColor : theme.barColor;
const accentLight = isPeak ? '#a78bfa' : '#818cf8';
return (
<div style={{
flex: 1,
height: '100%',
display: 'flex',
alignItems: 'flex-end',
position: 'relative',
}}>
{/* Value label above bar top */}
<div style={{
position: 'absolute',
bottom: `${barHeightPct}%`,
left: '50%',
transform: 'translateX(-50%) translateY(-10px)',
fontSize: 22,
fontWeight: 700,
color: isPeak ? theme.peakColor : theme.text,
opacity: labelOpacity,
whiteSpace: 'nowrap',
fontVariantNumeric: 'tabular-nums',
}}>
{displayValue}
</div>
{/* Bar */}
<div style={{
width: '100%',
height: `${barHeightPct}%`,
background: `linear-gradient(to top, ${accent} 0%, ${accentLight} 100%)`,
borderRadius: `${theme.radius}px ${theme.radius}px 0 0`,
boxShadow: isPeak
? `0 -4px 20px ${theme.peakColor}44`
: `0 -2px 12px ${theme.barColor}22`,
}} />
</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 BarChartReveal out/BarChartReveal.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 BarChartReveal out/R-004.mp4 --codec h264
# → out/R-004.mp4 (1920×1080 / 30fps / H.264)
# ── Step 2: DevSnips の assets/ に配置
# (DevSnips リポジトリのルートで実行)
mkdir -p src/assets/remotion
cp /path/to/remotion-project/out/R-004.mp4 src/assets/remotion/R-004.mp4
# ── Step 3: poster 画像を生成(ffmpeg がある場合)
ffmpeg -i src/assets/remotion/R-004.mp4 \
-ss 00:00:06.000 -frames:v 1 \
src/assets/remotion/R-004.png
# ── Step 4: DevSnips をビルド & デプロイ
npm run build
# → dist/assets/remotion/R-004.mp4 として
# https://devsnips.dev/assets/remotion/R-004.mp4 で配信されます
# Root.tsx に BarChartRevealVertical が定義済み (1080×1920)
npx remotion render BarChartRevealVertical out/R-004-vertical.mp4 --codec h264
# poster 生成
ffmpeg -i out/R-004-vertical.mp4 \
-ss 00:00:06.000 -frames:v 1 \
out/R-004-vertical.png
注意とバリエーション Notes & Variations
- データの差し替え: `BARS` 配列を編集するだけで月ラベル・数値・ピーク指定を変更できます。バー数を変える場合は `durationInFrames` も調整してください(目安: バー1本あたり +`barStagger`フレーム)。 Swapping data: edit the BARS array to change month labels, values, and peak flag. If you add or remove bars, adjust `durationInFrames` accordingly (approx. +`barStagger` frames per bar).
- Y軸のスケール: `MAX_VALUE` を変えるとグリッドラインと棒の高さが自動で再スケールされます。グリッドラインの位置 `GRIDLINE_PCTS` も必要に応じて変更してください(例: 最大値が 10,000 なら `[2500, 5000, 7500]` など)。 Y-axis scale: changing `MAX_VALUE` automatically rescales both gridlines and bar heights. Also update `GRIDLINE_PCTS` if needed (e.g. for MAX_VALUE=10000, use `[25, 50, 75]`).
- 値ラベルのフォーマット: `formatK()` 関数を変更することでラベル表示を変えられます。例: `$48,200` のフル表示にするなら `'$' + Math.round(raw).toLocaleString('en-US')` に変更。 Value label format: modify the `formatK()` function to change how values are displayed. For example, use `'$' + Math.round(raw).toLocaleString('en-US')` for full dollar amounts like $48,200.
- コールアウトのテキスト: `BarChartReveal.tsx` 内のコールアウト JSX を直接編集してください。「Jan → Jun」の比較対象や「85.7%」の数値はデータに合わせて変更します。 Callout text: edit the callout JSX directly in `BarChartReveal.tsx`. Update "Jan → Jun" and "85.7%" to match your actual data comparison.
- 縦動画でのレイアウト: `BarChartRevealVertical`(1080×1920)を使う場合、バーの本数を4本以下に絞るとラベルが読みやすくなります。`fontSize` を拡大し、`padding` も広めに取ってください。 Vertical video layout: when using `BarChartRevealVertical` (1080×1920), limit bars to 4 or fewer for readable labels. Scale up `fontSize` and increase padding accordingly.
- フォント: デフォルトは `system-ui`(ネットワーク依存なし)。ローカル Remotion Studio で Inter を使いたい場合は `styles.css` の `@import` コメントを外してください。Lambda 等のサーバーレスレンダーでは `@remotion/google-fonts` パッケージか `staticFile()` 参照を推奨します。 Fonts: defaults to `system-ui` (no network dependency). Uncomment the `@import` in `styles.css` for Inter in Remotion Studio. For Lambda or serverless rendering, use `@remotion/google-fonts` or a font file referenced via `staticFile()`.
- Remotion Studio でのパラメータ調整: `barChartRevealSchema` を zod で定義しているため、Remotion Studio の GUI から `barStagger`・`countDuration`・`theme` をリアルタイムに変更できます。コードを書かなくてもタイミングや配色をカスタマイズ可能です。 Parameter tuning in Remotion Studio: because `barChartRevealSchema` is defined with zod, Remotion Studio generates a GUI panel where you can tweak `barStagger`, `countDuration`, and `theme` in real time without touching code.