关于 vid-motion-graphics
vid-motion-graphics 是一项将多场景 HTML/CSS 动画渲染为 MP4 的创作技能,适用于需要制作图表或信息视频的创作者。它通过逐帧控制动画并用 FFmpeg 合成画面,强调确定性渲染。
主要功能
- 01生成多场景 MP4 视频
- 02支持 1–6 个场景
- 03提供五种视觉风格
- 04可设置画幅、帧率和配乐
- 05通过 Chromium 与 FFmpeg 导出
适用场景
- 01将内容简报制作成动态图形视频
- 02制作包含数据和要点的短片
- 03按平台画幅导出视频
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ varnan-tech/ opendirectory --skill vid-motion-graphics安装后在 Claude Code 中输入/vid-motion-graphics确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
README.mdevals/evals.jsonreferences/scene-library.mdstyle-presets.mdscripts/capture-frames.mjsexport-video.sh详细介绍
vid-motion-graphics
将多场景动态图形生成为 MP4。通过 Playwright 在无头 Chromium 中渲染 HTML/CSS 动画(使用 Web Animations API 跳转帧),再用 FFmpeg 组装 PNG 帧。无需 React、AI API 或 Python——除 graphic-gif 系列之外,不新增任何依赖。
仅使用 CDN 字体。HTML 中不使用外部库。
关键规则(每次生成前阅读)
- 使用
window.renderFrame(t)——场景转场不要使用 CSS@keyframes。 在 Chromium 中,CSS 动画的currentTime在向后跳转时会被静默忽略。renderFrame方法:纯 JS 函数直接根据毫秒数计算opacity/transform。Playwright 每帧调用一次。确定性高,无竞态。 - 任何元素都不要设置
animation-delay。 使用renderFrame时不需要。如果发现自己要写animation-delay,请停下——你使用的架构不对。 - 仅在
document.fonts.ready.then(...)中设置window.__videoReady = true。 绝不要同步设置——必须先加载字体,再由 Playwright 捕获第 1 帧,否则文本会使用回退字体渲染。 - 暴露
window.__stopPreview()。 浏览器的 rAF 预览循环会与 Playwright 的 evaluate/screenshot 调用发生竞态。capture-frames.mjs会在帧循环开始前调用__stopPreview()。务必包含它。 - 场景边界检查使用
t < startMs(而不是<=)。 在第 0 帧,t <= 0会导致场景 1 显示为黑屏。正确的判断条件是if (t < startMs || t >= endMs) return hidden。 - Body = 精确像素尺寸。 宽度和高度均为整数(
1080px、1920px)。不要使用%、vw/vh或响应式单位。 - 不能同时显示两个场景(10% 的进入重叠除外)。所有场景在各自
renderFrame时间窗口之外都应为opacity: 0。 - 转场仅使用
opacity。 不要切换display,也不要使用visibility——GPU 合成的opacity能确保帧级精确。 - 绝不要在聊天中贴出 HTML。 保存到文件,只展示摘要。
- 标题应表达核心信息,而不是主题。 用“Q4 业绩超额达成目标的 3 个原因”,而不是“2024 年 Q4 业绩视频”。
- 生成任何 HTML 前,先阅读
references/scene-library.md。 使用该文件中的确切 HTML 结构和 CSS 类名。
第 1 步:需求收集
必填: content_brief
可选参数及默认值:
| 参数 | 默认值 | 说明 |
|---|---|---|
| content_brief | — | 描述视频要传达什么的文本(必填) |
| scenes | auto | 场景数量(1–6)。auto = 根据简报确定。 |
| duration_per_scene | 3s | 每个场景的时长,单位为秒(1–8s) |
| style | kinetic-dark | kinetic-dark / editorial-light / data-pulse / bold-type / minimal-clean |
| aspect_ratio | 1:1 | 1:1(1080×1080)/ 16:9(1920×1080)/ 9:16(1080×1920) |
| fps | 30 | 每秒帧数(24、30 或 60) |
| music | none | 背景音轨的音频文件路径(mp3/m4a/wav) |
| source | — | 最后一帧页脚中显示的来源署名 |
如果缺少 content_brief,请原样询问:
“要创建视频,我需要一份内容简报——视频应该传达什么信息?
示例:‘展示 Q4 收入增长 85% 的 3 个原因:新增企业客户交易、降低客户流失率、提高价格。使用醒目的数字。风格:data-pulse。’
可选项:风格(默认:kinetic-dark)、宽高比(默认:1:1)、每个场景的秒数(默认:3s)”
如果已提供 content_brief → 立即继续第 2 步。
步骤 2:内部架构(绝不展示给用户)
1. 将简介解析为场景(最多 6 个):
- 场景 1 = 吸引注意或标题(始终包含)
- 场景 2–N-1 = 补充要点、数据指标或故事节点
- 场景 N = CTA 或结尾总结(场景数大于 1 时始终包含)
- 每个场景一个核心观点——如果简介包含 7 个或更多观点,请合并最弱的观点
2. 阅读 references/scene-library.md——为每个场景选择场景类型:
- 吸引注意/开场 →
title-card - 单个数据指标 →
stat-reveal - 2–4 个要点的列表 →
bullet-list - 前后对比/两个数值 →
split-screen - 推荐语/引言 →
quote-card - 结尾/CTA →
cta-card
3. 阅读 references/style-presets.md——加载所选样式的 CSS 令牌和动画风格。
4. 计算时间:
totalDuration = sceneCount × duration_per_scene (seconds)
totalFrames = totalDuration × fps
每个场景占整个 @keyframes 范围的 (100 / sceneCount)%。
| 场景 | 起始 % | 结束 % |
|---|---|---|
| 1 | 0% | (100/N)% |
| 2 | (100/N)% | (200/N)% |
| … | … | … |
| N | ((N-1)×100/N)% | 100% |
在每个场景的范围内:
- 进入:场景范围的前 10%
- 保持:场景范围的 10% 到 85%
- 退出:场景范围的 85% 到 100%
5. 确定像素尺寸:
1:1→ W=1080, H=108016:9→ W=1920, H=10809:16→ W=1080, H=1920
步骤 3:生成 HTML
编写任何代码前,先阅读 references/scene-library.md 和 references/style-presets.md。
必需的 HTML 结构:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
[font CDN link from style preset]
<style>
:root {
[all CSS tokens from style preset]
}
*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
html, body {
width: [W]px; height: [H]px;
overflow: hidden;
background: var(--bg);
font-family: var(--font-body);
position: relative;
}
.scene {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 80px;
opacity: 0;
will-change: opacity, transform;
}
.scene-inner {
width: 100%;
max-width: 960px;
}
/* Scene-type CSS from scene-library.md */
[paste scene-type CSS here]
</style>
</head>
<body>
<div class="scene scene-1">
<div class="scene-inner">
[scene 1 HTML from scene-library.md template]
</div>
</div>
[repeat for each scene]
<script>
window.__videoReady = false;
window.TOTAL_DURATION_MS = [totalDuration * 1000];
// ── Animation helpers ─────────────────────────────────────────────────────────
function lerp(a, b, p) { return a + (b - a) * p; }
function clamp(v, lo, hi) { return Math.max(lo, Math.min(hi, v)); }
function easeOutCubic(t) { return 1 - Math.pow(1 - clamp(t, 0, 1), 3); }
function sceneState(t, startMs, endMs) {
if (t < startMs || t >= endMs) return { opacity: 0, ty: 0 };
const prog = (t - startMs) / (endMs - startMs);
if (prog < 0.10) {
const p = easeOutCubic(prog / 0.10);
return { opacity: p, ty: lerp(24, 0, p) };
}
if (prog < 0.85) return { opacity: 1, ty: 0 };
const p = (prog - 0.85) / 0.15;
return { opacity: 1 - p, ty: lerp(0, -12, p) };
}
function applySceneState(el, state) {
el.style.opacity = state.opacity;
el.style.transform = state.ty !== 0 ? `translateY(${state.ty.toFixed(2)}px)` : '';
}
// ── Main render function — called by Playwright once per frame ────────────────
window.renderFrame = function(t) {
// Scene 1: 0ms – [D]ms
applySceneState(document.querySelector('.scene-1'), sceneState(t, 0, [D]));
// Scene 2: [D]ms – [2D]ms
applySceneState(document.querySelector('.scene-2'), sceneState(t, [D], [2D]));
// ... repeat per scene ...
};
// ── Preview loop — stopped by Playwright before frame capture ─────────────────
let __previewActive = false;
let __previewRafId = null;
window.__stopPreview = function() {
__previewActive = false;
if (__previewRafId !== null) { cancelAnimationFrame(__previewRafId); __previewRafId = null; }
};
document.fonts.ready.then(() => {
window.renderFrame(0);
window.__videoReady = true;
__previewActive = true;
const startTime = performance.now();
function previewTick() {
if (!__previewActive) return;
const elapsed = performance.now() - startTime;
if (elapsed < window.TOTAL_DURATION_MS) {
window.renderFrame(elapsed);
__previewRafId = requestAnimationFrame(previewTick);
} else {
window.renderFrame(window.TOTAL_DURATION_MS - 1);
__previewActive = false;
}
}
__previewRafId = requestAnimationFrame(previewTick);
});
</script>
</body>
</html>
设计质量规则:
- 标题字号:不得小于 60px——文本必须在手机缩略图尺寸下清晰可读
- 数字和统计数据:始终是屏幕上最大的元素(120–200px)
- 展示文字使用紧凑的字间距:
-0.02em到-0.04em - 每个场景只使用一种强调色——不要在多个元素上零散使用强调色
- 深色预设:分隔线使用
rgba(255,255,255,0.10),不得使用纯色 - 浅色预设:分隔线使用
rgba(0,0,0,0.10),不得使用纯色 - 每个使用
transform的元素都要设置transform-origin: center center .scene内边距:至少 80px——文本不得贴近视口边缘
步骤 4:自我 QA(在步骤 5 前修复所有问题)
renderFrame 正确性:
- 已定义
window.renderFrame(t)——纯函数,不在样式写入之外产生副作用 - 场景边界使用
t < startMs(而不是t <= startMs)——避免第 0 帧黑屏 - 场景过渡中,
animation-delay或@keyframes的使用次数均为 0 - 没有两个场景的
opacity: 1时间窗口重叠(10% 进入重叠除外) - 已暴露
window.__stopPreview(),且预览 rAF 循环会检查__previewActive
就绪信号:
- 在
document.fonts.ready前声明window.__videoReady = false - 仅在
document.fonts.ready.then(...)中设置window.__videoReady = true - 在设置
__videoReady = true前,于document.fonts.ready.then(...)中调用window.renderFrame(0)
布局:
-
html, body使用精确的像素尺寸([W]px、[H]px) -
body的宽度/高度不使用%、vw、vh、rem单位 -
html, body设置overflow: hidden - 所有场景均设置
position: absolute; inset: 0
设计:
- 所有颜色均来自样式预设令牌——不得使用未定义的十六进制值
- 所有字体均来自样式预设——不得使用未定义的 font-family 字符串
- 标题字号 ≥ 60px
- 如果提供了
source参数,最后一个场景中必须包含来源
步骤 5:导出
根据简介内容确定 slug(kebab-case,≤30 个字符):
mkdir -p chart/[slug]
保存 HTML:chart/[slug]/video.html
浏览器预览:
open chart/[slug]/video.html
运行导出(将 [skill-root] 替换为此技能目录的路径):
bash [skill-root]/scripts/export-video.sh \
chart/[slug]/video.html \
chart/[slug]/video.mp4 \
--duration [totalDuration] \
--fps [fps] \
--width [W] \
--height [H] \
[--music path/to/audio.mp3]
脚本首次运行时会安装 Playwright(Chromium 约 200MB),捕获所有帧,然后使用 FFmpeg 合成 MP4。
步骤 6:输出摘要
## Video: [title from brief]
Date: [YYYY-MM-DD] | Scenes: [N] | Style: [style] | Aspect: [ratio]
Duration: [N]s | FPS: [fps] | Frames: [N]
Files
Source: chart/[slug]/video.html
Output: chart/[slug]/video.mp4
Size: [X] MB
Checklist
- [ ] All scenes appear in sequence with no blank frames
- [ ] Text legible at mobile thumbnail size
- [ ] Scene transitions smooth (no jump cuts)
- [ ] Final scene includes CTA or closing message
- [ ] Source attribution present in final frame (if provided)
提示词建议(用户请求指导时显示)
“提供一份内容简报,以项目符号列出每个场景应表达的内容。”
“直接点明洞察。“第四季度增长 85% 的 3 个原因”能为视频确立主线。”
“如果有偏好的风格,请说明:动感暗色(默认)、编辑浅色、数据脉冲、醒目字体、极简简洁。”
“根据平台指定宽高比:LinkedIn/Instagram 信息流用 1:1,Stories/Reels 用 9:16,YouTube/演示文稿用 16:9。”
✅ 好:“制作一段 9 秒的视频。第四季度营收达到 420 万美元(增长 85%)。增长驱动因素:企业客户交易、流失率 1.2%、涨价。行动号召:acme.com/q4。风格:数据脉冲。宽高比:1:1。”
❌ 不好:“制作一段关于我们公司的视频”