ライブデモ Live Demo
CSS アニメーションで Remotion 動画の内容をシミュレートしています。
CSS animation simulating the Remotion video output.
AI向け説明 AI Description
`R-005` はコードが1文字ずつタイプされながら現れるコードエディター風シーンを生成する Remotion テンプレートです。 1920×1080 / 30fps / 12秒(360フレーム)で、次のシーン進行を自動で再現します: ①エディターウィンドウが spring でスライドインしてフェードイン(f0〜30)→ ②コードが1文字ずつタイピングアニメで出現しシンタックスハイライトが付く(f30〜330)→ ③タイピング完了後にカーソルが 0.5秒間隔でブリンク、紫のグローが出現(f330〜360)。 シンタックスハイライトは keyword(赤)・string(水色)・comment(グレー)・type(オレンジ)・jsx(緑)の5トークンタイプに対応。 差し替えポイントは `filename`・`code`(コード文字列)・`theme`(配色)の3箇所です。
`R-005` is a Remotion template that renders a code-editor scene where code appears character by character with syntax highlighting and a blinking cursor. 1920×1080 / 30fps / 12s (360 frames), with this automatic scene progression: ① Editor window spring-slides in and fades in (f0–30) → ② Code types character by character with syntax highlighting applied in real time (f30–330) → ③ After typing, cursor blinks at 0.5s intervals and a purple glow appears (f330–360). Syntax highlighting supports 5 token types: keyword (red), string (light blue), comment (gray), type (orange), jsx (green). The three swap points are: `filename`, `code` (the code string), and `theme` (colors).
調整可能パラメータ Adjustable Parameters
- fps — フレームレート(デフォルト: 30)Frame rate (default: 30)
- durationInFrames — 動画の長さ(デフォルト: 360 = 12秒)。コード量を増やす場合は typingEnd に合わせて延長Video duration (default: 360 = 12s). Extend to match typingEnd when adding more code
- filename — エディタータブに表示するファイル名(デフォルト: 'StatCard.tsx')Filename shown in the editor tab (default: 'StatCard.tsx')
- code — タイプさせるコード文字列。差し替えるだけで別のコードが登場するThe code string to type out. Swap this to change the displayed code entirely
- typingStart — タイピング開始フレーム(デフォルト: 30 = 1秒)。エディター登場アニメの後を想定Frame to start typing (default: 30 = 1s). Intended to follow the editor entrance animation
- typingEnd — タイピング終了フレーム(デフォルト: 330 = 11秒)。コード量に応じて調整Frame to finish typing (default: 330 = 11s). Adjust based on code length
- theme.bg — 動画背景色(デフォルト: #0d1117 GitHub Dark)Video background (default: #0d1117 GitHub Dark)
- theme.editorBg — エディター本体の背景色(デフォルト: #161b22)Editor body background (default: #161b22)
- theme.keyword / .string / .comment / .type / .jsx — 各トークンタイプのシンタックスカラーSyntax highlight colors per token type
- theme.cursor — カーソルの色(デフォルト: #f78166)Cursor color (default: #f78166)
実装 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 { TypingCodeReveal, typingCodeRevealSchema } from './compositions/TypingCodeReveal';
const defaultTheme = {
bg: '#0d1117',
editorBg: '#161b22',
tabBg: '#1c2128',
keyword: '#ff7b72',
string: '#a5d6ff',
comment: '#8b949e',
type: '#ffa657',
jsx: '#7ee787',
plain: '#e6edf3',
lineNum: '#30363d',
cursor: '#f78166',
};
export const RemotionRoot: React.FC = () => (
<>
{/* ── Landscape 16:9 ──────────────────────────────── */}
<Composition
id="TypingCodeReveal"
component={TypingCodeReveal}
schema={typingCodeRevealSchema}
durationInFrames={360}
fps={30}
width={1920}
height={1080}
defaultProps={{ theme: defaultTheme }}
/>
{/* ── Vertical 9:16 (Stories / Reels) ─────────────── */}
<Composition
id="TypingCodeRevealVertical"
component={TypingCodeReveal}
schema={typingCodeRevealSchema}
durationInFrames={360}
fps={30}
width={1080}
height={1920}
defaultProps={{ theme: defaultTheme }}
/>
</>
);
import React from 'react';
import {
AbsoluteFill,
interpolate,
spring,
useCurrentFrame,
useVideoConfig,
} from 'remotion';
import { z } from 'zod';
import '../styles.css';
// ─── Default code ────────────────────────────────────────────────────────────
const DEFAULT_CODE = `import React, { useState } from 'react';
interface CardProps {
title: string;
count: number;
}
export const StatCard: React.FC<CardProps> = ({
title,
count,
}) => {
const [active, setActive] = useState(false);
const cls = active ? 'card card--on' : 'card';
return (
<div className={cls} onClick={() => setActive(!active)}>
<h2>{title}</h2>
<span className="count">{count}</span>
</div>
);
};`;
// ─── Schema ──────────────────────────────────────────────────────────────────
const themeSchema = z.object({
bg: z.string().default('#0d1117'),
editorBg: z.string().default('#161b22'),
tabBg: z.string().default('#1c2128'),
keyword: z.string().default('#ff7b72'),
string: z.string().default('#a5d6ff'),
comment: z.string().default('#8b949e'),
type: z.string().default('#ffa657'),
jsx: z.string().default('#7ee787'),
plain: z.string().default('#e6edf3'),
lineNum: z.string().default('#30363d'),
cursor: z.string().default('#f78166'),
});
export const typingCodeRevealSchema = z.object({
filename: z.string().default('StatCard.tsx'),
code: z.string().default(DEFAULT_CODE),
typingStart: z.number().int().default(30),
typingEnd: z.number().int().default(330),
theme: themeSchema.default({}),
});
type Props = z.infer<typeof typingCodeRevealSchema>;
// ─── Tokenizer ───────────────────────────────────────────────────────────────
type TType = 'keyword' | 'string' | 'comment' | 'type' | 'jsx' | 'plain';
interface Token { type: TType; text: string }
const KEYWORDS = new Set([
'import', 'export', 'const', 'let', 'var', 'from',
'interface', 'return', 'type', 'default', 'function',
'async', 'await', 'if', 'else', 'true', 'false',
'null', 'undefined', 'React', 'FC', 'useState',
]);
function tokenize(code: string): Token[] {
const out: Token[] = [];
let i = 0;
while (i < code.length) {
// Line comment
if (code[i] === '/' && code[i + 1] === '/') {
let j = i + 2;
while (j < code.length && code[j] !== '\n') j++;
out.push({ type: 'comment', text: code.slice(i, j) });
i = j; continue;
}
// String (single / double quote)
if (code[i] === "'" || code[i] === '"' || code[i] === '`') {
const q = code[i];
let j = i + 1;
while (j < code.length && code[j] !== q && code[j] !== '\n') {
if (code[j] === '\\') j++;
j++;
}
if (j < code.length && code[j] === q) j++;
out.push({ type: 'string', text: code.slice(i, j) });
i = j; continue;
}
// JSX tag
if (code[i] === '<' && i + 1 < code.length && /[a-zA-Z/]/.test(code[i + 1])) {
let j = i + 1;
while (j < code.length && code[j] !== '>' && code[j] !== '\n') j++;
if (j < code.length && code[j] === '>') j++;
out.push({ type: 'jsx', text: code.slice(i, j) });
i = j; continue;
}
// Identifier / keyword / type
if (/[a-zA-Z_$]/.test(code[i])) {
let j = i + 1;
while (j < code.length && /[a-zA-Z0-9_$]/.test(code[j])) j++;
const word = code.slice(i, j);
out.push({
type: KEYWORDS.has(word) ? 'keyword' : /^[A-Z]/.test(word) ? 'type' : 'plain',
text: word,
});
i = j; continue;
}
out.push({ type: 'plain', text: code[i] });
i++;
}
return out;
}
function revealTokens(tokens: Token[], n: number): Token[] {
let rem = n;
const out: Token[] = [];
for (const tok of tokens) {
if (rem <= 0) break;
if (rem >= tok.text.length) { out.push(tok); rem -= tok.text.length; }
else { out.push({ type: tok.type, text: tok.text.slice(0, rem) }); rem = 0; }
}
return out;
}
// ─── Component ───────────────────────────────────────────────────────────────
export const TypingCodeReveal: React.FC<Props> = (props) => {
const {
filename = 'StatCard.tsx',
code = DEFAULT_CODE,
typingStart = 30,
typingEnd = 330,
theme,
} = props;
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
// Editor entrance spring
const entranceSpring = spring({ frame, fps, config: { damping: 20, stiffness: 80 } });
const editorY = interpolate(entranceSpring, [0, 1], [80, 0]);
const editorOpacity = interpolate(entranceSpring, [0, 1], [0, 1]);
// Typing progress
const progress = interpolate(frame, [typingStart, typingEnd], [0, 1], {
extrapolateLeft: 'clamp', extrapolateRight: 'clamp',
});
const charsToShow = Math.floor(progress * code.length);
// Cursor blink after typing
const typingDone = frame >= typingEnd;
const cursorVisible = typingDone
? Math.floor(frame / (fps / 2)) % 2 === 0
: true;
// Tokenize and reveal
const allTokens = tokenize(code);
const visibleTokens = revealTokens(allTokens, charsToShow);
const visibleText = visibleTokens.map(t => t.text).join('');
const lines = visibleText.split('\n');
const C: Record<TType, string> = {
keyword: theme?.keyword ?? '#ff7b72',
string: theme?.string ?? '#a5d6ff',
comment: theme?.comment ?? '#8b949e',
type: theme?.type ?? '#ffa657',
jsx: theme?.jsx ?? '#7ee787',
plain: theme?.plain ?? '#e6edf3',
};
const renderedLines = lines.map((lineText, li) => {
const lineToks = tokenize(lineText);
const isLast = li === lines.length - 1;
return (
<div key={li} style={{ display: 'flex', minHeight: 34, lineHeight: '34px' }}>
<span style={{
width: 64, textAlign: 'right', paddingRight: 20,
color: theme?.lineNum ?? '#30363d',
flexShrink: 0, userSelect: 'none',
}}>
{li + 1}
</span>
<span style={{ flex: 1, whiteSpace: 'pre' }}>
{lineToks.map((t, ti) => (
<span key={ti} style={{ color: C[t.type] }}>{t.text}</span>
))}
{isLast && cursorVisible && (
<span style={{
display: 'inline-block', width: 3, height: 26,
background: theme?.cursor ?? '#f78166',
marginLeft: 1, verticalAlign: 'text-bottom',
}} />
)}
</span>
</div>
);
});
// Post-typing glow
const glowOpacity = interpolate(frame, [typingEnd + 10, typingEnd + 30], [0, 0.5], {
extrapolateLeft: 'clamp', extrapolateRight: 'clamp',
});
return (
<AbsoluteFill style={{
background: theme?.bg ?? '#0d1117',
display: 'flex', alignItems: 'center', justifyContent: 'center',
}}>
{/* Editor window */}
<div style={{
transform: `translateY(${editorY}px)`,
opacity: editorOpacity,
width: 1200,
borderRadius: 14, overflow: 'hidden',
boxShadow: '0 40px 100px rgba(0,0,0,0.7)',
border: '1px solid #30363d',
}}>
{/* Title bar */}
<div style={{
background: theme?.tabBg ?? '#1c2128',
padding: '14px 20px',
display: 'flex', alignItems: 'center', gap: 10,
borderBottom: `1px solid ${theme?.lineNum ?? '#30363d'}`,
}}>
{(['#ff5f57', '#febc2e', '#28c840'] as const).map((c, i) => (
<div key={i} style={{ width: 14, height: 14, borderRadius: '50%', background: c }} />
))}
<div style={{
marginLeft: 16, background: theme?.editorBg ?? '#161b22',
borderRadius: 6, padding: '5px 18px',
fontSize: 22, color: theme?.plain ?? '#e6edf3',
fontFamily: '"Fira Code", Consolas, monospace',
}}>
{filename}
</div>
</div>
{/* Code body */}
<div style={{
background: theme?.editorBg ?? '#161b22',
padding: '20px 20px 20px 0',
fontSize: 22,
fontFamily: '"Fira Code", Consolas, monospace',
minHeight: 500,
}}>
{renderedLines}
</div>
</div>
{/* Post-typing glow */}
{glowOpacity > 0 && (
<div style={{
position: 'absolute', inset: 0, pointerEvents: 'none',
background: 'radial-gradient(ellipse at center, rgba(127,86,217,0.2) 0%, transparent 70%)',
opacity: glowOpacity,
}} />
)}
</AbsoluteFill>
);
};
/* 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 a monospace font in Remotion Studio, uncomment the @import below.
* For Lambda / serverless rendering, use @remotion/google-fonts or place
* the font file in public/ and reference it via staticFile().
*
* @import url('https://fonts.googleapis.com/css2?family=Fira+Code:wght@400;600&display=swap');
*/
import { Config } from '@remotion/cli/config';
Config.setVideoImageFormat('jpeg');
Config.setOverwriteOutput(true);
{
"scripts": {
"remotion:preview": "remotion studio",
"remotion:render": "remotion render TypingCodeReveal out/TypingCodeReveal.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 TypingCodeReveal out/R-005.mp4 --codec h264
# → out/R-005.mp4 (1920×1080 / 30fps / H.264)
# ── Step 2: DevSnips の assets/ に配置
# (DevSnips リポジトリのルートで実行)
mkdir -p src/assets/remotion
cp /path/to/remotion-project/out/R-005.mp4 src/assets/remotion/R-005.mp4
# ── Step 3: poster 画像を生成(ffmpeg がある場合)
ffmpeg -i src/assets/remotion/R-005.mp4 \
-ss 00:00:08.000 -frames:v 1 \
src/assets/remotion/R-005.png
# ── Step 4: DevSnips をビルド & デプロイ
npm run build
# → dist/assets/remotion/R-005.mp4 として
# https://devsnips.dev/assets/remotion/R-005.mp4 で配信されます
# Root.tsx に TypingCodeRevealVertical が定義済み (1080×1920)
npx remotion render TypingCodeRevealVertical out/R-005-vertical.mp4 --codec h264
# poster 生成
ffmpeg -i out/R-005-vertical.mp4 \
-ss 00:00:08.000 -frames:v 1 \
out/R-005-vertical.png
注意とバリエーション Notes & Variations
- コードの差し替え: `code` プロパティに任意のコード文字列を渡すだけで別のコードが表示されます。コード量が増えた場合は `typingEnd` と `durationInFrames` を調整してください(目安: 100文字あたり +70フレーム)。 Swapping the code: pass any code string to the `code` prop. If the code is longer, increase `typingEnd` and `durationInFrames` accordingly (roughly +70 frames per 100 characters).
- シンタックスハイライトの精度: 組み込みのトークナイザーは TSX / JS に最適化されたシンプルな実装です。ネストした JSX や複数行コメント(`/* */`)は対応していません。複雑なコードでは一部の色が付かない場合があります。 Syntax highlight accuracy: the built-in tokenizer is a simple implementation optimized for TSX/JS. It does not support nested JSX or block comments (`/* */`). Some tokens may not be colored for complex code structures.
- 配色のカスタマイズ: `theme` オブジェクトを差し替えることで配色を変えられます。例: VS Code の「One Dark Pro」風にするなら `bg: '#282c34'`, `editorBg: '#21252b'`, `keyword: '#c678dd'`, `string: '#98c379'`, `type: '#e5c07b'` など。 Theme customization: swap the `theme` object to change colors. For a VS Code "One Dark Pro" look, try `bg: '#282c34'`, `editorBg: '#21252b'`, `keyword: '#c678dd'`, `string: '#98c379'`, `type: '#e5c07b'`.
- タイピング速度の調整: `typingStart` と `typingEnd` の差を変えることで速度を変えられます。300フレーム(10秒)でゆっくり、150フレーム(5秒)で速めのタイピングになります。 Typing speed: adjust the gap between `typingStart` and `typingEnd`. 300 frames (10s) gives slow typing; 150 frames (5s) gives fast typing.
- 縦動画でのフォントサイズ: `TypingCodeRevealVertical`(1080×1920)を使う場合、`fontSize` を 28〜32px に拡大して1行あたりの文字数を減らすと読みやすくなります。 Vertical video font size: when using `TypingCodeRevealVertical` (1080×1920), increase `fontSize` to 28–32px and shorten line lengths for better readability.
- フォント: デフォルトは `system-ui`(ネットワーク依存なし)。ローカル Remotion Studio で Fira Code を使いたい場合は `styles.css` の `@import` コメントを外してください。Lambda 等のサーバーレスレンダーでは `@remotion/google-fonts` パッケージか `staticFile()` 参照を推奨します。 Fonts: defaults to `system-ui` (no network dependency). Uncomment the `@import` in `styles.css` for Fira Code in Remotion Studio. For Lambda or serverless rendering, use `@remotion/google-fonts` or a font referenced via `staticFile()`.
- Remotion Studio でのパラメータ調整: `typingCodeRevealSchema` を zod で定義しているため、Remotion Studio の GUI から `filename`・`code`・`typingStart`・`typingEnd`・`theme` をリアルタイムに変更できます。 Parameter tuning in Remotion Studio: because `typingCodeRevealSchema` is defined with zod, Remotion Studio generates a GUI panel for `filename`, `code`, `typingStart`, `typingEnd`, and `theme` — all adjustable in real time.