
Remotion
- 12 installs
- 32 repo stars
- Updated February 13, 2026
- ceeon/remotion-skill
Generates Remotion animation code from a user's ASCII sketch or text description and renders it to a GIF, reusing a bundled template library.
About
An animated-diagram factory that detects its bundled Remotion environment, matches a description to existing templates, adapts or builds a composition, then renders to GIF. A developer uses it to produce flowchart and logic-diagram animations without setup.
- Ships a ready Remotion environment with a reusable case-template library
- Workflow: describe, match template, register, render GIF, export
Remotion by the numbers
- 12 all-time installs (skills.sh)
- Ranked #1,038 of 1,335 Generative Media skills by installs in the Skillselion catalog
- Data as of Jul 28, 2026 (Skillselion catalog sync)
npx skills add https://github.com/ceeon/remotion-skill --skill remotionAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 12 |
|---|---|
| repo stars | ★ 32 |
| Last updated | February 13, 2026 |
| Repository | ceeon/remotion-skill ↗ |
What it does
Generates Remotion animation code from a user's ASCII sketch or text description and renders it to a GIF, reusing a bundled template library.
Files
Remotion 动画逻辑图 Skill
核心工作流
检测环境 → 用户描述 → 匹配现有模板 → 适配/新建 → 注册 → 渲染 GIF → 导出核心原则:优先复用现有模板,不要从零新建。
用户不需要搭环境、不需要懂代码。Skill 自带完整运行环境。
目录结构
skills/remotion/
├── SKILL.md # 本文件
├── package.json # 依赖配置(已安装)
├── tsconfig.json # TypeScript 配置
├── node_modules/ # 依赖(已安装)
├── src/
│ └── index.tsx # 入口文件(注册所有 Composition)
├── 案例库/ # 现有模板(可直接渲染)
│ ├── 循环流程图/
│ ├── 莫兰迪卡片网格/
│ ├── 可爱流程图/
│ ├── 对比流程图/
│ ├── 技能流程图/
│ ├── 终端流程图/
│ ├── 人物卡片/
│ ├── 时间线/
│ ├── 代码展示/
│ └── 饼图/
├── 脚本库/
│ ├── check-env.sh # 环境自检
│ └── init-project.sh # 初始化(已废弃,环境已内置)
└── 设计规范/
└── 动画设计规范.md执行步骤
当用户要求生成动画逻辑图时,按以下步骤执行:
第 1 步:检测 Remotion 环境
检查 skills/remotion/ 目录是否存在:
- 不存在 → 自动克隆仓库到 skills 目录下,然后
npm install - 存在但无 node_modules → 自动
npm install - 一切就绪 → 进入下一步
# 检查目录是否存在
ls skills/remotion/package.json
# 如果不存在,克隆仓库
git clone https://github.com/Ceeon/remotion-skill.git skills/remotion
cd skills/remotion && npm install
# 如果存在但无 node_modules
cd skills/remotion && npm install
# 一切就绪,检查环境
bash 脚本库/check-env.sh第 2 步:理解用户需求
用户会提供以下任意一种输入:
- ASCII 草图:用文字画出大概的布局和逻辑
- 文字描述:自然语言描述想要的动画
- 参考案例:指定案例库中的某个模板
第 3 步:匹配现有模板(优先)
先看案例库,不要直接新建。
1. 浏览 案例库/ 下所有子目录,读取每个模板的 .tsx 文件 2. 找到与用户需求最接近的模板 3. 判断匹配度:
- 高匹配(类型相同,只需改文字/颜色)→ 复制模板,修改内容
- 中匹配(类型相近,需调整布局/动画)→ 基于模板改造
- 无匹配(案例库没有类似的)→ 才从零新建
案例库现有模板:
├── 循环流程图/ → 循环关系、闭环流程
├── 莫兰迪卡片网格/ → 多卡片并列展示
├── 可爱流程图/ → 轻松风格的步骤图
├── 对比流程图/ → A vs B 对比
├── 技能流程图/ → 技能/能力展示
├── 终端流程图/ → 代码/终端风格
├── 人物卡片/ → 人物介绍
├── 时间线/ → 时间轴
├── 代码展示/ → 代码演示
└── 饼图/ → 数据可视化第 4 步:生成/适配组件代码
基于模板适配或从零新建,在 案例库/ 下创建新目录,写入 .tsx 组件文件。
代码规范:
- 画布尺寸:800×600(默认)
- fps:30
- 使用
frame % totalFrames实现循环 - 动画使用
spring()弹性效果 - 动画结尾留 60 帧静止缓冲(防闪烁)
- 字体:
PingFang SC, Microsoft YaHei, sans-serif - 入场动画:依次弹入,每个元素间隔 30-40 帧
必须导入:
import { useCurrentFrame, useVideoConfig, interpolate, AbsoluteFill, spring } from 'remotion';第 5 步:注册 Composition
将新组件添加到 src/index.tsx:
import { NewComponent } from '../案例库/新组件/NewComponent';
// 在 Root 组件的 <> 内添加:
<Composition
id="new-component"
durationInFrames={240}
fps={30}
width={800}
height={600}
component={NewComponent}
/>第 6 步:渲染 GIF
cd /path/to/skills/remotion
npx remotion render src/index.tsx <composition-id> "/目标路径/文件名.gif" --codec=gif --every-nth-frame=2--every-nth-frame=2相当于 15fps,体积和流畅度的最佳平衡- 输出路径由用户指定(通常是文章所在目录)
渲染已有模板
案例库中的模板可以直接渲染,无需额外操作:
# 查看可用 composition
grep 'id="' src/index.tsx
# 渲染指定模板
npx remotion render src/index.tsx cycle-flowchart /输出路径/循环流程图.gif --codec=gif --every-nth-frame=2
npx remotion render src/index.tsx morandi-grid /输出路径/莫兰迪卡片网格.gif --codec=gif --every-nth-frame=2
npx remotion render src/index.tsx cute-flowchart /输出路径/可爱流程图.gif --codec=gif --every-nth-frame=2设计规范速查
| 项目 | 默认值 |
|---|---|
| 画布 | 800×600 |
| fps | 30 |
| 背景 | #fff 或 #f5f0eb(莫兰迪) |
| 入场间隔 | 30-40 帧/元素 |
| 静止缓冲 | 尾部 60 帧 |
| 弹性动画 | spring({ damping: 8-12 }) |
| 字体 | PingFang SC |
| 节点圆角 | 50%(圆)或 20px(卡片) |
触发条件
当用户提到以下内容时触发此 skill:
- 动画逻辑图、动态配图、GIF 配图
- Remotion 渲染
- 把 ASCII 变成动画
- 渲染模板、导出 GIF
node_modules/
package-lock.json
*.gif
.DS_Store
{
"name": "remotion-skill",
"version": "1.0.0",
"scripts": {
"dev": "remotion studio src/index.tsx",
"render:all": "node scripts/render-all.mjs",
"render": "remotion render src/index.tsx"
},
"dependencies": {
"@remotion/cli": "^4.0.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"remotion": "^4.0.0"
},
"devDependencies": {
"@types/react": "^18.2.0",
"typescript": "^5.0.0"
}
}
import { Composition, registerRoot } from 'remotion';
import React from 'react';
import { CycleFlowchart } from '../案例库/循环流程图/CycleFlowchart';
import { SkillsFlowchart } from '../案例库/莫兰迪卡片网格/SkillsFlowchart';
import { CuteFlowchart } from '../案例库/可爱流程图/CuteFlowchart';
import { StaticVsDynamic } from '../案例库/静态vs动画对比/StaticVsDynamic';
import { ThreeSteps } from '../案例库/三步流程/ThreeSteps';
const Root: React.FC = () => {
return (
<>
<Composition
id="cycle-flowchart"
durationInFrames={240}
fps={30}
width={800}
height={600}
component={CycleFlowchart}
/>
<Composition
id="morandi-grid"
durationInFrames={210}
fps={30}
width={800}
height={600}
component={SkillsFlowchart}
/>
<Composition
id="cute-flowchart"
durationInFrames={180}
fps={30}
width={800}
height={600}
component={CuteFlowchart}
/>
<Composition
id="static-vs-dynamic"
durationInFrames={240}
fps={30}
width={800}
height={600}
component={StaticVsDynamic}
/>
<Composition
id="three-steps"
durationInFrames={210}
fps={30}
width={800}
height={600}
component={ThreeSteps}
/>
</>
);
};
registerRoot(Root);
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"lib": ["DOM", "ESNext"],
"jsx": "react-jsx",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"moduleResolution": "bundler"
},
"include": ["src", "案例库"]
}
import React from 'react';
import { useCurrentFrame, useVideoConfig, interpolate, AbsoluteFill, spring } from 'remotion';
export const ThreeSteps: React.FC = () => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const totalFrames = 210;
const loopFrame = frame % totalFrames;
const appear = (delay: number) =>
spring({ frame: loopFrame - delay, fps, config: { damping: 10 } });
const steps = [
{ icon: '📦', title: '装 Skill', sub: '一次性', color: '#6c5ce7' },
{ icon: '💬', title: '画 ASCII', sub: 'Agent 画', color: '#0984e3' },
{ icon: '🎬', title: '/remotion', sub: '生成 GIF', color: '#00b894' },
];
const arrow1 = interpolate(loopFrame, [50, 70], [0, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' });
const arrow2 = interpolate(loopFrame, [80, 100], [0, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' });
const arrows = [arrow1, arrow2];
const numLabels = ['①', '②', '③'];
const numAppear = [appear(90), appear(100), appear(110)];
return (
<AbsoluteFill style={{
backgroundColor: '#f8f6f3',
fontFamily: 'PingFang SC, Microsoft YaHei, sans-serif',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
padding: 60,
}}>
<div style={{ display: 'flex', alignItems: 'center' }}>
{steps.map((step, i) => {
const scale = appear(i * 25 + 10);
return (
<React.Fragment key={i}>
<div style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
transform: `scale(${scale})`,
}}>
<div style={{
width: 180,
height: 180,
borderRadius: 20,
background: '#fff',
border: `3px solid ${step.color}`,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
boxShadow: `0 6px 24px ${step.color}22`,
gap: 8,
}}>
<div style={{ fontSize: 42 }}>{step.icon}</div>
<div style={{ fontSize: 22, fontWeight: 700, color: '#2d2d2d' }}>{step.title}</div>
<div style={{ fontSize: 15, color: '#888', marginTop: 2 }}>{step.sub}</div>
</div>
<div style={{
marginTop: 16,
fontSize: 20,
color: step.color,
fontWeight: 600,
opacity: numAppear[i],
transform: `scale(${numAppear[i]})`,
}}>
{numLabels[i]}
</div>
</div>
{i < 2 && (
<div style={{
fontSize: 32,
color: '#bbb',
margin: '0 16px',
marginBottom: 40,
opacity: arrows[i],
transform: `translateX(${(1 - arrows[i]) * -10}px)`,
}}>
──▶
</div>
)}
</React.Fragment>
);
})}
</div>
</AbsoluteFill>
);
};
import { Composition, registerRoot } from 'remotion';
import React from 'react';
import { PersonCard } from './PersonCard';
const MainVideo: React.FC = () => {
return (
<Composition
id="person-card"
durationInFrames={180}
fps={30}
width={800}
height={600}
component={PersonCard}
/>
);
};
registerRoot(MainVideo);
import { useCurrentFrame, interpolate, AbsoluteFill } from 'remotion';
// 人物数据
const PEOPLE = [
{
name: '张三',
role: '技术负责人',
desc: '10年开发经验,擅长架构设计',
avatar: '👨💻',
color: '#3498db',
},
{
name: '李四',
role: '产品经理',
desc: '深耕AI产品多年,用户思维强',
avatar: '👩💼',
color: '#e74c3c',
},
{
name: '王五',
role: '算法工程师',
desc: '专注大模型应用与优化',
avatar: '🧑🔬',
color: '#2ecc71',
},
];
const PersonCardItem: React.FC<{ person: typeof PEOPLE[0]; index: number; frame: number }> = ({ person, index, frame }) => {
const animDelay = index * 25;
const opacity = interpolate(frame, [animDelay, animDelay + 20], [0, 1], { extrapolateRight: 'clamp' });
const translateY = interpolate(frame, [animDelay, animDelay + 20], [30, 0], { extrapolateRight: 'clamp' });
return (
<div style={{
position: 'absolute',
left: 60 + index * 240,
top: 150,
width: 200,
opacity,
transform: `translateY(${translateY}px)`,
textAlign: 'center',
}}>
{/* 头像 */}
<div style={{
width: 100,
height: 100,
borderRadius: '50%',
backgroundColor: '#f5f5f5',
border: `4px solid ${person.color}`,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: 50,
margin: '0 auto',
boxShadow: `0 4px 12px ${person.color}30`,
}}>
{person.avatar}
</div>
{/* 名字 */}
<div style={{
marginTop: 16,
fontSize: 22,
fontWeight: 'bold',
color: '#333',
fontFamily: 'PingFang SC, Microsoft YaHei, sans-serif',
}}>
{person.name}
</div>
{/* 职位 */}
<div style={{
marginTop: 4,
fontSize: 14,
color: person.color,
fontFamily: 'PingFang SC, Microsoft YaHei, sans-serif',
fontWeight: 500,
}}>
{person.role}
</div>
{/* 描述 */}
<div style={{
marginTop: 12,
fontSize: 13,
color: '#666',
fontFamily: 'PingFang SC, Microsoft YaHei, sans-serif',
lineHeight: 1.5,
}}>
{person.desc}
</div>
</div>
);
};
export const PersonCard: React.FC = () => {
const frame = useCurrentFrame();
return (
<AbsoluteFill style={{ backgroundColor: '#fff' }}>
{PEOPLE.map((person, i) => (
<PersonCardItem key={i} person={person} index={i} frame={frame} />
))}
</AbsoluteFill>
);
};
import { useCurrentFrame, interpolate, AbsoluteFill } from 'remotion';
// 代码内容
const CODE_LINES = [
{ type: 'keyword', text: 'import' },
{ type: 'text', text: ' { useState } ' },
{ type: 'keyword', text: 'from' },
{ type: 'string', text: "'react'" },
{ type: 'punctuation', text: ';' },
{ type: 'empty', text: '' },
{ type: 'keyword', text: 'function' },
{ type: 'function', text: ' Counter' },
{ type: 'punctuation', text: '()' },
{ type: 'punctuation', text: ' {' },
{ type: 'empty', text: '' },
{ type: 'keyword', text: 'const' },
{ type: 'variable', text: ' [count' },
{ type: 'punctuation', text: ',' },
{ type: 'function', text: ' setCount' },
{ type: 'variable', text: '] ' },
{ type: 'operator', text: '=' },
{ type: 'function', text: ' useState' },
{ type: 'punctuation', text: '(' },
{ type: 'number', text: '0' },
{ type: 'punctuation', text: ')' },
{ type: 'punctuation', text: ';' },
{ type: 'empty', text: '' },
{ type: 'keyword', text: ' return ' },
{ type: 'punctuation', text: '(' },
{ type: 'empty', text: '' },
{ type: 'punctuation', text: '<' },
{ type: 'tag', text: 'button' },
{ type: 'punctuation', text: '>' },
{ type: 'text', text: '点击次数: ' },
{ type: 'punctuation', text: '{' },
{ type: 'variable', text: 'count' },
{ type: 'punctuation', text: '}' },
{ type: 'punctuation', text: '</' },
{ type: 'tag', text: 'button' },
{ type: 'punctuation', text: '>' },
{ type: 'punctuation', text: ');' },
{ type: 'empty', text: '' },
{ type: 'punctuation', text: '}' },
];
// 颜色配置
const COLORS = {
keyword: '#0000ff',
string: '#a31515',
function: '#795e26',
variable: '#001080',
number: '#098658',
operator: '#000000',
punctuation: '#000000',
tag: '#800000',
text: '#333333',
};
const CodeLine: React.FC<{ line: typeof CODE_LINES[0]; index: number; frame: number }> = ({ line, index, frame }) => {
if (line.type === 'empty') {
return <div style={{ height: 24 }} />;
}
const delay = index * 5;
const opacity = interpolate(frame, [delay, delay + 10], [0, 1], { extrapolateRight: 'clamp' });
return (
<div style={{
opacity,
fontFamily: "'SF Mono', 'Monaco', 'Menlo', monospace",
fontSize: 15,
lineHeight: 1.6,
}}>
<span style={{ color: COLORS[line.type as keyof typeof COLORS] || '#d4d4d4' }}>{line.text}</span>
</div>
);
};
const TitleBar: React.FC<{ frame: number }> = ({ frame }) => {
const opacity = interpolate(frame, [0, 15], [0, 1], { extrapolateRight: 'clamp' });
return (
<div style={{
position: 'absolute',
top: 0,
left: 0,
right: 0,
height: 40,
backgroundColor: '#f5f5f5',
borderBottom: '1px solid #e0e0e0',
display: 'flex',
alignItems: 'center',
paddingLeft: 16,
opacity,
}}>
<div style={{ display: 'flex', gap: 8, marginRight: 12 }}>
<div style={{ width: 12, height: 12, borderRadius: '50%', backgroundColor: '#ff5f56' }} />
<div style={{ width: 12, height: 12, borderRadius: '50%', backgroundColor: '#ffbd2e' }} />
<div style={{ width: 12, height: 12, borderRadius: '50%', backgroundColor: '#27c93f' }} />
</div>
<div style={{
flex: 1,
textAlign: 'center',
color: '#666',
fontSize: 13,
}}>
App.tsx — React
</div>
<div style={{ width: 60 }} />
</div>
);
};
const LineNumbers: React.FC<{ frame: number }> = ({ frame }) => {
const opacity = interpolate(frame, [5, 20], [0, 1], { extrapolateRight: 'clamp' });
return (
<div style={{
position: 'absolute',
left: 30,
top: 80,
opacity,
fontFamily: "'SF Mono', 'Monaco', 'Menlo', monospace",
fontSize: 15,
lineHeight: 1.6,
color: '#858585',
textAlign: 'right',
}}>
{Array.from({ length: 15 }, (_, i) => (
<div key={i} style={{ height: 24 }}>{i + 1}</div>
))}
</div>
);
};
export const CodeShowcase: React.FC = () => {
const frame = useCurrentFrame();
const contentOpacity = interpolate(frame, [20, 40], [0, 1], { extrapolateRight: 'clamp' });
return (
<AbsoluteFill style={{ backgroundColor: '#fff' }}>
{/* 边框容器 */}
<div style={{
position: 'absolute',
top: 80,
left: 30,
right: 30,
bottom: 30,
backgroundColor: '#fafafa',
border: '1px solid #e0e0e0',
borderRadius: 12,
overflow: 'hidden',
boxShadow: '0 4px 20px rgba(0,0,0,0.08)',
}}>
<TitleBar frame={frame} />
<LineNumbers frame={frame} />
<div style={{
position: 'absolute',
left: 70,
top: 80,
opacity: contentOpacity,
}}>
{CODE_LINES.map((line, i) => (
<CodeLine key={i} line={line} index={i} frame={frame} />
))}
</div>
</div>
</AbsoluteFill>
);
};
import { Composition, registerRoot } from 'remotion';
import React from 'react';
import { CodeShowcase } from './CodeShowcase';
const MainVideo: React.FC = () => {
return (
<Composition
id="code-showcase"
durationInFrames={180}
fps={30}
width={800}
height={600}
component={CodeShowcase}
/>
);
};
registerRoot(MainVideo);
import { useCurrentFrame, useVideoConfig, interpolate, AbsoluteFill, spring } from 'remotion';
// ============ Emoji 图标组件 ============
const EmojiIcon: React.FC<{ emoji: string; size: number }> = ({ emoji, size }) => (
<div style={{
fontSize: size,
lineHeight: 1,
textAlign: 'center',
}}>
{emoji}
</div>
);
// ============ 箭头组件 ============
const CuteArrow: React.FC<{ opacity: number }> = ({ opacity }) => (
<div style={{
display: 'flex',
alignItems: 'center',
opacity,
marginLeft: -8,
marginRight: -8,
}}>
<svg width="60" height="30" viewBox="0 0 60 30">
<line x1="4" y1="15" x2="48" y2="15" stroke="#8a7a6a" strokeWidth="3" strokeLinecap="round" />
<path d="M42 8 L52 15 L42 22" stroke="#8a7a6a" strokeWidth="3" fill="none" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</div>
);
// ============ 单个流程节点 ============
interface FlowNodeProps {
icon: React.ReactNode;
label: string;
index: number;
frame: number;
fps: number;
}
const FlowNode: React.FC<FlowNodeProps> = ({ icon, label, index, frame, fps }) => {
const loopFrames = 180; // 6秒循环
const frameInLoop = frame % loopFrames;
const showFrame = 30 + index * 40; // 1秒后开始,每个间隔40帧
const delayFrame = Math.max(0, frameInLoop - showFrame);
// 入场动画
const scale = spring({ frame: delayFrame, fps, from: 0.3, to: 1, config: { damping: 8 } });
const opacity = interpolate(delayFrame, [0, 12], [0, 1], { extrapolateRight: 'clamp' });
const translateY = spring({ frame: delayFrame, fps, from: 40, to: 0, config: { damping: 10 } });
// 浮动呼吸动画(入场后)
const breathFrame = Math.max(0, delayFrame - 25);
const float = Math.sin(breathFrame * 0.08) * 3;
return (
<div style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
opacity,
transform: `translateY(${translateY + float}px) scale(${scale})`,
}}>
{/* 图标 */}
<div style={{
width: 130,
height: 130,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
filter: 'drop-shadow(0 4px 12px rgba(0,0,0,0.1))',
}}>
{icon}
</div>
{/* 标签 */}
<div style={{
marginTop: 14,
fontSize: 24,
fontWeight: 'bold',
color: '#5a4a3a',
fontFamily: 'PingFang SC, Microsoft YaHei, sans-serif',
letterSpacing: 2,
}}>
{label}
</div>
</div>
);
};
// ============ 主组件 ============
export const CuteFlowchart: React.FC = () => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const loopFrames = 180;
const frameInLoop = frame % loopFrames;
// 标题动画
const titleOpacity = interpolate(frameInLoop, [0, 20], [0, 1], { extrapolateRight: 'clamp' });
const titleY = spring({ frame: frameInLoop, fps, from: -20, to: 0, config: { damping: 12 } });
// 箭头出现时机
const arrow1Opacity = interpolate(frameInLoop, [75, 85], [0, 1], { extrapolateRight: 'clamp' });
const arrow2Opacity = interpolate(frameInLoop, [115, 125], [0, 1], { extrapolateRight: 'clamp' });
const nodes = [
{ icon: <EmojiIcon emoji="🎙️" size={90} />, label: '硬件采集' },
{ icon: <EmojiIcon emoji="🧠" size={90} />, label: 'AI理解' },
{ icon: <EmojiIcon emoji="🤖" size={90} />, label: 'Agent交互' },
];
return (
<AbsoluteFill style={{ backgroundColor: '#fff' }}>
{/* 背景装饰圆 */}
<div style={{
position: 'absolute',
top: '45%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: 600,
height: 400,
// background: 'radial-gradient(ellipse, rgba(220,200,180,0.3) 0%, transparent 70%)',
borderRadius: '50%',
}} />
{/* 流程图主体 */}
<div style={{
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
paddingTop: 30,
}}>
<div style={{
display: 'flex',
alignItems: 'center',
gap: 0,
}}>
<FlowNode icon={nodes[0].icon} label={nodes[0].label} index={0} frame={frame} fps={fps} />
<CuteArrow opacity={arrow1Opacity} />
<FlowNode icon={nodes[1].icon} label={nodes[1].label} index={1} frame={frame} fps={fps} />
<CuteArrow opacity={arrow2Opacity} />
<FlowNode icon={nodes[2].icon} label={nodes[2].label} index={2} frame={frame} fps={fps} />
</div>
</div>
</AbsoluteFill>
);
};
import { Composition, registerRoot } from 'remotion';
import React from 'react';
import { CuteFlowchart } from './CuteFlowchart';
const MainVideo: React.FC = () => {
return (
<Composition
id="cute-flowchart"
durationInFrames={180}
fps={30}
width={800}
height={600}
component={CuteFlowchart}
/>
);
};
registerRoot(MainVideo);
import { useCurrentFrame, interpolate, AbsoluteFill } from 'remotion';
// 卡片数据
const LEFT_CARD = {
title: '传统开发',
color: '#e74c3c',
items: [
'📝 手动编写代码',
'🔄 需重启服务',
'⏰ 周期长',
'🐛 调试困难',
],
};
const RIGHT_CARD = {
title: 'AI辅助开发',
color: '#27ae60',
items: [
'🤖 AI生成代码',
'⚡ 热加载生效',
'🚀 快速迭代',
'✨ 智能调试',
],
};
const ItemLine: React.FC<{ text: string; index: number; baseFrame: number; color: string }> = ({ text, index, baseFrame, color }) => {
const animFrame = baseFrame + index * 15;
const opacity = interpolate(animFrame, [0, 12], [0, 1], { extrapolateRight: 'clamp' });
const translateX = interpolate(animFrame, [0, 12], [20, 0], { extrapolateRight: 'clamp' });
return (
<div style={{
opacity,
transform: `translateX(${translateX}px)`,
fontSize: 18,
color: '#333',
fontFamily: 'PingFang SC, Microsoft YaHei, sans-serif',
}}>
{text}
</div>
);
};
const CompareFlowchart: React.FC = () => {
const frame = useCurrentFrame();
const loopFrames = 240;
const frameInLoop = frame % loopFrames;
// 左侧卡片入场
const leftOpacity = interpolate(frameInLoop, [0, 20], [0, 1], { extrapolateRight: 'clamp' });
const leftX = interpolate(frameInLoop, [0, 20], [-60, 0], { extrapolateRight: 'clamp' });
// 右侧卡片入场
const rightOpacity = interpolate(frameInLoop, [15, 35], [0, 1], { extrapolateRight: 'clamp' });
const rightX = interpolate(frameInLoop, [15, 35], [60, 0], { extrapolateRight: 'clamp' });
// VS 出现
const vsOpacity = interpolate(frameInLoop, [40, 55], [0, 1], { extrapolateRight: 'clamp' });
const vsScale = interpolate(frameInLoop, [40, 55], [0.3, 1], { extrapolateRight: 'clamp' });
// 底部结论
const resultOpacity = interpolate(frameInLoop, [70, 90], [0, 1], { extrapolateRight: 'clamp' });
return (
<AbsoluteFill style={{ backgroundColor: '#fff' }}>
{/* 左侧卡片 */}
<div style={{
position: 'absolute',
left: 60,
top: 130,
width: 280,
opacity: leftOpacity,
transform: `translateX(${leftX}px)`,
}}>
<div style={{
backgroundColor: LEFT_CARD.color,
padding: '16px 24px',
borderRadius: '20px 20px 0 0',
textAlign: 'center',
fontSize: 24,
fontWeight: 'bold',
color: '#fff',
fontFamily: 'PingFang SC, Microsoft YaHei, sans-serif',
}}>
{LEFT_CARD.title}
</div>
<div style={{
backgroundColor: '#fff',
border: `2px solid ${LEFT_CARD.color}`,
borderTop: 'none',
borderRadius: '0 0 20px 20px',
padding: '24px 20px',
display: 'flex',
flexDirection: 'column',
gap: 16,
boxShadow: '0 4px 12px rgba(0,0,0,0.08)',
}}>
{LEFT_CARD.items.map((item, i) => (
<ItemLine key={i} text={item} index={i} baseFrame={25} color={LEFT_CARD.color} />
))}
</div>
</div>
{/* 右侧卡片 */}
<div style={{
position: 'absolute',
right: 60,
top: 130,
width: 280,
opacity: rightOpacity,
transform: `translateX(${rightX}px)`,
}}>
<div style={{
backgroundColor: RIGHT_CARD.color,
padding: '16px 24px',
borderRadius: '20px 20px 0 0',
textAlign: 'center',
fontSize: 24,
fontWeight: 'bold',
color: '#fff',
fontFamily: 'PingFang SC, Microsoft YaHei, sans-serif',
}}>
{RIGHT_CARD.title}
</div>
<div style={{
backgroundColor: '#fff',
border: `2px solid ${RIGHT_CARD.color}`,
borderTop: 'none',
borderRadius: '0 0 20px 20px',
padding: '24px 20px',
display: 'flex',
flexDirection: 'column',
gap: 16,
boxShadow: '0 4px 12px rgba(0,0,0,0.08)',
}}>
{RIGHT_CARD.items.map((item, i) => (
<ItemLine key={i} text={item} index={i} baseFrame={40} color={RIGHT_CARD.color} />
))}
</div>
</div>
{/* VS */}
<div style={{
position: 'absolute',
left: '50%',
top: 220,
transform: 'translateX(-50%)',
opacity: vsOpacity,
}}>
<div style={{
width: 60,
height: 60,
borderRadius: '50%',
backgroundColor: '#333',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: 20,
fontWeight: 'bold',
color: '#fff',
transform: `scale(${vsScale})`,
}}>
VS
</div>
</div>
{/* 底部结论 */}
<div style={{
position: 'absolute',
left: 0,
right: 0,
bottom: 80,
textAlign: 'center',
opacity: resultOpacity,
}}>
<div style={{
display: 'inline-block',
padding: '16px 40px',
backgroundColor: '#fff',
border: '2px solid #27ae60',
borderRadius: 30,
fontSize: 22,
color: '#27ae60',
fontFamily: 'PingFang SC, Microsoft YaHei, sans-serif',
fontWeight: 'bold',
boxShadow: '0 4px 12px rgba(39,174,96,0.2)',
}}>
AI 辅助开发效率提升 10x
</div>
</div>
</AbsoluteFill>
);
};
export { CompareFlowchart };
import { Composition, registerRoot } from 'remotion';
import React from 'react';
import { CompareFlowchart } from './CompareFlowchart';
const MainVideo: React.FC = () => {
return (
<Composition
id="compare-flowchart"
durationInFrames={180}
fps={30}
width={800}
height={600}
component={CompareFlowchart}
/>
);
};
registerRoot(MainVideo);
import { useCurrentFrame, useVideoConfig, interpolate, AbsoluteFill, spring } from 'remotion';
// ============ 配置 ============
const CANVAS_W = 800;
const CANVAS_H = 600;
const CENTER_X = CANVAS_W / 2;
const CENTER_Y = CANVAS_H / 2 + 10;
const RADIUS = 180; // 三个节点围绕中心的半径
const NODE_SIZE = 120; // 节点圆的直径
// 三个节点的角度位置(从顶部左开始,顺时针)
// -150° = 左上, -30° = 右上, 90° = 下方
const ANGLES = [-150, -30, 90];
const getNodePos = (angleDeg: number) => {
const rad = (angleDeg * Math.PI) / 180;
return {
x: CENTER_X + RADIUS * Math.cos(rad),
y: CENTER_Y + RADIUS * Math.sin(rad),
};
};
// ============ 圆形节点 ============
interface CycleNodeProps {
emoji: string;
label: string;
angleDeg: number;
index: number;
frame: number;
fps: number;
}
const CycleNode: React.FC<CycleNodeProps> = ({ emoji, label, angleDeg, index, frame, fps }) => {
const loopFrames = 240;
const frameInLoop = frame % loopFrames;
const showFrame = 20 + index * 35;
const delayFrame = Math.max(0, frameInLoop - showFrame);
const scale = spring({ frame: delayFrame, fps, from: 0.2, to: 1, config: { damping: 8 } });
const opacity = interpolate(delayFrame, [0, 15], [0, 1], { extrapolateRight: 'clamp' });
// 呼吸浮动
const breathFrame = Math.max(0, delayFrame - 30);
const float = Math.sin(breathFrame * 0.06 + index * 2) * 3;
const pos = getNodePos(angleDeg);
return (
<div style={{
position: 'absolute',
left: pos.x - NODE_SIZE / 2,
top: pos.y - NODE_SIZE / 2,
width: NODE_SIZE,
height: NODE_SIZE,
opacity,
transform: `translateY(${float}px) scale(${scale})`,
}}>
{/* 外圈 */}
<div style={{
width: NODE_SIZE,
height: NODE_SIZE,
borderRadius: '50%',
background: '#fff',
border: '3px solid #3498db',
boxShadow: '0 4px 16px rgba(52,152,219,0.2)',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
gap: 2,
}}>
<div style={{ fontSize: 42, lineHeight: 1 }}>{emoji}</div>
<div style={{
fontSize: 15,
fontWeight: 'bold',
color: '#333',
fontFamily: 'PingFang SC, Microsoft YaHei, sans-serif',
letterSpacing: 1,
}}>{label}</div>
</div>
</div>
);
};
// ============ 弧形箭头(SVG) ============
interface CurvedArrowProps {
fromAngle: number;
toAngle: number;
frame: number;
index: number;
}
const CurvedArrow: React.FC<CurvedArrowProps> = ({ fromAngle, toAngle, frame, index }) => {
const loopFrames = 240;
const frameInLoop = frame % loopFrames;
const showFrame = 55 + index * 35;
const opacity = interpolate(frameInLoop, [showFrame, showFrame + 20], [0, 1], { extrapolateRight: 'clamp' });
// 箭头起止点:从节点边缘出发,偏移一点留间隙
const gap = NODE_SIZE / 2 + 14; // 节点半径 + 间距
const arrowOffset = 22; // 箭头从节点边缘偏移的角度
const startAngleDeg = fromAngle + arrowOffset;
const endAngleDeg = toAngle - arrowOffset;
const startRad = (startAngleDeg * Math.PI) / 180;
const endRad = (endAngleDeg * Math.PI) / 180;
// 起点和终点
const sx = CENTER_X + RADIUS * Math.cos(startRad);
const sy = CENTER_Y + RADIUS * Math.sin(startRad);
const ex = CENTER_X + RADIUS * Math.cos(endRad);
const ey = CENTER_Y + RADIUS * Math.sin(endRad);
// 控制点:向外弯曲(顺时针方向,处理角度跨越问题)
let adjustedEnd = endAngleDeg;
if (adjustedEnd < startAngleDeg) adjustedEnd += 360;
const midAngle = (startAngleDeg + adjustedEnd) / 2;
const midRad = (midAngle * Math.PI) / 180;
const bulge = RADIUS + 55;
const cx1 = CENTER_X + bulge * Math.cos(midRad);
const cy1 = CENTER_Y + bulge * Math.sin(midRad);
// 箭头方向(终点处的切线方向)
const arrowLen = 18;
const dx = ex - cx1;
const dy = ey - cy1;
const len = Math.sqrt(dx * dx + dy * dy);
const ux = dx / len;
const uy = dy / len;
// 垂直方向
const px = -uy;
const py = ux;
const tip1x = ex - ux * arrowLen + px * 9;
const tip1y = ey - uy * arrowLen + py * 9;
const tip2x = ex - ux * arrowLen - px * 9;
const tip2y = ey - uy * arrowLen - py * 9;
return (
<svg
width={CANVAS_W}
height={CANVAS_H}
viewBox={`0 0 ${CANVAS_W} ${CANVAS_H}`}
style={{ position: 'absolute', top: 0, left: 0, opacity }}
>
{/* 弧线 */}
<path
d={`M${sx},${sy} Q${cx1},${cy1} ${ex},${ey}`}
stroke="#3498db"
strokeWidth="4"
fill="none"
strokeLinecap="round"
/>
{/* 箭头 */}
<polygon
points={`${ex},${ey} ${tip1x},${tip1y} ${tip2x},${tip2y}`}
fill="#3498db"
/>
</svg>
);
};
// ============ 背景光点 ============
const GlowDots: React.FC<{ frame: number }> = ({ frame }) => {
const dots = [
{ x: 80, y: 80, size: 12, speed: 0.04 },
{ x: 720, y: 100, size: 10, speed: 0.05 },
{ x: 650, y: 480, size: 14, speed: 0.035 },
{ x: 120, y: 500, size: 8, speed: 0.06 },
{ x: 400, y: 50, size: 10, speed: 0.045 },
{ x: 700, y: 300, size: 11, speed: 0.038 },
{ x: 60, y: 300, size: 9, speed: 0.055 },
];
return (
<>
{dots.map((dot, i) => {
const pulse = interpolate(
Math.sin(frame * dot.speed + i * 1.5),
[-1, 1],
[0.2, 0.6]
);
return (
<div
key={i}
style={{
position: 'absolute',
left: dot.x - dot.size,
top: dot.y - dot.size,
width: dot.size * 2,
height: dot.size * 2,
borderRadius: '50%',
// background: 'radial-gradient(circle, rgba(52,152,219,0.6), transparent 70%)',
opacity: pulse,
}}
/>
);
})}
</>
);
};
// ============ 主组件 ============
export const CycleFlowchart: React.FC = () => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const loopFrames = 240;
const frameInLoop = frame % loopFrames;
// 中心标题动画
const titleDelay = Math.max(0, frameInLoop - 80);
const titleScale = spring({ frame: titleDelay, fps, from: 0.5, to: 1, config: { damping: 10 } });
const titleOpacity = interpolate(frameInLoop, [80, 100], [0, 1], { extrapolateRight: 'clamp' });
const nodes = [
{ emoji: '🤖', label: 'AI识别', angle: ANGLES[0] },
{ emoji: '👤', label: '你审核', angle: ANGLES[1] },
{ emoji: '📋', label: '反馈学习', angle: ANGLES[2] },
];
// 箭头:0→1, 1→2, 2→0
const arrows = [
{ from: ANGLES[0], to: ANGLES[1] },
{ from: ANGLES[1], to: ANGLES[2] },
{ from: ANGLES[2], to: ANGLES[0] },
];
return (
<AbsoluteFill style={{
backgroundColor: '#fff',
}}>
{/* 背景光点 */}
{/* <GlowDots frame={frame} /> */}
{/* 弧形箭头 */}
{arrows.map((arrow, i) => (
<CurvedArrow
key={i}
fromAngle={arrow.from}
toAngle={arrow.to}
frame={frame}
index={i}
/>
))}
{/* 节点 */}
{nodes.map((node, i) => (
<CycleNode
key={i}
emoji={node.emoji}
label={node.label}
angleDeg={node.angle}
index={i}
frame={frame}
fps={fps}
/>
))}
</AbsoluteFill>
);
};
import { Composition, registerRoot } from 'remotion';
import React from 'react';
import { CycleFlowchart } from './CycleFlowchart';
const MainVideo: React.FC = () => {
return (
<Composition
id="cycle-flowchart"
durationInFrames={240}
fps={30}
width={800}
height={600}
component={CycleFlowchart}
/>
);
};
registerRoot(MainVideo);
import { Composition, registerRoot } from 'remotion';
import React from 'react';
import { SkillsFlowchart } from './SkillsFlowchart';
const MainVideo: React.FC = () => {
return (
<Composition
id="skills-flowchart"
durationInFrames={210}
fps={30}
width={800}
height={600}
component={SkillsFlowchart}
/>
);
};
registerRoot(MainVideo);
import { useCurrentFrame, useVideoConfig, interpolate, AbsoluteFill, spring } from 'remotion';
const BOX_WIDTH = 140;
const BOX_HEIGHT = 140;
const GAP_X = 40;
const GAP_Y = 40;
// 3列布局,居中计算
const COLS = 3;
const TOTAL_WIDTH = COLS * BOX_WIDTH + (COLS - 1) * GAP_X;
const START_X = (800 - TOTAL_WIDTH) / 2 + 10;
const START_Y = 140;
interface FlowBoxProps {
title: string;
desc: string;
index: number;
frame: number;
fps: number;
total: number;
}
// 布局: 1→2→3→4→5
// 第1行: 1, 2, 3
// 第2行: 4, 5
const positions = [
{ row: 0, col: 0, next: 'right' }, // 1
{ row: 0, col: 1, next: 'right' }, // 2
{ row: 0, col: 2, next: 'down' }, // 3 → 4
{ row: 1, col: 2, next: 'left' }, // 4 → 5
{ row: 1, col: 1, next: 'none' }, // 5 结束
];
const FlowBox: React.FC<FlowBoxProps> = ({ title, desc, index, frame, fps, total }) => {
// 开头1秒 + 5个格子(每个1秒) + 结尾1秒 = 7秒 @30fps = 210帧
const loopFrames = 210;
const frameInLoop = frame % loopFrames;
const showFrame = index * 40;
const delayFrame = Math.max(0, frameInLoop - showFrame);
// 入场动画
const scale = spring({ frame: delayFrame, fps, from: 0.5, to: 1, config: { damping: 10 } });
const opacity = interpolate(delayFrame, [0, 15], [0, 1], { extrapolateRight: 'clamp' });
const translateY = spring({ frame: delayFrame, fps, from: 30, to: 0 });
const pos = positions[index];
const x = START_X + pos.col * (BOX_WIDTH + GAP_X);
const y = START_Y + pos.row * (BOX_HEIGHT + GAP_Y);
const arrowOpacity = delayFrame > 20 ? 1 : 0;
// 莫兰迪色系
const colors = [
{ bg: '#e8c4c4', border: '#d4a5a5', accent: '#b89090' },
{ bg: '#b8d4e3', border: '#a3c4d4', accent: '#82a4b5' },
{ bg: '#c4e8c4', border: '#a8d4a8', accent: '#88b888' },
{ bg: '#e3d4b8', border: '#d4c4a3', accent: '#b4a483' },
{ bg: '#d4c4e3', border: '#c4a8d4', accent: '#a488b5' },
];
const color = colors[index] || colors[0];
// 箭头方向
const arrowDir = pos.next;
return (
<div style={{ position: 'absolute', top: y, left: x }}>
{/* 右箭头 */}
{arrowDir === 'right' && (
<div style={{
position: 'absolute',
top: BOX_HEIGHT / 2 - 10,
left: BOX_WIDTH,
width: GAP_X,
height: 20,
opacity: arrowOpacity,
display: 'flex',
alignItems: 'center',
}}>
<div style={{ flex: 1, height: 2, background: '#666' }} />
<svg width="18" height="18" viewBox="0 0 18 18">
<path d="M5 9 L13 9 M13 9 L9 5 M13 9 L9 13" stroke="#666" strokeWidth="2.5" fill="none" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
</div>
)}
{/* 左箭头 */}
{arrowDir === 'left' && (
<div style={{
position: 'absolute',
top: BOX_HEIGHT / 2 - 10,
left: -GAP_X,
width: GAP_X,
height: 20,
opacity: arrowOpacity,
display: 'flex',
alignItems: 'center',
flexDirection: 'row-reverse',
}}>
<div style={{ flex: 1, height: 2, background: '#666' }} />
<svg width="18" height="18" viewBox="0 0 18 18">
<path d="M13 9 L5 9 M5 9 L9 5 M5 9 L9 13" stroke="#666" strokeWidth="2.5" fill="none" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
</div>
)}
{/* 下箭头 */}
{arrowDir === 'down' && (
<div style={{
position: 'absolute',
top: BOX_HEIGHT,
left: BOX_WIDTH / 2 - 1,
width: 2,
height: GAP_Y,
opacity: arrowOpacity,
}}>
{/* 线条 */}
<div style={{ width: '100%', height: '100%', background: '#666' }} />
{/* 箭头 */}
<svg width="20" height="20" viewBox="0 0 20 20" style={{ position: 'absolute', bottom: -2, left: -9 }}>
<path d="M10 3 L10 17 M10 17 L6 12 M10 17 L14 12" stroke="#666" strokeWidth="2.5" fill="none" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
</div>
)}
{/* 卡片 */}
<div style={{
width: BOX_WIDTH,
height: BOX_HEIGHT,
backgroundColor: color.bg,
borderRadius: 20,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
opacity,
transform: `translateY(${translateY}px) scale(${scale})`,
boxShadow: `6px 6px 16px rgba(0,0,0,0.12), -3px -3px 10px rgba(255,255,255,0.1)`,
border: `2px solid ${color.border}`,
position: 'relative',
}}>
{/* 序号 */}
<div style={{
position: 'absolute',
top: 10,
left: 14,
width: 26,
height: 26,
borderRadius: '50%',
backgroundColor: '#fff',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: 12,
fontWeight: 'bold',
color: color.accent,
boxShadow: '2px 2px 5px rgba(0,0,0,0.08)',
}}>
{index + 1}
</div>
<span style={{
color: '#4a4a4a',
fontSize: 16,
fontFamily: '-apple-system, BlinkMacSystemFont, "PingFang SC", sans-serif',
WebkitFontSmoothing: 'antialiased',
MozOsxFontSmoothing: 'grayscale',
fontWeight: 'bold',
textAlign: 'center',
padding: '0 8px',
}}>{title}</span>
<span style={{
color: '#7a7a7a',
fontSize: 10,
marginTop: 6,
fontFamily: '-apple-system, BlinkMacSystemFont, "PingFang SC", sans-serif',
WebkitFontSmoothing: 'antialiased',
MozOsxFontSmoothing: 'grayscale',
textAlign: 'center',
padding: '0 8px',
}}>{desc}</span>
</div>
</div>
);
};
export const SkillsFlowchart: React.FC = () => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const items = [
{ title: '原理研究', desc: '根基(决定上限)' },
{ title: '热加载', desc: '基础条件(边用边改)' },
{ title: '过程反馈', desc: '用时给反馈' },
{ title: '数据反馈', desc: '发布后数据' },
{ title: '自更新机制', desc: 'AI帮你写skill' },
];
return (
<AbsoluteFill style={{ backgroundColor: '#fff' }}>
{/* 背景装饰 */}
{/*
<div style={{
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: 500,
height: 500,
background: 'radial-gradient(circle, rgba(200,180,170,0.2) 0%, transparent 60%)',
}} />
*/}
{/* 流程图 */}
{items.map((item, index) => (
<FlowBox
key={index}
title={item.title}
desc={item.desc}
index={index}
frame={frame}
fps={fps}
total={items.length}
/>
))}
</AbsoluteFill>
);
};
import { Composition, registerRoot } from 'remotion';
import React from 'react';
import { Timeline } from './Timeline';
const MainVideo: React.FC = () => {
return (
<Composition
id="timeline"
durationInFrames={180}
fps={30}
width={800}
height={600}
component={Timeline}
/>
);
};
registerRoot(MainVideo);
import { useCurrentFrame, interpolate, AbsoluteFill } from 'remotion';
// 时间线数据
const TIMELINE = [
{ time: '2024 Q1', title: '项目启动', desc: '完成需求分析和架构设计', color: '#3498db' },
{ time: '2024 Q2', title: 'MVP发布', desc: '核心功能上线,开始内测', color: '#2ecc71' },
{ time: '2024 Q3', title: '功能迭代', desc: '新增AI分析和自动化功能', color: '#9b59b6' },
{ time: '2024 Q4', title: '正式发布', desc: '全面上线,用户突破10万', color: '#e67e22' },
];
const TimelineItem: React.FC<{ item: typeof TIMELINE[0]; index: number; frame: number }> = ({ item, index, frame }) => {
const animDelay = index * 30;
const opacity = interpolate(frame, [animDelay, animDelay + 20], [0, 1], { extrapolateRight: 'clamp' });
const translateY = interpolate(frame, [animDelay, animDelay + 20], [30, 0], { extrapolateRight: 'clamp' });
const isLeft = index % 2 === 0;
// 画布宽800,中心线在400,时间点宽16(中心8),左右时间点要对齐到400
// 左边: left + (-30) + 8 = 400 → left = 422
// 右边: left + 174 + 8 = 400 → left = 218
const cardLeft = isLeft ? 422 : 218;
return (
<div style={{
position: 'absolute',
top: 70 + index * 95,
left: cardLeft,
width: 160,
height: 80,
opacity,
transform: `translateY(${translateY}px)`,
}}>
{/* 时间点 */}
<div style={{
position: 'absolute',
left: isLeft ? -30 : 174,
top: 32,
width: 16,
height: 16,
borderRadius: '50%',
backgroundColor: item.color,
border: '3px solid #fff',
boxShadow: `0 2px 8px ${item.color}40`,
}} />
{/* 内容卡片 */}
<div style={{
position: 'absolute',
top: 0,
left: isLeft ? 0 : 20,
width: 140,
backgroundColor: '#fff',
borderRadius: 12,
padding: '12px 14px',
boxShadow: '0 2px 10px rgba(0,0,0,0.08)',
border: `1px solid ${item.color}30`,
}}>
<div style={{
fontSize: 12,
color: item.color,
fontWeight: 'bold',
fontFamily: 'PingFang SC, Microsoft YaHei, sans-serif',
}}>
{item.time}
</div>
<div style={{
fontSize: 15,
fontWeight: 'bold',
color: '#333',
marginTop: 4,
fontFamily: 'PingFang SC, Microsoft YaHei, sans-serif',
}}>
{item.title}
</div>
<div style={{
fontSize: 12,
color: '#666',
marginTop: 4,
fontFamily: 'PingFang SC, Microsoft YaHei, sans-serif',
}}>
{item.desc}
</div>
</div>
</div>
);
};
const CenterLine: React.FC<{ frame: number }> = ({ frame }) => {
// 4个节点,间距95,从70开始,总高度约400
const height = interpolate(frame, [10, 40], [0, 400], { extrapolateRight: 'clamp' });
const opacity = interpolate(frame, [10, 20], [0, 1], { extrapolateRight: 'clamp' });
return (
<div style={{
position: 'absolute',
left: '50%',
top: 95,
width: 4,
height,
backgroundColor: '#ddd',
transform: 'translateX(-50%)',
opacity,
borderRadius: 2,
}} />
);
};
export const Timeline: React.FC = () => {
const frame = useCurrentFrame();
return (
<AbsoluteFill style={{ backgroundColor: '#fff' }}>
{/* 中心线 */}
<CenterLine frame={frame} />
{/* 时间线节点 */}
{TIMELINE.map((item, i) => (
<TimelineItem key={i} item={item} index={i} frame={frame} />
))}
</AbsoluteFill>
);
};
import { Composition, registerRoot } from 'remotion';
import React from 'react';
import { TerminalFlowchart } from './TerminalFlowchart';
const MainVideo: React.FC = () => {
return (
<Composition
id="terminal-flowchart"
durationInFrames={300}
fps={30}
width={800}
height={600}
component={TerminalFlowchart}
/>
);
};
registerRoot(MainVideo);
import { useCurrentFrame, useVideoConfig, interpolate, AbsoluteFill } from 'remotion';
// ============ 终端内容 ============
const TERMINAL_LINES = [
{ prefix: '➜', color: '#28a745', text: '~/projects/aios' },
{ prefix: '➜', color: '#28a745', text: 'git status' },
{ prefix: '', color: '#dc3545', text: 'On branch main' },
{ prefix: '', color: '#dc3545', text: 'Your branch is up to date with \'origin/main\'.' },
{ prefix: '➜', color: '#28a745', text: 'git add .' },
{ prefix: '➜', color: '#28a745', text: 'git commit -m "添加新功能"' },
{ prefix: '', color: '#ffc107', text: '[main a1b2c3d] 添加新功能' },
{ prefix: '', color: '#ffc107', text: ' 2 files changed, 50 insertions(+)' },
{ prefix: '➜', color: '#28a745', text: 'git push' },
{ prefix: '', color: '#6f42c1', text: 'Enumerating objects: 5, done.' },
{ prefix: '', color: '#6f42c1', text: 'Writing objects: 100% (5/5), 420 bytes | 420.00 KiB/s, done.' },
{ prefix: '', color: '#6f42c1', text: 'To github.com:user/aios.git' },
{ prefix: '', color: '#6f42c1', text: ' a1b2c3d..d4e5f6g main -> main' },
{ prefix: '➜', color: '#28a745', text: '~' },
];
// 计算每行需要的帧数
const getLineDuration = (text: string) => {
return Math.ceil(text.length * 1.5) + 8;
};
interface TerminalLineProps {
line: { prefix: string; color: string; text: string };
index: number;
frame: number;
currentLineIndex: number;
currentLineProgress: number;
}
const TerminalLine: React.FC<TerminalLineProps> = ({ line, index, frame, currentLineIndex, currentLineProgress }) => {
if (index > currentLineIndex) return null;
const totalChars = line.prefix.length + line.text.length;
let charsToShow: number;
if (index === currentLineIndex) {
charsToShow = Math.min(totalChars, Math.floor(currentLineProgress * totalChars));
} else {
charsToShow = totalChars;
}
let displayPrefix = line.prefix;
let displayText = '';
if (charsToShow <= line.prefix.length) {
displayPrefix = line.prefix.slice(0, charsToShow);
} else {
displayPrefix = line.prefix;
displayText = line.text.slice(0, charsToShow - line.prefix.length);
}
const cursorOpacity = index === currentLineIndex ? (Math.sin(frame * 0.3) > 0 ? 1 : 0.3) : 0;
return (
<div style={{
position: 'absolute',
left: 40,
top: 80 + index * 28,
fontFamily: "'SF Mono', 'Monaco', 'Menlo', 'Ubuntu Mono', monospace",
fontSize: 16,
lineHeight: 1.5,
display: 'flex',
alignItems: 'center',
}}>
<span style={{ color: line.color, marginRight: 8 }}>{displayPrefix}</span>
<span style={{ color: '#333' }}>{displayText}</span>
{index === currentLineIndex && charsToShow < totalChars && (
<span style={{
display: 'inline-block',
width: 8,
height: 16,
backgroundColor: '#333',
marginLeft: 2,
opacity: cursorOpacity,
}} />
)}
</div>
);
};
const TitleBar: React.FC<{ frame: number }> = ({ frame }) => {
const titleOpacity = interpolate(frame, [0, 15], [0, 1], { extrapolateRight: 'clamp' });
return (
<div style={{
position: 'absolute',
top: 0,
left: 0,
right: 0,
height: 40,
backgroundColor: '#fff',
borderBottom: '1px solid #ddd',
display: 'flex',
alignItems: 'center',
paddingLeft: 16,
opacity: titleOpacity,
}}>
<div style={{ display: 'flex', gap: 8, marginRight: 12 }}>
<div style={{ width: 12, height: 12, borderRadius: '50%', backgroundColor: '#ff5f56' }} />
<div style={{ width: 12, height: 12, borderRadius: '50%', backgroundColor: '#ffbd2e' }} />
<div style={{ width: 12, height: 12, borderRadius: '50%', backgroundColor: '#27c93f' }} />
</div>
<div style={{
flex: 1,
textAlign: 'center',
color: '#888',
fontSize: 14,
}}>
Terminal — zsh
</div>
<div style={{ width: 60 }} />
</div>
);
};
export const TerminalFlowchart: React.FC = () => {
const frame = useCurrentFrame();
const lineDurations = TERMINAL_LINES.map(line => getLineDuration(line.text));
const loopFrames = 300;
const frameInLoop = frame % loopFrames;
let elapsedFrames = 0;
let currentLineIndex = -1;
let currentLineProgress = 0;
for (let i = 0; i < TERMINAL_LINES.length; i++) {
if (frameInLoop < elapsedFrames + lineDurations[i]) {
currentLineIndex = i;
currentLineProgress = (frameInLoop - elapsedFrames) / lineDurations[i];
break;
}
elapsedFrames += lineDurations[i];
}
return (
<AbsoluteFill style={{ backgroundColor: '#fff' }}>
{/* 边框效果 */}
<div style={{
position: 'absolute',
top: 20,
left: 20,
right: 20,
bottom: 20,
border: '1px solid #eee',
borderRadius: 12,
backgroundColor: '#fff',
boxShadow: '0 4px 20px rgba(0,0,0,0.08)',
}} />
<TitleBar frame={frame} />
{TERMINAL_LINES.map((line, i) => (
<TerminalLine
key={i}
line={line}
index={i}
frame={frame}
currentLineIndex={currentLineIndex}
currentLineProgress={currentLineProgress}
/>
))}
</AbsoluteFill>
);
};
import { Composition, registerRoot } from 'remotion';
import React from 'react';
import { SkillsFlowchart } from './SkillsFlowchart';
const MainVideo: React.FC = () => {
return (
<Composition
id="main"
durationInFrames={210}
fps={30}
width={800}
height={600}
component={SkillsFlowchart}
/>
);
};
registerRoot(MainVideo);
import { useCurrentFrame, useVideoConfig, interpolate, AbsoluteFill, spring } from 'remotion';
const BOX_WIDTH = 140;
const BOX_HEIGHT = 140;
const GAP_X = 40;
const GAP_Y = 40;
// 3列布局,居中计算
const COLS = 3;
const TOTAL_WIDTH = COLS * BOX_WIDTH + (COLS - 1) * GAP_X;
const START_X = (800 - TOTAL_WIDTH) / 2 + 10;
const START_Y = 140;
interface FlowBoxProps {
title: string;
desc: string;
index: number;
frame: number;
fps: number;
total: number;
}
// 布局: 1→2→3→4→5
// 第1行: 1, 2, 3
// 第2行: 4, 5
const positions = [
{ row: 0, col: 0, next: 'right' }, // 1
{ row: 0, col: 1, next: 'right' }, // 2
{ row: 0, col: 2, next: 'down' }, // 3 → 4
{ row: 1, col: 2, next: 'left' }, // 4 → 5
{ row: 1, col: 1, next: 'none' }, // 5 结束
];
const FlowBox: React.FC<FlowBoxProps> = ({ title, desc, index, frame, fps, total }) => {
// 开头1秒 + 5个格子(每个1秒) + 结尾1秒 = 7秒 @30fps = 210帧
const loopFrames = 210;
const frameInLoop = frame % loopFrames;
const showFrame = index * 40;
const delayFrame = Math.max(0, frameInLoop - showFrame);
// 入场动画
const scale = spring({ frame: delayFrame, fps, from: 0.5, to: 1, damping: 10 });
const opacity = interpolate(delayFrame, [0, 15], [0, 1], { extrapolateRight: 'clamp' });
const translateY = spring({ frame: delayFrame, fps, from: 30, to: 0 });
const pos = positions[index];
const x = START_X + pos.col * (BOX_WIDTH + GAP_X);
const y = START_Y + pos.row * (BOX_HEIGHT + GAP_Y);
const arrowOpacity = delayFrame > 20 ? 1 : 0;
// 莫兰迪色系
const colors = [
{ bg: '#e8c4c4', border: '#d4a5a5', accent: '#b89090' },
{ bg: '#b8d4e3', border: '#a3c4d4', accent: '#82a4b5' },
{ bg: '#c4e8c4', border: '#a8d4a8', accent: '#88b888' },
{ bg: '#e3d4b8', border: '#d4c4a3', accent: '#b4a483' },
{ bg: '#d4c4e3', border: '#c4a8d4', accent: '#a488b5' },
];
const color = colors[index] || colors[0];
// 箭头方向
const arrowDir = pos.next;
return (
<div style={{ position: 'absolute', top: y, left: x }}>
{/* 右箭头 */}
{arrowDir === 'right' && (
<div style={{
position: 'absolute',
top: BOX_HEIGHT / 2 - 10,
left: BOX_WIDTH,
width: GAP_X,
height: 20,
opacity: arrowOpacity,
display: 'flex',
alignItems: 'center',
}}>
<div style={{ flex: 1, height: 2, background: '#666' }} />
<svg width="18" height="18" viewBox="0 0 18 18">
<path d="M5 9 L13 9 M13 9 L9 5 M13 9 L9 13" stroke="#666" strokeWidth="2.5" fill="none" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
</div>
)}
{/* 左箭头 */}
{arrowDir === 'left' && (
<div style={{
position: 'absolute',
top: BOX_HEIGHT / 2 - 10,
left: -GAP_X,
width: GAP_X,
height: 20,
opacity: arrowOpacity,
display: 'flex',
alignItems: 'center',
flexDirection: 'row-reverse',
}}>
<div style={{ flex: 1, height: 2, background: '#666' }} />
<svg width="18" height="18" viewBox="0 0 18 18">
<path d="M13 9 L5 9 M5 9 L9 5 M5 9 L9 13" stroke="#666" strokeWidth="2.5" fill="none" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
</div>
)}
{/* 下箭头 */}
{arrowDir === 'down' && (
<div style={{
position: 'absolute',
top: BOX_HEIGHT,
left: BOX_WIDTH / 2 - 1,
width: 2,
height: GAP_Y,
opacity: arrowOpacity,
}}>
{/* 线条 */}
<div style={{ width: '100%', height: '100%', background: '#666' }} />
{/* 箭头 */}
<svg width="20" height="20" viewBox="0 0 20 20" style={{ position: 'absolute', bottom: -2, left: -9 }}>
<path d="M10 3 L10 17 M10 17 L6 12 M10 17 L14 12" stroke="#666" strokeWidth="2.5" fill="none" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
</div>
)}
{/* 卡片 */}
<div style={{
width: BOX_WIDTH,
height: BOX_HEIGHT,
backgroundColor: color.bg,
borderRadius: 20,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
opacity,
transform: `translateY(${translateY}px) scale(${scale})`,
boxShadow: `6px 6px 16px rgba(0,0,0,0.12), -3px -3px 10px rgba(255,255,255,0.1)`,
border: `2px solid ${color.border}`,
position: 'relative',
}}>
{/* 序号 */}
<div style={{
position: 'absolute',
top: 10,
left: 14,
width: 26,
height: 26,
borderRadius: '50%',
backgroundColor: '#fff',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: 12,
fontWeight: 'bold',
color: color.accent,
boxShadow: '2px 2px 5px rgba(0,0,0,0.08)',
}}>
{index + 1}
</div>
<span style={{
color: '#4a4a4a',
fontSize: 16,
fontWeight: 'bold',
fontFamily: 'PingFang SC, Microsoft YaHei, sans-serif',
textAlign: 'center',
padding: '0 8px',
}}>{title}</span>
<span style={{
color: '#7a7a7a',
fontSize: 10,
marginTop: 6,
fontFamily: 'PingFang SC, Microsoft YaHei, sans-serif',
textAlign: 'center',
padding: '0 8px',
}}>{desc}</span>
</div>
</div>
);
};
export const SkillsFlowchart: React.FC = () => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const items = [
{ title: '原理研究', desc: '根基(决定上限)' },
{ title: '热加载', desc: '基础条件(边用边改)' },
{ title: '过程反馈', desc: '用时给反馈' },
{ title: '数据反馈', desc: '发布后数据' },
{ title: '自更新机制', desc: 'AI帮你写skill' },
];
return (
<AbsoluteFill style={{ backgroundColor: '#f5f0eb' }}>
{/* 背景装饰 */}
<div style={{
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: 500,
height: 500,
background: 'radial-gradient(circle, rgba(200,180,170,0.2) 0%, transparent 60%)',
}} />
{/* 标题 */}
<div style={{
position: 'absolute',
top: 30,
left: 0,
right: 0,
textAlign: 'center',
fontSize: 30,
fontWeight: 'bold',
color: '#5a4a42',
fontFamily: 'PingFang SC, Microsoft YaHei, sans-serif',
letterSpacing: 6,
}}>
Skills 自更新体系
</div>
{/* 流程图 */}
{items.map((item, index) => (
<FlowBox
key={index}
title={item.title}
desc={item.desc}
index={index}
frame={frame}
fps={fps}
total={items.length}
/>
))}
</AbsoluteFill>
);
};
import React from 'react';
import { useCurrentFrame, useVideoConfig, interpolate, AbsoluteFill, spring } from 'remotion';
export const StaticVsDynamic: React.FC = () => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const totalFrames = 240;
const loopFrame = frame % totalFrames;
const appear = (delay: number) =>
spring({ frame: loopFrame - delay, fps, config: { damping: 10 } });
const fadeIn = (delay: number) =>
interpolate(loopFrame - delay, [0, 15], [0, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' });
// Title
const titleScale = appear(0);
// Left box (static)
const leftAppear = appear(20);
// Right box (dynamic)
const rightAppear = appear(40);
// Comparison rows
const row1 = appear(70);
const row2 = appear(90);
const row3 = appear(110);
// Right side animation: arrows animate
const arrowProgress = interpolate(loopFrame, [50, 80], [0, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' });
// Highlight pulse on right side
const pulse = interpolate(loopFrame, [130, 150, 170], [1, 1.05, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' });
return (
<AbsoluteFill style={{ backgroundColor: '#f8f6f3', fontFamily: 'PingFang SC, Microsoft YaHei, sans-serif', padding: 40 }}>
{/* Title */}
<div style={{
textAlign: 'center',
fontSize: 28,
fontWeight: 700,
color: '#2d2d2d',
marginBottom: 30,
transform: `scale(${titleScale})`,
}}>
动画比静态图多了什么?
</div>
<div style={{ display: 'flex', gap: 30, flex: 1, alignItems: 'flex-start' }}>
{/* Left - Static */}
<div style={{
flex: 1,
background: '#fff',
borderRadius: 16,
padding: 24,
opacity: leftAppear,
transform: `translateY(${(1 - leftAppear) * 20}px)`,
border: '2px solid #e0dbd5',
}}>
<div style={{ textAlign: 'center', fontSize: 22, fontWeight: 600, color: '#999', marginBottom: 20 }}>
静态图 📊
</div>
<div style={{
background: '#f5f3f0',
borderRadius: 10,
padding: 20,
textAlign: 'center',
fontSize: 20,
color: '#aaa',
marginBottom: 16,
}}>
A ── B ── C
<div style={{ fontSize: 14, marginTop: 8, color: '#bbb' }}>(关系靠猜)</div>
</div>
{/* Comparison points - left */}
<div style={{ display: 'flex', flexDirection: 'column', gap: 10, marginTop: 10 }}>
<div style={{ opacity: row1, fontSize: 16, color: '#c0392b', display: 'flex', alignItems: 'center', gap: 8 }}>
<span>❌</span> <span>逻辑:谁连谁?</span>
</div>
<div style={{ opacity: row2, fontSize: 16, color: '#c0392b', display: 'flex', alignItems: 'center', gap: 8 }}>
<span>❌</span> <span>顺序:从哪看起?</span>
</div>
<div style={{ opacity: row3, fontSize: 16, color: '#c0392b', display: 'flex', alignItems: 'center', gap: 8 }}>
<span>❌</span> <span>重点:全都一样大</span>
</div>
</div>
</div>
{/* Right - Dynamic */}
<div style={{
flex: 1,
background: '#fff',
borderRadius: 16,
padding: 24,
opacity: rightAppear,
transform: `scale(${pulse}) translateY(${(1 - rightAppear) * 20}px)`,
border: '2px solid #27ae60',
boxShadow: '0 4px 20px rgba(39,174,96,0.1)',
}}>
<div style={{ textAlign: 'center', fontSize: 22, fontWeight: 600, color: '#2d2d2d', marginBottom: 20 }}>
动画 🎬
</div>
<div style={{
background: '#f0f9f4',
borderRadius: 10,
padding: 20,
textAlign: 'center',
fontSize: 20,
color: '#2d2d2d',
marginBottom: 16,
position: 'relative',
}}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 6 }}>
<span style={{ opacity: interpolate(arrowProgress, [0, 0.2], [0, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' }) }}>A</span>
<span style={{ opacity: interpolate(arrowProgress, [0.15, 0.4], [0, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' }), color: '#27ae60', fontWeight: 700 }}> ─▶ </span>
<span style={{ opacity: interpolate(arrowProgress, [0.3, 0.55], [0, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' }) }}>B</span>
<span style={{ opacity: interpolate(arrowProgress, [0.5, 0.75], [0, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' }), color: '#27ae60', fontWeight: 700 }}> ─▶ </span>
<span style={{ opacity: interpolate(arrowProgress, [0.7, 1], [0, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' }) }}>C</span>
</div>
<div style={{ fontSize: 14, marginTop: 8, color: '#27ae60' }}>(演给你看)</div>
</div>
{/* Comparison points - right */}
<div style={{ display: 'flex', flexDirection: 'column', gap: 10, marginTop: 10 }}>
<div style={{ opacity: row1, fontSize: 16, color: '#27ae60', display: 'flex', alignItems: 'center', gap: 8 }}>
<span>✅</span> <span>逻辑:箭头直接演</span>
</div>
<div style={{ opacity: row2, fontSize: 16, color: '#27ae60', display: 'flex', alignItems: 'center', gap: 8 }}>
<span>✅</span> <span>顺序:依次弹入</span>
</div>
<div style={{ opacity: row3, fontSize: 16, color: '#27ae60', display: 'flex', alignItems: 'center', gap: 8 }}>
<span>✅</span> <span>重点:高亮+放大</span>
</div>
</div>
</div>
</div>
</AbsoluteFill>
);
};
import { Composition, registerRoot } from 'remotion';
import React from 'react';
import { PieChart } from './PieChart';
const MainVideo: React.FC = () => {
return (
<Composition
id="pie-chart"
durationInFrames={180}
fps={30}
width={800}
height={600}
component={PieChart}
/>
);
};
registerRoot(MainVideo);
import { useCurrentFrame, interpolate, AbsoluteFill } from 'remotion';
// 数据配置
const DATA = [
{ label: 'AI生成', value: 65, color: '#3498db' },
{ label: '人工优化', value: 25, color: '#2ecc71' },
{ label: '其他', value: 10, color: '#95a5a6' },
];
const CIRCLE_RADIUS = 90;
const STROKE_WIDTH = 28;
const CENTER_X = 200;
const CENTER_Y = 220;
// 计算弧形路径
const getArcPath = (startAngle: number, endAngle: number, radius: number) => {
const startRad = (startAngle - 90) * Math.PI / 180;
const endRad = (endAngle - 90) * Math.PI / 180;
const x1 = CENTER_X + radius * Math.cos(startRad);
const y1 = CENTER_Y + radius * Math.sin(startRad);
const x2 = CENTER_X + radius * Math.cos(endRad);
const y2 = CENTER_Y + radius * Math.sin(endRad);
const largeArc = endAngle - startAngle > 180 ? 1 : 0;
return `M ${x1} ${y1} A ${radius} ${radius} 0 ${largeArc} 1 ${x2} ${y2}`;
};
const PieSegment: React.FC<{ data: typeof DATA[0]; startAngle: number; frame: number; index: number }> = ({ data, startAngle, frame, index }) => {
const endAngle = startAngle + data.value * 3.6;
const animFrame = index * 30;
const progress = interpolate(frame, [animFrame, animFrame + 30], [0, 1], { extrapolateRight: 'clamp' });
const currentEndAngle = startAngle + (endAngle - startAngle) * progress;
if (progress <= 0) return null;
const path = getArcPath(startAngle, currentEndAngle, CIRCLE_RADIUS);
return (
<svg width="500" height="400" style={{ position: 'absolute', top: 0, left: 0 }}>
<path
d={path}
fill="none"
stroke={data.color}
strokeWidth={STROKE_WIDTH}
strokeLinecap="round"
/>
</svg>
);
};
const LegendItem: React.FC<{ data: typeof DATA[0]; index: number; frame: number }> = ({ data, index, frame }) => {
const animFrame = 40 + index * 20;
const opacity = interpolate(frame, [animFrame, animFrame + 15], [0, 1], { extrapolateRight: 'clamp' });
const translateX = interpolate(frame, [animFrame, animFrame + 15], [20, 0], { extrapolateRight: 'clamp' });
return (
<div style={{
opacity,
transform: `translateX(${translateX}px)`,
display: 'flex',
alignItems: 'center',
gap: 12,
lineHeight: 1,
height: 40,
marginBottom: 30,
}}>
<div style={{
width: 16,
height: 16,
borderRadius: 4,
backgroundColor: data.color,
}} />
<div style={{
fontSize: 18,
color: '#333',
fontFamily: 'PingFang SC, Microsoft YaHei, sans-serif',
}}>
{data.label}
</div>
<div style={{
fontSize: 18,
color: '#666',
fontFamily: 'PingFang SC, Microsoft YaHei, sans-serif',
}}>
{data.value}%
</div>
</div>
);
};
const CenterText: React.FC<{ frame: number }> = ({ frame }) => {
const opacity = interpolate(frame, [60, 80], [0, 1], { extrapolateRight: 'clamp' });
const scale = interpolate(frame, [60, 80], [0.8, 1], { extrapolateRight: 'clamp' });
return (
<div style={{
position: 'absolute',
left: CENTER_X,
top: CENTER_Y,
transform: `translate(-50%, -50%) scale(${scale})`,
opacity,
textAlign: 'center',
}}>
<div style={{
fontSize: 48,
fontWeight: 'bold',
color: '#333',
}}>
65%
</div>
<div style={{
fontSize: 16,
color: '#666',
fontFamily: 'PingFang SC, Microsoft YaHei, sans-serif',
}}>
AI生成占比
</div>
</div>
);
};
export const PieChart: React.FC = () => {
const frame = useCurrentFrame();
// 计算起始角度
let currentAngle = 0;
const segments = DATA.map(d => {
const start = currentAngle;
currentAngle += d.value * 3.6;
return { data: d, startAngle: start };
});
return (
<AbsoluteFill style={{ backgroundColor: '#fff' }}>
{/* 环形图 - 居左 */}
<div style={{
position: 'absolute',
left: 100,
top: 100,
width: 400,
height: 440,
}}>
{segments.map((seg, i) => (
<PieSegment
key={i}
data={seg.data}
startAngle={seg.startAngle}
frame={frame}
index={i}
/>
))}
<CenterText frame={frame} />
</div>
{/* 图例 - 居右,垂直居中 */}
<div style={{
position: 'absolute',
left: 520,
top: 300,
}}>
{DATA.map((d, i) => (
<LegendItem key={i} data={d} index={i} frame={frame} />
))}
</div>
</AbsoluteFill>
);
};
#!/bin/bash
# Remotion Skill 环境自检 + 自动修复
# 用法: bash check-env.sh
SKILL_DIR="$(cd "$(dirname "$0")/.." && pwd)"
echo "🔍 检查 Remotion Skill 环境..."
echo ""
# 1. Node.js
if ! command -v node &> /dev/null; then
echo "❌ Node.js 未安装"
echo " 请先安装: https://nodejs.org"
exit 1
fi
echo "✅ Node.js $(node --version)"
# 2. package.json
if [ ! -f "$SKILL_DIR/package.json" ]; then
echo "❌ package.json 不存在"
exit 1
fi
echo "✅ package.json"
# 3. node_modules(没有就自动装)
if [ ! -d "$SKILL_DIR/node_modules" ]; then
echo "⚠️ node_modules 不存在,正在自动安装..."
cd "$SKILL_DIR" && npm install --silent
if [ $? -eq 0 ]; then
echo "✅ 依赖安装完成"
else
echo "❌ 依赖安装失败"
exit 1
fi
else
echo "✅ node_modules"
fi
# 4. src/index.tsx
if [ ! -f "$SKILL_DIR/src/index.tsx" ]; then
echo "⚠️ src/index.tsx 不存在"
else
echo "✅ src/index.tsx"
fi
# 5. 案例库
TEMPLATE_COUNT=$(ls -d "$SKILL_DIR/案例库"/*/ 2>/dev/null | wc -l | tr -d ' ')
echo "✅ 案例库: ${TEMPLATE_COUNT} 个模板"
echo ""
echo "🟢 环境就绪,可以直接渲染"
echo ""
echo "渲染命令示例:"
echo " cd $SKILL_DIR"
echo " npx remotion render src/index.tsx <composition-id> /输出路径/xxx.gif --codec=gif --every-nth-frame=2"
echo ""
echo "可用 composition-id:"
grep -o 'id="[^"]*"' "$SKILL_DIR/src/index.tsx" 2>/dev/null | sed 's/id="//;s/"//' | while read id; do
echo " - $id"
done
#!/bin/bash
# Remotion 项目初始化脚本
echo "检查 Node.js..."
if ! command -v node &> /dev/null; then
echo "❌ Node.js 未安装,请先安装: https://nodejs.org"
exit 1
fi
echo "✅ Node.js 已安装: $(node --version)"
echo "✅ npm 已安装: $(npm --version)"
# 创建项目目录
PROJECT_NAME=${1:-my-remotion}
echo ""
echo "创建项目: $PROJECT_NAME"
mkdir -p $PROJECT_NAME
cd $PROJECT_NAME
# 创建 package.json
cat > package.json << 'EOF'
{
"name": "remotion-project",
"version": "1.0.0",
"scripts": {
"dev": "remotion dev src/index.tsx",
"build": "remotion render src/index.tsx main out.gif"
},
"dependencies": {
"@remotion/cli": "^4.0.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"remotion": "^4.0.0"
},
"devDependencies": {
"@types/react": "^18.2.0",
"typescript": "^5.0.0"
}
}
EOF
# 创建 src 目录和入口文件
mkdir -p src
cat > src/index.tsx << 'EOF'
import { Composition, registerRoot } from 'remotion';
import React from 'react';
const MyVideo: React.FC = () => {
return (
<Composition
id="main"
durationInFrames={150}
fps={30}
width={800}
height={600}
component={({}) => null}
/>
);
};
registerRoot(MyVideo);
EOF
# 创建 tsconfig.json
cat > tsconfig.json << 'EOF'
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"lib": ["DOM", "ESNext"],
"jsx": "react-jsx",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"moduleResolution": "bundler"
},
"include": ["src"]
}
EOF
echo ""
echo "✅ 项目结构已创建"
echo ""
echo "下一步:"
echo "1. cd $PROJECT_NAME"
echo "2. npm install"
echo "3. npm run dev"
Remotion 动画设计规范
时间规范
循环时长计算
- 开头等待:1 秒
- 每个格子:1 秒
- 结尾等待:1 秒
- 总时长 = (N + 2) 秒(N 为格子数量)
示例
- 5 个格子:开头1秒 + 5秒 + 结尾1秒 = 7秒
- 帧率:30fps
- 总帧数:7 × 30 = 210 帧
// 配置示例(5个格子)
const loopFrames = 210; // 7秒 @30fps
// index.tsx 中
durationInFrames={210}布局规范
网格布局
- 画布尺寸:800 × 600
- 卡片尺寸:140 × 140
- 间距:40 × 40
- 列数:3
居中计算
const COLS = 3;
const TOTAL_WIDTH = COLS * BOX_WIDTH + (COLS - 1) * GAP_X;
const START_X = (800 - TOTAL_WIDTH) / 2 + 10; // 微调标题位置
- 顶部距离:30px
- 流程图起始 Y:140px
箭头规范
方向
- right:向右
- left:向左
- down:向下
- none:无箭头
箭头位置
- 线条宽度:2px
- 箭头SVG居中于线条